/* Simple JavaScript Inheritance
 * By John Resig http://ejohn.org/
 * MIT Licensed.
 */
// Inspired by base2 and Prototype
(function() {
  var initializing = false, fnTest = /xyz/.test(function() {
    xyz;
  }) ? /\b_super\b/ : /.*/;
  // The base Class implementation (does nothing)
  this.Class = function() {
  };

  // Create a new Class that inherits from this class
  Class.extend = function(prop) {
    var _super = this.prototype;

    // Instantiate a base class (but only create the instance,
    // don't run the init constructor)
    initializing = true;
    var prototype = new this();
    initializing = false;

    // Copy the properties over onto the new prototype
    for (var name in prop) {
      // Check if we're overwriting an existing function
      prototype[name] = typeof prop[name] == "function" &&
                        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
                        (function(name, fn) {
                          return function() {
                            var tmp = this._super;

                            // Add a new ._super() method that is the same method
                            // but on the super-class
                            this._super = _super[name];

                            // The method only need to be bound temporarily, so we
                            // remove it when we're done executing
                            var ret = fn.apply(this, arguments);
                            this._super = tmp;

                            return ret;
                          };
                        })(name, prop[name]) :
                        prop[name];
    }

    // The dummy class constructor
    function Class() {
      // All construction is actually done in the init method
      if (!initializing && this.init) {
        this.init.apply(this, arguments);
      }
    }

    // Populate our constructed prototype object
    Class.prototype = prototype;

    // Enforce the constructor to be what we expect
    Class.prototype.constructor = Class;

    // And make this class extendable
    Class.extend = arguments.callee;

    return Class;
  };
})();
if(!this.JSON){this.JSON={};}
(function(){"use strict";function f(n){return n<10?'0'+n:n;}
if(typeof Date.prototype.toJSON!=='function'){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+'-'+
f(this.getUTCMonth()+1)+'-'+
f(this.getUTCDate())+'T'+
f(this.getUTCHours())+':'+
f(this.getUTCMinutes())+':'+
f(this.getUTCSeconds())+'Z':null;};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf();};}
var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==='string'?c:'\\u'+('0000'+a.charCodeAt(0).toString(16)).slice(-4);})+'"':'"'+string+'"';}
function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==='object'&&typeof value.toJSON==='function'){value=value.toJSON(key);}
if(typeof rep==='function'){value=rep.call(holder,key,value);}
switch(typeof value){case'string':return quote(value);case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';}
gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==='[object Array]'){length=value.length;for(i=0;i<length;i+=1){partial[i]=str(i,value)||'null';}
v=partial.length===0?'[]':gap?'[\n'+gap+
partial.join(',\n'+gap)+'\n'+
mind+']':'['+partial.join(',')+']';gap=mind;return v;}
if(rep&&typeof rep==='object'){length=rep.length;for(i=0;i<length;i+=1){k=rep[i];if(typeof k==='string'){v=str(k,value);if(v){partial.push(quote(k)+(gap?': ':':')+v);}}}}else{for(k in value){if(Object.hasOwnProperty.call(value,k)){v=str(k,value);if(v){partial.push(quote(k)+(gap?': ':':')+v);}}}}
v=partial.length===0?'{}':gap?'{\n'+gap+partial.join(',\n'+gap)+'\n'+
mind+'}':'{'+partial.join(',')+'}';gap=mind;return v;}}
if(typeof JSON.stringify!=='function'){JSON.stringify=function(value,replacer,space){var i;gap='';indent='';if(typeof space==='number'){for(i=0;i<space;i+=1){indent+=' ';}}else if(typeof space==='string'){indent=space;}
rep=replacer;if(replacer&&typeof replacer!=='function'&&(typeof replacer!=='object'||typeof replacer.length!=='number')){throw new Error('JSON.stringify');}
return str('',{'':value});};}
if(typeof JSON.parse!=='function'){JSON.parse=function(text,reviver){var j;function walk(holder,key){var k,v,value=holder[key];if(value&&typeof value==='object'){for(k in value){if(Object.hasOwnProperty.call(value,k)){v=walk(value,k);if(v!==undefined){value[k]=v;}else{delete value[k];}}}}
return reviver.call(holder,key,value);}
text=String(text);cx.lastIndex=0;if(cx.test(text)){text=text.replace(cx,function(a){return'\\u'+
('0000'+a.charCodeAt(0).toString(16)).slice(-4);});}
if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,'@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']').replace(/(?:^|:|,)(?:\s*\[)+/g,''))){j=eval('('+text+')');return typeof reviver==='function'?walk({'':j},''):j;}
throw new SyntaxError('JSON.parse');};}}());Array.prototype.addAllNewIn = function(list, args) {
  for (var i = 0; i < list.length; i++) {
    if (!this.contains(list[i], args)) {
      this.push(list[i]);
    }
  }
  return this;
};

Array.prototype.removeAllIn = function(list, args) {
  for (var i = 0; i < this.length; i++) {
    if (list.contains(this[i], args)) {
      this.splice(i--, 1);
    }
  }
  return this;
};

Array.prototype.removeAllNotIn = function(list, args) {
  for (var i = 0; i < this.length; i++) {
    if (!list.contains(this[i], args)) {
      this.splice(i--, 1);
    }
  }
  return this;
};

/**
 * Deprecated. Use contains()
 * @param value
 */
Array.prototype.inArray = function (value) {
  for (var i = 0; i < this.length; i++) {
    if (this[i] == value) {
      return true;
    }
  }
  return false;
};

Array.prototype.contains = function (value, args) {
  args = $.extend(
    {
      useEquals:false // If true, use equalsObject() instead of == when comparing objects.
    }, args);
  for (var i = 0; i < this.length; i++) {
    var b = (args.useEquals && equalsObject(this[i], value)) ||
            (!args.useEquals && this[i] == value);
    if (b) {
      return true;
    }
  }
  return false;
};

Array.prototype.containsById = function (value) {
  validateArgument(value, {rules:{allowUndefined:false, requireObject:true}}, "value");
  var hasUndefined = false;
  var undefinedIndex = -1;
  var result = false;
  for (var i = 0; i < this.length; i++) {
    if (!this[i]) {
      hasUndefined = true;
      if (undefinedIndex < 0) undefinedIndex = i;
    } else if (this[i].id == value.id) {
      result = true;
      break;
    }
  }
  if (hasUndefined) {
    ibewarning("Array.containsById(..) found element in self that is undefined. index=" + undefinedIndex, this);
  }
  return result;
};

Array.prototype.containsOnlyNumbers = function (value) {
  return this.containsOnly(value, "number");
};

Array.prototype.containsOnlyStrings = function (value) {
  return this.containsOnly(value, "string");
};

Array.prototype.containsOnly = function (value, type) {
  for (var i = 0; i < this.length; i++) {
    if (typeof this[i] !== type) {
      return false;
    }
  }
  return true;
};

Array.prototype.peek = function () {
  if (this.length === 0) return undefined;
  return this[this.length - 1];
};

Array.prototype.pushAll = function (list) {
  try {
    var that = this;
    $.each(list, function(key, obj) {
      that.push(obj);
    });
  } catch (e) {
    ibeerror("Array.pushAll failed: " + e);
  }
};

Array.prototype.remove = function (value) {
  for (var i = 0; i < this.length; i++) {
    if (this[i] == value) {
      this.splice(i, 1);
    }
  }
};

Array.prototype.indexInArray = function (value) {
  for (var i = 0; i < this.length; i++) {
    if (this[i] == value) {
      return i;
    }
  }
  return -1;
};

Array.prototype.equalsArray = function(b) {
  return equalsObject(this, b);
  /*
   if (this == b) {
   return true;
   }
   if (this.keyCount() != b.keyCount()) {
   return false;
   }
   for (key in this) {
   if (this[key] != b[key]) {
   return false;
   }
   }
   return true;
   */
};

Array.prototype.equals = function(a) {
  if (this == a) return true;
  if (this.length != a.length) return false;
  for (var i = 0; i < this.length; i++) {
    var b = equalsObject(a[i], this[i]);
    if (!b) return false;
  }
  return true;
};

Array.prototype.clone = function() {
  var a = new Array();
  for (var i = 0; i < this.length; i++) {
    a[i] = this[i];
  }
  return a;
};

String.prototype.splitTwice = function (separator1, separator2) {
    var splitLeft = this.split(separator1);
    var stack = new Array();
    return splitList(splitLeft, separator2);
};

String.prototype.format = function () {
    var s = this;
    for (var i = 0; i < arguments.length; i++) {
        var reg = new RegExp("\\{" + i + "\\}", "gm");
        s = s.replace(reg, arguments[i]);
    }
    return s;
};

String.prototype.trim = function () {
    return (this.replace(/^[\s\xA0]+/, "").replace(/[\s\xA0]+$/, ""));
};

String.prototype.startsWith = function (str) {
    return (this.match("^" + str) == str);
};

String.prototype.endsWith = function (str) {
    return (this.match(str + "$") == str);
};

String.prototype.getLastCharacter = function () {
    if (this.length > 0) {
        return this.charAt(this.length - 1);
    } else {
        ibeerror("String.getLastCharacter(), but string is empty.");
        return undefined;
    }
};

String.prototype.getLast = function (i) {
    if (i > this.length) i = this.length;
    return this.substr(this.length - i, i);
};


String.prototype.removeLast = function (i) {
    if (i > this.length) i = this.length;
    return this.substr(0, this.length - i);
};

String.prototype.getFirst = function (i) {
    if (i > this.length) i = this.length;
    return this.substr(0, i);
};


String.prototype.removeFirst = function (i) {
    if (i > this.length) i = this.length;
    return this.substr(i, this.length - i);
};

// extend Number object with methods for converting degrees/radians

Number.prototype.toRad = function () {  // convert degrees to radians
  return this * Math.PI / 180;
};

Number.prototype.toDeg = function () {  // convert radians to degrees (signed)
  return this * 180 / Math.PI;
};

Number.prototype.toBrng = function () {  // convert radians to degrees (as bearing: 0...360)
  return (this.toDeg() + 360) % 360;
};

Number.prototype.formatPrice = function () {
	return formatNumber(this.valueOf(), {decimalCharacter:",", formatString:UiText.get("General.Currency.Format")});
};

Number.prototype.formatPriceForCountry = function (countryCode) {
  return formatNumber(this.valueOf(), PriceFormat[countryCode]);
};
/* Cookie context class, requires json2-min.js */
var CookieContext = Class.extend(
  {
    init : function(args) {
      args = $.extend(
        {
          parser : JSON.parse,
          formatter : JSON.stringify,
          options : {
            expires : undefined,
            path : undefined,
            domain : undefined,
            secure : undefined
          },
          name : undefined
        }, args);
      this.name = args.name;
      this.parser = args.parser;
      this.formatter = args.formatter;
    },

    get : function(key) {
      if (key) {
        if (this.name) {
          return YAHOO.util.Cookie.getSub(this.name, key);
        } else {
          return YAHOO.util.Cookie.get(key);
        }
      }
    },

    getValue : function(key) {
      return this.get(key);
    },

    getObject : function(key) {
      return this.parser(this.get(key));
    },

    set : function(key, obj) {
      if (key) {
        if (this.name) {
          YAHOO.util.Cookie.setSub(this.name, key, obj, this.options);
        } else {
          YAHOO.util.Cookie.set(key, obj, this.options);
        }
      }
    },

    setValue : function(key, obj) {
      this.set(key, obj);
    },

    setObject : function(key, obj) {
      this.set(key, this.formatter(obj));
    },

    remove : function(key) {
      if (key) {
        if (this.name) {
          YAHOO.util.Cookie.removeSub(this.name, key);
        } else {
          YAHOO.util.Cookie.remove(key);
        }
      }
    },

    /**
     * Delete all for this context or the context specified by "key"
     * @param key
     */
    removeAll : function(key) {
      if (this.name) {
        YAHOO.util.Cookie.remove(this.name);
      } else {
        YAHOO.util.Cookie.remove(key);
      }
    }

  });
/**
 * Deprecated
 */
function Context(key) {

  this.contextKey = key;

  this.get = function(key) {
    if (key) {
      if (this.contextKey) {
        return YAHOO.util.Cookie.getSub(this.contextKey, key);
      } else {
        return YAHOO.util.Cookie.get(key);
      }
    }
  };

  this.getObject = function(key) {
    return JSON.parse(this.get(key));
  };

  this.set = function(key, obj) {
    if (key) {
      if (this.contextKey) {
        YAHOO.util.Cookie.setSub(this.contextKey, key, obj);
      } else {
        YAHOO.util.Cookie.set(key, obj);
      }
    }
  };

  this.setObject = function(key, obj) {
    this.set(key, JSON.stringify(obj));
  };

  this.remove = function(key) {
    if (key) {
      if (this.contextKey) {
        YAHOO.util.Cookie.removeSub(this.contextKey, key);
      } else {
        YAHOO.util.Cookie.remove(key);
      }
    }
  };
  /**
   * Delete all for this context or the context specified by "key"
   * @param key
   */
  this.removeAll = function(key) {
    if (this.contextKey) {
      YAHOO.util.Cookie.remove(this.contextKey);
    } else {
      YAHOO.util.Cookie.remove(key);
    }
  };
}

function getCookie(name) {
  //console.log('getCookie name=' + name);
  if (!name) return null;
  var start = document.cookie.indexOf(name + "=");
  var len = start + name.length + 1;
  if (( !start ) && ( name != document.cookie.substring(0, name.length) )) {
    return null;
  }
  if (start == -1) return null;
  var end = document.cookie.indexOf(';', len);
  if (end == -1) end = document.cookie.length;
  var r = unescape(document.cookie.substring(len, end));
  //console.log('=' + r);
  return r;
}

function setCookie(name, value, expires, path, domain, secure) {
  //console.log('setcookie name=' + name + ' value=' + value);
  if (!name) return;
  var today = new Date();
  today.setTime(today.getTime());
  if (expires) {
    expires = expires * 1000 * 60 * 60 * 24;
  }
  var expires_date = new Date(today.getTime() + (expires));
  document.cookie = name + '=' + escape(value) +
                    ( ( expires ) ? ';expires=' + expires_date.toGMTString() : '' ) + //expires.toGMTString()
                    ( ( path ) ? ';path=' + path : '' ) +
                    ( ( domain ) ? ';domain=' + domain : '' ) +
                    ( ( secure ) ? ';secure' : '' );
}

function deleteCookie(name, path, domain) {
  if (getCookie(name)) {
    document.cookie = name + '=' +
                      ( ( path ) ? ';path=' + path : '') +
                      ( ( domain ) ? ';domain=' + domain : '' ) +
                      ';expires=Thu, 01-Jan-1970 00:00:01 GMT';
  }
}
function Statistics() {

  this.data = {};

}

Statistics.prototype.toString = function() {
  var s = "";
  for (key in this.data) {
    s += key + " : " + this.data[key] + "\n";
  }
  return s;
};

Statistics.prototype.get = function(name) {
  return this.data[name];
};

Statistics.prototype.set = function(name, value) {
  this.data[name] = value;
};

Statistics.prototype.clear = function(name) {
  this.data[name] = undefined;
};

Statistics.prototype.inc = function(name) {
  if (!this.data[name]) this.data[name] = 0;
  return this.data[name]++;
};

Statistics.prototype.dec = function(name) {
  if (!this.data[name]) this.data[name] = 0;
  return this.data[name]--;
};

function Timer(name) {

  this.name = name;

  this.state = "init";
  this.running = false;
  this.time = undefined;
  this.startTime = undefined;
  this.endTime = undefined;
}

Timer.prototype.start = function() {
  this.startTime = new Date();
  this.state = "running";
  this.running = true;
};

Timer.prototype.stop = function() {
  this.endTime = new Date();
  this.state = "stopped";
  this.running = false;
  this.time = this._calculateTime(this.startTime, this.endTime);
};

Timer.prototype.result = function() {
  return this.time;
};

Timer.prototype._calculateTime = function(startDate, endDate) {
  return endDate.getTime() - startDate.getTime();
};

Timer.prototype.toString = function() {
  if (this.time === undefined) {
    return "No timing data.";
  } else {
    if (this.running) {
      return "Timer is running '" + this.name + "', current time : " + this._calculateTime(this.startTime, this.endTime) + " ms.";
    } else {
      return "Timer '" + this.name + "' : " + this.time + " ms.";
    }
  }
};

/* IBE Common-always-loaded utilities. */
var KEY_LEFT = 0x01;
var KEY_UP = 0x02;
var KEY_RIGHT = 0x04;
var KEY_DOWN = 0x08;
var KEY_LSHIFT = 0x10;
var KEY_CTRL = 0x11;
var KEY_TAB = 9;
var KEY_ESC = 27;
var KEY_DEL = 46;
var KEY_BACKSPACE = 8;

var __logAlertLimit = 5;
var __logAlertCounter = 0;

var EVENT_KEYDOWN = "keydown";
var EVENT_KEYUP = "keyup";
var EVENT_KEYPRESS = "keypress";

var TYPE_OBJECT = "object";
var TYPE_FUNCTION = "function";
var TYPE_NUMBER = "number";
var TYPE_STRING = "string";

function clearField(f) {
  if (f.value == f.defaultValue) {
    f.value = "";
  }
}
function checkField(f) {
  if (f.value == "") {
    f.value = f.defaultValue;
  }
}

function getObj(id) {
  if (id == undefined) return null;
  var obj = null;
  if (document.getElementById) {
    obj = document.getElementById(id);
  } else {
    if (document.all) {
      obj = document.all[id];
    }
  }
  return obj;
}

function getObjsByName(id) {
  if (id == undefined) return null;
  return document.getElementsByName(id);
}

function getObjByName(id) {
  if (id == undefined) return null;
  var e = document.getElementsByName(id);
  if (e.length > 0)  return e[0];
  return null;
}

function getObjByIdThenName(id) {
  var e = getObj(id);
  if (e == null) e = getObjByName(id);
  return e;
}

function getEnabledObjByName(id) {
  if (id == undefined) return null;
  var es = document.getElementsByName(id);
  for (var i = 0; i < es.length; i++) {
    var e = es[i];
    if (!e.disabled) return e;
  }
  return null;
}

function getEnabledObjById(id) {
  if (id == undefined) return null;
  var e = document.getElementById(id);
  if (!e.disabled) return e;
  return null;
}

function toggleLayer(whichLayer, show) {
  if (document.getElementById) {
    var obj = getObj(whichLayer);
    if (obj) obj.style.display = show ? "" : "none";
  }
}

function toggleId(id) {
  if (document.getElementById) {
    var obj = getObj(id);
    var show = obj.style.display == "none";
    if (obj) obj.style.display = show ? "" : "none";
  }
}

function activate(id, isActive) {
  var obj = getObj(id);
  if (obj != null) {
    obj.readOnly = !isActive;
    obj.disabled = !isActive;
  }
}

function replaceHtml(e, html) {
  var obj = getObj(e);
  if (!obj) return;
  obj.innerHTML = html;
}

function showHide(e, show, setDisplay) {
  if (show) {
    setVisible(e, setDisplay);
  } else {
    setHidden(e, setDisplay);
  }
  return getObj(e);
}

function toggleVisible(elementId) {
  var obj = getObj(elementId);
  if (obj) {
    var s = obj.style;
    if (s.display == '' || s.display == 'visible') {
      setHidden(elementId, true);
    } else {
      setVisible(elementId, true);
    }
  }
}

function setVisible(elementID, setDisplay) {
  // If second arg is true then set style.display to apropriate value depending on type
  var obj = getObj(elementID);
  if (obj != null) {
    if (setDisplay) {
      obj.style.display = '';
    }
    obj.style.visibility = 'visible';
  }
}

function setVisibleObj(obj, setDisplay) {
  // If second arg is true then set style.display to apropriate value depending on type
  if (obj != null) {
    if (setDisplay) {
      obj.style.display = '';
    }
    obj.style.visibility = 'visible';
  }
}

function setHidden(elementID, setDisplay) {
  // If second arg is true then set style.display to 'none'
  var obj = getObj(elementID);
  if (obj != null) {
    obj.style.visibility = 'hidden';
    if (setDisplay) {
      obj.style.display = 'none';
    }
  }
}

function setHiddenObj(obj, setDisplay) {
  // If second arg is true then set style.display to 'none'
  if (obj != null) {
    obj.style.visibility = 'hidden';
    if (setDisplay) {
      obj.style.display = 'none';
    }
  }
}

function displayFor(e) {
  var tn = e.tagName.toUpperCase();
  var t = 'block';
  if (tn == 'TR')t = 'table-row';
  if (tn == 'TD')t = 'table-cell';
  if (tn == 'TABLE')t = 'table';
  return t;
}

function winStat(s) {
  window.status = s;
  return true;
}

function omo(e, isOver, cssClass) {
  var cl = e.className;
  if (isOver) {
    e.style.cursor = 'pointer';
    if (cssClass) e.className = (cl ? cl : '') + '___xxx ' + cssClass;
  } else {
    e.style.cursor = 'default';
    if (cssClass) e.className = cl.replace('___xxx ' + cssClass, '');
  }
}

function getPosition(o) {
  var start = getSelectionStart(o);
  if (start == getSelectionEnd(o)) {
    return start;
  } else {
    return -1;
  }
}

function setPosition(o, p) {
  if (o.createTextRange) {
    var r = document.selection.createRange();
    r.moveStart('character', -o.value.length);
    r.moveStart('character', p);
    r.moveEnd('character', 0);
    r.select();
  } else {
    o.selectionStart = p;
    o.selectionEnd = p;
  }
}

function getSelectionStart(o) {
  if (o.createTextRange) {
    var r = document.selection.createRange().duplicate();
    r.moveEnd('character', o.value.length);
    if (r.text == '') {
      return o.value.length;
    }
    return o.value.lastIndexOf(r.text);
  } else {
    return o.selectionStart;
  }
}

function getSelectionEnd(o) {
  if (o.createTextRange) {
    var r = document.selection.createRange().duplicate();
    r.moveStart('character', -o.value.length);
    return r.text.length;
  } else {
    return o.selectionEnd;
  }
}
function airlineWinOpen(doc) {
  var w = window.open(doc, 'CarrierPromo', 'scrollbars=no,resizable=no,height=' + IBE.CarrierPromoHeight + ',width=' + IBE.CarrierPromoWidth);
  if (w) w.focus();
}

function openPopupWindow(url) {
  var a = arguments;
  var h = a.length > 1 ? a[1] : 600;
  var v = a.length > 2 ? a[2] : 500;
  if (url.indexOf(':') == -1 && url.indexOf('?') == -1) url += ':popup';
  var w = window.open(url, 'PopupWindow', 'scrollbars=yes,resizable=yes,height=' + h + ',width=' + v);
  if (w) w.focus();
}

function openNewBrowserWindow(url) {
  window.open(url);
}

function goToUrl(url) {
  document.location.href = url
}

/*****************Globals****************/
var ckpaste_currentText = '';//initualizes the text of the text area.
var ckpaste_elem = null;//this will be the textarea.
var ckpaste_pasteMsg = 'Cannot paste into this field';
/****************************************/

/****************************************************************************
 void ckpaste_preventPaste();
 initializes the copy paste monitor.

 ****************************************************************************/

function ckpaste_preventPaste(elem, msg) {
  ckpaste_elem = elem;

  if (msg) {
    ckpaste_pasteMsg = msg
  } else {
    if (msg === false) {
      ckpaste_pasteMsg = msg;
    }
  }

  ckpaste_currentText = ckpaste_elem.defaultValue;
  if (document.all) {//it it is ie do the onpaste function
    ckpaste_elem.onpaste = ckpaste_showMessage;
    ckpaste_elem.oncontextmenu = ckpaste_showMessage;
    ckpaste_elem.onkeyup = ckpaste_checkText;
    ckpaste_elem.onblur = ckpaste_checkText;
    ckpaste_elem.onchange = ckpaste_checkText;
  } else {
    ckpaste_elem.addEventListener("keyup", ckpaste_checkText, true);
    ckpaste_elem.addEventListener("blur", ckpaste_checkText, true);
    ckpaste_elem.addEventListener("change", ckpaste_checkText, true);
  }
}

/****************************************************************************
 void ckpaste_preventPaste()
 checks the length of the string in the textbox and compares it with
 the length of the string in the saved ckpaste_currentText string. If the
 text box text is 10 characters longer than the saved string then it
 assumes that they have pasted.  it puts the current string back into
 the textarea over what they pasted and then shows them the message.
 if it isn't longer then it just puts the text into the saved text.

 ****************************************************************************/

function ckpaste_checkText(event) {
  event = event || window.event; // window.event for IE
  if (ckpaste_elem && event && event.keyCode) {
    var newTextLength = ckpaste_elem.value.length;//gets length of the textarea right now.
    var ckpaste_currentTextLength = ckpaste_currentText.length;//gets length of the saved text from the textarea
    //    if (newTextLength > (1 + ckpaste_currentTextLength)) {
    //      alert('event.keyCode: ' + event.keyCode + ', newTextLength: ' + newTextLength + ", ckpaste_currentTextLength: " + ckpaste_currentTextLength);
    //    }
    var codeForSelectingFromPrevList = 13;
    if (newTextLength > (ckpaste_currentTextLength + 10) && //is the new more then 10 characters longer?
        event.keyCode != codeForSelectingFromPrevList) {
      ckpaste_elem.value = ckpaste_currentText;//put the saved text back in/
      ckpaste_showMessage();//tell them they cannot paste.
    } else {
      ckpaste_currentText = ckpaste_elem.value;//if it is ok then save the text.
    }
  }
}
/****************************************************************************
 void ckpaste_showMessage();
 if they calling function wants to show a message when they paste then
 the message is alerted.
 ****************************************************************************/

function ckpaste_showMessage() {
  if (ckpaste_pasteMsg !== false) {
    alert(ckpaste_pasteMsg);
    return false;
  }
  return true;
}

function validEmail(f) {
  return f && f.length > 4; // Fusk
}
function submitNewsMail(f) {
  var nl = f.CRM_EMAIL;
  if (nl.defaultValue != nl.value && validEmail(nl.value)) {
    f.submit();
  } else {
    alert(UiText.get("Javascript.Util.Email"));
    nl.focus();
    nl.select();
  }
  return false;
}

function clickLink(link) {
  if (!link.nodeName) {
    link = getObj(link);
  }

  if (link.nodeName == 'a' || link.nodeName == 'A') {
    if (!link.onclick || link.onclick()) {
      var target = link.target ? link.target : '_self';
      window.open(link.href, target, '');
    }
    return false;
  }

  return true;
}

function findChildById(root, id) {
  if (nullOrUndefined(root)) {
    return document.getElementById(id);
  }
  else {
    ibeerror("Function not yet implemented.");
  }
}

function findChildrenByName(root, name) {
  if (nullOrUndefined(root)) {
    return document.getElementsByName(name);
  }
  else {
    var list = new Array();
    ibeerror("Function not yet implemented.");
  }
}

function getFieldName(elementId) {
  var d = getObjByIdThenName(elementId);
  if (d != null) return d.name;
  return '';
}

function getFieldValue(elementId, form) {
  var d = findChildById(form, elementId);
  if (d == null || d.type == 'radio') {
    var es = findChildrenByName(form, elementId);
    if (es != null) {
      if (es.length == 1) {
        var e = es[0];
        return e.value;
      } else {
        for (var i = 0; i < es.length; i++) {
          var r = es.item(i);
          if (r.checked) return r.value;
        }
      }
    }
  } else {
    if (d.type == 'checkbox') {
      return d.checked;
    } else {
      return d.value;
    }
  }
  return '';
}

function getValueFromFieldObject(field) {
  if (field == null || field.type == 'radio') {
    var es = getFieldsFromForm(field.form, field.name);
    if (es != null) {
      if (es.length == 1) {
        var e = es[0];
        return e.value;
      } else {
        for (var i = 0; i < es.length; i++) {
          var r = es.item(i);
          if (r.checked) return r.value;
        }
      }
    }
  } else {
    if (field.type == 'checkbox') {
      return field.checked;
    } else {
      return field.value;
    }
  }
  return '';
}

function stringToBool(s) {
  return s == "true";
}

function doInnerHTML(elementId, html, mode) {
  if (mode == 'append') {
    appendToInnerHTML(elementId, html);
  } else {
    setInnerHTML(elementId, html);
  }
}

function doElementsInnerHTML(element, html, mode) {
  if (mode == 'append') {
    appendToElementsInnerHTML(element, html);
  } else {
    setElementsInnerHTML(element, html);
  }
}

function setInnerHTML(elementID, html) {
  var e = getObj(elementID);
  if (e != null && propertyExists(e.innerHTML)) {
    e.innerHTML = html;
  }
}

function setElementsInnerHTML(e, html) {
  if (e != null && propertyExists(e.innerHTML)) {
    e.innerHTML = html;
  }
}

function appendToInnerHTML(elementId, html) {
  var e = getObj(elementId);
  if (e != null && propertyExists(e.innerHTML)) {
    e.innerHTML = e.innerHTML + html;
  }
}

function appendToElementsInnerHTML(e, html) {
  if (e != null && propertyExists(e.innerHTML)) {
    e.innerHTML = e.innerHTML + html;
  }
}

function getInnerHTML(elementId) {
  var e = getObj(elementId);
  if (e != null && propertyExists(e.innerHTML)) {
    return e.innerHTML;
  }
  return '';
}

function hasInnerHTML(elementId) {
  var e = getObj(elementId);
  if (e != null && propertyExists(e.innerHTML)) {
    return notEmptyString(trimString(e.innerHTML));
  }
  return false;
}

function clearInnerHTML(elementId) {
  setInnerHTML(elementId, '');
}

function notEmptyString(s) {
  return !emptyString(s);
}

function emptyString(s) {
  return s === undefined || s === null || s === "";
}

function trimString(s) {
  return s.replace(/^\s*/, "").replace(/\s*$/, "");
}

function setSrc(id, src) {
  var e = getObj(id);
  if (e != null && propertyExists(e.src)) {
    e.src = src;
  }
}

function propertyExists(property) {
  return typeof(property) !== 'undefined';
}

/**
 * Finds first parent element of type type. Searches recursively upwards the tree.
 * @param type The type to search for.
 * @param fromObj The object whos parent we examine.
 */
function findParentElementOfType(type, fromObj) {
  if (fromObj == null || fromObj == undefined) return null;
  if (fromObj.parentNode == null || fromObj.parentNode == undefined) return null;
  if (fromObj.parentNode.tagName == type) return fromObj.parentNode;
  return findParentElementOfType(type, fromObj.parentNode);
}

function removeAllTrFromTd(e) {
  do {
    var tdes = e.getElementsByTagName('td');
    if (tdes.length < 1) return;
    var tde = tdes[0];
    tde.parentNode.removeChild(tde);
  } while (tdes.length > 0)
}

/**
 * Checks if javascript function exists, returns true if that is the case.
 * @param funcName
 */
function functionExists(funcName) {
  return typeof funcName == 'string' && eval('typeof ' + funcName) == 'function';
}

function stringContainsAString(s) {
  if (s.startsWith('"') && s.endsWith('"')) return true;
  if (s.startsWith("'") && s.endsWith("'")) return true;
  return false;
}

function stringIsNumeric(s) {
  var validChars = "0123456789., ";
  return stringIncludesValidCharsOnly(s, validChars);
}

function stringIsInteger(s) {
  var validChars = "0123456789";
  return stringIncludesValidCharsOnly(s, validChars);
}

function stringIsPhoneNumber(s) {
  var validChars = "0123456789+-() ";
  return stringIncludesValidCharsOnly(s, validChars);
}

function stringIsDigitsAndDash(s) {
  var validChars = "0123456789-";
  return stringIncludesValidCharsOnly(s, validChars);
}

function stringIsPersonsName(s) {
  var validChars = "abcdefghijklmnopqrstuvwxyzåäöABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ- ";
  return stringIncludesValidCharsOnly(s, validChars);
}

/**
 * Does not allow -
 * @param s
 */
function stringIsPersonsNameAirlineFormat(s) {
  var validChars = "abcdefghijklmnopqrstuvwxyzåäöABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ ";
  return stringIncludesValidCharsOnly(s, validChars);
}

function isAlphanumericCharCode(c) {
  if (c >= 65 && c <= 90) return true; // a-z
  if (c >= 48 && c <= 57) return true; // 0-9
  if (c == 219) return true; // å
  if (c == 222) return true; // ä
  if (c == 186) return true; // ö
  return false;
}

/**
 * Returns true if the char code is a character, false if it is only a key press but not a character.
 * That is, < is a character, a is a character, but shift is not a character.
 * @param c
 */
function charCodeIsChar(c) {
  if (isAlphanumericCharCode(c)) return true;
  if (c >= 186) return true;
  return false;
}

function charCodeIsCharacter(c) {
  return String.fromCharCode(c).length ? true : false;
}

function stringIncludesValidCharsOnly(s, validChars) {
  if (s === undefined || s === null) return true;
  for (var i = 0; i < s.length; i++) {
    var c = s.charAt(i);
    if (validChars.indexOf(c) == -1) return false;
  }
  return true;
}

function stringExcludesInvalidChars(s, invalidChars) {
  if (s === undefined || s === null) return true;
  for (var i = 0; i < s.length; i++) {
    var c = s.charAt(i);
    if (invalidChars.indexOf(c) >= 0) return false;
  }
  return true;
}

function findItemWithId(list, id) {
  if (list && list.length) {
    for (var i = 0; i < list.length; i++) {
      if (list[i] && list[i].id == id) return list[i];
    }
  }
  return undefined;
}

function findItemWithPropertyEquals(list, propertyName, propertyValue) {
  if ($.isArray(list)) {
    for (var i = 0; i < list.length; i++) {
      if (list[i] && list[i][propertyName] == propertyValue) return list[i];
    }
  } else {
    ibeerror("First argument of findItemWithPropertyEquals() is not a list.");
  }
  return undefined;
}

function findItemsWithIdStartingWith(list, idPrefix) {
  idPrefix = idPrefix.toString();
  var outList = [];
  if (list && list.length) {
    for (var i = 0; i < list.length; i++) {
      if (("" + list[i] && list[i].id).toString().startsWith(idPrefix)) outList.push(list[i]);
    }
  }
  return outList;
}

function setPropertyOnList(list, propertyName, propertyValue) {
  if (list && list.length) {
    for (var i = 0; i < list.length; i++) {
      list[i][propertyName] = propertyValue;
    }
  }
  return list;
}

function hasItemWithId(list, id) {
  return findItemWithId(list, id) ? true : false;
}

function removeItemWithId(list, id) {
  if (list && list.length) {
    for (var i = 0; i < list.length; i++) {
      if (list[i] && list[i].id == id) {
        list.splice(i, 1);
      }
    }
  }
}

function removeItem(list, item) {
  if (list && list.length) {
    for (var i = 0; i < list.length; i++) {
      if (equalsObject(list[i], item)) {
        list.splice(i, 1);
      }
    }
  }
}

function ibedebug(s) {
  ibelog(s);
}

/**
 * Logs a string, if id is set, appends it to the element with that id. If not it checks if console.log is available
 * and uses that. If not, see if element with id 'console' is available. If not, alert is used.
 * @param s The string to be logged.
 * @param id The id of the element.
 */
function ibelog(s, id) {
  if (isProdEnvironment()) return;
  var e = getObj('console');
  if (getObj(id) != null) {
    appendToInnerHTML(id, s + '<br>\n');
  }
  else {
    if (typeof console == 'object' && typeof console.log == 'function') {
      console.log(s);
    }
    else {
      if (e != null) {
        appendToInnerHTML('console', s + '<br>\n');
      } else {
        if (isDevEnvironment()) {
          return;
          if (__logAlertCounter < __logAlertLimit) alert(s);
          __logAlertCounter++;

          if (__logAlertCounter == __logAlertLimit) {
            alert('Alert limit has been reached, no more alerts will be shown. Limit=' + __logAlertLimit);
          }
        }
      }
    }
  } // Do not allow alert in production environment.
}

/**
 * Singleton instance that manages JS-unit testing.
 */
var IBETests = (function() {
  var noticesAreLoggedStack = [true];
  var warningThrowsExceptionStack = [false];
  var errorThrowsExceptionStack = [false]; // Contains booleans, topmost value is current value

  var eventStartPrefix = "testModuleStart";
  var eventEndPrefix = "testModuleEnd";

  var debugMode = false;

  // Private function
  function runIbeTests(args) {

  }

  // public interface methods
  return {
    getNoticesAreLogged:function() {
      if (!noticesAreLoggedStack.length) return false;
      return noticesAreLoggedStack.peek();
    },
    getWarningThrowsException:function() {
      if (!warningThrowsExceptionStack.length) return false;
      return warningThrowsExceptionStack.peek();
    },
    getErrorThrowsException:function() {
      if (!errorThrowsExceptionStack.length) return false;
      return errorThrowsExceptionStack.peek();
    },

    pushNoticesAreLogged:function(v) {
      noticesAreLoggedStack.push(v ? true : false);
    },
    pushWarningThrowsException:function(v) {
      warningThrowsExceptionStack.push(v ? true : false);
    },
    pushErrorThrowsException:function(v) {
      errorThrowsExceptionStack.push(v ? true : false);
    },

    popNoticesAreLogged:function() {
      noticesAreLoggedStack.pop();
      if (noticesAreLoggedStack.length == 0) noticesAreLoggedStack.push(false); // Ensure has value
    },
    popWarningThrowsException:function() {
      warningThrowsExceptionStack.pop();
      if (warningThrowsExceptionStack.length == 0) warningThrowsExceptionStack.push(false); // Ensure has value
    },
    popErrorThrowsException:function() {
      errorThrowsExceptionStack.pop();
      if (errorThrowsExceptionStack.length == 0) errorThrowsExceptionStack.push(false); // Ensure has value
    },

    /**
     * @param args is an object containing properties to be used when the test are run.
     * @see runIbeTest(args) function for the default properties.
     */
    runTests:function(args) {
      runIbeTests(args);
    },
    init:function(args) {
      QUnit.moduleStart = function(args) {
        var eventId = formatCustomEventName(eventStartPrefix);
        $.event.trigger(eventId, {});
        eventId = formatCustomEventName(eventStartPrefix + args.name);
        $.event.trigger(eventId, {});
      };
      QUnit.moduleDone = function(args) {
        var eventId = formatCustomEventName(eventEndPrefix);
        $.event.trigger(eventId, {});
        eventId = formatCustomEventName(eventEndPrefix + args.name);
        $.event.trigger(eventId, {});
      }
    },

    /**
     * Set current test module, for example "ibe-component" or "charter". You can set callbacks for before and after done with that module.
     * You can use this to enable exceptions for warning and errors for example, which can be detected by test framework.
     *
     * @param moduleName
     * @param beforeCallback
     * @param afterCallback
     */
    setTestModule:function(moduleName, beforeCallback, afterCallback) {
      if (debugMode) ibelogs("setTestModule", moduleName);
      var eventId = formatCustomEventName(eventStartPrefix + moduleName);
      if (typeof beforeCallback === "function") {
        $('body').bind(eventId, {name:moduleName }, function() {
          if (debugMode) ibelogs("beforeModule()", moduleName);
          beforeCallback({ name:name });
        });
      } else {
        ibeerror("Trying to set module start callback for QUnit testing, but callback is not a function. Module name=" + moduleName);
      }
      eventId = formatCustomEventName(eventEndPrefix + moduleName);
      if (typeof afterCallback === "function") {
        $('body').bind(eventId, {name:moduleName }, function() {
          if (debugMode) ibelogs("afterModule()", moduleName);
          afterCallback({ name:name });
        });
      } else {
        ibeerror("Trying to set module end callback for QUnit testing, but callback is not a function. Module name=" + moduleName);
      }
      module(moduleName);
    }
  };

})();

/**
 * Formats a string so that it can be used as custom event name in jQuery.
 * @param s
 */
function formatCustomEventName(s) {
  s = s.replace(/ /g, ''); // Remove all spaces, not allowed.
  s = s.replace(/\./g, ''); // Remove all spaces, not allowed.
  s = s.replace(/\-/g, ''); // Remove all spaces, not allowed.
  return s;
}

function stripNonNumeric(str) {
  str += '';
  var rgx = /^\d|\.|-$/;
  var out = '';
  for (var i = 0; i < str.length; i++) {
    if (rgx.test(str.charAt(i))) {
      if (!( ( str.charAt(i) == '.' && out.indexOf('.') != -1 ) ||
             ( str.charAt(i) == '-' && out.length != 0 ) )) {
        out += str.charAt(i);
      }
    }
  }
  return out;
}

function stringDiff(s1, s2) {
  var messages = [];
  if (s1.length !== s2.length) messages.push("Lengths are not equal.");

  var firstDiffIndex = undefined;
  for (var i = 0; i < s1.length && i < s2.length; i++) {
    if (s1.charAt(i) !== s2.charAt(i)) {
      firstDiffIndex = i;
      messages.push("Difference at index=" + firstDiffIndex);
      messages.push("String 1 diff:" + s1.substring(firstDiffIndex, firstDiffIndex + 50));
      messages.push("String 2 diff:" + s2.substring(firstDiffIndex, firstDiffIndex + 50));
      break;
    }
  }
  return {
    messages:messages,
    firstDiffIndex:firstDiffIndex,
    length1:s1.length,
    length2:s2.length
  }
}

function pushCopy(args, list) {
  $.each(args, function(i, arg) {
    // Make a copy of the array/object, so that if the original object is updated, the log is not.
    if ($.isArray(arg)) {
      list.push(arg.clone());
    } else if (typeof arg == "object") {
      var o = {};
      copyObject(arg, o);
      list.push(o);
    } else {
      list.push(arg);
    }
  });
  return list;
}

function ibelogs() {
  if (isProdEnvironment()) return;
  if (
    (typeof console == 'object' && typeof console.log == 'function') ||
    (typeof console == 'object' && typeof console.log == 'object' && browserIsIE9()) // IE9 console.log is of type "object"
    ) {
    console.log(pushCopy(arguments, []));
  }
}

/**
 * Logs a warning message to console. Should not output anything if in production!
 * @param s The string to be logged.
 */
function ibewarning() {
  if (isProdEnvironment()) return;
  if (IBETests.getWarningThrowsException()) {
    ibethrow(arguments);

  } else if (typeof console == 'object' && typeof console.warning == 'function') {
    console.warning(pushCopy(arguments, []));

  } else {
    ibelogs.apply(this, pushCopy(arguments, ["Warning!"]));
  }
}

/**
 * Logs an error message to console. Should not output anything if in production!
 * @param str The string to be logged.
 */
function ibeerror(str) {
  if (isProdEnvironment()) return;
  if (IBETests.getErrorThrowsException()) {
    ibethrow(str);

  } else if (typeof console == 'object' && typeof console.error == 'function') {
    console.error(str);
    ibetrace();

  } else {
    alert("Error: " + str);
  }
}


function ibeloga(a) {
  if (a && a.length) {
    for (var i = 0; i < a.length; i++) {
      ibelogs(i, +a[i]);
    }
  }
}

/**
 *
 * @param s
 * @param s2
 */
function ibenotice(s, s2) {
  if (isProdEnvironment()) return;
  if (IBETests.getNoticesAreLogged()) {
    if (s2) {
      ibelogs(s, s2);
    } else {
      ibelogs(s);
    }
  }
}

function ibetrace() {
  if (typeof console == 'object' && typeof console.trace == 'function') {
    console.trace();
  }
}

function ibethrow(m) {
  if (isProdEnvironment()) return;
  throw m;
}

/**
 * Checks to see if we are in debug environment, that is, NOT in production environment.
 */
function isDebugEnvironment() {
  return IBE.debug;
}

/**
 * Checks to see if we are in debug environment, that is, NOT in production environment.
 */
function isProdEnvironment() {
  return IBE.environment === 'prod';
}

function isTestEnvironment() {
  return IBE.environment === 'test';
}

function isDevEnvironment() {
  return IBE.environment === 'dev';
}

function nlToBr(s) {
  var tmp = undefined;
  var limit = 1000;
  while (limit > 0) {
    tmp = s.replace('\n', '<br>');
    if (tmp == s) break;
    s = tmp;
    limit--;
  }
  return tmp;
}

function spaceToNbsp(s) {
  var tmp = undefined;
  var limit = 1000;
  while (limit > 0) {
    tmp = s.replace(' ', '&nbsp;');
    if (tmp == s) break;
    s = tmp;
    limit--;
  }
  return tmp;
}


/**
 * Prints all properties for an object.
 * @param o
 */
function printAll(o) {
  ibelog(printAllToString(o));
}

function printArray(a) {
  ibelog('Array[' + a.length + '] =');
  for (var i = 0; i < a.length; i++) {
    var v = a[i];
    ibelog('  [' + i + '] =>');
    ibelog(v);
  }
}

function printAllToString(o) {
  var r = new String();
  r += '-- All variables in Object --\n';
  for (var prop in o) {
    if (typeof prop !== 'string') r += prop + ' (' + typeof prop + ') =' + o[prop] + '\n';
  }
  r += '----\n';
  return r;
}

function getObjectsProperties(o) {
  var l = [];
  var i = 0;
  for (var p in o) {
    l[i++] = p;
  }
  return l;
}

function enableId(id) {
  var e = getObj(id);
  if (e) e.disabled = false;
}

function disableId(id) {
  var e = getObj(id);
  if (e) e.disabled = true;
}

function autoCompleteOff(f) {
  f.setAttribute("autocomplete", "off");
}

function setElementsClass(id, className) {
  var e = getObj(id);
  if (e != null && propertyExists(e.className)) {
    e.className = className;
  }
}

function executeFunctionList(list) {
  if (list && list.length) {
    var i, s = list.length;
    for (i = 0; i < s; i++) {
      var f = list[i];
      if (typeof f === "function") {
        setTimeout(f, 0);
      } else {
        ibewarning("Trying to execute list of functions, but item i=" + i + " is not a function.");
      }
    }
  }
}

function disableSelection(element) {
  if (element) {
    if (typeof element.onselectstart != "undefined") {
      //IE
      element.onselectstart = function() {
        return false;
      };
    } else {
      if (typeof element.style.MozUserSelect != "undefined") {
        //Firefox
        element.style.MozUserSelect = "none";
      } else {
        //Others
        element.onmousedown = function() {
          return false;
        };
      }
    }
  }
}

function useDefault(value, defaultValue) {
  if (value === undefined || value === null) {
    return defaultValue;
  } else {
    return value;
  }
}

function trim(str) {
  return str.replace(/^\s*|\s*$/g, "");
}

function cssClassFriendly(str) {
  str += '';
  return str.replace(/\.|\,/g, '_');
}

function implode(list, separator) {
  var r = '';
  var isFirst = true;
  for (var i = 0; i < list.length; i++) {
    if (isFirst === true || separator === undefined) {
      isFirst = false;
    } else {
      r = r + separator;
    }
    r = r + list[i];
  }
  return r;
}

function removeListHead(list) {
  var nl = new Array();
  for (var i = 1; i < list.length; i++) {
    nl[i - 1] = list[i];
  }
  return nl;
}

/**
 * Same as split, but for lists. For example.
 * 0: "mattias is"
 * 1: "awesome"
 * separator: " "
 * will result in
 * 0: "mattias"
 * 1: "is"
 * 2: "awesome"
 * @param list
 * @param separator
 */
function splitList(list, separator) {
  var nlist = new Array();
  for (var i = 0; i < list.length; i++) {
    var l = list[i].split(separator);
    for (var j = 0; j < l.length; j++) {
      nlist[nlist.length] = l[j];
    }
  }
  return nlist;
}

function startsWith(str, v) {
  return str.substring(0, v.length) == v;
}

function endsWith(str, v) {
  return str.substring(v.length - 1, v.length) == v;
}

function subList(list, startIndex, length) {
  var rlist = new Array();
  var endIndex = startIndex + length;
  if (length < 0) endIndex = list.length - 1;
  for (var i = startIndex; i < endIndex; i++) {
    rlist[rlist.length] = list[i];
  }

  return rlist;
}

function nullOrUndefined(s) {
  return s === null || s === undefined;
}

function nonFalse(s) {
  return s === undefined || s;
}

function nonTrue(s) {
  return !(s === undefined || !s);
}

function keyCount(obj) {
  var i = 0;
  for (key in obj) {
    i++;
  }
  return i;
}

/* Will return false if input is not an object! */
function equalsObject(a, b) {
  if (a == b) {
    return true;
  }
  if (typeof a !== "object") {
    return false;
  }
  if (keyCount(a) != keyCount(b)) {
    return false;
  }
  for (key in a) {
    if (key === "__proto__") {
      //ibelogs("Skipping __proto__");
      continue;
    }
    var v1 = a[key];
    var v2 = b[key];
    if (typeof v1 === "function") {
      //ibelogs("Skipping function");
      continue;
    }
    if (typeof v1 === "number" || typeof v1 === "string") {
      if (v1 != v2) {
        //ibelogs("equalsObject FALSE!", v1, v2, a, b);
        return false;
      }
    } else {
      if (!equalsObject(v1, v2)) return false;
    }
  }
  return true;
}


function getElementsByClass(searchClass, node, tag) {
  var classElements = new Array();
  if (node == null) {
    node = document;
  }
  if (tag == null) {
    tag = '*';
  }
  var els = node.getElementsByTagName(tag);
  var elsLen = els.length;
  var pattern = new RegExp('(^|\\\\s)' + searchClass + '(\\\\s|$)');
  var j = 0;
  for (var i = 0; i < elsLen; i++) {
    if (pattern.test(els[i].className)) {
      classElements[j] = els[i];
      j++;
    }
  }
  return classElements;
}

function removeFnutts(s) {
  if (s.length > 2) {
    if ((s.startsWith('\'') && s.endsWith('\'')) ||
        (s.startsWith('"') && s.endsWith('"')        )) {
      return s.substring(1, s.length - 1);
    }
  }
  return s;
}

function isArray(obj) {
  if (nullOrUndefined(obj)) return false;
  return $.isArray(obj);
}

function addSelectOption(selectbox, text, value, cssClass, selected) {
  if (selectbox) {
    var e = document.createElement("OPTION");
    e.text = text;
    e.value = value;
    if (cssClass) e.className = cssClass;
    if (selected) {
      e.selected = true;
    }
    selectbox.options.add(e);
  }
}

function addSelectOptionObject(selectbox, option) {
  if (selectbox) {
    selectbox.options.add(option);
  }
}

function findOptionByValue(selectbox, value) {
  for (i = selectbox.options.length - 1; i >= 0; i--) {
    if (selectbox.options[i].value == value) return selectbox.options[i];
  }
  return null;
}

function selectOption(selectbox, value) {
  for (i = selectbox.options.length - 1; i >= 0; i--) {
    if (selectbox.options[i].value == value) {
      selectbox.selectedIndex = i;
      return;
    }
  }
}

function clearSelectOptions(selectbox) {
  for (var i = selectbox.options.length - 1; i >= 0; i--) {
    // Must be done backwards, otherwise it doesn't work.
    selectbox.options[i] = null;
  }
}

function greatCircleDistance(lat1, lon1, lat2, lon2) {
  var R = 6371; // km
  var d = Math.acos(Math.sin(lat1) * Math.sin(lat2) +
                    Math.cos(lat1) * Math.cos(lat2) *
                    Math.cos(lon2 - lon1)) * R;
  return d;
}

function greatCircleDistance2(lat1, lon1, lat2, lon2) {
  var R = 6371; // km
  var dLat = (lat2 - lat1).toRad();
  var dLon = (lon2 - lon1).toRad();
  var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
          Math.cos(lat1.toRad()) * Math.cos(lat2.toRad()) *
          Math.sin(dLon / 2) * Math.sin(dLon / 2);
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  var d = R * c;
  return d;
}

function distanceBetweenTwoPoints(x1, y1, x2, y2) {
  var dx = Math.abs(x2 - x1);
  var dy = Math.abs(y2 - y1);
  return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
}

function setInnerHtmlToLoadingAnimation(elementID, className) {
  className = className || "largeLoadingAnimationContainer";
  setInnerHTML(elementID, '<div class="' + className + '">' +
                          '<img src="/system/image/loading_animation_small.gif" alt="Loading"/>' +
                          '</div>');

}

function setInnerHtmlToSmallLoadingAnimation(elementID, width, paddingTop) {
  if (paddingTop === undefined) paddingTop = "2px";
  setInnerHTML(elementID, '<img style="padding-left:8px; padding-top: ' + paddingTop + ';' +
                          (width ? 'width:' + width + ';' : '') +
                          '" src="/system/image/loading_16x16.gif" alt="Loading"/>');

}

function getRandomUnusedId(id) {
  while (getObj(id)) {
    id = id + (Math.random() * 10);
  }
  return id;
}

function clearElement(element) {
  if (element) while (element.firstChild) element.removeChild(element.firstChild);
}


function logoutCustomer() {
  sendRequest('/ajax.user.logoutuser.do.action', function(response) {
    $('body').hasClass('user-profiles') ? redirect("") : reloadPage();
  }, function(response) {
    ibeerror("Unable to logout user.");
  });
}

function getSmallerAnimationHTML() {
  return '<img alt="Loading..." src="/system/image/loading_animation_smaller.gif"/>';
}

function getSmallestAnimationHTML() {
  return '<img alt="Loading..." src="/system/image/loading16x16.gif"/>';
}

/**
 * If used within an iframe, only the iframe will reload!
 */
function reloadPage() {
  window.location.reload();
}

/**
 * Reloads the window, this can be used even if in an iframe. Iframe will not reload, but instead the whole page.
 */
function reloadWindow() {
  top.location.reload();
}

function reloadParent() {
  if (parent) {
    parent.location.reload();
  }
}

function redirectOnTimeout(action, timeOut) {
  timeOut = useDefault(timeOut, 5000);
  setTimeout(function() {
    redirect(action);
  }, timeOut);
}

function redirect(action) {
  redirectPure(checkAction(action));
}

function redirectPure(path) {
  window.location.pathname = path;
}

/**
 * Shows an alert if the message is defined and not empty string, if the condition is undefined or true.
 * @param s
 * @param condition
 */
function ibealert(s, condition) {
  if (condition === undefined || condition === true) {
    if (s) alert(s);
  }
}

function checkAction(url) {
  if (emptyString(url)) return "";
  url = prependIfNotThere(url, "/");
  if (url != "/" && url.indexOf("?") == -1) {
    url = appendIfNotThere(url, ".action");
  }
  return url;
}

function appendIfNotThere(base, add) {
  return (base.endsWith(add) ? base : base + add);
}

function prependIfNotThere(base, add) {
  return (base.startsWith(add) ? base : add + base);
}

/**
 * Returns all fields in a form with a given name. Usually just one, but radio buttons usually have more than one.
 * @param form
 * @param fieldName
 */
function getFieldsFromForm(form, fieldName) {
  var result = new Array();
  if (!form) {
    ibeerror("Trying to get field in form that is undefined.");
    return undefined;
  }
  var elements = form.elements;
  if (elements) {
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      if (element.name == fieldName) {
        result.push(element);
      }
    }
  }
  if (result.length == 0) return null;
  return result;
}

function getFieldFromForm(form, fieldName) {
  var r = getFieldsFromForm(form, fieldName);
  if (r) {
    return r[0];
  }
  else {
    return null;
  }
}

function getSelectFieldValue(field) {
  var options = field.options;
  for (var i = 0; i < options.length; i++) {
    var option = options[i];
    if (option.selected) return option.value;
  }
  return null;
}

function getSelectedOption(selectField) {
  var options = selectField.options;
  if (options && options.length) {
    for (var i = 0; i < options.length; i++) {
      var option = options[i];
      if (option.selected) return option;
    }
  }
  return null;
}

function getIndexOfOptionWithInnerHtml(selectField, html) {
  var options = selectField.options;
  if (options && options.length) {
    for (var i = 0; i < options.length; i++) {
      var option = options[i];
      if (option.innerHTML === html) return i;
    }
  }
  return -1;
}

function getValueOfOptionWithInnerHtml(selectField, html) {
  var options = selectField.options;
  if (options && options.length) {
    for (var i = 0; i < options.length; i++) {
      var option = options[i];
      if (option.innerHTML === html) return option.value;
    }
  }
  return null;
}

//See ValidationUtil.java for description and ValidationUtilTest.java for tests
function isValidPassword(password) {
  var regex = /^[^\s]{6,}$/;
  return regex.test(password);
}

function isValidEmail(email) {
  var regex = /^\w+([\.\-\+]?\w+)*@\w+([\.\-\+]?\w+)*(\.\w{2,})+$/;
  return regex.test(email);
}

/* Matches 6 to 15 digits, used for phone numbers, see standard E.164 */
function isValidPhoneNumber(phoneNumber) {
  var regex = /^\d{6,15}$/;
  return regex.test(trimPhoneNumber(phoneNumber));
}

/* Trims away " .-" and leading "00" or "+" */
function trimPhoneNumber(phoneNumber) {
  var regex = /^00|^\+|[\s.-]+/;
  return phoneNumber.replace(regex, "");
}

function parseJsonList(s) {
  var e = eval(s);
  return e;
}

function parseJsonObject(s) {
  var e = eval('[' + s + ']');
  return e[0];
}

function isEmptyArray(o) {
  if (o === undefined) return true;
  if (o === null) return true;
  if (o.length == 0) {
    return true;
  } else {
    return false;
  }
}

function isNotEmptyArray(array) {
  return !isEmptyArray(array);
}

function deleteElementInArray(array, index) {
  array.splice(index, 1);
}

function addAll(arr1, arr2) {

}

function removeAll(arr1, arr2) {

}

function retainAll(arr1, arr2) {
  var r = [], o = {}, l = arr2.length, i, v;
  for (i = 0; i < l; i++) {
    o[arr2[i]] = true;
  }
  l = arr1.length;
  for (i = 0; i < l; i++) {
    v = arr1[i];
    if (v in o) {
      r.push(v);
    }
  }
  return r;
}

function runSoon(func, delayInMs) {
  setTimeout(func, delayInMs);
}

/**
 * @param url The URL (as string) that contains URL parameters.
 *
 * @return an object representing the URL parameters. If URL is not defined, returns empty object.
 */
function deserializeUrlString(url) {
  if (!url) return {};
  var values = url.split("&");
  var o = {};
  for (var i = 0; i < values.length; i++) {
    var v = values[i].split("=");
    o[v[0]] = decodeURIComponent(v[1]);
  }
  return o;
}

/****************
 Fading functions
 ****************/

function fade(elementId, timeToFade) {
  if (!timeToFade) timeToFade = 1000.0;

  var element = document.getElementById(elementId);
  if (element == null) return;
  element.FadeState = null;

  if (element.FadeState == null) {
    if (element.style.opacity == null || element.style.opacity == '' || element.style.opacity == '1') {
      element.FadeState = 2;
    } else {
      element.FadeState = -2;
    }
  }

  if (element.FadeState == 1 || element.FadeState == -1) {
    element.FadeState = element.FadeState == 1 ? -1 : 1;
    element.FadeTimeLeft = timeToFade - element.FadeTimeLeft;
  } else {
    element.FadeState = element.FadeState == 2 ? -1 : 1;
    element.FadeTimeLeft = timeToFade;
    setTimeout("animateFade(" + new Date().getTime() + ",'" + elementId + "', " + timeToFade + ")", 33);
  }
}

function animateFade(lastTick, elementId, timeToFade) {
  if (!timeToFade) timeToFade = 1000.0;
  var curTick = new Date().getTime();
  var elapsedTicks = curTick - lastTick;

  var element = document.getElementById(elementId);

  if (element.FadeTimeLeft <= elapsedTicks) {
    element.style.opacity = element.FadeState == 1 ? '1' : '0';
    element.style.filter = 'alpha(opacity = '
                             + (element.FadeState == 1 ? '100' : '0') + ')';
    element.FadeState = element.FadeState == 1 ? 2 : -2;
    return;
  }

  element.FadeTimeLeft -= elapsedTicks;
  var newOpVal = element.FadeTimeLeft / timeToFade;
  if (element.FadeState == 1)        newOpVal = 1 - newOpVal;

  element.style.opacity = newOpVal;
  element.style.filter = 'alpha(opacity = ' + (newOpVal * 100) + ')';


  setTimeout("animateFade(" + curTick + ",'" + elementId + "')", 33);
}

function insertArgument(text, arg0, arg1, arg2, arg3) {
  for (var i = 1; i < arguments.length; i++) {
    text = text.split('{' + (i - 1) + '}').join(arguments[i]);
  }
  return text;
}

function setZIndex(elementId, zIndex) {
  var e = getObj(elementId);
  if (e) e.style.zIndex = zIndex;
}

function exceptionToString(e) {
  if (e.description) {
    return e.description;
  }
  else {
    if (e.toString) {
      return e.toString();
    }
    else {
      return e;
    }
  }
}

function elementExists(elementId) {
  return getObj(elementId) ? true : false;
}

function enableOptionDisabledInIE(select) {
  window.select_current = [];
  $(select).focus(function() {
    window.select_current[this.id] = this.selectedIndex;
  });
  /*
   select.onfocus = function() {
   window.select_current[this.id] = this.selectedIndex;
   };
   */
  $(select).click(function() {
    restoreOptionDisable(this);
  });
  /*select.onchange = function() {
   restoreOptionDisable(this);
   };*/
  emulateOptionDisable(select);
}

function restoreOptionDisable(e) {
  if (e.options[e.selectedIndex].disabled) {
    e.selectedIndex = window.select_current[e.id];
  }
}

function emulateOptionDisable(e) {
  for (var i = 0, option; option = e.options[i]; i++) {
    var color;
    if (option.disabled) {
      color = "graytext";
    } else {
      color = "menutext";
    }
    option.style.color = color;
  }
}

function dateStringToDateObject(dateString) {
  if (!dateString) {
    return null;
  } else {
    var arr = dateString.split("-");
    return dateArrayToDateObject(arr);
  }
}

function dateArrayToDateObject(dateArray) {
  return new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
}

function _isDecSepa(c) {
  return c == '.' || c == ',';
}

function _decCharIdxOf(s, c) {
  var i = s.indexOf(c);
  if (i == -1) {
    if (c == ',') {
      i = s.indexOf('.');
    } else {
      if (c == '.') i = s.indexOf(',');
    }
  }
  return i;
}

function formatNumber(number, format) {
  format = $.extend({
                      decimals:2, // Will force this amount of decimals. Will round the number to closest if number has more decimals than specified.
                      groupSize:3, // The numbers are separated into groups. Ex: 1 000 000 SEK instead of 1000000 SEK
                      decimalCharacter:".",
                      groupSeparator:" ",
                      prefix:"",
                      suffix:"",
                      formatString:undefined, // Ex: $${0}, or ${0} €. This formatting is applied last.
                      usePositiveSign:false
                    }, format);
  validateArgument(number, {rules:{allowUndefined:false, requireNumber:true}}, "number");
  var isNegative = number < 0;
  number = Math.abs(number);
  var numberAsInteger = Math.floor(number);

  // Create decimals
  var decimalPart = number - numberAsInteger;
  var decimalsAsString = decimalPart.toFixed(format.decimals).split(".")[1];
  var useDecimals = format.decimals > 0 && decimalsAsString;

  // Create the groups.
  var groups = [];
  var s = numberAsInteger.toString();
  while (s) {
    groups.push(s.getLast(format.groupSize));
    s = s.removeLast(format.groupSize);
  }

  // Assemble the result
  var result = (format.prefix ? format.prefix : "") + (isNegative ? "-" : (format.usePositiveSign ? "+" : "")) + groups.reverse().join(format.groupSeparator) + (useDecimals ? format.decimalCharacter + decimalsAsString : "") + (format.suffix ? format.suffix : "");
  return format.formatString ? insertArgument(format.formatString, result) : result;
}

var PriceFormat = {};
PriceFormat["us"] = {decimalCharacter:".", groupSeparator:",", prefix:"$"};
PriceFormat["se"] = {decimalCharacter:",", suffix:":-"};
PriceFormat["fi"] = {decimalCharacter:",", suffix:" €"};

/**
 * Wrapper method for old fNum. Do not use this, is only for backward compatibility. Use formatNumber instead.
 * @param number
 * @param decimals
 * @param decimalSepa
 * @param formatString
 * @param prefixPositiveValueWithPlusChar
 */
function fNum(number, decimals, decimalSepa, formatString, prefixPositiveValueWithPlusChar) {
  var format = {
    decimals:decimals,
    decimalCharacter:decimalSepa,
    formatString:UiText.get(formatString),
    usePositiveSign:prefixPositiveValueWithPlusChar
  };

  return formatNumber(number, format);
}

function fEnc(s) {
  return parseInt(Math.round(new Number(s).valueOf() * 1000).toString()).toString(16);
}

function fDec(s) {
  return (new Number(parseInt(s, 16) / 1000).valueOf());
}

function iEnc(s) {
  return parseInt(s).toString(16);
}

function iDec(s) {
  return parseInt(s, 16);
}

var EFloat = function(s) {
  this.s = s;
  this.val = typeof s == 'string' ? fDec(s) : s;
};

var EInt = function(s) {
  this.s = s;
  this.val = typeof s == 'string' ? iDec(s) : s;
};

function isTrue(s) {
  return (s && ('' + s) == 'true') || s === true;
}

var BrowserDetect = {
  init:function() {
    this.browser = this.searchString(this.dataBrowser) || "An unknown browser";
    this.version = this.searchVersion(navigator.userAgent)
                     || this.searchVersion(navigator.appVersion)
      || "an unknown version";
    this.OS = this.searchString(this.dataOS) || "an unknown OS";
  },
  searchString:function(data) {
    for (var i = 0; i < data.length; i++) {
      var dataString = data[i].string;
      var dataProp = data[i].prop;
      this.versionSearchString = data[i].versionSearch || data[i].identity;
      if (dataString) {
        if (dataString.indexOf(data[i].subString) != -1) {
          return data[i].identity;
        }
      }
      else {
        if (dataProp) {
          return data[i].identity;
        }
      }
    }
  },
  searchVersion:function(dataString) {
    var index = dataString.indexOf(this.versionSearchString);
    if (index == -1) return;
    return parseFloat(dataString.substring(index + this.versionSearchString.length + 1));
  },
  dataBrowser:[
    {
      string:navigator.userAgent,
      subString:"Chrome",
      identity:"Chrome"
    },
    {                 string:navigator.userAgent,
      subString:"OmniWeb",
      versionSearch:"OmniWeb/",
      identity:"OmniWeb"
    },
    {
      string:navigator.vendor,
      subString:"Apple",
      identity:"Safari",
      versionSearch:"Version"
    },
    {
      prop:window.opera,
      identity:"Opera"
    },
    {
      string:navigator.vendor,
      subString:"iCab",
      identity:"iCab"
    },
    {
      string:navigator.vendor,
      subString:"KDE",
      identity:"Konqueror"
    },
    {
      string:navigator.userAgent,
      subString:"Firefox",
      identity:"Firefox"
    },
    {
      string:navigator.vendor,
      subString:"Camino",
      identity:"Camino"
    },
    {                                // for newer Netscapes (6+)
      string:navigator.userAgent,
      subString:"Netscape",
      identity:"Netscape"
    },
    {
      string:navigator.userAgent,
      subString:"MSIE",
      identity:"Explorer",
      versionSearch:"MSIE"
    },
    {
      string:navigator.userAgent,
      subString:"Gecko",
      identity:"Mozilla",
      versionSearch:"rv"
    },
    {                                 // for older Netscapes (4-)
      string:navigator.userAgent,
      subString:"Mozilla",
      identity:"Netscape",
      versionSearch:"Mozilla"
    }
  ],
  dataOS:[
    {
      string:navigator.platform,
      subString:"Win",
      identity:"Windows"
    },
    {
      string:navigator.platform,
      subString:"Mac",
      identity:"Mac"
    },
    {
      string:navigator.userAgent,
      subString:"iPhone",
      identity:"iPhone/iPod"
    },
    {
      string:navigator.platform,
      subString:"Linux",
      identity:"Linux"
    }
  ]

};
BrowserDetect.init();

function browserIsIE() {
  return BrowserDetect.browser == "Explorer";
}

function browserIsIE9() {
  return navigator.userAgent.indexOf("Trident/5") > -1;
}

function browserIsIE9OrLater() {
  return $.browser.msie && parseInt($.browser.version) >= 9;
}

function isBadBrowser() {
  var b = BrowserDetect.browser;
  var v = BrowserDetect.version;
  var r = false;
  if (b == "Explorer") {
    r = v < 9;
  }
  return r;
}

function measureBrowserSpeed() {
  if (typeof ibeBenchmark == "function") {
    var t = new Timer("benchmark");
    t.start();
    ibeBenchmark();
    t.stop();
    return t.result();
  } else {
    ibewarning("Tried to measure browser speed, but benchmark library has not been included.");
    return undefined;
  }
}

var __isSlowBrowser = {};

function isSlowBrowser() {
  if (!__isSlowBrowser.result) {
    __isSlowBrowser.result = measureBrowserSpeed();
  }
  return  __isSlowBrowser.result > 100;
}

var __scheduler = {};

/**
 * This function works like runSoon, except you give the timer a name, and you can then overwrite it. Used in XS filter,
 * when clicking on any fields, the timer starts, if nothing happens in 1 sec, a search is triggered. If a new field
 * is interacted with, the timer is aborted and a new timer is triggered.
 * Arguments: (name: type [default value])
 * overwrite: true/false [true], if false, already existing timers will not be overwritten.
 * name: string ["default"], the name of the timer. Use same name again to overwrite the first timer.
 * time: int [10], the time in milliseconds before the function is triggered.
 * @param func
 * @param args
 */
function schedule(func, args) {
  args = $.extend({
                    overwrite:true,
                    name:"default",
                    time:10
                  }, args);
  var name = args.name;
  var time = args.time;
  __scheduler[name] = {};
  var now = new Date();
  var targetTime = now.getTime() + time;
  if (args.overwrite || __scheduler[name].targetTime === undefined) {
    __scheduler[name].targetTime = targetTime;
    __scheduler[name].func = func;
    setTimeout(function() {
      if (__scheduler[name].targetTime == targetTime) {
        // Nothing has changed, just run it. Otherwise other setTimeout will trigger the function.
        __scheduler[name].func();
        // Reset the scheduled item
        __scheduler[name].targetTime = undefined;
        __scheduler[name].func = undefined;
      }
    }, time);
  }
}

function sendKeyEvent(eventType, characterToPress, targetElement) {
  var evObj = undefined;
  if (window.KeyEvent) {
    evObj = document.createEvent('KeyEvents');
    evObj.initKeyEvent(eventType, true, true, window, false, false, false, false, characterToPress, 0);
    console.log('KeyEvent');
  } else {
    console.log('UIEvent');
    evObj = document.createEvent('UIEvents');
    evObj.initUIEvent(eventType, true, true, window, 1);
    evObj.keyCode = characterToPress;
  }
  targetElement.dispatchEvent(evObj);
}

function addTooltipToDomIfNotExists(className, id) {
  if (!getObj(id)) {
    var div = document.createElement("div");
    div.id = id;
    div.className = className;
    document.body.appendChild(div);
  }
}

function overlib(text, element, className, effect) {
  if (!className) className = "overlibTooltip";
  if (!effect) effect = "slide";
  $(element).attr("title", text);
  $(element).tooltip({
                       tipClass:className,
                       effect:effect
                     });
  $(element).removeAttr("title");
  // Show it immediately.
  var api = $(element).data("tooltip");
  api.show();
}

function nd(element) {
  $(element).tooltip("destroy");
}

function setOverLibStartHeight(h) {
  if (stringIsNumeric(h)) {
    // Set height
  } else {
    if (h.toLowerCase().trim() == "auto") {

    } else {
      ibewarning("Invalid overlib start height set: " + h);
    }
  }
}

function copyObject(sourceObject, targetObject) {
  if (targetObject === undefined) targetObject = {};
  for (prop in sourceObject) {
    var t = typeof(sourceObject[prop]);
    if (t === "string" || t === "number" || t === "undefined" || t === "boolean" || t === "object") {
      // Allow undefined properties, they don't do any harm.
      targetObject[prop] = sourceObject[prop];
    } else {
      // No warning message for functions.
      if (t !== "function") ibewarning("Cloning object, skipping property '" + prop + '", invalid type. Typeof=' + t);
    }
  }
  return targetObject;
}

function sendRequest(url, successFunction, failFunction, args) {
  args = $.extend({forceFail:false}, args);

  var processManager = args.processManager;

  if (args.forceFail) {
    if (typeof failFunction === "function") {
      runSoon(function() {
        failFunction();
      }, 1000);
    }
  } else {
    var p = new IBERequestProcess(url, successFunction, failFunction, args);

    if (processManager) {
      processManager.addProcess(p);
    } else {
      p.startProcess();
    }
  }
}


function sendPostRequest(url, postData, successFunction, failFunction, args) {
  args = $.extend({}, args);
  args.method = "post";
  if (postData) args.postData = postData;
  sendRequest(url, successFunction, failFunction, args);
}

function sendFormRequest(form, successFunction, failFunction, args) {
  //function sendFormRequest(form, successFunction, failFunction, customAction, customObject, loadAnimation, forceAutoMessage, disableAlerts) {
  if (args === undefined) args = {};
  if (!args.customAction) args.customAction = form.action;
  var postData = $(form).serialize();
  sendPostRequest(args.customAction, postData, successFunction, failFunction, args);
}

/**
 * A request which uses the same mechanics as sendRequest, etc. Which means that callback method receives a predefined object with message, resultType, model, etc.
 * @param url
 * @param successFunction
 * @param failureFunction
 * @param args
 */
function IBERequestProcess(url, successFunction, failureFunction, args) {
  this.args = args = $.extend({
                                customObject:undefined, // Used to pass data to callback functions.
                                resultReference:undefined, // If set, response model properties will be copied to this object.
                                method:"get"
                              }, args);
  validateArgument(args.method, {rules:{allowUndefined:false, requireValueIsOneOf:["post", "get"]}}, "args.method");

  this.postData = args.postData;
  this.method = args.method;
  this.successFunction = successFunction;
  this.failureFunction = failureFunction;
  this.cancelFunction = args.cancelFunction;
  this.resultReference = args.resultReference;
  this.isAllowedToCancel = args.isAllowedToCancel || args.isAllowedToCancel === undefined ? true : false;
  this.customObject = args.customObject;
  this.forceAutoMessage = args.forceAutoMessage;
  this.disableAlerts = args.disableAlerts;
  this.id = args.id;

  this.isRunning = false;
  this.isCompleted = false;
  this.isCancelled = false;
  this.evaluateResponse = false;

  // Is aware of its manager, so it can callback when it is finished. These values are set by the manager when the process is added.
  this.parentManager = undefined;
  this.processIndex = undefined;

  this.startProcess = function() {
    if (this.isRunning === false) {
      this.isRunning = true;
      this.isCompleted = false;
      this.isCancelled = false;

      var that = this;
      $.ajax({
               url:url,
               dataType:'json',
               data:this.postData,
               type:this.method.toUpperCase(),
               success:function(response, textStatus, XMLHttpRequest) {
                 var p = that;
                 if (response == null) {
                   ibeerror("Trying to execute RPC, but got null as response. Connection failed? URL: " + url);
                 }
                 if (!p.isCancelled) {
                   p.isCompleted = true;
                   p.isRunning = false;
                   if (p.resultReference && response.model) {
                     copyObject(response.model, p.resultReference);
                   }

                   // Start callback to manager first, new processes are triggered and HTTP request will run while more callbacks are executed.
                   if (p.parentManager && p.parentManager.processDoneCallback) {
                     p.parentManager.processDoneCallback(p);
                   }
                   // Do rest

                   if (p.forceAutoMessage === true && response.message) ibealert(response.message, !p.disableAlerts);

                   if (response && (response.resultType === 'OK' || response.resultType === 'SUCCESS')) {
                     if (!p.forceAutoMessage) ibealert(response.message, !p.disableAlerts);
                     p.successFunction.apply(p, [response, p.customObject]);
                   } else {
                     // Not "OK", exception in server request
                     if (response == null) {
                       var message = 'Could not connect to server, please try again later. If the problem persists, please contact us.';
                       response = {message:message};
                     }
                     if (p.failureFunction && typeof p.failureFunction == 'function') {
                       p.failureFunction.apply(p, [response, p.customObject]);
                     } else {
                       ibewarning("RPC request failed: " + url);
                       if (!p.forceAutoMessage) ibealert(response.message, p.disableAlerts !== true);
                     }
                   }
                 }
               },
               error:function(request, textStatus, error) {
                 var p = that;
                 // Invalid JSON received, there is no response or response.message.
                 if (!p.isCancelled) {
                   p.isCompleted = true;
                   p.isRunning = false;
                   // Start callback to manager first, new processes are triggered and HTTP request will run while more callbacks are executed.
                   if (p.parentManager && p.parentManager.processDoneCallback) {
                     p.parentManager.processDoneCallback(p);
                   }

                   var message = 'An internal system error occurred, please try again later. If the problem persists, please contact us.';
                   var response = {message:message};
                   if (p.failureFunction && typeof p.failureFunction === 'function') {
                     p.failureFunction.apply(p, [response, p.customObject, error]);
                   } else {
                     ibewarning("RPC connection failed: " + url);
                     ibealert(message, !p.disableAlerts);
                   }
                 }
               }
             });
    }
  };

  this.cancelProcess = function() {
    if (this.isRunning) {
      //ibelogs("this.cancelProcess");
      if (this.isAllowedToCancel) {
        this.isRunning = false;
        this.isCancelled = true;
        // Start callback to manager first, new processes are triggered and HTTP request will run while more callbacks are executed.
        if (this.parentManager && this.parentManager.processDoneCallback) {
          this.parentManager.processDoneCallback(this);
        }
        if (this.cancelFunction) {
          this.cancelFunction.apply(this.callbackScope ? this.callbackScope : this, [o, this, this.customObject]);
        }
      } else {
        ibewarning("Trying to cancel request process, but cancel is not allowed.");
      }
    }
    this.isCompleted = false;
  };
}

/**
 * A barebone process. Doesn't do anything with the response from the server.
 * @param url
 * @param resultReference
 * @param successCallbackFunction
 * @param failureCallbackFunction
 * @param cancelCallbackFunction
 * @param parameters
 * @param method
 * @param callbackScope
 * @param parentObject
 */
function IBEProcess(url, resultReference, successCallbackFunction, failureCallbackFunction, cancelCallbackFunction,
                    parameters, method, callbackScope, parentObject) {

  this.url = url;
  this.parameters = parameters;
  this.method = method ? method : 'GET';
  this.successFunction = successCallbackFunction;
  this.failureFunction = failureCallbackFunction;
  this.cancelFunction = cancelCallbackFunction;
  this.callbackScope = callbackScope;
  this.resultReference = resultReference;
  this.isRunning = false;
  this.isCompleted = false;
  this.isCancelled = false;
  this.parentObject = parentObject;
  this.evaluateResponse = false;

  // Is aware of its manager, so it can callback when it is finished.
  this.parentManager = undefined;
  this.processIndex = undefined;

  this.copyToReference = function(o) {
    if (this.resultReference) {
      if (this.evaluateResponse) {
        try {
          var res = eval(o.responseText);
          for (var prop in res) {
            this.resultReference[prop] = res[prop];
          }
        } catch (e) {
          ibeerror('Unable to evaluate and store process JSON response in reference object.');
          ibelogs(e);
        }
      } else {
        this.resultReference.responseText = o.responseText;
        this.resultReference.responseXML = o.responseXML;
        this.resultReference.statusText = o.responseXML;
        this.resultReference.argument = o.argument;
        this.resultReference.getAllResponseHeaders = o.getAllResponseHeaders;
        this.resultReference.getResponseHeader = o.getResponseHeader;
        this.resultReference.statusText = o.statusText;
        this.resultReference.status = o.status;
        this.resultReference.tId = o.tId;
      }
    }

  };

  this.localSuccess = function(o) {
    if (!this.isCancelled) {
      this.isCompleted = true;
      this.isRunning = false;
      this.copyToReference(o);
      // Start callback to manager first, new processes are triggered and HTTP request will run while more callbacks are executed.
      if (this.parentManager && this.parentManager.processDoneCallback) {
        this.parentManager.processDoneCallback(this);
      }
      // Run external callback last.
      if (this.successFunction) {
        this.successFunction.apply(this.callbackScope ? this.callbackScope : this, [o, this, this.parentObject]);
      }
    }
  };

  this.localFailure = function(o) {
    if (!this.isCancelled) {
      this.isCompleted = true;
      this.isRunning = false;
      // Start callback to manager first, new processes are triggered and HTTP request will run while more callbacks are executed.
      if (this.parentManager && this.parentManager.processDoneCallback) {
        this.parentManager.processDoneCallback(this);
      }
      if (this.failureFunction) {
        this.failureFunction.apply(this.callbackScope ? this.callbackScope : this, [o, this, parentObject]);
      }
    }
  };

  this.localCancel = function(o) {
    if (this.isRunning) {
      this.isRunning = false;
      this.isCancelled = true;
      // Start callback to manager first, new processes are triggered and HTTP request will run while more callbacks are executed.
      if (this.parentManager && this.parentManager.processDoneCallback) {
        this.parentManager.processDoneCallback(this);
      }
      if (this.cancelFunction) {
        this.cancelFunction.apply(this.callbackScope ? this.callbackScope : this, [o, this, parentObject]);
      }
    }
  };

  this.startProcess = function() {
    if (this.isRunning === false) {
      var u = this.url;
      if (this.method == 'GET') {
        var time = new Date().getTime();
        var key = time / 15;
        u += (((u.indexOf('?') == -1) ? '?' : '&') + ('b' + key + '=' + time));
      }
      this.isRunning = true;
      this.isCompleted = false;
      this.isCancelled = false;
      YAHOO.util.Connect.asyncRequest(this.method, u, {
        success:this.localSuccess,
        failure:this.localFailure,
        scope:this
      }, this.method == 'POST' ? this.parameters : null);
    }
  };

  this.cancelProcess = function() {
    if (this.isRunning) {
      this.localCancel();
    }
    this.isCompleted = false;
  };
}

function IBEProcessManager(pmode, allDoneCallback, callbackScope) {

  /**
   * Mode:
   * "pushover" = add process and all current processes are cancelled and removed. Only one is allowed.
   * "allowone" = add process only if no process is running.
   * "seq" = one at the time, in priority order
   * "allowall" = all processes can be run freely and independently. They will run directly when added.
   *
   * default: allowall
   */

  this.possibleModes = ["allowall", "pushover", "allowone", "seq"];
  this.mode = pmode ? pmode : this.possibleModes[0];
  if (!this.possibleModes.contains(this.mode)) {
    ibewarning("Creating process manager, but the selected mode (" + this.mode + ") is not valid. Use only " + this.possibleModes.join("/") + ".");
  }
  this.queueMaxSize = 10;
  this.processesRunning = 0;
  this.queueFullAction = 'discard'; // 'discard' or 'expand', defaults to discard
  this.allDoneCallback = allDoneCallback;
  this.callbackScope = callbackScope;

  // private:
  this.processQueue = new Array();
  this.priorityList = new Array();

  this.addProcessList = function(list) {
    var i;
    for (i = 0; i < list.length; i++) this.addProcess(list[i], undefined, true);
    for (i = 0; i < list.length; i++) this.triggerScheduler(); // Trigger all of them, AFTER adding them.
  };

  this.addProcess = function(process, priority, disableTrigger) {
    if (this.processQueue.length >= this.queueMaxSize) {
      if (this.queueFullAction == 'expand') {
        this.queueMaxSize *= 2; // Double the size of the queue.
      } else {
        // Discard the process
        return;
      }
    }
    if (priority === undefined) {
      priority = 0; // if not specified, no priority!
    }
    if (this.mode == 'pushover') {
      this.clearQueue();
    } else {
      if (this.mode == 'allowone' && this.processesRunning > 0) {
        return; // Only one process allowed, and it is already running. Can be implemented with queueMaxSize as well..
      }
    }
    var i = this.findAvailableProcessIndex();
    this.processQueue[i] = process;
    this.priorityList[i] = priority;
    process.processIndex = i;
    process.parentManager = this;
    if (disableTrigger === undefined || disableTrigger === false) {
      this.triggerScheduler();
    }
    return i;
  };

  this.triggerScheduler = function() {
    var i = this.findHighestPriorityProcessIndex();
    if (this.mode == 'pushover' ||
        this.mode == 'allowall' ||
        (this.mode == 'allowone' && this.processesRunning < 1) ||
        (this.mode == 'seq' && this.processesRunning < 1)) {
      if (i >= 0) {
        this.processesRunning++;
        this.processQueue[i].startProcess();
      }
    }
  };

  this.findHighestPriorityProcessIndex = function() {
    var running = 0; // count running processes at the same time.
    var highestPrio = -1;
    var highestPrioIndex = -1;
    for (var i = 0; i < this.processQueue.length; i++) {
      if (this.processQueue[i]) {
        if (this.processQueue[i].isRunning) {
          running++;
        } else {
          if (this.priorityList[i] > highestPrio) {
            highestPrioIndex = i;
            highestPrio = this.priorityList[i];
          }
        }
      }
    }
    this.processesRunning = running;
    return highestPrioIndex;
  };

  this.findAvailableProcessIndex = function() {
    for (var i = 0; i < this.processQueue.length; i++) {
      if (this.processQueue[i] === undefined) {
        break;
      }
    }
    return i;
  };

  this.removeProcess = function(i) {
    if (this.processQueue !== undefined) {
      if (this.processQueue[i] !== undefined) {
        if (this.processQueue[i].isRunning) {
          this.processesRunning--;
        }
        this.processQueue[i].cancelProcess();
      }

      this.processQueue.splice(i, 1); // remove process from queue, length will decrease.
      // Update index of all processes
      for (; i < this.processQueue.length; i++) {
        this.processQueue[i].processIndex = i;
      }
      return true;
    }
    return false;
  };

  this.cancelAllProcesses = function() {
    for (var i = 0, l = this.processQueue.length; i < l; i++) {
      this.cancelProcessWithIndex(i);
    }
  };

  /**
   * Cancels the one process with given index.
   * @param i
   */
  this.cancelProcessWithIndex = function(i) {
    if (i && this.processQueue) {
      if (this.processQueue[i].isRunning) {
        this.processQueue[i].cancelProcess();
        this.processesRunning--;
      }
    }
  };

  /**
   * Cancels all processes with a given id.
   * @param id
   */
  this.cancelProcessWithId = function(id) {
    if (id && this.processQueue) {
      for (var i = 0, l = this.processQueue.length; i < l; i++) {
        var p = this.processQueue[i];
        if (p.isRunning && p.id === id) {
          p.cancelProcess();
          this.processesRunning--;
          break;
        }
      }
    }
  };

  /**
   * Returns the first process found with given id. If none is found, null is returned.
   * @param id
   */
  this.getProcessWithId = function(id) {
    if (this.processQueue) {
      for (var i = 0, l = this.processQueue.length; i < l; i++) {
        var p = this.processQueue[i];
        if (p.isRunning && p.id === id) {
          return p;
        }
      }
    }
    return null;
  };

  this.clearQueue = function() {
    for (var i = 0, l = this.processQueue.length; i < l; i++) {
      this.removeProcess(i);
    }
  };

  this.clearIdleProcesses = function() {
    for (var i = 0, l = this.processQueue.length; i < l; i++) {
      if (this.processQueue[i]) {
        if (this.processQueue[i].isRunning) {
          if (this.removeProcess(i)) {
            i--;
          }
        }
      }
    }
  };

  this.processDoneCallback = function(process) {
    this.processesRunning--;
    this.removeProcess(process.processIndex);

    if (this.processQueue.length === 0) {
      if (typeof this.allDoneCallback == 'function') {
        this.allDoneCallback();
      }
    } else {
      this.triggerScheduler();
    }

  };
}

/**
 * Takes all fields of a form and assembles them into a GET formatted string with all variable names and values.
 * Example: id=35&name=mattias&age=12
 * @param form The form to use.
 */
function formParametersToStringFormat(form) {
  if (!form) {
    ibeerror('Form element is null.');
    return null;
  }
  var parameters = new RequestParameters({form:form});
  return parameters.toString();
}

function RequestParameters(args) {
  args = $.extend({
                    form:undefined,
                    includeEmptyParameterValues:true
                  }, args);

  this.parameters = [];

  this.addParameter = function(parameterName, parameterValue, _args) {
    _args = $.extend({allowMoreThanOneValue:false}, _args);
    if (!_args.allowMoreThanOneValue) {
      this.removeParameter(parameterName);
    }
    this.pushParameter(parameterName, parameterValue);
  };

  this.addParameterObject = function(obj, _args) {
    var that = this;
    $.each(obj, function(key, member) {
      var t = typeof member;
      if (t === "string" || t === "number") that.addParameter(key, member, _args);
    });
  };

  this.addListParameter = function(parameterName, parameterValue) {
    this.removeParameter(parameterName);
    if (parameterValue && parameterValue.length) {
      for (i = 0; i < parameterValue.length; i++) {
        this.pushParameter(parameterName, parameterValue[i]);
      }
    }
  };

  this.pushParameter = function(parameterName, parameterValue) {
    this.parameters.push({name:parameterName, value:parameterValue});
  };

  this.mergeParameterObject = function(parameterObject) {
    for (var i = 0; i < parameterObject.parameters.length; i++) {
      this.parameters.push(parameterObject.parameters[i]);
    }
  };

  this.getParameter = function(parameterName) {
    for (var i = 0; i < this.parameters.length; i++) {
      if (this.parameters[i].name == parameterName) return this.parameters[i];
    }
  };

  this.getParameters = function(parameterName) {
    var params = [];
    for (var i = 0, numParams = this.parameters.length; i < numParams; i++) {
      var parameter = this.parameters[i];
      if (parameter.name === parameterName) {
        params.push(parameter);
      }
    }
    return params;
  };

  this.removeParameter = function(parameterName) {
    for (var i = 0; i < this.parameters.length; i++) {
      // Might cause a problem if there are more parameters of the same name, which is fully possible
      if (this.parameters[i].name == parameterName) return this.parameters.splice(i, 1);
    }
  };

  this.removeParameterWithValue = function(parameterName, parameterValue) {
    for (var i = 0; i < this.parameters.length; i++) {
      // Might cause a problem if there are more parameters of the same name, which is fully possible
      if (this.parameters[i].name == parameterName && this.parameters[i].value == parameterValue) return this.parameters.splice(i, 1);
    }
  };

  this.getParameterValue = function(parameterName) {
    var p = this.getParameter(parameterName);
    return p ? p.value : null;
  };

  this._addArrayReadyObject = function(obj) {
    this.parameters.push(obj);
  };

  this.clone = function() {
    var i = 0, size = this.parameters.length, cloned = new RequestParameters();
    for (; i < size; i++) {
      // not a perfect clone (need to deep copy really - assuming users will replace parameters). see if this enough.
      cloned._addArrayReadyObject(this.parameters[i]);
    }
    return cloned;
  };

  if (args.form) {
    var es = args.form.elements;
    if (nullOrUndefined(es)) {
      ibeerror('No elements attribute in formElement with id=' + args.form.id);
      return;
    }
    for (var i = 0; i < es.length; i++) {
      var inputElement = es[i], value = inputElement.value;
      if (!args.includeEmptyParameterValues && value === "") {
        continue;
      }
      if ((inputElement.type == "radio" || inputElement.type == "checkbox")) {
        if (inputElement.checked) this.pushParameter(inputElement.name, value);
      } else {
        this.pushParameter(inputElement.name, value);
      }
    }
  }

}

RequestParameters.prototype.toString = function() {
  var output = [];
  for (var i = 0, length = this.parameters.length; i < length; i++) {
    var parameter = this.parameters[i], value = parameter.value;
    if (value && (typeof value === "string" || typeof value === "double" || typeof value === "float")) {
      value = encodeURIComponent(value);
    }
    if (value && value !== 'undefined') {
      output.push(parameter.name + "=" + value);
    }
  }
  return output.join("&");
};

function isValidHtml(html, args) {
  args = $.extend({}, args);
  var r = validateHtml(html, args);
  return r.ok;
}


/**
 * Validates a string containing HTML code. You can specify some options and it returns an object with the result.
 *
 * Result:
 * result.ok = true or false, true if there are no criticals or errors. warnings are allowed.
 * result.perfect = true or false, true if the html was perfectly formed.
 * result.warnings = list of strings containing the warning
 * result.errors = list of strings containing the errors
 * result.criticals = list of strings containing critical errors. these are errors that may harm the system and should prevent the user from storing the html.
 * result.hasWarnings
 * result.hasErrors
 * result.hasCriticals
 *
 * The options object can contain the following:
 * removeScriptTags = true or false, if true, all script tags will be removed (requires the tag to be lower case!)
 * allowHtmlTag = true or false, if true, fails validation if it finds a html tag.
 * allowBodyTag = true or false, if true, fails validation if it finds a body tag.
 * allowTitleTag = true or false, if true, fails validation if it finds a title tag.
 * allowMso = true or false, if false, fails validation if it finds "if gte mso".
 * allowWordDocumentTag = true or false, if false, fails validation if it finds a <w:WordDocument> tag.
 * allowXmlTag = true or false, if false, fails validation if it finds <xml> tag, which is used by Microsoft Word.
 * allowHtmlComments = true or false
 * requireCorrectTables = true or false, if true
 *
 * @param html
 * @param args
 */
function validateHtml(html, args) {


  // TODO: <!-- måste den ju klara!


  args = $.extend({
                    allowMso:false,
                    allowWordDocumentTag:false,
                    allowHtmlComments:true
                  }, args);
  var i;

  if (args.removeScriptTags) {
    var jsSplit = html.splitTwice('<script type="text/javascript">', '</script>');
    html = '';
    for (i = 0; i < jsSplit.length; i += 2) {
      html += jsSplit[i];
    }
  }

  var stack = new Array();
  var result = new Object();
  result.warnings = new Array();
  result.errors = new Array();
  result.criticals = new Array();
  result.ok = true;
  result.saveAllowed = true;

  var split = html.splitTwice("<", ">");
  result.containsHtml = split.length > 1;

  var WARN_ON = ['class="MsoNormal"',
                 'mso-ansi-language',
                 'seolinx-',
                 'addthis_toolbox',
                 '/favicon.ico'];
  for (i = 0; i < WARN_ON.length; i++) {
    if (html.indexOf(WARN_ON[i]) >= 0) {
      result.warnings.push('Text contains "' + WARN_ON[i] + '" which is undesired or disallowed');
    }
  }

  for (i = 1; i < split.length; i += 2) {
    var tag = split[i].toLowerCase();
    var type = tag.split(" ")[0]; // Remove all after first space
    if (tag.startsWith("/")) {
      if (stack.length <= 0) {
        result.errors.push("Incomplete HTML, not enough closing tags.");
        break;
      }
      type = type.substring(1);
      var peek = stack[stack.length - 1];
      while (peek !== type && stack.length > 0) {
        stack.pop();
        checkCloseRequirement(peek, args, result);
        peek = stack[stack.length - 1];
      }
      if (stack.length == 0) {
        result.errors.push("Trying to close " + type + " tag, but there is no matching start tag.");
        break;
      }
      stack.pop();
    } else {
      if (tag.endsWith("/")) {
        // Do nothing, it closes itself.
      } else {
        // Start tag
        var error = false;
        error = (type == "html" && args.allowHtmlTag === false) || error;
        error = (type == "body" && args.allowBodyTag === false) || error;
        error = (type == "title" && args.allowTitleTag === false) || error;
        error = (type == "w:WordDocument" && args.allowWordDocumentTag === false) || error;
        error = (type == "xml" && args.allowXmlTag === false) || error;
        if (error) {
          result.errors.push("Found " + type + " tag, " + type + " tag not allowed.");
          break;
        }

        stack.push(type);
      }
    }
  }

  while (stack.length > 0) {
    checkCloseRequirement(stack.pop(), args, result);
  }

  if (args.allowMso && html.contains("if gte mso")) {
    result.errors.push("Found 'if gte mso' from Microsoft Office 2000 which is not allowed.");
  }

  result.hasErrors = result.errors.length != 0;
  result.hasWarnings = result.warnings.length != 0;
  result.hasCriticals = result.criticals.length != 0;
  result.saveAllowed = result.criticals.length == 0;
  result.ok = !result.hasErrors && !result.hasCriticals;
  result.perfect = !result.hasErrors && !result.hasCriticals && !result.hasWarnings;
  return result;
}

function checkCloseRequirement(type, options, result) {
  var target = result.errors;
  switch (type) {
    case "table":
    case "tr":
    case "td":
      if (options.requireCorrectTables) {
        target = result.criticals;
      }
      break;
    case "br":
    case "img":
      target = result.warnings;
      break;
    default:

  }
  var msg = type + " tag is not closed properly.";
  for (i = 0; i < target.length; i++) {
    if (target[i] == msg) return;
  }
  target.push(msg);
}

function renderHtmlValidationResult(result) {

  if (!result.containsHtml) return "";

  // Icon
  var color = "#55dd55";
  if (result.hasWarnings) color = "yellow";
  if (result.hasErrors) color = "#ff5555";
  if (result.hasCriticals) color = "#ff9999";
  var icon = '<div style="background-color:' + color + '; height:12px;width:12px;">&nbsp;</div>';

  // Text
  var text = "";
  if (result.perfect) {
    text += "HTML looks OK!";
  }
  else {
    if (result.hasCriticals) {
      text += "HTML is in <b>critical</b> state. You should <b>NOT save</b>! Saving might cause problems in the system and might not easily be fixed.";
    }
    else {
      if (result.hasErrors) {
        text += "HTML contains errors. You really should fix them before saving, saving now might mess up how the site looks.";
      }
      else {
        if (result.hasWarnings) text += "HTML contains warnings. Don't worry, it should be OK to save but you should fix the problems so that there is no bad HTML.";
      }
    }
  }

  var html = '<table>';
  var i;
  html += '<tr><td>' + icon + '</td><td>' + text + '</td></tr>';
  for (i = 0; i < result.warnings.length; i++) html += '<tr><td>&nbsp;</td><td><b>Warning:</b> ' + result.warnings[i] + '</td></tr>';
  for (i = 0; i < result.errors.length; i++) html += '<tr><td>&nbsp;</td><td><b>Error:</b> ' + result.errors[i] + '</td></tr>';
  for (i = 0; i < result.criticals.length; i++) html += '<tr><td>&nbsp;</td><td><b>Critical: ' + result.criticals[i] + '</b></td></tr>';
  html += '</table>';
  return html;

}

function setupHtmlValidationOnTextArea(textAreaId) {
  var targetTextElement = document.createElement("div");
  targetTextElement.id = textAreaId + "_html_validation_message";
  var area = $("#" + textAreaId);
  area.parent().append(targetTextElement);
  area.bind("keyup", function() {
    validateHtmlInTextArea(textAreaId, targetTextElement.id);
  });
  triggerHtmlValidationUpdate(textAreaId);
}

function validateHtmlInTextArea(textAreaId, targetTextElementId) {
  var html = getObj(textAreaId).value;
  var result = validateHtml(html, {
    requireCorrectTables:true,
    allowHtmlTag:false,
    allowBodyTag:false,
    allowTitleTag:false
  });
  var r = renderHtmlValidationResult(result);
  $("#" + targetTextElementId).html(r);
}

function triggerHtmlValidationUpdate(textAreaId) {
  targetTextElementId = textAreaId + "_html_validation_message";
  validateHtmlInTextArea(textAreaId, targetTextElementId);
}

function showHtmlValidation(textAreaId) {
  $("#" + textAreaId + "_html_validation_message").show();
}

function hideHtmlValidation(textAreaId) {
  $("#" + textAreaId + "_html_validation_message").hide();
}

/**
 * Fetches the location of the user using the browsers built-in GPS/location services.
 * @param doneCallback Executed if success, passes a position object as parameter containing lat and long.
 * @param failCallback Executed if failed, passes no parameters.
 * @param noSupportCallback Executed if browser doesn't support location services.
 */
function getBrowserGeoLocation(doneCallback, failCallback, noSupportCallback) {
  if (navigator.geolocation) {
    try {
      navigator.geolocation.getCurrentPosition(function(position) {
        if (doneCallback && typeof doneCallback === "function") doneCallback(position);
      }, function() {
        if (failCallback && typeof failCallback === "function") failCallback();
      });
    } catch (e) {
      ibewarning("Exception when using browser geolocation:");
      ibewarning(e);
    }
    // Try Google Gears Geolocation
  } else {
    if (google.gears) {
      var geo = google.gears.factory.create('beta.geolocation');
      geo.getCurrentPosition(function(position) {
        if (doneCallback && typeof doneCallback === "function") doneCallback(position);
      }, function() {
        if (failCallback && typeof failCallback === "function") failCallback();
      });
      // Browser doesn't support Geolocation
    } else {
      if (noSupportCallback && typeof noSupportCallback === "function") noSupportCallback();
    }
  }

}

function openProgressBarDialogueBox(args) {
  args = $.extend({
                    isProgress:true,
                    picture:undefined,
                    showCloseIcon:true,
                    copyContentFrom:$("#progressBarContentContainerPlaceHolder").children()[0], // First child, this is the container div for the container. We do not want to copy it, it has a unique id.
                    textAndCopy:true,
                    enableIcon:false,
                    resizable:true,
                    height:200
                  }, args);
  openDialogueBox(args);
}


/**
 * Opens a dialogue box. Requires jQuery UI.
 * See $.extend(..) to see all args you can use.
 */
function openDialogueBox(args) {
  args = $.extend({
                    isProgress:false,
                    title:"Are you sure?",
                    text:"What is going on?",
                    picture:undefined,
                    showCloseIcon:true,
                    copyContentFrom:undefined,
                    textAndCopy:false, // Both text and copyContentFrom will be used.
                    textFirst:true, // Which content comes first, text or copyContentFrom?
                    resizable:false,
                    enableIcon:true, // Shows a small icon left of body text.
                    height:140,
                    width:300,
                    copiedContentCss:undefined, // CSS class applied to div around the copied content (if any)
                    copiedContentStyle:undefined // CSS style applied to div around the copied content (if any)
                  }, args);

  var imageHtml = "";
  if (args.picture && args.isProgress) {/* is not currently using textFirst param.*/
    imageHtml = '<div class="progressImage mt8 mb8 center"><img src="' + args.picture + '"/></div>';
  }

  var dialogueHtml = '<div id="dialogueBoxDiv" title="Are you sure?" class="center mb8" style="display:none;">' +
                     imageHtml +
                     '<span id="dialogueBoxIcon" class="ui-icon ui-icon-alert left" style="margin:0 7px 20px 0;"></span>' +
                     '<span id="dialogueBoxDivText"></span>' +
                     '</div>';

  if (!getObj('dialogueBoxDiv')) {
    $('body').append(dialogueHtml);
  } else {
    $("#dialogueBoxDiv").dialog("destroy"); // Restore it to pre-dialog state, with title attribute, etc.
  }

  if (!args.enableIcon) {$("#dialogueBoxIcon").hide();}

  if (args.title) {$("#dialogueBoxDiv").attr("title", args.title);}

  var copiedHtml = "";
  var baseHtml = "";
  if (args.copyContentFrom) {
    if (args.textAndCopy && args.textFirst) baseHtml += args.text;
    var copiedContentCss = args.copiedContentCss ? ' class="' + args.copiedContentCss + '"' : "";
    var copiedContentStyle = args.copiedContentStyle ? ' style="' + args.copiedContentStyle + '"' : "";
    baseHtml += '<div id="progressBarContentPlaceHolder" ' + copiedContentCss + copiedContentStyle + '></div>';
    if (args.textAndCopy && !args.textFirst) baseHml += args.text;

    copiedHtml += $(args.copyContentFrom).html();

    $("#dialogueBoxDivText").html(baseHtml);
    if (args.text) $("#progressBarContentPlaceHolder").html(copiedHtml);
  } else {
    if (args.text) $("#dialogueBoxDivText").html(args.text);
  }

  var dialogCssClass = "";
  if (!args.showCloseIcon) {dialogCssClass = "noCloseIcon";}

  $("#dialogueBoxDiv").dialog({
                                resizable:args.resizable,
                                zIndex:10100,
                                height:args.height,
                                modal:true,
                                buttons:args.buttons,
                                width:args.width,
                                dialogClass:dialogCssClass
                              });

}
/**
 * Used by hideProgressPanel().
 */
function closeDialogueBox() {
  $("#dialogueBoxDiv").dialog("close");
}

function decodeHtmlEntity(str) {
  try {
    var tarea = document.createElement('textarea');
    tarea.innerHTML = str;
    return tarea.value;
    tarea.parentNode.removeChild(tarea);
  }
  catch (e) {
    //for IE add <div id="htmlconverter" style="display:none;"></div> to the page
    document.getElementById("htmlconverter").innerHTML = '<textarea id="innerConverter">' + str + '</textarea>';
    var content = document.getElementById("innerConverter").value;
    document.getElementById("htmlconverter").innerHTML = "";
    return content;
  }
}

/*
 * Proposed usage:
 *
 * var x = new IbeMetrics('Links');
 *
 * <a href="/" onclick="x.inc('Click');">click here!</a>
 *
 * <a href="/" onmouseover="x.inc('Look');"> look at this ! </a>
 *
 * @param prefix A prefix, which all calls to inc(key) wil be
 *               prefixed with, for example:
 *               new IbeMetrics('APrefix').inc('Key');
 *               will generate a key named 'APrefix.Key'
 *               when recorded on the server.
 */
var IbeMetrics = Class.extend(
  {
    init:function(args) {
      this.args = args = $.extend({
                                    prefix:undefined
                                  }, args);
      this.prefix = args.prefix;
    },

    /**
     * Count given key prefixed by possible prefix
     *
     * @param key  Key to increment
     *
     * @return true if the event was sent to the server.
     */
    inc:function(key) {
      var fullKey = (this.prefix ? (this.prefix + (key ? '.' : '')) : '') + (key ? key : '');
      // Trigger event.
      try {
        var url = '/ibe-metric/' + fullKey;
        if (this.args.site) url += "?site=true";
        var image = new Image();
        image.src = url;
        return true;
      } catch (e) {}
      return false;
    }

  });

var IbeSiteMetrics = IbeMetrics.extend(
  {
    init:function(args) {
      args.site = true;
      this._super(args);
    }
  });

var getSiteName = function() {
  return IBE.siteName;
};

var getHostUrl = function(args) {
  args = $.extend({
                    secure:false,
                    prefixProtocol:true,
                    path:undefined,
                    port:undefined
                  }, args);
  validateArgument(args.secure, {rules:{allowUndefined:true, requireBoolean:true}}, "args.secure");
  validateArgument(args.prefixProtocol, {rules:{allowUndefined:true, requireBoolean:true}}, "args.prefixProtocol");
  validateArgument(args.path, {rules:{allowUndefined:true, requireString:true}}, "args.path");
  validateArgument(args.port, {rules:{allowUndefined:true, requireNumber:true}}, "args.port");

  var url = "";
  var port = args.port ? args.port : window.location.port;
  var isDefaultPort = port == "" || (port == "80" && !args.secure) || (port == "443" && args.secure);
  port = isDefaultPort ? "" : ":" + port;
  var path = args.path ? args.path : "";
  if (path && !path.startsWith("/")) path = "/" + path;
  if (args.prefixProtocol) url = ((args.secure) ? 'https://' : 'http://');
  return  url + window.location.hostname + port + path;
};

function loginIbeUser(email, password, successCallback, failCallback, args) {
  validateArgument(successCallback, {rules:{allowUndefined:false, requireFunction:true}}, "successCallback");
  validateArgument(failCallback, {rules:{allowUndefined:false, requireFunction:true}}, "failCallback");
  args = $.extend({}, args);
  var params = new RequestParameters({});
  params.addParameter("email", email);
  params.addParameter("password", password);
  var url = "/ajax.user.loginuser.do.action?" + params.toString();
  sendRequest(url, function(response) {
    if (successCallback && typeof successCallback === "function") successCallback(response);
  }, function(response, customObject, error) {
    if (failCallback && typeof failCallback === "function") failCallback(response, customObject, error);
  }, args);
}

function isWithinGoogleMapBound(lat, lng, mapBounds) {
  return lat < mapBounds.getNorthEast().lat() &&
         lat > mapBounds.getSouthWest().lat() &&
         lng < mapBounds.getNorthEast().lng() &&
         lng > mapBounds.getSouthWest().lng();
}

function logoutIbeUser(successCallback, failCallback) {
  var url = "/ajax.user.logoutuser.do.action";
  sendRequest(url, function(response) {
    if (successCallback && typeof successCallback === "function") successCallback(response);
  }, function(response, customObject, error) {
    if (failCallback && typeof failCallback === "function") failCallback(response, customObject, error);
  });
}

function cacheImage(url) {
  if (url) {
    var image = new Image();
    image.src = url;
  }
}

/**
 * This function is horrible and should only be used for test purposes!
 * @param delay milliseconds
 */
function sleep(delay) {
  if (isProdEnvironment()) return;
  var start = new Date().getTime();
  while (new Date().getTime() < start + delay) {

  }
}
/**
 * Converts a String representation of time to an int time.
 *
 * @param time in format HH:MM
 * @returns HHMM as an int value.
 */
function timeAsInt(time) {
  if (!time) {
    return undefined;
  } else {
    var s = removeInitCharacters(time.getHours().toString(), "0") + time.getMinutes().toString();
    return parseInt(s);
  }
}

function timeStringAsInt(time) {
  if (!time || time.indexOf(':') == -1) {
    return undefined;
  } else {
    var hhmm = time.split(':');
    var s = removeInitCharacters(hhmm[0], "0") + hhmm[1];
    return parseInt(s);
  }
}

var __everyOther = new Object();

function everyOther(sub) {
  if (!sub) sub = "default";
  return __everyOther[sub] = !__everyOther[sub];
}

/**
 * Counts how many charToCount-characters that are in the beginning of the string.
 * For example: "00123", "0" -> 2 .. There are 2 zeroes in the beginning.
 * For example "00123", "0" -> "123".
 * @param s
 * @param character
 */
function countInitChars(s, charToCount) {
  var i = 0;
  for (i = 0; i < s.length; i++) {
    if (s.charAt(i) != charToCount) break;
  }
  return i;
}

/**
 * If the string starts with n characters of type "character" (the parameter), these are removed.
 * For example "00123", "0" -> "123".
 * @param s
 * @param character
 */
function removeInitCharacters(s, character) {
  if (typeof s !== "string") {
    ibeerror("removeInitZeroes argument must be a string.");
    return s;
  }
  var zeroes = countInitChars(s, character);
  return s.substring(zeroes, s.length);
}

function removeInitZeroes(s) {
  return removeInitCharacters(s, "0");
}

/**
 * Calculates the sum of all numbers in a string. All characters in the string must be numbers.
 * @param s
 */
function sumOfSequence(s) {
  s = s.toString();
  if (!stringIsNumeric(s)) {
    ibeerror("String must be numeric.");
    return 0;
  } else {
    if (s.length == 1) return parseInt(s);
    var sum = 0;
    var max = s.length;
    for (var i = 0; i < max; i++) {
      sum += parseInt(s.charAt(i));
    }
    return sum;
  }
}

function validateSwedishSso(no) {
  // TODO: Allow "-" ? If so, just remove any "-" before next row.
  if (stringIsNumeric(no) && no.length == 10) {
    var lastNumber = parseInt(no.getLastCharacter());
    var multiplier = 2;
    var sum = 0;
    for (var i = 0; i < 9; i++) {
      sum += sumOfSequence(parseInt(no.charAt(i) * multiplier));
      multiplier = multiplier == 1 ? 2 : 1;
    }
    var controlNumber = 10 - parseInt(sum.toString().getLastCharacter());
    return controlNumber == lastNumber;
  } else {
    ibeerror("Swedish social security number passed to validateSso(..) most only contain numbers and be 10 characters (only numbers) long.");
    return false;
  }
}

function validateFinnishSso(no) {
  if (no && no.length == 11) {
    var controlArray = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "h", "j", "k", "l", "m", "n", "p", "r", "s", "t", "u", "v", "w", "x", "y", "ö"];
    var day = no.substring(0, 2);
    var month = no.substring(2, 4);
    var year = no.substring(4, 6);
    var b = no.substring(7, 10);
    if (day < 1 || day > 31) return false;
    if (month < 1 || month > 12) return false;
    var c = parseInt(removeInitZeroes(day + month + year + b)); // Must remove any init zeroes, otherwise parseInt will parse octal number instead of base 10.
    var control = c % 31;
    var controlCharacter = controlArray[control];
    return no.getLastCharacter().toLowerCase() == controlCharacter.toLowerCase();
  } else {
    ibeerror("Finnish social security number passed to validateSso(..) most be 11 characters long.");
    return false;
  }
}

function validateNorwegianSso(no) {
  if (no && no.length == 11 && stringIsNumeric(no)) {
    var day = no.substring(0, 2);
    var month = no.substring(2, 4);
    var year = no.substring(4, 6);
    if (day < 1 || day > 31) return false;
    if (month < 1 || month > 12) return false;
    var controlNumber1 = 11 - (((3 * no.charAt(0)) + (7 * no.charAt(1)) + (6 * no.charAt(2)) + (1 * no.charAt(3)) + (8 * no.charAt(4)) + (9 * no.charAt(5)) + (4 * no.charAt(6)) + (5 * no.charAt(7)) + (2 * no.charAt(8))) % 11);
    var controlNumber2 = 11 - (((5 * no.charAt(0)) + (4 * no.charAt(1)) + (3 * no.charAt(2)) + (2 * no.charAt(3)) + (7 * no.charAt(4)) + (6 * no.charAt(5)) + (5 * no.charAt(6)) + (4 * no.charAt(7)) + (3 * no.charAt(8)) + (2 * no.charAt(9))) % 11);
    return ((controlNumber1 == no.charAt(9)) || (controlNumber1 > 10)) && ((controlNumber2 == no.charAt(10)) || (controlNumber2 > 10));

  } else {
    ibeerror("Norwegian social security number passed to validateSso(..) most be 11 characters long and only numbers!");
    return false;
  }
}

function validateSso(no, language) {
  if (!no) {
    ibewarning("validateSso(..) did not get a language parameter. Defaulting to swedish.");
    language = 2;
  }
  if (!stringIsNumeric(language)) {
    switch (language.toLowerCase().trim()) {
      case "se":
      case "swe":
      case "swedish":
      case "sweden":
        language = 2;
        break;
      case "no":
      case "nor":
      case "norwegian":
      case "norway":
        language = 9;
        break;
      case "fi":
      case "fin":
      case "finnish":
      case "finland":
        language = 5;
        break;
      default:
        ibeerror("Running validateSso(..) with language parameter='" + language + "', but it could not be resolved.");
        return true;
    }
  }
  switch (language) {
    case 2:
      return validateSwedishSso(no);
      break;
    case 5:
      return validateFinnishSso(no);
      break;
    case 9:
      return validateNorwegianSso(no);
      break;
    default:
      ibeerror("Runnig validateSso(..) but specified language is not supported. Only finnish, swedish and norwegian is supported.");
      return true;
  }
}

/**
 * public static final long ENGLISH = 1L;
 public static final long SWEDISH = 2L;
 public static final long DANISH = 3L;
 public static final long GERMAN = 4L;
 public static final long FINNISH = 5L;
 public static final long FRENCH = 6L;
 public static final long ITALIAN = 7L;
 public static final long DUTCH = 8L;
 public static final long NORWEGIAN = 9L;
 public static final long SPANISH = 10L;
 public static final long POLISH = 11L;

 *
 */


function IbeEventManager() {

  this._eventListeners = new Array();

  this.signal = function(eventType, args) {
    args = $.extend({}, args);
    var listToRun = new Array();
    if (!eventType) {
      ibewarning("Tried to signal listeners, but no eventType is specified. Defaulting to 'all'.");
      eventType = "all";
    }
    if (typeof eventType !== "string") {
      ibewarning("Tried to signal listeners, but eventType is not a string. Defaulting to 'all'.");
      eventType = all;
    }
    // Make a copy of the list, since it might be modified by the listeners.
    for (var i = 0, l = this._eventListeners.length; i < l; i++) {
      var listener = this._eventListeners[i];
      if ((listener && listener.f && typeof listener.f === "function") &&
          (!eventType || !listener.eventType || listener.eventType.toUpperCase() === "ALL" ||
           eventType.toUpperCase() === listener.eventType.toUpperCase())) {
        // Run if listener has subscribed to this eventType, or if no eventType was specified, or if listener listens to all eventTypes.
        listToRun.push(listener.f);
      }
    }
    if (listToRun.length) {
      for (i = 0; i < listToRun.length; i++) {
        listToRun[i](args);
      }
    } else {
      ibenotice("Signaling event type '" + eventType + "', but there are no listeners.");
    }
  };

  /**
   * 999 executes before 55.
   * @param eventType
   * @param f
   * @param args
   */
  this.listen = function(eventType, f, args) {
    args = $.extend({
                      priority:0
                    }, args);
    if (!eventType) {
      ibewarning("Tried to add listener, but no eventType is specified. Defaulting to 'all'.");
      eventType = "all";
    }
    if (typeof eventType !== "string") {
      ibewarning("Tried to add listener, but eventType is not a string. Defaulting to 'all'.");
      eventType = all;
    }
    if (!this.hasEventListenerOnEventType(f, eventType)) {
      if (f && typeof f === "function") {
        this._eventListeners.push({
                                    f:f,
                                    priority:args.priority,
                                    eventType:eventType.toUpperCase()
                                  });
      }
      this._sortEventListeners();
    } else {
      ibewarning("Tried to add listener to event '" + eventType + "', but that listener is already subscribed to that event.");
    }
  };

  this._sortEventListeners = function() {
    this._eventListeners = IBESorter._insertionSort(this._eventListeners, "priority", true);
  };

  this.hasEventListener = function(f) {
    for (var i = 0, l = this._eventListeners.length; i < l; i++) {
      if (this._eventListeners[i].f == f) return true;
    }
    return false;
  };

  this.hasEventListenerOnEventType = function(f, eventType) {
    for (var i = 0, l = this._eventListeners.length; i < l; i++) {
      var listener = this._eventListeners[i];
      if (listener.f == f && listener.eventType == eventType) return true;
    }
    return false;
  };

  this.removeEventListener = function(f) {
    for (var i = 0, l = this._eventListeners.length; i < l; i++) {
      if (this._eventListeners[i].f == f) {
        this._eventListeners.splice(i, 1);
      }
    }
  };

  this.removeAllEventListeners = function() {
    this._eventListeners = new Array();
  };

  this.debugPrintEventListener = function(f) {
    for (var i = 0; i < this._eventListeners.length; i++) {
      ibedebug(this._eventListeners[i]);
    }
  };

}
var IBEPopupWindow = (function() {
  var popupWindowProcessManager, popupWindowContainers = {};

  function createIframe(suffix, url) {
    var iframe = document.createElement('iframe');
    iframe.src = url;
    iframe.id = 'iframe_' + suffix;
    iframe.frameBorder = 0;
    iframe.style.border = 0;
    iframe.style.width = '500px';
    iframe.style.height = '500px';
    return iframe;
  }

  function isContainerCreated(popupWindowContainerId) {
    if (popupWindowContainers[popupWindowContainerId]) {
      return true;
    }
    // set container id to be created
    popupWindowContainers[popupWindowContainerId] = popupWindowContainerId;
    return false;
  }

  /**
   * This function binds close events after the new content has been rendered in the container. The event can only be
   * triggered once.
   *
   * @param $overlay the jquery overlay to close.
   * @param options this is the config object.
   */
  function attachCloseEvents($overlay, options) {
    // Close overlay when clicking outside. This event is bound when the ajax request
    // has returned.
    if (options.closeOnClickOutside) {
      $(document).one('click', function() {
        $overlay.close();
      });
    }
    // Close when the close button is clicked
    $("#" + options.container + " .close").one('click', function() {
      $overlay.close();
    });
  }

  return {
    /**
     * Popup window function - only supports ajax response is of dataType HTML.
     *
     * Usage: IBEPopupWindow.show($triggerElement, {url:'http://....'});
     *
     * @param $el that the overlay should be attached on, e.g. the trigger element for the overlay.
     * @param options is an object containing some default options to override, or can be undefined then the default
     * values will be used.
     */
    show:function($el, options) {
      var defaults, overlay;
      // Instantiate process manager used in popup window method.
      if (!popupWindowProcessManager) popupWindowProcessManager = new IBEProcessManager("pushover");
      // Some default properties that can be overridden
      defaults = {
        container:'ibePopupWindowContainer',
        cssClass:'simpleOverlay ibePopupWindowContainer',
        loadingAnimation:true,
        iframe:false,
        closeOnClickOutside:true
      };
      // extend default properties
      options = $.extend({}, defaults, options);

      if (!options.url) throw "No url defined.";
      if (!options.container) throw "No container defined.";

      if (!isContainerCreated(options.container)) {
        $("body").append('<div id="' + options.container + '" class="' + options.cssClass + '"></div>');
      }

      overlay = $el.data('overlay');
      if (overlay) {
        overlay.load(); // An overlay is already attached to this element -- display it!
      } else {
        // initialize overlay for this element
        $el.overlay({
                      target:'#' + options.container, // the overlay content goes here.
                      effect:'default',
                      api:true,
                      load:true,
                      onBeforeLoad:function() {
                        var $this = this;
                        if (options.iframe) {
                          var content = createIframe(options.container, options.url);
                          $("#" + options.container).html('<div class="close"></div>').append(content);
                          attachCloseEvents($this, options);
                        } else {
                          // When overlay is visible display loading animation
                          if (options.loadingAnimation) {
                            setInnerHtmlToLoadingAnimation(options.container);
                          }
                          // Fetch content via ajax and place response in the container
                          $.ajax({
                                   url:options.url,
                                   dataType:'html',
                                   success:function(response, textStatus) {
                                     $("#" + options.container).html('<div class="close"></div>').append(response);
                                     attachCloseEvents($this, options);
                                   },
                                   error:function(request, textStatus, error) {
                                     $("#" + options.container).html('');
                                     $this.close(); // If an error occurs - close overlay
                                   }
                                 });
                        }
                      }
                    });
      }
    } // end show()

  };

})();

/*****
 Argument validation
 *****/


function validateArgumentList(variableList, config, variableNameList) {
  var hasNameList = variableNameList && variableNameList.length;
  if (variableList && variableList.length) {
    if (!$.isArray(variableList)) {
      ibeerror("Specified variable list is not an array.");
      return;
    }
    for (var i = 0; i < variableList.length; i++) {
      validateArgument(variableList[i], config, hasNameList ? variableNameList[i] : undefined);
    }
  }
}

/**
 *
 * @param variable
 * @param config
 * @param variableName
 */
function validateArgument(variable, config, variableName) {

  config = extendValidationConfig(config);

  this.validationFailed = function(message) {
    //ibelog(message, variable, config);
    ibeerror(message);
  };

  if (!variableName) variableName = "unknown name";

  if (config.rules.allowUndefined === false && variable === undefined) {
    this.validationFailed(insertArgument(config.messages.isUndefined, variableName));
  } else if (config.rules.allowUndefined === true && variable === undefined) {
    // If it is undefined, and we allow it, no more tests are needed.
    return;
  }

  if (config.rules.allowTypes && (config.rules.allowTypes.length && !config.rules.allowTypes.contains(typeof variable))) {
    this.validationFailed(insertArgument(config.messages.isWrongType, variableName, typeof variableName, config.rules.allowTypes));
  }
  if (config.rules.requireValueIsOneOf && (config.rules.requireValueIsOneOf.length && !config.rules.requireValueIsOneOf.contains(variable))) {
    this.validationFailed(insertArgument(config.messages.isWrongValue, variableName, variable, config.rules.requireValueIsOneOf));
  }
  if (config.rules.requireString) {
    if (typeof variable === "string") {} // OK
    else {
      this.validationFailed(insertArgument(config.messages.isNotString, variableName, variable));
    }
  }
  if (config.rules.requireStringOrNumber) {
    if (typeof variable === "string") {} // OK
    else if (typeof variable === "number") {} // OK
    else {
      this.validationFailed(insertArgument(config.messages.isNotString, variableName, variable));
    }
  }
  if (config.rules.requireNumber) {
    if (typeof variable === "number") {} // OK
    else {
      this.validationFailed(insertArgument(config.messages.isNotNumber, variableName, variable));
    }
  }
  if (config.rules.requireObject) {
    if (typeof variable === "object") {} // OK
    else {
      this.validationFailed(insertArgument(config.messages.isNotObject, variableName, variable));
    }
  }
  if (config.rules.requireFunction) {
    if (typeof variable === "function") {} // OK
    else {
      this.validationFailed(insertArgument(config.messages.isNotFunction, variableName, variable));
    }
  }

  // Boolean

  if (config.rules.requireBoolean) {
    if (variable === true || variable === false || (config.rules.allowUndefined && variable === undefined)) {
      // OK
    } else {
      this.validationFailed(insertArgument(config.messages.isNotBoolean, variableName, variable));
    }
  }

  // Check all types of lists.

  if (config.rules.requireList || config.rules.requireListOfNumbers || config.rules.requireListOfStrings) {
    if (config.rules.allowUndefined && variable === undefined) {
      // OK
    } else {
      if (!$.isArray(variable)) {
        this.validationFailed(insertArgument(config.messages.isNotList, variableName));
      } else {
        if (!variable.length) {
          if (config.rules.requireElementsInList) {
            this.validationFailed(insertArgument(config.messages.listHasNoElements, variableName));
          }
        } else {
          if (config.rules.requireListOfStrings && !variable.containsOnlyStrings()) {
            this.validationFailed(insertArgument(config.messages.isNotListOfStrings, variableName));
          }
          if (config.rules.requireListOfNumbers && !variable.containsOnlyNumbers()) {
            this.validationFailed(insertArgument(config.messages.isNotListOfNumbers, variableName));
          }
        }
      }
    }
  }

  if (config.rules.requireIdExistsInDom) {
    if (!document.getElementById(variable)) {
      this.validationFailed(insertArgument(config.messages.idDoesNotExistInDom, variable));
    }
  }

  if (config.rules.requireDomElement) {
    if (!variable.nodeType) {
      this.validationFailed(insertArgument(config.messages.isNotDomElement, variable));
    }
  }

}

function extendValidationConfig(config) {

  config = $.extend({
                      rules:{
                        allowUndefined:false, // undefined, true, false

                        allowTypes:undefined, // ["string", "number"]

                        requireValueIsOneOf:undefined, // ["mattias", "is", 5]

                        requireBoolean:false,
                        requireStringOrNumber:false,
                        requireString:false,
                        requireNumber:false,
                        requireObject:false,
                        requireFunction:false,
                        requireList:false,
                        requireListOfNumbers:false,
                        requireListOfStrings:false,
                        requireElementsInList:false,
                        requireIdExistsInDom:false,
                        requireDomElement:false
                      },
                      messages:{
                        isUndefined:"Argument '{0}' is undefined, which is not allowed.",
                        isWrongType:"Argument '{0}' is of wrong type: {1}. Allowed types are: {2}.",

                        isWrongValue:"Argument '{0}' is of wrong value: {1}. Allowed values are: {2}.",

                        isNotBoolean:"Argument '{0}' must be a boolean, value is: {1}.",
                        isNotString:"Argument '{0}' must be a string, value is: {1}.",
                        isNotStringOrNumber:"Argument '{0}' must be a string or a number, value is: {1}.",
                        isNotNumber:"Argument '{0}' must be a number, value is: {1}.",
                        isNotObject:"Argument '{0}' must be an object, value is: {1}.",
                        isNotFunction:"Argument '{0}' must be a function, value is: {1}.",

                        isNotList:"Argument '{0}' must be a list.",
                        isNotListOfStrings:"Argument '{0}' must be a list of strings.",
                        isNotListOfNumbers:"Argument '{0}' must be a list of numbers.",
                        listHasNoElements:"Argument '{0}' must be a list with elements, it is empty.",

                        idDoesNotExistInDom:"Argument '{0}' must be the id of a DOM element, but it is not.",
                        isNotDomElement:"Argumenet '{0}' must be a DOM element, it is not."
                      }
                    }, config);
  return config;
}

function validateArgumentValidationConfig(config) {
  config = extendValidationConfig(config);

  validateArgument(config.rules.allowUndefined, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.allowUndefined");

  validateArgument(config.rules.allowTypes, {rules:{allowUndefined:true, requireListOfStrings:true}}, "config.rules.allowTypes");
  validateArgument(config.rules.requireValueIsOneOf, {rules:{allowUndefined:true, requireList:true}}, "config.rules.requireValueIsOneOf");

  validateArgument(config.rules.requireBoolean, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireBoolean");
  validateArgument(config.rules.requireString, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireString");
  validateArgument(config.rules.requireStringOrNumber, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireString");
  validateArgument(config.rules.requireNumber, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireNumber");
  validateArgument(config.rules.requireObject, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireObject");
  validateArgument(config.rules.requireFunction, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireFunction");

  validateArgument(config.rules.requireList, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireList");
  validateArgument(config.rules.requireListOfNumbers, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireListOfNumbers");
  validateArgument(config.rules.requireListOfStrings, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireListOfStrings");

  validateArgument(config.rules.requireIdExistsInDom, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireIdExistsInDom");
  validateArgument(config.rules.requireDomElement, {rules:{allowUndefined:true, requireBoolean:true}}, "config.rules.requireDomElement");

  // Messages

  validateArgument(config.messages.isUndefined, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isUndefined");
  validateArgument(config.messages.isWrongType, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isWrongType");

  validateArgument(config.messages.isWrongValue, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isWrongValue");

  validateArgument(config.messages.isNotBoolean, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotBoolean");
  validateArgument(config.messages.isNotString, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotString");
  validateArgument(config.messages.isNotStringOrNumber, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotStringOrNumber");
  validateArgument(config.messages.isNotNumber, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotNumber");
  validateArgument(config.messages.isNotObject, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotObject");
  validateArgument(config.messages.isNotFunction, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotFunction");

  validateArgument(config.messages.isNotList, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotList");
  validateArgument(config.messages.isNotListOfStrings, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotListOfStrings");
  validateArgument(config.messages.isNotListOfNumbers, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotListOfNumbers");
  validateArgument(config.messages.listHasNoElements, {rules:{allowUndefined:false, requireString:true}}, "config.messages.listHasNoElements");

  validateArgument(config.messages.idDoesNotExistInDom, {rules:{allowUndefined:false, requireString:true}}, "config.messages.idDoesNotExistInDom");
  validateArgument(config.messages.isNotDomElement, {rules:{allowUndefined:false, requireString:true}}, "config.messages.isNotDomElement");
}

/**
 * Used to apply gradient for IE9 or later. The gradient for IE8 or earlier
 * is applied with a filter and that filter must be removed because it will
 * override the gradient for IE9. How to create gradients can be found here:
 * http://www.colorzilla.com/gradient-editor/
 * The element with the gradient must have the class iegradient.
 */
function applyIEGradient() {
  if (browserIsIE9OrLater()) {
    $(".iegradient").css("filter", "none");
  }
}

/**
 * Links can be an HTML A node, or a jQuery object containing one or more A elements.
 * @param links
 * @param args
 */
function makeLinksOpenInOverlay(links, args) {
  args = $.extend({
                    width:undefined, // TODO: Implement these
                    height:undefined
                  }, args);

  var overlayId = "pageOverlay";
  var $aList = $(links);

  // Setup overlay in DOM.
  var $overlayElement = $("#" + overlayId);

  if (!$overlayElement || !$overlayElement.length) {
    // Not specified and not found in DOM -> Create new
    var $overlayElement = $(document.createElement("div"))
      .attr("id", overlayId)
      .hide()
      .addClass("apple_overlay")
      .appendTo($("body"));

    $contentElement = $(document.createElement("div"))
      .attr("id", "pageOverlayContent")
      .addClass("contentWrap")
      .appendTo($overlayElement);

  }


  $aList.each(function(index, node) {

    $a = $(node); // Turn node into jQuery object
    $a.attr("rel", "#" + overlayId);

    $a.overlay({
                 mask:'white',
                 effect:'apple',

                 onBeforeLoad:function() {

                   // grab wrapper element inside content
                   var wrap = this.getOverlay().find(".contentWrap");

                   // load the page specified in the trigger
                   wrap.load(this.getTrigger().attr("href"));
                 }

               });

  })

}

/*
 * Proposed usage:
 *
 * var _ibe_ue = new IbeUserEvents(123, 123, 123, 123, 123);
 *
 * <a href="/" onclick="_ibe_ue.r(1,2,3);">click here!</a>
 *
 * <a href="/" onmouseover="_ibe_ue.r(3,2,3);"> look at this ! </a>
 *
 */

var IBE_USER_EVENT_TRACKER_PATH_PREFIX = '/ibe-user-event';

/**
 * Sole Constructor
 *
 * @param sessionId The session Id
 * @param requestId The request ID
 * @param domainId domain Id
 * @param resourceId resourceId of current resource
 * @param pageServerRenderTimeMillis the server-time
 */
var IbeUserEvents = function (sessionId, requestId, domainId, resourceId, pageServerRenderTimeMillis) {
  this.sessionId = sessionId;
  this.requestId = requestId;
  this.domainId = domainId;
  this.resourceId = resourceId;
  this.pageServerRenderTimeMillis = pageServerRenderTimeMillis;
  this.sentTrackingEvents = new Array();
  this.pageUQ = sessionId + ':' +
                requestId + ':' +
                domainId + ':' +
                resourceId + ':' +
                pageServerRenderTimeMillis;

  /**
   * Register the Entity Statistics.
   *
   * @param entityId entity of the Object
   * @param objectId the unique Object Id
   *
   * @param contextId event context. OPTIONAL.
   *
   * @return true if the event was sent to the server.
   */
  this.s = function(entityId, objectId) {
    var contextId = 3;
    if (arguments.length > 2) {
      contextId = arguments[2];
    }

    // The order of this data must correspond to the order in UiUserEvent class.
    var stat = contextId + ':' + entityId + ':' + objectId;

    //
    // We do NOT want to add the same within-page-event more than once.
    //
    var eventUrl = IBE_USER_EVENT_TRACKER_PATH_PREFIX + '?stat=' + stat;
    try {
      var image = new Image();
      image.src = eventUrl;
    } catch (e) {
      return false;
    }
    return true;
  }

  /**
   * Register the unique event.
   *
   * @param contextId event context
   * @param entityId entity of the Object
   * @param objectId the unique Object Id
   *
   * @param componentDefinitionId. Optional.
   *
   * @return true if the event was sent to the server.
   */
  this.r = function(contextId, entityId, objectId) {

    var compDefId = -999;
    if (arguments.length > 3) {
      compDefId = arguments[3];
    }

    // The order of this data must correspond to the order in UiUserEvent class.
    var uniquePageEvent = this.pageUQ + ':' +
                          contextId + ':' +
                          entityId + ':' +
                          objectId + ':' +
                          compDefId;

    //
    // We do NOT want to add the same within-page-event more than once.
    //
    var shouldTellServer = this._shouldTriggerEvent(uniquePageEvent);
    if (shouldTellServer) {
      // Trigger event.

      // The order of this data must correspond to the order in UiUserEvent class.
      var stat = '';
      if (contextId >= 300 && contextId < 400) stat = '&stat=3:' + entityId + ':' + objectId;
      var eventUrl = IBE_USER_EVENT_TRACKER_PATH_PREFIX + '?event=' + uniquePageEvent + stat;
      try {
        var image = new Image();
        image.src = eventUrl;
      } catch (e) {}
    }
    return shouldTellServer;
  };

  /**
   * If We do NOT want to trigger the same event more than once, this method ensures this.
   *
   * @param uniquePageEvent the first part of the event to trigger, excluding trigger-time.
   *
   * @return true if we should trigger the event.
   */
  this._shouldTriggerEvent = function(uniquePageEvent) {
    var exist = 0;
    var c = this.sentTrackingEvents;
    for (var i = 0; i < c.length; i++) {
      if (c[i] == uniquePageEvent) {
        exist = 1;
        break;
      }
    }
    if (exist == 0) {
      this.sentTrackingEvents[this.sentTrackingEvents.length] = uniquePageEvent;
    }
    return exist == 0;
  };

  /**
   * @return string info about page's events.
   */
  this.eventInfo = function() {
    var b = '';

    b += 'sessionId  = ' + this.sessionId + '\n';
    b += 'requestId  = ' + this.requestId + '\n';
    b += 'domainId   = ' + this.domainId + '\n';
    b += 'resourceId = ' + this.resourceId + '\n';
    b += 'pageTime   = ' + this.pageServerRenderTimeMillis + '\n';
    var c = this.sentTrackingEvents;
    b += 'PageEvents = ' + c.length + '\n';
    for (var i = 0; i < c.length; i++) {
      b += '  ' + (i + 1) + ' ' + c[i].replace(this.pageUQ + ':', '') + '\n';
    }

    return b;
  }
};
/**
 * Depends on jQuery.
 */
var IBEMenuManager = Class.extend(
  {
    init : function(args) {
      args = $.extend({});
      this.menuList = [];
    },

    addMenu : function (tabMenu) {
      this.menuList.push(tabMenu);
    },

    findMenuWithId : function (id) {
      if (id) {
        for (var i = 0, l = this.menuList.length; i < l; i++) {
          var menu = this.menuList[i];
          if (menu.id === id) {
            return menu;
          }
        }
      }
      return null;
    },

    clickTabOnMenu : function (menuId, tabId) {
      var menu = this.findMenuWithId(menuId);
      if (menu) {
        menu.selectTabWithId(tabId);
      } else {
        ibeerror('IBEMenuManager trying to click tab of menu that does not exist, menu ID=' + menuId);
      }
    }
  });

function IBETab(args) {
  args = $.extend({
                    id : undefined,
                    title : undefined,
                    callback : undefined,
                    url : undefined,
                    cssClass : undefined,
                    parentMenu : undefined,
                    selected : false,
                    index : undefined,
                    tdLeft : undefined,
                    tdRight : undefined,
                    tdMiddle : undefined,
                    parentObject : undefined
                  }, args);
  validateArgument(args.id, {rules:{allowUndefined:false, requireString:true}}, "args.id");
  validateArgument(args.title, {rules:{allowUndefined:true, requireString:true}}, "args.title");
  validateArgument(args.url, {rules:{allowUndefined:true, requireString:true}}, "args.url");
  validateArgument(args.callback, {rules:{allowUndefined:true, requireFunction:true}}, "args.callback");

  this.title = args.title;
  this.selected = args.selected;
  this.parentMenu = args.parentMenu;
  this.url = args.url;
  this.id = args.id;
  this.index = args.index;
  this.parentObject = args.parentObject;
  this.callbackScope = window;
  this.callback = args.callback;

  var left = 'Left', right = 'Right', middle = 'Middle', selected = 'Selected';

  var cssClass = args.cssClass;
  this.leftCss = cssClass ? cssClass + left : undefined;
  this.middleCss = cssClass ? cssClass + middle : undefined;
  this.rightCss = cssClass ? cssClass + right : undefined;
  this.leftCssSelected = cssClass ? cssClass + left + selected : this.leftCss;
  this.middleCssSelected = cssClass ? cssClass + middle + selected : this.middleCss;
  this.rightCssSelected = cssClass ? cssClass + right + selected : this.rightCss;

  this.tdLeft = args.tdLeft;
  this.tdMiddle = args.tdMiddle;
  this.tdRight = args.tdRight;

  this.getTitle = function() {
    return this.title;
  };

  this.setTitle = function(title) {
    this.title = title;
  };

  this.getId = function() {
    return this.id;
  };

  this.getIndex = function() {
    return this.index;
  };

  this.getUrl = function() {
    return this.url;
  };

  this.isSelected = function() {
    return this.selected;
  };

  this.setSelected = function(selected) {
    this.selected = selected;
  };

  /**
   * Internal callback function. Is executed by parent class to select the tab.
   * @param doCallback If true, the callback will be executed. Defaults to true if not specified.
   * @param forceRender Forces the updates of the tabs CSS classes.
   */
  this.selectTab = function (doCallback, forceRender) {
    if (doCallback === undefined) doCallback = true;
    var isOk = undefined;
    if (!this.isSelected()) {
      if (this.callback && doCallback) {
        isOk = this.callback.call(this.callbackScope, this.getIndex(), this, this.parentObject);
      }
      if (this.getUrl() && doCallback) {
        window.location = this.getUrl();
        isOk = true;
      }
      if (isOk === undefined) isOk = true;
      if (isOk) {
        if (this.parentMenu.shouldRenderTabOnClick() || forceRender) {
          if (this.leftCssSelected) this.tdLeft.className = this.leftCssSelected;
          if (this.middleCssSelected) this.tdMiddle.className = this.middleCssSelected;
          if (this.rightCssSelected) this.tdRight.className = this.rightCssSelected;
        }
        this.setSelected(true);
      }

    } else {
      isOk = false;
    }
    return isOk;
  };

  this.unselectTab = function (forceRender) {
    if (this.parentMenu.shouldRenderTabOnClick() || forceRender) {
      if (this.leftCss) this.tdLeft.className = this.leftCss;
      if (this.middleCss) this.tdMiddle.className = this.middleCss;
      if (this.rightCss) this.tdRight.className = this.rightCss;
    }
    this.setSelected(false);
  };

  this.setCallbackScope = function(obj) {
    this.callbackScope = obj;
  };

}

function IBETabMenu(args) {
  args = $.extend({
                    id : undefined,
                    placeHolderId : undefined,
                    defaultTabIndex : 0,
                    selectedIndex : undefined,
                    runCallbackAtRender : false,
                    renderTabOnClick : true,
                    startWithSeparator : false,
                    useHref : false, // Defines if we use href when using URL. Otherwise, use only window.location
                    leftCss: undefined,
                    rightCss: undefined,
                    middleCss: undefined,
                    leftCssSelected: undefined,
                    rightCssSelected: undefined,
                    middleCssSelected: undefined,
                    separatorCss : undefined,
                    tableCss : undefined,
                    rowCss : undefined
                  }, args);

  validateArgument(args.id, {rules:{allowUndefined:true}}, "args.id");
  validateArgument(args.placeHolderId, {rules:{allowUndefined:false, requireString:true}}, "args.placeHolderId");

  this.tabList = [];
  this.id = args.id;
  this.defaultTabIndex = args.defaultTabIndex;
  this.startWithSeparator = args.startWithSeparator;

  function getCssOrEmpty(css) {
    return css ? (' ' + css) : '';
  }

  this.leftCss = 'leftTab' + getCssOrEmpty(args.leftCss);
  this.rightCss = 'rightTab' + getCssOrEmpty(args.rightCss);
  this.middleCss = 'middleTab' + getCssOrEmpty(args.middleCss);
  this.leftCssSelected = 'leftTab selected' + getCssOrEmpty(args.leftCssSelected);
  this.rightCssSelected = 'rightTab selected' + getCssOrEmpty(args.rightCssSelected);
  this.middleCssSelected = 'middleTab selected' + getCssOrEmpty(args.middleCssSelected);
  this.separatorCss = 'tabMenuEmpty' + getCssOrEmpty(args.separatorCss);
  this.tableCss = 'tabMenuTable' + getCssOrEmpty(args.tableCss);
  this.rowCss = 'tabMenuRow' + getCssOrEmpty(args.rowCss);

  var selectedIndex = args.selectedIndex,
    renderTabOnClick = args.renderTabOnClick;

  this.getSelectedIndex = function() {
    return selectedIndex;
  };

  this.setSelectedIndex = function(index) {
    selectedIndex = index;
  };

  this.setRenderOnClick = function(enabled) {
    renderTabOnClick = enabled;
  };

  this.shouldRenderTabOnClick = function() {
    return renderTabOnClick;
  };

  this.addTab = function (tab) {
    this.tabList.push(tab);
    tab.parentMenu = this;
  };

  this.getTabAtIndex = function(index) {
    return this.tabList[index];
  };

  this.getTabWithId = function (id) {
    if (id) {
      for (var i = 0, l = this.tabList.length; i < l; i++) {
        if (this.tabList[i].id === id) {
          return this.tabList[i];
        }
      }
    }
    return null;
  };

  this.getTabIndexWithId = function (id) {
    if (id) {
      for (var i = 0, l = this.tabList.length; i < l; i++) {
        if (this.tabList[i].id === id) {
          return i;
        }
      }
    }
    return null;
  };

  this.getTabIndexWithTitle = function (title) {
    if (title) {
      for (var i = 0,l = this.tabList.length; i < l; i++) {
        if (this.tabList[i].getTitle() === title) {
          return i;
        }
      }
    }
    return null;
  };

  this.getFirstTab = function () {
    return this.tabList[0];
  };

  this.getLastTab = function () {
    return this.tabList[this.tabList.length - 1];
  };

  this.clearSelected = function () {
    for (var i = 0, l = this.tabList.length; i < l; i++) {
      this.tabList[i].setSelected(false);
      setElementsClass('tabMenuButton' + i + args.id, 'tabMenuButton');
    }
  };

  this.setDefault = function (id) {
    if (id) {
      for (var i = 0, l = this.tabList.length; i < l; i++) {
        var tab = this.tabList[i];
        if (tab.id === id) {
          this.setDefaultTabIndex(i);
          return;
        }
      }
    }
  };

  /**
   * Sets default tab to the tab with title=title. If success, returns true. If false, uses defaultIndex if it is set,
   * and then returns false.
   * @param title
   */
  this.setDefaultTabWithTitle = function (title, defaultIndex) {
    var index = this.getTabIndexWithTitle(title);
    if (!nullOrUndefined(index) && index >= 0) {
      this.setDefaultTabIndex(index);
      return true;
    } else {
      if (!nullOrUndefined(defaultIndex) && defaultIndex >= 0) this.setDefaultTabIndex(defaultIndex);
      return false;
    }
  };

  this.getDefaultTabIndex = function() {
    return this.defaultTabIndex;
  };

  this.setDefaultTabIndex = function (i) {
    this.defaultTabIndex = i;
  };

  this.getDefaultId = function() {
    return this.tabList[this.getDefaultTabIndex()].id;
  };

  this.render = function () {
    var that = this, placeHolder = getObj(args.placeHolderId), emptyTd, table = document.createElement('table'), tr;
    if (placeHolder == null) {
      ibeerror('Trying to render tab view to non existing element, id=' + args.placeHolderId);
      return;
    }
    table.id = args.id + 'Table';
    table.className = this.tableCss;
    table.cellSpacing = '0';
    table.cellPadding = '0';
    table.border = '0';
    tr = table.insertRow(0);
    tr.className = this.rowCss;

    if (this.startWithSeparator) {
      emptyTd = tr.insertCell(-1);
      emptyTd.className = this.separatorCss;
    }

    for (var i = 0, l = this.tabList.length; i < l; i++) {
      if (i > 0) {        // Add empty
        emptyTd = tr.insertCell(-1);
        emptyTd.className = this.separatorCss;
      }
      var tab = this.tabList[i];
      var tdL = tab.leftCss || this.leftCss ? tr.insertCell(-1) : undefined;
      var tdM = tr.insertCell(-1);
      var tdR = tab.rightCss || this.rightCss ? tr.insertCell(-1) : undefined;
      if (tdL) {
        if (!tab.leftCss) tab.leftCss = this.leftCss;
        if (!tab.leftCssSelected) tab.leftCssSelected = this.leftCssSelected;
        tdL.id = args.id + '_Button_' + i + '_Left';
        $(tdL).bind('click', {that:that, tabIndex:i}, that.selectTabEvent);
      }
      tdM.id = args.id + '_Button_' + i + '_Middle';
      if (!tab.middleCss) tab.middleCss = this.middleCss;
      if (!tab.middleCssSelected) tab.middleCssSelected = this.middleCssSelected;
      $(tdM).bind('click', {that:that, tabIndex:i}, that.selectTabEvent);
      if (tab.getUrl() && args.useHref) {
        tdM.innerHTML = '<a href="' + tab.getUrl() + '">' + tab.getTitle() + '</a>';
      } else {
        tdM.innerHTML = '<div class="middleTabDiv">' + tab.getTitle() + '</div>';
      }
      if (tdR) {
        if (!tab.rightCss) tab.rightCss = this.rightCss;
        if (!tab.rightCssSelected) tab.rightCssSelected = this.rightCssSelected;
        tdR.id = args.id + '_Button_' + i + '_Right';
        $(tdR).bind('click', {that:that, tabIndex:i}, that.selectTabEvent);
      }
      // Store td nodes.
      tab.tdLeft = tdL;
      tab.tdMiddle = tdM;
      tab.tdRight = tdR;
      // Appends 'first' and 'last' css class to the tabs.
      if (i == 0) {
        tab.leftCss += ' first';
        tab.leftCssSelected += ' first';
        tab.middleCss += ' first';
        tab.middleCssSelected += ' first';
        tab.rightCss += ' first';
        tab.rightCssSelected += ' first';
      } else if (l > 1 && i === (l - 1)) {
        tab.leftCss += ' last';
        tab.leftCssSelected += ' last';
        tab.middleCss += ' last';
        tab.middleCssSelected += ' last';
        tab.rightCss += ' last';
        tab.rightCssSelected += ' last';
      }
      tab.index = i;
      tab.unselectTab(true); // Activate unselected CSS
    }


    while (placeHolder.firstChild) placeHolder.removeChild(placeHolder.firstChild);
    placeHolder.appendChild(table);
    disableSelection(table);
    this.selectTabWithIndex(this.getDefaultTabIndex(), args.runCallbackAtRender, undefined, true);
  };

  /**
   * @param ev is an jQuery event.
   */
  this.selectTabEvent = function (ev) {
    var data = ev.data, doCallback = true;
    if (data === undefined || (data && data.tabIndex === undefined)) {
      ibewarning("Trying trigger tab select event, without any data passed along with the event.", ev);
    } else {
      data.that.selectTabWithIndex(data.tabIndex, doCallback);
    }
  };

  this.getSelectedTabIndex = function() {
    return this.selectedTabIndex;
  };

  this.setSelectedTabIndex = function(index) {
    this.selectedTabIndex = index;
  };

  /**
   * This function is run when a user clicks on a tab. If callback returns false, selectOk will be false and tab switch
   * will be cancelled.
   * @param index The index of the tab that has been clicked.
   * @param doCallback Defines whether we should run the callback or not. When selected default tab at render, this is
   * false.
   * @param forceSelect If true, we ignore callback result and switch tab regardless.
   * @param forceRender Forces the update of tabs CSS classes, regardless of anything else.
   */
  this.selectTabWithIndex = function (index, doCallback, forceSelect, forceRender) {
    if (index === undefined) {
      ibewarning("Trying to select a tab with index, but index is undefined");
    } else {
      if (index !== this.getSelectedTabIndex()) {
        var indexToUnselect = this.getSelectedTabIndex(), tab = this.tabList[index];
        if (tab) {
          var selectOk = tab.selectTab(doCallback, forceRender);
          if (selectOk || forceSelect) {
            this.setSelectedTabIndex(index);
            if (indexToUnselect >= 0 && indexToUnselect !== this.getSelectedTabIndex()) {
              this.tabList[indexToUnselect].unselectTab();
            }
          }
        } else {
          ibewarning('Trying to select tab which doesn\'t exist, index=' + index);
        }
      }
    }
  };

  this.selectTabWithId = function (id, doCallback, forceRender) {
    var i = this.getTabIndexWithId(id);
    if (i != null) {
      this.selectTabWithIndex(i, doCallback, forceRender);
    }
  };

  this.selectTabWithTitle = function (title, doCallback, forceRender) {
    var i = this.getTabIndexWithTitle(title);
    if (i != null) {
      this.selectTabWithIndex(i, doCallback);
    }
  };

  this.tabCount = function() {
    return this.tabList.length;
  };

  this.hasTabs = function() {
    return this.tabCount() > 0;
  };

  this.hasMoreThanOneTab = function() {
    return this.tabCount() > 1;
  };
}
YAHOO.namespace("ibe");

function lookupProgressMessage(codeOrText, defaultText) {
  var isCode = codeOrText.indexOf('ProgressMessage.') == 0 || (codeOrText.indexOf('.') != -1 && codeOrText.indexOf(' ') == -1);
  var resolvedMessage = isCode ? UiText.get(codeOrText) : codeOrText;
  if (resolvedMessage == codeOrText && resolvedMessage && resolvedMessage.indexOf('ProgressMessage.') == 0) {
    return defaultText;
  }
  return resolvedMessage;
}

function openProgressPanel(message, header) {
  var content = document.getElementById("progressDiv");
  content.innerHTML = '';
  var def_header = pp_def_header;
  var def_message = pp_def_message;
  var def_loading_im = pp_def_loading_im;
  var def_loading_swf = pp_def_loading_swf;
  var def_loading_swf_h = pp_def_loading_swf_h;
  var def_loading_swf_w = pp_def_loading_swf_w;
  var def_hasCloseIcon = pp_def_hasCloseIcon;

  message = message ? lookupProgressMessage(message, def_message) : def_message;
  header = header ? lookupProgressMessage(header, def_header) : def_header;

  // Initialize the temporary Panel to display while waiting for external content to load
  YAHOO.ibe.progressPanel = new YAHOO.widget.Panel("wait", {
    width: "540px",
    fixedcenter: true,
    close: def_hasCloseIcon,
    draggable: true,
    zindex:4,
    modal: true,
    visible: false
  });
  YAHOO.ibe.progressPanel.setHeader(header);

  var d_content = '<img src="' + def_loading_im + '"/>';

  // Does NOT work!
  if (def_loading_swf && def_loading_swf_w && def_loading_swf_h) {
    d_content = '<iframe frameborder="0" width="' + def_loading_swf_w + '" height="' + def_loading_swf_h + '" src="' + def_loading_swf + '"></iframe>';
    //'<div id="apansson"></div>' +
    //            '<script language="javascript">flash_component(def_loading_swf, def_loading_swf_w, def_loading_swf_h, def_loading_im,undefined,"apansson");</script>';
  }

  YAHOO.ibe.progressPanel.setBody('<div class="progressInfo">' +
                                  '<div class="progressImage">' + d_content + '</div>' +
                                  '<div class="progressText">' + message + '</div>' +
                                  '</div>');
  YAHOO.ibe.progressPanel.render(document.body);
  // Show the Panel
  YAHOO.ibe.progressPanel.show();

}


/**
 * Shows the progress panel. Is used by the doSearch(..) function.
 *
 * Both arguments are optional and has defaults.
 */
function showProgressPanel(message, header) {
  // showHide('mainContentArea', false);
  openProgressPanel(message, header);
  scroll(0, 0);
}

/**
 * Hides the progress panel. It defaults to hidden.
 */
function hideProgressPanel() {
  if (YAHOO.ibe && YAHOO.ibe.progressPanel) {
    YAHOO.ibe.progressPanel.hide();
  }
  // showHide('mainContentArea', true);
}

/**
 * Use as an onClick-parameter to a submit button to use the progress panel when form is submitted.
 * A correct validation function (JavaScript) must be supplied.
 * @param form The form being submitted
 * @param validation_result The validation result for the form, e.g. "validateForm_air_search_do()".
 * @param formQueueSubmit (optinal) when provided will submit the form on the event queue to let the progress message render (if form is valid).
 * @returns The result of the validation. 0 is failed validation, 1 is OK.
 */
function formProgress(form, validation_result, message, header, formQueueSubmit) {
  if (validation_result) {
    var showPanel = true;
    if (progressToTransition && form && form.action && transitionURL) {
      var transUrl = getFieldFromForm(form, 'TransitionUrl');
      if (transUrl && transUrl.value) {
        if (typeof ol_transitionURL === 'string') {
          form.action = ol_transitionURL;
        } else {
          form.action = transitionURL;
        }
        showPanel = false;
      }
    }
    if (showPanel) {
      showProgressPanel(message, header);
    }
    /* Attempt at giving time to render by putting submit action on event queue. (YAHOO progress panel is also queued).*/
    if (formQueueSubmit && formQueueSubmit.submit) {
      setTimeout(function() {
        formQueueSubmit.submit();
      }, 1);
      return false;
    }
  }
  return validation_result;
}

/**
 * Displays the progress panel and then forwards to new link.
 * @param l Link to forward browser to.
 */
function linkProgress(l, message, header) {
  showProgressPanel(message, header);
  window.location = l;
}

/**
 * Does NOT Display the progress panel, then forwards to new link.
 * @param l Link to forward browser to.
 */
function linkPlain(l) {
  window.location = l;
}

function link(url, newWindow) {
  if (newWindow) {
    window.open(url, '_blank').focus();
  } else {
    linkPlain(url);
  }
}
//
// Requires the ui-dynamics.jsp to be loaded.
//
// h_* for helpers.
//
// 
// All Validation functions are prefixed with 'v_' and has this spec:
//
// v_${functionName}(${ matching form element }, ${ ValidationErrors instance }, ${ fieldNamePrefix or '' }){
//
//   // If required add errors not trapped by default message here.
//   // Return true if the default message is NOT wanted.
//   validationErrors.addError(element, uiKey);
//
//   return true;   // if OK
//   return false;  // if NOT OK
// }
//

// Helper Methods. for validation

/**
 * Finds ONE value for given field.
 *
 *  - If multiselect, first selected.
 *  - If Checkbox, the value of it, not necessarily "true".
 *  - If radio, the first checked value of radios with the same name as the one given.
 *
 * Special treatment for nice default behaviour:
 *  - if given element is an array of Radio:s, get its checked value if any
 *  - if given element is an array of Option:s, get value for first selected if any
 *
 * @param theField to get Value for
 *
 * @return not null value, default is empty string.
 */
function h_getValue(theField) {
  // Return empty if the Field is not valid of any reason
  if (!theField) return '';
  var t = theField.type;
  var v = '';
  if (!t) {
    if (h_isRadioArray(theField)) {
      // Radio
      v = h_getRadioValue(theField);
    } else {
      if (theField.selected && theField.index >= 0) {
        // Option for Multiselect
        v = theField.value;
      }
    }
    return v;
  }

  //
  // Figure out the value of the current field
  if (t == "text" || t == "number" || t == "email" || t == "tel") {
    v = theField.value;
  } else {
    if (t == "textarea") {
      v = theField.value;
    } else {
      if (t == "hidden") {
        v = theField.value;
      } else {
        if (t == "select-one") {
          v = h_getSelectValue(theField);
        } else {
          if (t == "select-multiple") {
            v = h_getSelectValue(theField);
          } else {
            if (t == "checkbox") {
              if (theField.checked) {
                v = theField.value;
              }
            } else {
              if (t == "radio") {
                v = h_getRadioValue(theField);
              } else {
                if (t == "password") {
                  v = theField.value;
                } else {
                  if (t == "button") {
                    v = theField.value;
                  } else {
                    if (t == "submit") {
                      v = theField.value;
                    } else {
                      if (t == "reset") {
                        v = theField.value;
                      } else {
                        if (t == "file") {
                          v = theField.value;
                        } else {
                          // If we gotten here,we do not handle the type
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }

  return v ? v : '';
}

// Gets the selected value in a select-one form-element
function h_getSelectValue(select) {
  if (!select)return '';
  var o = select.options;
  if (o) {
    var l = o.length;
    for (var i = 0; i < l; i++) {
      if (o[i].selected) {
        return o[i].value;
      }
    }
  }
  return "";
}

// Gets the checked value of a RadioGroup
function h_getRadioValue(rdo) {
  var elems;
  if (h_isRadioArray(rdo)) {
    elems = rdo;
  } else {
    return h_getCheckedRadioValue(rdo.form, rdo.name);
  }
  var l = elems.length;
  var v = '';
  for (var i = 0; i < l; i++) {
    if (elems[i].checked) {
      v = elems[i].value;
      break;
    }
  }
  return v;
}

function h_getCheckedRadioValue(f, n) {
  if (!f || !n || !f.elements) return null;
  var e = f.elements;
  var l = e.length;
  for (var j = 0; j < l; j++) {
    if (e[j].name == n && e[j].checked) {
      return e[j].value;
    }
  }
  return null;
}

function h_isRadioArray(arr) {
  return arr && arr.length > 0 && arr[0].type == 'radio';
}

// Gets the checked values of a set of Checkboxes with the same name
function h_getCheckboxValues(cb) {
  var vals = new Array();
  if (h_isCheckboxArray(cb)) {
    cb = cb[0];
  }
  if (cb.type == 'checkbox') {
    var elems = cb.form.elements;
    for (var i = 0; i < elems.length; i++) {
      if (elems[i].name == cb.name &&
          elems[i].type == 'checkbox' &&
          elems[i].checked) {
        vals[vals.length] = elems[i].value;
      }
    }
  }
  return vals;
}

function h_isCheckboxArray(arr) {
  return arr && arr.length > 0 && arr[0].type == 'checkbox';
}

function h_elemFocus(e) {
  if (e && e.focus && e.type != "hidden") {
    try {
      e.focus();
    } catch (err) {}
  }
}

function h_elemError(e) {
  if (e && e.style && e.type != "hidden") {
    e.style.border = 'solid red 1px;';
    e.hadError = true;
  }
}

function h_elemReset(e) {
  if (e && e.style && e.type != "hidden" && e.hadError) {
    e.style.border = 'solid #cacaca 1px;';
  }
  e.hadError = false;
}

function h_trim(str) {
  if (!str) { return ''; }
  if (typeof str != "string") { return ''; }
  if (!str.replace) { return ''; }
  if (!str.length) { return ''; }
  return str.replace(/^\s+|\s+$/g, "");
}

function h_hasValue(str) {
  var v = h_trim(str);
  return v && v.length > 0;
}

function h_keepDigits(str) {
  if (!str) { return ''; }
  if (typeof str != "string") { return ''; }
  if (!str.replace) { return ''; }
  if (!str.length) { return ''; }
  return str.replace(/[^\d]/g, "");
}


// validates alphabetic string (can only contain letters and space)
function h_isAlphabeticString(alphaString, minLen) {
  if (!minLen) minLen = 1;
  var re = WebConstants.REGEXP_JS_ALPHABETIC_STRING;
  var trimmedStr = h_trim(alphaString);
  return trimmedStr.length >= minLen && re.test(trimmedStr);
}


// validates numeric string (can only contain numbers and space)
function h_isNumericString(numString) {
  var re = /^[-]?[\d\s]+$/;
  return re.test(numString);
}

// validates alphabetic and numeric string (can only contain letters, numbers and space)
function h_isAlphaNumericString(alphaNumString, minLen) {
  if (!minLen) minLen = 1;
  var re = WebConstants.REGEXP_JS_ALPHANUMERIC_STRING;
  var trimmedStr = h_trim(alphaNumString);
  return trimmedStr.length >= minLen && re.test(trimmedStr);
}

// Splits on last dot. unless dot, first returned ard in the array is ''.
function h_resolveFieldNameParts(field) {
  var name = field.name;
  var idxOfDot = name.lastIndexOf('.');
  if (idxOfDot >= 0) {
    var namePrefixPart = name.substring(0, idxOfDot + 1);
    var nameSuffixPart = name.substring(idxOfDot + 1, name.length);
    return [namePrefixPart, nameSuffixPart];
  }
  return ['', name];
}

function h_toNum(value) {
  if (h_isNumericString(value)) {
    return new Number(value);
  }
  return new Number(-1);
}

// All Dates are yyyy-mm-dd
function h_toDate(s) {
  if (s) {
    var parts = s.split('-');

    if (parts && parts.length == 3) {
      var dateParts = [-1, -1, -1];
      for (var i = 0; i < 3; i++) {
        dateParts[i] = h_toNum(parts[i]);
        if (dateParts[i] == -1) return null;
      }
      return new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
    }
  }
  return null;
}

/**
 * @param d1 earliest date
 * @param d2 latest date
 *
 * @return -1 if unparsable or non-dates
 */
function h_daysUntil(d1, d2) {
  if (d1 && d2 && d1.getTime && d2.getTime) {
    var d1UTC = new Date(Date.UTC(d1.getFullYear(), d1.getMonth(), d1.getDate(), 0, 0, 0));
    var d2UTC = new Date(Date.UTC(d2.getFullYear(), d2.getMonth(), d2.getDate(), 0, 0, 0));
    var dm = d2UTC.getTime() - d1UTC.getTime();
    return parseInt('' + (dm / h_ms(1)));
  }
  return -1;
}

function h_ms(off) {
  return(off * 60 * 60 * 24 * 1000);
}

function h_m(d) {
  return(d.getMonth() < 9) ? "0" + (1 + d.getMonth()) : (1 + d.getMonth()) + ""
}

function h_d(d) {
  return(d.getDate() < 10) ? "0" + (0 + d.getDate()) : (0 + d.getDate()) + ""
}

function h_y(d) {
  return(d.getFullYear() + "");
}

function h_date2String(d) {
  if (d) {
    return h_y(d) + '-' + h_m(d) + '-' + h_d(d);
  }
  return null;
}

function h_matchesRegExp(pattern, s) {
  return pattern.test(s);
}

function h_checkExpiryDate(month, year) {
  var now = new Date();
  var currentMonth = now.getMonth() + 1;
  var currentYear = now.getFullYear();

  if (month < 1 || 12 < month) return false;
  if (year < currentYear) return false;

  return year > currentYear || month >= currentMonth;
}

function h_minlength(str, minLen) {
  if (!minLen) minLen = 1;
  return str && str.length >= minLen;
}

function h_maxlength(str, maxLen) {
  return !str || str.length <= maxLen;
}

// ***********************************************************************************
//
// Validation methods
//
// ***********************************************************************************

function v_maxlength(field, maxLen) {
  var value = h_getValue(field);
  return h_maxlength(value, maxLen);
}

// Element As arguments
function v_required(field) {
  var value = h_getValue(field);
  var minLength = 1;
  return value && h_minlength(value, minLength);
}

function v_enabled(field) {
  var value = h_getValue(field);
  return !field.disabled;
}

function v_orderNumber(field) {
  var value = h_getValue(field);
  return value && h_minlength(value, 6);
}

function v_isAlphabetic(field) {
  var value = h_getValue(field);
  return value && h_isAlphabeticString(value);
}

function v_isAlphaNumeric(field) {
  var value = h_getValue(field);
  return value && h_isAlphaNumericString(value);
}

function v_isNumeric(field) {
  var value = h_getValue(field);
  if (value && value.length > 0) {
    return h_isNumericString(value);
  }
  return false;
}


function v_toNum(field) {
  return h_toNum(h_getValue(field));
}


function v_toDate(field) {
  var value = h_getValue(field);
  var date = h_toDate(value);
  if (date && value && value == h_date2String(date)) return date;
  return null;
}


function v_isPhoneFax(field) {
  var value = h_getValue(field);
  return (value ? isValidPhoneNumber(value) : false);
}

function v_isEmail(field) {
  var value = h_getValue(field);
  if (value && value.length > 0) {
    return isValidEmail(value);
  }
  return false;
}

function v_isEmailList(field) {
  var value = h_getValue(field);
  if (value && value.length > 0) {
    var parts = value.split(',');
    if (parts) {
      for (var i = 0; i < parts.length; i++) {
        if (!isValidEmail(parts[i])) {
          return false;
        }
      }
      return parts.length > 0;
    }
  }
  return false;
}

function v_isAddress(field) {
  return v_required(field) && v_maxlength(field, 32);
}

function v_isTitle(field) {
  if (field.type != 'select-one') return true;
  var r = v_enabled(field) ? v_required(field) : true;
  return r;
}

function v_isTown(field) {
  return v_required(field) && v_maxlength(field, 32);
}

function v_isZipCode(field) {
  if (WebConstants.ALLOW_LETTERS_IN_ZIPCODE) {
    return v_required(field) && v_maxlength(field, 10);
  }
  return v_isNumeric(field) && v_maxlength(field, 10);
}

function v_isCountry(field) {
  return v_isAlphabetic(field) && v_maxlength(field, 32);
}

//checking if the customer has accepted the travel conditions
function v_isTravelCond(field) {
  if (field && !field.checked) {
    return false;
  }
  return true;
}

// checks paytype element
function is_validatePaytype(field) {
  return v_isAlphabetic(field);
}

function v_isCVVCode(field) {
  if (field.type != 'text') return true;
  return v_isNumeric(field);
}

function v_isEmailVerify(field, validationErrors, prefix) {
  var thisEmail = h_getValue(field);
  var firstEmail = h_getValue(validationErrors.getFirstFormField('email', prefix));
  return firstEmail.toUpperCase() == thisEmail.toUpperCase();
}

function v_isCityWithCode(field, validationErrors, prefix) {
  var value = h_getValue(field);
  if (h_minlength(value, 2)) {
    validationErrors.debug('v_isCityWithCode : field ' + field.name + ' is Alpha: ' + value, 2);
    var cityCodeElem = validationErrors.getFirstFormField(field.name + 'Code', '');
    validationErrors.debug('v_isCityWithCode : cityCodeElem: ' + cityCodeElem, 2);
    var code = h_getValue(cityCodeElem);
    if (h_minlength(code, 2)) {
      return code.length == 3;
    }
    return value.length > 1; // Allow also empty codes.
  }
  return false;
}

function v_isCityWithId(cityElem, validationErrors, prefix) {
  var value = h_getValue(cityElem);
  if (h_minlength(value, 2)) {
    validationErrors.debug('v_isCityWithId : field ' + cityElem.name + ' is Alpha: ' + value, 2);
    var cityIdElem = validationErrors.getFirstFormField(cityElem.name.replace('cityName', 'city') + 'Id', '');
    validationErrors.debug('v_isCityWithId : cityIdElem: ' + cityIdElem, 2);
    if (v_isNumeric(cityIdElem)) {
      return true;
    }
    return value.length > 1; // Allow also empty id.
  }
  return false;

}

// checking we don't get too many children per adult.
// checks the selects/drop downs for children. user
// must choose an age if she has chosen to include children.
function v_validatePaxAndChildAges(field, validationErrors, prefix) {
  var maxSeats = 9;
  var noChildren = v_toNum(field);
  var noAdults = v_toNum(validationErrors.getFirstFormField('adults', prefix));
  var noInfants = 0;

  for (var i = 0; i < noChildren; i++) {
    var ageName = 'childRefs[' + i + '].age';
    //var ageElem = validationErrors.getFirstFormField(ageName, prefix);
    var ageElem = validationErrors.getFirstFormField(prefix + ageName);
    validationErrors.debug('ageElem   : ' + ageElem, 2);
    var age = v_toNum(ageElem);
    validationErrors.debug(ageName + '  : ' + age, 2);
    if (age < 0) {
      return false;
    } else {
      if (age < 2) {
        noInfants++;
        noChildren--;
      }
    }
  }
  validationErrors.debug('noChildren : ' + noChildren, 2);
  validationErrors.debug('noAdults   : ' + noAdults, 2);
  validationErrors.debug('noInfants  : ' + noInfants, 2);

  // invalid no of adults and children
  if (noAdults + noChildren > maxSeats) {
    validationErrors.addError(ageElem, UiText.get('Javascript.Validation.MaxNumSeats') +
                                       maxSeats +
                                       UiText.get('Javascript.Validation.Pieces'));
    return true;
  } else {
    if (noInfants > noAdults) {
      validationErrors.addError(ageElem, 'Javascript.Validation.MaxNumInfantsAdult');
      return true;
    }
  }
  return true;
}

// also see v_validatePaxAndChildAges
function v_validateComboNumPassengers(field, ve, prefix) {
  if (!ve.getFirstFormField('fromCity', 'air.')) return true;// if hotel only
  var maxSeats = " 9 ";
  var numberOfRooms = (+h_getValue(field));
  var adultPrefix = 'adult';
  var childPrefix = 'child';
  var infantPrefix = 'infant';
  var nAdult = 0, nChild = 0, nInfant = 0;

  for (var v = 0; v < numberOfRooms; v++) {
    nAdult += (+h_getValue(getObj(adultPrefix + v)));
    var numChildrenInRoom = (+h_getValue(getObj(childPrefix + v)));
    nChild += numChildrenInRoom;
    for (var i = 0; i < numChildrenInRoom; i++) {
      if ((+h_getValue(getObj(infantPrefix + v + i))) < 2) {
        nInfant++;
        nChild--;
      }
    }
  }

  if (nAdult + nChild > (+maxSeats)) {
    ve.addError(field, UiText.get('Javascript.Validation.MaxNumSeats') +
                       maxSeats +
                       UiText.get('Javascript.Validation.Pieces'));
    return true;
  } else {
    if (nInfant > nAdult) {
      ve.addError(field, 'Javascript.Validation.MaxNumInfantsAdult');
      return true;
    }
  }
  return true;
}

function v_validateAirDates(field, ve, prefix) {
  var depDate = v_toDate(field);
  ve.debug('depDate: ' + depDate);
  if (!depDate) return false;

  var firstPossibleDate = v_toDate(ve.getFirstFormField('firstPossibleDate', prefix));
  if (h_daysUntil(firstPossibleDate, depDate) < 0) {
    ve.addError(field, UiText.get('Air.Search.EarlyTripToDate', h_date2String(firstPossibleDate)));
    return true; // Indicate error message was added
  }

  var lastPossibleDate = v_toDate(ve.getFirstFormField('lastPossibleDate', prefix));
  if (h_daysUntil(depDate, lastPossibleDate) < 0) {
    ve.addError(field, UiText.get('Air.Search.LateTripToDate', h_date2String(lastPossibleDate)));
    return true; // Indicate error message was added
  }

  var oneway = ve.getFirstFormField('oneway', prefix);
  var oneWayVal = h_getValue(oneway);
  var isOneWay = oneWayVal == 'true';
  if (isOneWay) return true;

  var retDateElem = ve.getFirstFormField('retDate', prefix);
  var retDate = v_toDate(retDateElem);
  if (!retDate) {
    ve.addError(retDateElem, 'Air.Search.InvalidReturnTripDate');
    return true; // Indicate error message was added
  }
  if (h_daysUntil(depDate, retDate) < 0) {
    ve.addError(retDateElem, 'Air.Search.ReturnTripDateBeforeTripToDate');
    return true; // Indicate error message was added
  }

  return true;
}

function v_validateHotelDates(field, ve, prefix) {
  var checkIn = v_toDate(field);

  var checkOutElem = ve.getFirstFormField('checkOut', prefix);
  var checkOut = v_toDate(checkOutElem);

  var useAirDatesElem = ve.getFirstFormField('partialHotelDate', 'hotel.');
  var useAirDates = null;
  if (useAirDatesElem) {
    useAirDates = h_getValue(ve.getFirstFormField('partialHotelDate', 'hotel.')) != 'true';
    if (useAirDates) {
      field = ve.getFirstFormField('depDate', 'air.');
      checkOutElem = ve.getFirstFormField('retDate', 'air.');
      checkOut = v_toDate(checkOutElem);
      checkIn = v_toDate(field);
    }
  }

  ve.debug('checkIn: ' + checkIn);
  ve.debug('checkOut: ' + checkOut);
  if (!checkIn) return false;

  var firstPossibleDate = v_toDate(ve.getFirstFormField('firstPossibleDate', prefix));
  if (h_daysUntil(firstPossibleDate, checkIn) < 0) {
    ve.addError(field, UiText.get('Hotel.Search.EarlyCheckInDate', h_date2String(firstPossibleDate)));
    return true; // Indicate error message was added
  }

  var lastPossibleDate = v_toDate(ve.getFirstFormField('lastPossibleDate', prefix));
  if (h_daysUntil(checkIn, lastPossibleDate) < 0) {
    ve.addError(field, UiText.get('Hotel.Search.LateCheckOutDate', h_date2String(lastPossibleDate)));
    return true; // Indicate error message was added
  }

  if (!checkOut) {
    ve.addError(checkOutElem, 'Hotel.Search.InvalidCheckOutDate');
    return true; // Indicate error message was added
  }
  if (h_daysUntil(checkIn, checkOut) < 1) {
    ve.addError(checkOutElem, 'Combo.Search.CheckOutDateBeforeCheckInDate');
    return true; // Indicate error message was added
  }

  if (h_daysUntil(checkIn, checkOut) > 30) {
    ve.addError(checkOutElem, 'Hotel.Search.TooManyNights');
    return true; // Indicate error message was added
  }

  var depDateElem = ve.getFirstFormField('depDate', 'air.');
  var retDateElem = ve.getFirstFormField('retDate', 'air.');
  if (depDateElem && !useAirDates) { // Combo
    var depDate = v_toDate(depDateElem);
    var retDate = v_toDate(retDateElem);
    var oneway = h_getValue(ve.getFirstFormField('oneway', 'air.')) == 'true';
    ve.debug('oneway: ' + oneway);
    if (depDate) {
      ve.debug('h_daysUntil(depDate, checkIn): ' + h_daysUntil(depDate, checkIn));
      if (h_daysUntil(depDate, checkIn) < 0) {
        ve.addError(field, 'Combo.Search.CheckInDateBeforeDepartureDate');
        return true; // Indicate error message was added
      }
    }
    if (!oneway && retDate) {
      ve.debug('h_daysUntil(checkOut, retDate): ' + h_daysUntil(checkOut, retDate));
      if (h_daysUntil(checkOut, retDate) < 0) {
        ve.addError(field, 'Combo.Search.CheckOutDateAfterReturnDate');
        return true; // Indicate error message was added
      }
    }
  }

  return true;
}

/**
 *  This function checks that the first guest name for each room is unique.
 *  Ex: If room 1 has first guest named KALLE kula and room 3 has first guest named kalle KULA: we indicate error situation.*/
function v_validateFirstNameAllRooms(validationErrors) {
  var firstNameSuffix = "firstName";
  var lastNameSuffix = "lastName";
  var uniqueSelector = "[0]\.";
  var findFirstRoomFirstName = uniqueSelector + firstNameSuffix;
  var elems = validationErrors.getFormFields(firstNameSuffix, null, false);
  var i, names = [];
  for (i = 0; i < elems.length; i++) {
    var parts = elems[i].name.split(findFirstRoomFirstName);
    if (parts.length > 1) {
      var firstName = h_trim(h_getValue(elems[i]));
      var lastName = h_trim(h_getValue(validationErrors.getFormFields(parts[0] + uniqueSelector + lastNameSuffix, null, false)));
      names[names.length] = {"name":(firstName + lastName).toLowerCase(), "field":elems[i]};
    }
  }
  var k;
  do {
    var first = names.shift();
    for (k = 0; k < names.length; k++) {
      if (first.name === names[k].name) {
        validationErrors.addError(names[k].field, 'Javascript.Validation.SameNameRestriction');
        return true;// continue with other validators (most likely v_validateAnyNameSameRestriction)
      }
    }
  } while (names.length > 0);

  return true;
}

/**
 * Checks that all names are unique.
 * @param validationErrors */
function v_validateAnyNameSameRestriction(validationErrors) {
  var firstNameSuffix = "firstName";
  var lastNameSuffix = "lastName";
  var elems = validationErrors.getFormFields(firstNameSuffix, null, false);
  var i, names = [];
  for (i = 0; i < elems.length; i++) {
    var parts = elems[i].name.split(firstNameSuffix);
    if (parts.length > 1) {
      var firstName = h_trim(h_getValue(elems[i]));
      var lastName = h_trim(h_getValue(validationErrors.getFormFields(parts[0] + lastNameSuffix, null, false)));
      names[names.length] = {"name":(firstName + lastName).toLowerCase(), "field":elems[i]};
    }
  }
  var k;
  do {
    var first = names.shift();
    for (k = 0; k < names.length; k++) {
      if (first.name === names[k].name) {
        validationErrors.addError(names[k].field, 'Javascript.Validation.AnySameNameRestriction');
        return true;
      }
    }
  } while (names.length > 0);

  return true;
}

function v_validateCarPickupCity(field, validationErrors) {
  var value = h_getValue(field);
  if (h_minlength(value, 2)) {
    var cityIdElem = validationErrors.getFirstFormField(field.name.replace('pickupCityName', 'city') + 'Id', '');
    validationErrors.debug('v_isCityWithId : cityIdElem: ' + cityIdElem, 2);
    if (v_isNumeric(cityIdElem)) {
      return true;
    }
    return value.length > 2; // Allow also empty.
  }
  return false;
}

function v_validateCarDates(field, ve, prefix) {
  var pickupDate = v_toDate(field);
  ve.debug('pickupDate: ' + pickupDate);
  if (!pickupDate) return false;

  var firstPossibleDate = v_toDate(ve.getFirstFormField('firstPossibleDate', prefix));
  if (h_daysUntil(firstPossibleDate, pickupDate) < 0) {
    ve.addError(field, UiText.get('Car.Search.EarlyPickupToDate', h_date2String(firstPossibleDate)));
    return true; // Indicate error message was added
  }

  var lastPossibleDate = v_toDate(ve.getFirstFormField('lastPossibleDate', prefix));
  if (h_daysUntil(pickupDate, lastPossibleDate) < 0) {
    ve.addError(field, UiText.get('Car.Search.LatePickupToDate', h_date2String(lastPossibleDate)));
    return true; // Indicate error message was added
  }

  var returnDateElem = ve.getFirstFormField('returnDate', prefix);
  var returnDate = v_toDate(returnDateElem);
  if (!returnDate) {
    ve.addError(returnDateElem, 'Car.Search.InvalidReturnDate');
    return true; // Indicate error message was added
  }
  if (h_daysUntil(pickupDate, returnDate) < 0) {
    ve.addError(returnDateElem, 'Car.Search.ReturnDateBeforePickupDate');
    return true; // Indicate error message was added
  }

  return true;
}


function v_combinedCarShortCircuit(validationErrors) {
  var doValidation = getObj('doReturnCityValidationId'), oneway;

  if (!doValidation) return true;

  oneway = validationErrors.getFirstFormField('usePickupAsReturn', '');

  return oneway && !oneway.checked;
}

function v_validateCombinedCarReturnCity(field, validationErrors) {
  if (v_combinedCarShortCircuit(validationErrors)) return true;
  var value = h_getValue(field);
  if (h_minlength(value, 2)) {
    var cityIdElem = validationErrors.getFirstFormField(field.name.replace('returnCityName', 'returnCityId'), '');
    validationErrors.debug('v_isCityWithId : cityIdElem: ' + cityIdElem, 2);
    if (v_isNumeric(cityIdElem)) {
      return true;
    }
    return value.length > 2; // Allow also empty.
  }
  return false;
}

function v_validateCombinedCarDate(field, ve, prefix) {
  if (v_combinedCarShortCircuit(ve)) return true;
  //console.log(["date"])
  /*var pickupDate = v_toDate(field);
   ve.debug('pickupDate: ' + pickupDate);
   if (!pickupDate) return false;

   var firstPossibleDate = v_toDate(ve.getFirstFormField('firstPossibleDate', prefix));
   if (h_daysUntil(firstPossibleDate, pickupDate) < 0) {
   ve.addError(field, UiText.get('Car.Search.EarlyPickupToDate', h_date2String(firstPossibleDate)));
   return true; // Indicate error message was added
   }

   var lastPossibleDate = v_toDate(ve.getFirstFormField('lastPossibleDate', prefix));
   if (h_daysUntil(pickupDate, lastPossibleDate) < 0) {
   ve.addError(field, UiText.get('Car.Search.LatePickupToDate', h_date2String(lastPossibleDate)));
   return true; // Indicate error message was added
   }*/

  var returDateElem = ve.getFirstFormField('returnDate', prefix);
  var returnDate = v_toDate(returDateElem);
  if (!returnDate) {
    //console.log(["d", returDateElem, returnDate])
    ve.addError(returDateElem, 'Combined.Car.Search.InvalidReturnDate');
    return true; // Indicate error message was added
  }

  var firstPossibleDate = v_toDate(ve.getFirstFormField('firstPossibleDate', prefix));
  if (h_daysUntil(firstPossibleDate, returnDate) < 0) {
    //console.log("f")
    ve.addError(field, UiText.get('Combined.Car.Search.EarlyReturnToDate', h_date2String(firstPossibleDate)));
    return true; // Indicate error message was added
  }

  var lastPossibleDate = v_toDate(ve.getFirstFormField('lastPossibleDate', prefix));
  if (h_daysUntil(returnDate, lastPossibleDate) < 0) {
    //console.log("l")
    ve.addError(field, UiText.get('Combined.Car.Search.LateReturnToDate', h_date2String(lastPossibleDate)));
    return true; // Indicate error message was added
  }

  /*  if (h_daysUntil(pickupDate, returnDate) < 0) {
   ve.addError(returDateElem, 'Car.Search.ReturnDateBeforePickupDate');
   return true; // Indicate error message was added
   }*/

  return true;
}

function v_validateDriverSelected(field, ve, prefix) {
  var drivers = field.form["selectedDriverIndex"];
  if (drivers.length == undefined) drivers = [drivers];
  for (var i = 0; i < drivers.length; i++) {
    if (drivers[i].checked) return true;
  }
  ve.addError(field, UiText.get('Combined.Traveller.NoDriverSelected'));
  return true;
}

function v_validateCVC(cvcElem, ve, prefix) {
  var value = h_getValue(cvcElem);
  return value && v_isNumeric(cvcElem) && h_minlength(value, 3);
}

function v_validatePaymentMethod(methodElem, ve, prefix) {
  var method = h_getValue(methodElem), elem;
  // alert(method);
  ve.debug('payment-method: \'' + method + '\'' + ', id: ' + methodElem.id);
  if (method == 'BANK') {
    elem = ve.getFirstFormField('code', 'bank.');
    if (!v_required(elem)) {
      ve.addError(elem, 'Javascript.Validation.InvalidBank');
    }
    return true;
  } else if (method == 'CARD') {
    elem = ve.getFirstFormField('cardType', 'creditCard.');
    if (!elem || !v_required(elem)) {
      ve.addError(elem, 'Javascript.Validation.InvalidCardType');
    }

    elem = ve.getFirstFormField('cardNumber', 'creditCard.');
    if (elem) { // Only for payment providers supporting sending CC details
      if (!v_validateCreditCard(elem, ve, 'creditCard.')) {
        ve.addError(elem, 'Javascript.CC.InvalidCardNum');
      }

      elem = ve.getFirstFormField('cvcCode', 'creditCard.');
      if (!elem || !v_validateCVC(elem, ve, 'creditCard.')) {
        ve.addError(elem, 'Javascript.Validation.InvalidCVVCode');
      }

      var monthElem = ve.getFirstFormField('expiryMonth', 'creditCard.');
      var yearElem = ve.getFirstFormField('expiryYear', 'creditCard.');
      if (monthElem && yearElem) {
        var month = v_toNum(monthElem);
        var year = v_toNum(yearElem);
        if (!h_checkExpiryDate(month, year)) {
          ve.addError(monthElem, 'Javascript.CC.InvalidExpiryDate');
        }
      }
    }
    return true;
  } else if (method == 'PAY_INTERNAL') {
    if (!v_required(methodElem)) {
      ve.addError(methodElem, 'Javascript.Validation.InvalidPaymentMethod');
    }
    return true;
  } else if (method == 'INSTALMENT') {
    if (!v_required(methodElem)) {
      ve.addError(methodElem, 'Javascript.Validation.InvalidPaymentMethod');
    } else {
      // Add validation.
    }
    return true;
  } else if (method == 'INVOICE') {
    if (!v_required(methodElem)) {
      ve.addError(methodElem, 'Javascript.Validation.InvalidPaymentMethod');
    } else {
      // Add validation.
    }
    return true;
  } else {
    ve.addError(methodElem, 'Javascript.Validation.InvalidPaymentMethod');
    return true;
  }
}

function v_validateCreditCard(cardElem, ve, prefix) {
  var cardNumber = h_getValue(cardElem);
  var re = /^[0-9]{13,16}$/i;
  if (cardNumber.length == 0 || !h_matchesRegExp(re, cardNumber))  return false;

  if (true) {
    var prefixes =
      [ "5019", "4571", "402005", "4711", "541303", "512586", "5100",
        "361480", "3040", "3747", "3700", "676927", "5020", "417500",
        "3528", "600722"];
    var i = 0;
    OUTER:
      for (; i < prefixes.length; i++) {
        if (cardNumber.indexOf(prefixes[i]) == 0) {
          var rest = cardNumber.substring(prefixes[i].length);
          var firstChar = rest.charAt(0);
          if (firstChar == '0' || firstChar == '1') {
            var len = (rest.length - 1);
            for (var x = 1; x < len; x++) {
              if (rest.charAt(x) != '0') {
                continue OUTER;
              }
            }
            return true;
          }
        }
      }
  }
  var sum = 0;
  var mul = 1;
  var l = cardNumber.length;
  for (i = 0; i < l; i++) {
    var digit = cardNumber.substring(l - i - 1, l - i);
    var tproduct = parseInt(digit, 10) * mul;
    if (tproduct >= 10) {
      sum += (tproduct % 10) + 1;
    } else {
      sum += tproduct;
    }
    if (mul == 1) {
      mul++;
    } else {
      mul--;
    }
  }

  if (!(sum % 10) == 0) return false;

  return true;
}

function v_paxFirstName(field, ve, prefix) {
  var firstNameFieldVal = h_getValue(field);

  if (v_enabled(field) &&
      firstNameFieldVal &&
      h_isAlphabeticString(firstNameFieldVal) &&
      h_minlength(firstNameFieldVal, 2)) {
    var maxLength = 60;

    var lastNameElem = ve.getFirstFormField('lastName', prefix);
    var titleElem = ve.getFirstFormField('title', prefix);
    var lastNameFieldVal = h_getValue(lastNameElem);
    var titleFieldVal = h_getValue(titleElem);

    var paxtup = firstNameFieldVal + lastNameFieldVal + titleFieldVal;
    var isOk = (paxtup.length <= maxLength);

    if (!isOk) {
      ve.addError(field, 'Javascript.Util.PaxNameTooLong');
      return true; // Indicate error message was added
    }
    return h_maxlength(firstNameFieldVal, 60);
  }
  return v_enabled(field) ? false : true;
}

function v_paxLastName(field, ve, prefix) {
  var lastNameFieldVal = h_getValue(field);
  if (v_enabled(field) &&
      lastNameFieldVal &&
      h_isAlphabeticString(lastNameFieldVal) &&
      h_minlength(lastNameFieldVal, 2)) {
    return h_maxlength(lastNameFieldVal, 60);
  }
  return v_enabled(field) ? false : true;
}

function v_paxFirstNames(field, ve, prefix) {
  var firstNameFieldVal = h_getValue(field);
  if (v_enabled(field) &&
      firstNameFieldVal &&
      h_isAlphabeticString(firstNameFieldVal) &&
      h_minlength(firstNameFieldVal, 2)) {
    var maxLength = 60;

    var lastNameElem = ve.getFirstFormField('lastName', prefix);
    var titleElem = ve.getFirstFormField('title', prefix);
    var lastNameFieldVal = h_getValue(lastNameElem);
    var titleFieldVal = h_getValue(titleElem);

    var paxtup = firstNameFieldVal + lastNameFieldVal + titleFieldVal;
    var isOk = (paxtup.length <= maxLength);

    if (!isOk) {
      ve.addError(field, 'Javascript.Util.PaxNameTooLong');
      return true; // Indicate error message was added
    }
    return h_maxlength(firstNameFieldVal, 60);
  }
  return v_enabled(field) ? false : true;
}

function v_paxLastNames(field, ve, prefix) {
  var lastNameFieldVal = h_getValue(field);
  if (v_enabled(field) &&
      lastNameFieldVal &&
      h_isAlphabeticString(lastNameFieldVal) &&
      h_minlength(lastNameFieldVal, 2)) {
    return h_maxlength(lastNameFieldVal, 60);
  }
  return v_enabled(field) ? false : true;
}

function v_birthDay(field, ve, prefix) {
  var yearElem = ve.getFirstFormField('birthYear', prefix);
  var monthElem = ve.getFirstFormField('birthMonth', prefix);
  var statedAge = h_getValue(ve.getFirstFormField('statedAge', prefix));
  var title = h_getValue(ve.getFirstFormField('title', prefix));
  var year = h_getValue(yearElem);

  //Do not validate the special null birth date fields
  if (yearElem.id.indexOf('birthYear_null') != -1) {
    return true;
  }

  if ((yearElem.disabled && monthElem.disabled) || !title) {
    return true;
  }

  var month = h_getValue(monthElem);
  var monthNum = h_toNum(month);
  var day = h_getValue(field);
  var givenDate = monthNum == -1 ? null : (h_toDate(year + '-' + (monthNum + 1) + '-' + day));

  ve.debug('givenDate: ' + givenDate, 2);
  ve.debug('monthNum: ' + monthNum, 2);
  ve.debug('year: ' + year, 2);
  ve.debug('day: ' + day, 2);

  var firstPossibleDate = v_toDate(ve.getFirstFormField('firstPossibleBirthDate', prefix));
  var lastPossibleDate = v_toDate(ve.getFirstFormField('lastPossibleBirthDate', prefix));

  var isAdult = (title && (title == 'MR' || title == 'MS'));

  if (firstPossibleDate) {
    if ((new Date().getFullYear() - firstPossibleDate.getFullYear()) > 20) {
      isAdult = true;
    }
  }

  if (!givenDate) {
    ve.addError(field, isAdult ? 'Javascript.Validation.AdultDateOfBirthRequired' : 'Input.Validation.ChildAgesRequired');
    //ve.addError(field, isAdult ? 'Javascript.Validation.AdultDateOfBirthRequired' : 'Input.Validation.ChildAgesRequired');
    return true; // Indicate error message was added
  }

  ve.debug('firstPossibleDate: ' + firstPossibleDate, 2);
  ve.debug('lastPossibleDate : ' + lastPossibleDate, 2);

  if (firstPossibleDate && lastPossibleDate) { // For some reason not calculated correctly
    ve.debug('h_daysUntil(firstPossibleDate/' + firstPossibleDate + ', givenDate/' + givenDate + '): ' + h_daysUntil(firstPossibleDate, givenDate), 2);
    ve.debug('h_daysUntil(lastPossibleDate/' + lastPossibleDate + ', givenDate/' + givenDate + '): ' + h_daysUntil(lastPossibleDate, givenDate), 2);
    if (!isAdult) {
      if (h_daysUntil(firstPossibleDate, givenDate) < 0) {
        ve.addError(monthElem, UiText.get('Javascript.Validation.ChildAgeMismatchTooOld', h_date2String(givenDate), statedAge, h_date2String(firstPossibleDate)));
        return true; // Indicate error message was added
      }
    }
    if (h_daysUntil(lastPossibleDate, givenDate) > 0) {
      ve.addError(monthElem, UiText.get(isAdult ? 'Javascript.Validation.AdultAgeMismatchTooYoung' : 'Javascript.Validation.ChildAgeMismatchTooYoung', h_date2String(givenDate), statedAge, h_date2String(lastPossibleDate)));
      return true; // Indicate error message was added
    }
  }

  return true;
}

function v_travellerSelected(field, ve, prefix) {
  if (!v_enabled(field)) return true;
  return field.selectedIndex > 0;
}

/**
 * Validate checkbox confirming travellers' names are input according to passport.
 * @param inputErrors
 */
function v_validateTravellerNamesConfirmation(validationErrors) {
  var field = validationErrors.getFormFields("travellerNamesConfirmation", null, false);
  if (field) {
    if (!field.checked) {
      validationErrors.addError(field, "Javascript.Validation.TravellerNamesConfirmation");
      return true;
    }
  }
  return true;
}

/*
 <s:hidden name="travellers.travellers[%{#status.index}].firstPossibleBirthDate"
 value="%{#bdContent.firstPossibleBirthDate}"/>
 <s:hidden name="travellers.travellers[%{#status.index}].lastPossibleBirthDate"
 value="%{#bdContent.lastPossibleBirthDate}"/>
 <s:select name="travellers.travellers[%{#status.index}].birthYear"
 list="%{#bdContent.yearOptions}"
 emptyOption="true"/>
 <s:select name="travellers.travellers[%{#status.index}].birthMonth"
 list="%{#bdContent.monthOptions}"
 emptyOption="true"/>
 <s:select name="travellers.travellers[%{#status.index}].birthDay"
 list="%{#bdContent.days}"
 emptyOption="true"/>
 */
//
// Requires these to be loaded:
//  ui-dynamics.jsp
//  formValidatorUtil.js
//
//
// See: http://wiki.etraveli.net/display/Documentation/WebSystem/JavaScriptFormValidation
//

var d_debug_local = false;     // Set to false not to see this in RuntimeEnvironment.DEV
var d_debug_level_local = 1;  // (0-3) 0 == not much, 3 alot.

// Holds the directives.
var ValidationEntry = function (uiKey, validationFunction) {
  this.uiKey = uiKey;
  this.validationFunction = validationFunction;
  this.namePrefix = arguments.length > 2 ? arguments[2] : '';
  this.matches = function (elementName) {
    if (this.namePrefix) {
      return elementName.indexOf(this.namePrefix) == 0;
    }
    return true;
  };
};

var VALIDATION_REGISTRY = {
  // Payment
  'town':new ValidationEntry('Javascript.Validation.InvalidCity', v_isTown),
  'zipCode':new ValidationEntry('Javascript.Validation.InvalidZipCode', v_isZipCode),
  'country':new ValidationEntry('Javascript.Validation.InvalidCountry', v_isCountry),
  'address1':new ValidationEntry('Javascript.Validation.InvalidAddress', v_isAddress),
  'chkTravelCond':new ValidationEntry('Javascript.Validation.InvalidTravelConditions', v_isTravelCond),

  // Below required if in form.
  'personalId':new ValidationEntry('Javascript.Validation.InvalidPersonalId', v_required),
  'province':new ValidationEntry('Javascript.Validation.InvalidProvince', v_required),
  'nationality':new ValidationEntry('Javascript.Validation.InvalidCountry', v_isCountry),

  //  'cardNumber'       : new ValidationEntry('Javascript.CC.InvalidCardNum', v_validateCreditCard),
  //  'cvcCode'          : new ValidationEntry('Javascript.Validation.InvalidCVVCode', v_validateCVC),
  //  'cardType'         : new ValidationEntry('Javascript.Validation.InvalidCardType', v_required),


  'paymentMethod':new ValidationEntry('Javascript.Validation.InvalidPaymentMethod', v_validatePaymentMethod),

  // Kalle Kula / Payment
  'title':new ValidationEntry('Javascript.Validation.InvalidTitle', v_isTitle),
  'firstNames':new ValidationEntry('Javascript.Util.FirstName', v_paxFirstNames),
  'lastNames':new ValidationEntry('Javascript.Util.LastName', v_paxLastNames),
  'firstName':new ValidationEntry('Javascript.Util.FirstName', v_paxFirstName),
  'lastName':new ValidationEntry('Javascript.Util.LastName', v_paxLastName),
  'phone':new ValidationEntry('Javascript.Util.Phone', v_isPhoneFax),
  'cellPhone':new ValidationEntry('Javascript.Validation.InvalidMobile', v_isPhoneFax),
  'email':new ValidationEntry('Javascript.Util.Email', v_isEmail),
  'emailAddressList':new ValidationEntry('Javascript.Util.EmailList', v_isEmailList),
  'emailVerify':new ValidationEntry('Javascript.Util.Emai2', v_isEmailVerify),
  'birthDay':new ValidationEntry('Javascript.Validation.AdultDateOfBirthRequired', v_birthDay),
  'selectedTraveller':new ValidationEntry('Javascript.Validation.TravellerSelectionRequired', v_travellerSelected),

  'recaptcha_response_field':new ValidationEntry('User.Alert.CaptchaFailed', v_required),

  'orderNumber':new ValidationEntry('OrderNumber', v_orderNumber),


  // SearchFormsN
  // Air/Combo
  'fromCity':new ValidationEntry('Javascript.Validation.InvalidOrigin', v_isCityWithCode),
  'toCity':new ValidationEntry('Javascript.Validation.InvalidDestination', v_isCityWithCode),
  'children':new ValidationEntry('Javascript.Validation.InvalidChildAge', v_validatePaxAndChildAges),
  'depDate':new ValidationEntry('Air.Search.InvalidTripToDate', v_validateAirDates),

  // Hotel/Combo
  'cityName':new ValidationEntry('Javascript.Validation.InvalidDestination', v_isCityWithId),
  'checkIn':new ValidationEntry('Hotel.Search.InvalidCheckInDate', v_validateHotelDates),

  // Combo
  'numberOfRooms':new ValidationEntry('Javascript.Validation.InvalidChildAge', v_validateComboNumPassengers),

  // name must start with 'air.', 'air.shit' or 'air.apa[12].shit' will match
  'shit':new ValidationEntry('Javascript.Util.Phone', v_isEmailVerify, 'air.'),

  // Car
  'pickupCityId':new ValidationEntry('Car.PickupStation.NotSelected', v_validateCarPickupCity),
  'pickupDate':new ValidationEntry('Car.Search.InvalidPickupDate', v_validateCarDates),

  // Combined
  'returnCityId':new ValidationEntry('Combined.Car.ReturnStation.NotSelected', v_validateCombinedCarReturnCity),
  'returnDate':new ValidationEntry('Combined.Car.Search.InvalidReturnDate', v_validateCombinedCarDate),
  'selectedDriverIndex':new ValidationEntry('Combined.Traveller.NoDriverSelected', v_validateDriverSelected),

  // For last comma...
  'non-existing-field':new ValidationEntry('*', v_required)
};


/**
 * Holds data about ONE NOT-validated field / or message which means the form is NOT validated.
 *
 * If the first argument is a String, it is used instead of errorKeyOrMessage,
 * in which case errorKeyOrMessage is not read.
 *
 * @param field                The field which was validated
 * @param errorKeyOrMessage    The KEY of the UI-text, or the resolved UI text to display to the user.
 */
var ValidationError = function (field, errorKeyOrMessage) {
  this.field = field;
  this.errorKeyOrMessage = errorKeyOrMessage;
  if (typeof field == 'string') {
    this.errorKeyOrMessage = field;
  }
};

/**
 * Collector of errors. Instantiated when validateIBEForm is called, and used to collect:
 *
 *   - Errors
 *   - Debug messages
 *
 * And has methods to add those and know if there are any so far.
 *
 * @param form The form to be validated
 */
var ValidationErrors = function (form) {
  this.form = form;
  this.errors = [];
  this.debugMessages = [];
  this.firstElementWithError = undefined;

  this.setFirstElementWithError = function (elem) {
    if (!this.firstElementWithError) this.firstElementWithError = elem;
  };

  this.getFirstFormField = function (explicitNameOrSuffix, prefix) {
    return this.getFormFields(explicitNameOrSuffix, prefix, true);
  };

  this.getFormFields = function (explicitNameOrSuffix, prefix, returnFirst) {
    var elems = [];
    for (var i = 0, l = this.form.elements.length; i < l; i++) {
      var elem = this.form.elements[i];
      if (!elem.name || !elem.type) continue;
      if (!prefix && explicitNameOrSuffix == elem.name) {
        this.debug('getFirstFormField(' + explicitNameOrSuffix + ', ' + prefix + ') -> ' + elem.name, 3);
        return elem;
      } else {
        var fieldNameTuple = h_resolveFieldNameParts(elem);
        var fieldNamePrefix = fieldNameTuple[0];
        var fieldNameSuffix = fieldNameTuple[1];
        if (explicitNameOrSuffix == fieldNameSuffix) {
          if (!prefix || prefix == fieldNamePrefix) {
            this.debug('   1 ------ getFirstFormField(' + explicitNameOrSuffix + ', ' + prefix +
                       ') -> (' + fieldNameSuffix + ', ' + fieldNamePrefix + ')', 3);

            if (!returnFirst) {
              elems.push(elem);
            } else {
              return elem;
            }
          }
        }
      }
    }

    if (!returnFirst && elems && elems.length > 0) {
      return elems;
    }

    return null;
  };

  this.addError = function (field, errorKeyOrMessage) {
    var errs = this.errors;
    for (var i = 0; i < errs.length; i++) {
      var validationErr = errs[i];
      if (validationErr.errorKeyOrMessage == errorKeyOrMessage) {
        return;
      }
    }
    errs[errs.length] = new ValidationError(field, errorKeyOrMessage);
  };

  this.debug = function (msg) {
    if (d_debug_local && IBE.debug) {
      if (arguments.length <= 1 || d_debug_level_local >= arguments[1]) {
        this.debugMessages[this.debugMessages.length] = msg;
      }
    }
  };

  this.hasErrors = function () {
    return this.errors.length > 0;
  };

  this.numErrors = function () {
    return this.errors.length;
  };

  this.createMessage = function () {
    var message = '', i;
    if (this.hasErrors()) {
      message += UiText.get('Javascript.ValidationHeader') + ': \n\n';
      for (i = 0; i < this.errors.length; i++) {
        var err = this.errors[i];
        message += (' - ' + UiText.get(err.errorKeyOrMessage) + '' +

                    ((d_debug_local && IBE.debug) ?
                     '\n    -   (Debug: for field: ' +
                     (err.field ?
                      err.field.name :
                      '[no-element]') + ', ' + err.errorKeyOrMessage + ')' : '') +

                    '\n');
      }
    }
    if (d_debug_local && IBE.debug) {
      if (this.debugMessages.length > 0) {
        message += '\n\n';
        message += '-**---D-E-B-U-G---**---------------**-\n';
        for (i = 0; i < this.debugMessages.length; i++) {
          message += ((1 + i) + '. ' + this.debugMessages[i] + '\n');
        }
        message += '-**---------------**---G-U-B-E-D---**-\n';
      }
    }
    return message;
  };
};


/**
 *
 * @param           form The form to validate.
 *
 * @optionalParam   breakAfterFirstError    if true, script will break after first error. Default is false.
 *
 * @optionalParam   addFuncs                Array Of JavaScript Functions To Call After Standard Checks
 *                                          Will not be executed if there are errors.
 *
 *                  The spec of those functions are:
 *
 *                  function(inputErrors){
 *                    // Any errors must be added by calling:
 *                    validationErrors.addError(element-with-error-for-debug || null, uiKeyOrResolvedMessage);
 *
 *                    return true; // If to continue with next function
 *                    return false; // If to break
 *                  }
 * @optionalParam   additionalValidators    Map Of {elemSuffix : ValidationEntry,elemSuffix2 : ValidationEntry}
 *                                          Will overload any pre-registered validator if already registered
 *                                          in central VALIDATION_REGISTRY.
 *
 * @return true If the form was validated, false otherwise.
 *              If false is returned a javascript alert warned the user of the inData which must be corrected.
 */
function validateIBEForm(form, breakAfterFirstError, addFuncs, additionalValidators) {
  var formId = form.id;
  var formAction = form.action;
  var formElements = form.elements;

  var validationErrors = new ValidationErrors(form);
  var alreadyFoundErrorForRule = new Array();
  var i;

  for (i = 0; i < formElements.length; i++) {
    var elem = formElements[i];
    if (!elem.name || !elem.type) continue;
    // if (elem.type == 'hidden') continue;
    // h_elemReset(elem);

    var fieldNameTuple = h_resolveFieldNameParts(elem);
    var fieldNamePrefix = fieldNameTuple[0];
    var fieldNameSuffix = fieldNameTuple[1];

    validationErrors.debug('Checking ' + elem.name + '/' + fieldNamePrefix + '/' + fieldNameSuffix);
    // Only check same suffix Once.

    var validator = VALIDATION_REGISTRY[fieldNameSuffix];

    if (additionalValidators) {
      var olValidator = additionalValidators[fieldNameSuffix];
      if (olValidator && olValidator.uiKey) {
        validator = olValidator;
        validationErrors.debug('Found Overloaded Validator ' + elem.name + '::' + validator.uiKey, 3);
      }
    }

    if (validator && validator.validationFunction) {
      validationErrors.debug('Found Validator ' + elem.name + '::' + validator.uiKey, 3);
      if (!validator.matches(elem.name)) {
        validationErrors.debug('  ...but its not matching ' + elem.name + '::' + validator.namePrefix, 3);
        continue;
      }
      if (!validator.validationFunction(elem, validationErrors, fieldNamePrefix)) {
        validationErrors.setFirstElementWithError(elem);
        // h_elemError(elem);
        validationErrors.debug('Adding Error ' + elem.name + '::' + validator.uiKey, 2);
        validationErrors.debug('    - alreadyFoundErrorForRule[' + fieldNameSuffix + ']: ' + alreadyFoundErrorForRule[fieldNameSuffix], 3);

        if (alreadyFoundErrorForRule[fieldNameSuffix]) continue;
        validationErrors.addError(elem, validator.uiKey);
        alreadyFoundErrorForRule[fieldNameSuffix] = true;
      }
      if (breakAfterFirstError && validationErrors.hasErrors()) {
        validationErrors.debug('<<<<<< Breaking, found error #1 >>>>>>', 1);
        break;
      }
    }
  }

  // Call Any Additional functions.
  if (!validationErrors.hasErrors() && addFuncs && addFuncs.length > 0) {
    for (i = 0; i < addFuncs.length; i++) {
      var func = addFuncs[i];
      validationErrors.debug('Additional Function:\n' + func, 3);
      if (typeof func == 'function') {
        if (!func(validationErrors)) {
          validationErrors.debug('<<<<<< Breaking since function returned != true >>>>>>', 2);
          break;
        }
      }
      if (breakAfterFirstError && validationErrors.hasErrors()) {
        validationErrors.debug('<<<<<< Breaking, found error #2 >>>>>>', 2);
        break;
      }
    }
  }

  if (validationErrors.hasErrors()) {
    validationErrors.debug('NumErrors ' + validationErrors.numErrors());
    if (validationErrors.firstElementWithError) {
      h_elemFocus(validationErrors.firstElementWithError);
    }
    alert(jQuery.trim(validationErrors.createMessage()));
    return false;
  } else {
    if (d_debug_level_local > 0 && validationErrors.debugMessages.length > 0) {
      if (!confirm('OK! but debug:' + validationErrors.createMessage())) return false;
    }
    return true;
  }
}
/**
 * Author: Mattias Andersson
 * Contact: mattias.andersson@etraveli.com or mattias800@gmail.com
 * Last change: 2011-02-xx
 */

var IBESorter = (function() {

  return {
    sort:function(list, sortBy, desc) {
      return this.jsSort(list, sortBy, desc);
    },

    jsSort:function(list, sortBy, desc) {
      if (!list) return list;

      /**
       * NOTE!
       * Comparators must only use the name that is given by sortBy variable.
       * If sortBy = "translatedName", only the member translatedName may be used in the sort.
       * If you need a custom, special-case, supercomparator, define it in your code where you need it.
       * IBESorter is meant to simplify sorting on member variables and should never be used for special cases.
       */
      switch (sortBy) {
        case 'distanceToCenter':
          list.sort(this.comparatorDistanceToCenter);
          break;

        case 'translatedName':
          list.sort(this.comparatorTranslatedName);
          break;

        case 'origin.translatedName':
          list.sort(this.comparatorOriginTranslatedName);
          break;

        case 'price.destination.translatedName':
          list.sort(function(a, b) {
            return IBESorter.compareStringTo(a.price.destination.translatedName, b.price.destination.translatedName);
          });
          break;

        case 'destination.translatedName':
          list.sort(function(a, b) {
            return IBESorter.compareStringTo(a.destination.translatedName, b.destination.translatedName);
          });
          break;

        case 'price.price':
          list.sort(this.comparatorPricePrice);
          break;

        case 'hotelName':
          list.sort(this.comparatorHotelName);
          break;

        case 'name':
          list.sort(function(a, b) {
            return IBESorter.compareStringTo(a.name, b.name);
          });
          break;

        case 'timeInformation.inDate.timestamp':
          list.sort(this.comparatorTimeInformationInDateTimestamp);
          break;

        case 'timeInformation.outDate.timestamp':
          list.sort(this.comparatorTimeInformationOutDateTimestamp);
          break;

        case 'timeInformation.numDays':
          list.sort(this.comparatorTimeInformationNumDays);
          break;

        case 'bookingPrice':
          list.sort(this.comparatorBookingPrice);
          break;

        case 'recommended':
          list.sort(this.comparatorRecommended);
          break;

        case 'starRating':
          list.sort(this.comparatorStarRating);
          break;

        case 'priority':
          list.sort(function(a, b) {
            return a.priority - b.priority;
          });
          break;

        case 'destination':
          list.sort(this.comparatorDestination);
          break;

        case 'checkInDateJs':
          list.sort(this.comparatorCheckInDate);
          break;

        case 'checkOutDateJs':
          list.sort(this.comparatorCheckOutDate);
          break;

        case 'numNights':
          list.sort(this.comparatorNumNights);
          break;

        case 'pricePerPerson':
          list.sort(this.comparatorPricePerPerson);
          break;

        case 'outDateJs':
          list.sort(this.comparatorDepartureDate);
          break;

        case 'returnDateJs':
          list.sort(this.comparatorReturnDate);
          break;

        case 'departureDateTime':
          list.sort(this.comparatorDepartureTime);
          break;

        case 'arrivalDateTime':
          list.sort(this.comparatorArrivalTime);
          break;

        case 'firstSegment.carrier.translatedName':
          list.sort(this.comparatorFirstSegmentCarrierTranslatedName);
          break;

        case 'firstSegment.flight':
          list.sort(this.comparatorFirstSegmentFlight);
          break;

        default:
          list = IBESorter._insertionSort(list, sortBy);
      }

      if (desc) list.reverse();

      return list;
    },

    _bubbleSort:function(list, sortBy, desc) {
      var start = new Date().getTime();
      var swapped = undefined;
      do {
        swapped = false;
        for (var i = 0, l = list.length; i < l - 1; i++) {
          var a = IBEUtil.lookup(sortBy, list[i]);
          var b = IBEUtil.lookup(sortBy, list[i + 1]);

          if (stringIsNumeric(a) && stringIsNumeric(b)) {
            a = parseInt(a);
            b = parseInt(b);
          }

          if (a > b && !desc) {
            IBESorter.toggle(list, i, i + 1);
            swapped = true;
          } else {
            if (a < b && desc) {
              IBESorter.toggle(list, i, i + 1);
              swapped = true;
            }
          }
        }
      } while (swapped);
      var end = new Date().getTime();
      var time = end - start;
      if (time > 100) ibewarning("Bubble sort took " + time + " ms. Create a custom comparator in ibe-components.js that sorts using the property '" + sortBy + "' and it will go a lot faster.");
      return list;
    },

    /**
     * Sorts a list using insertion sort. Is stable!
     * @param list
     * @param sortBy
     * @param desc
     */
    _insertionSort:function(list, sortBy, desc) {
      var start = new Date().getTime();
      if (list !== null && list !== undefined && propertyExists(list.length)) {
        for (var i = 0, temp = 0; i < list.length - 1; i++) {
          for (var j = i; 0 <= j; j--) {
            var c = this.compareObjectsTo(list[j], list[j + 1], sortBy, desc);
            if (c > 0) {
              this.toggle(list, j, j + 1);
            } else {
              break;
            }
          }
        }
      }
      return list;
    },

    /**
     * Sorts a list using insertion sort using a comparator. Is stable!
     * @param list The list to sort
     * @param comparator Name of comparator, or a comparator function.
     */
    _insertionSortWithComparator:function(list, comparator) {
      if (typeof comparator === "string") comparator = this[comparator];
      if (typeof comparator !== "function") ibeerror("IBESorter is trying to run insertion sort with comparator, but comparator is neither a function nor a valid comparator name.")
      var start = new Date().getTime();
      if (list !== null && list !== undefined && propertyExists(list.length)) {
        for (var i = 0, temp = 0; i < list.length - 1; i++) {
          for (var j = i; 0 <= j; j--) {
            var c = comparator(list[j], list[j + 1]);
            if (c > 0) {
              this.toggle(list, j, j + 1);
            } else {
              break;
            }
          }
        }
      }
      var end = new Date().getTime();
      var time = end - start;
      if (time > 100) ibewarning("Insertion sort took " + time + " ms. Create a custom comparator in ibe-components.js that sorts using the property '" + sortBy + "' and it will go a lot faster.");
      return list;
    },

    compareObjectsTo:function(o1, o2, sortBy, desc) {
      var v1 = sortBy ? IBEUtil.lookup(sortBy, o1) : o1;
      var v2 = sortBy ? IBEUtil.lookup(sortBy, o2) : o2;
      return this.compareTo(v1, v2) * (desc ? -1 : 1);
    },

    compareTo:function(v1, v2) {
      var r = undefined;
      if (stringIsNumeric(v1) && stringIsNumeric(v2)) {
        r = v1 - v2;
      } else {
        r = this.compareStringTo(v1, v2);
      }
      return r;
    },

    compareStringTo:function(v1, v2) {
      if (!v1 && !v2) {
        return 0;
      }
      else {
        if (!v1) {
          return 1;
        }
        else {
          if (!v2) {
            return -1;
          }
          else {
            v1 = v1.toLowerCase();
            v2 = v2.toLowerCase();
            var c1 = v1.charCodeAt(0);
            var c2 = v2.charCodeAt(0);
            if (c1 == c2) {
              return this.compareStringTo(v1.substring(1), v2.substring(1));
            } else {
              return c1 - c2;
            }
          }
        }
      }
    },

    toggle:function(list, i1, i2) {
      var tmp = list[i1];
      list[i1] = list[i2];
      list[i2] = tmp;
    },

    isBrowserSortingStable:function() {

      var stableList = [], stableTestCount = 1000, i;
      for (i = 0; i < stableTestCount; i++) stableList.push({priority:0, index:i});
      ibelogs("pre list", stableList);
      stableList.sort(function(a, b) {
        return a.priority - b.priority;
      });
      ibelogs("post list", stableList);
      for (i = 0; i < stableTestCount; i++) if (stableList[i].index != i) return false;
      return true;
    },


    // Comparators for use with array.sort.

    comparatorPricePerPerson:function(a, b) {
      return a.pricePerPerson - b.pricePerPerson;
    },

    comparatorNumNights:function(a, b) {
      return a.numNights - b.numNights;
    },

    comparatorCheckInDate:function(a, b) {
      return a.checkInDateJs - b.checkInDateJs;
    },

    comparatorCheckOutDate:function(a, b) {
      return a.checkOutDateJs - b.checkOutDateJs;
    },

    comparatorDepartureDate:function(a, b) {
      return a.outDateJs - b.outDateJs;
    },

    comparatorReturnDate:function(a, b) {
      return a.returnDateJs - b.returnDateJs;
    },

    comparatorDestination:function(a, b) {
      if (a && b) {
        return IBESorter.compareStringTo(a.destination, b.destination);
      } else {
        return 0;
      }
    },

    comparatorTranslatedName:function(a, b) {
      if (a && b) {
        return IBESorter.compareStringTo(a.translatedName, b.translatedName);
      } else {
        return 0;
      }
    },

    comparatorDistanceToCenter:function(a, b) {
      return a.distanceToCenter - b.distanceToCenter;
    },

    comparatorOriginTranslatedName:function(a, b) {
      return IBESorter.compareStringTo(a.origin.translatedName, b.origin.translatedName);
    },

    comparatorTimeInformationInDateTimestamp:function(a, b) {
      return a.timeInformation.inDate.timestamp - b.timeInformation.inDate.timestamp;
    },

    comparatorTimeInformationOutDateTimestamp:function(a, b) {
      return a.timeInformation.outDate.timestamp - b.timeInformation.outDate.timestamp;
    },

    comparatorTimeInformationNumDays:function(a, b) {
      return a.timeInformation.numDays - b.timeInformation.numDays;
    },

    comparatorBookingPrice:function(a, b) {
      return a.bookingPrice - b.bookingPrice;
    },

    comparatorPricePrice:function(a, b) {
      return a.price.price - b.price.price;
    },

    comparatorStarRating:function(a, b) {
      return a.starRating - b.starRating;
    },

    comparatorRecommended:function(a, b) {
      return b.hasPromo - a.hasPromo;
    },

    comparatorHotelName:function(a, b) {
      return IBESorter.compareStringTo(a.hotelName, b.hotelName);
    },

    comparatorDepartureTime:function(a, b) {
      return a.departureDateTime - b.departureDateTime;
    },

    comparatorArrivalTime:function(a, b) {
      return a.arrivalDateTime - b.arrivalDateTime;
    },

    comparatorFirstSegmentCarrierTranslatedName:function(a, b) {
      return IBESorter.compareStringTo(a.firstSegment.carrier.translatedName, b.firstSegment.carrier.translatedName);
    },

    comparatorFirstSegmentFlight:function(a, b) {
      return IBESorter.compareStringTo(a.firstSegment.flight, b.firstSegment.flight);
    }

  };

})();

var IBETag = function(name, param, html) {
  this.name = name;
  this.param = param;

  this.html = html;
  this.type = "IBETag";
};

function ensureIsIbeTag(tag) {
  return tag.type == "IBETag";
}

/**
 * Empty or undefined list returns true, if not empty, all elements must be of type IBETag. It only detects lists with other stuff than IBETag items.
 * @param tagList
 */
function ensureIsIbeTagList(tagList) {
  if (tagList && tagList.length) {
    for (var i = 0; i < tagList.length; i++) {
      if (!ensureIsIbeTag(tagList[i])) return false;
    }
  }
  return true;
}

var IBEParserContainer = function(args) {

  this.args = args = $.extend({
                                niterConfig:{}
                              }, args);

  this.clipboard = {};
  this.globals = {};
  this.niterConfig = args.niterConfig;

  this.debugMode = false;

  this.contextStack = new Array();

  this.getNiterState = function(niterName) {
    return this.niterConfig[niterName];
  };

  this.addComponentViewToClipboard = function(name, view) {
    if (!name) {
      ibeerror("Trying to add IBEWebComponentView to clipboard of component, but the clipboard name is undefined.");
      return;
    }
    if (!view) {
      ibeerror("Trying to add IBEWebComponentView to clipboard of component, but the view is undefined.");
      return;
    }
    var p = new IBEParser();
    p.setViewWithHtml(view.getView());
    var status = p.buildList();
    if (!status) {
      ibeerror("Trying to set component clipboard using a IBEWebComponentView, but parsing the specified view failed.");
      return;
    }
    this.addTagListToClipboard(name, p.getByteCodeList());
  };

  this.addStringViewToClipboard = function(name, string) {
    if (!name) {
      ibeerror("Trying to add a string to clipboard of component, but the clipboard name is undefined.");
      return;
    }
    if (!string) {
      ibeerror("Trying to add a string to clipboard of component, but the string is undefined.");
      return;
    }
    var view = new IBEWebComponentView();
    view.setViewWithHtml(string);
    this.addComponentViewToClipboard(name, view);
  };

  this.addTagListToClipboard = function(name, tagList) {
    if (!name) {
      ibeerror("Trying to add a byte code list to clipboard of component, but the clipboard name is undefined.");
      return;
    }
    if (!ensureIsIbeTagList(tagList)) {
      ibeerror("Trying to add content to component clipboard, but the content is not a list of only tags.");
      return;
    }
    this.clipboard[name] = tagList;
  };

  /*
   {
   model:m,
   context:c,
   custom:custom
   }
   */
};

var IBEParser = function(component, parserContainer, args) {
  args = $.extend({
                    clipboard:undefined
                  }, args);

  this.type = "IBEParser";

  var startTag = '{%';
  var endTag = '%}';
  var contextPrefix = '#';
  var componentPrefix = '&';

  this.parserContainer = parserContainer;
  if (!this.parserContainer) this.parserContainer = new IBEParserContainer();

  this.component = component; // The component which content is being parsed. So that we can access it from the {% .. %} code.
  this.list = new Array();
  this.view = undefined;
  this.resultList = new Array();
  this.pc = 0;
  this.debug = false;
  this.custom = new Object(); // The user can put custom stuff on this object. Through {% this.custom.xxx = 'xxx'; %}
  this.globals = this.parserContainer.globals; // Share globals between all parsers

  this.scriptFailed = false; // If set to true, further execution of the script will be cancelled. 

  this.componentList = undefined;

  this.itemsPushedToStack = 0;

  this.init = function() {
    if (args.clipboard) this.applyArgumentClipboard(args.clipboard);
  };

  this.setViewWithHtml = function(htmlCode) {
    this.view = htmlCode;
  };

  this.applyArgumentClipboard = function(clipboard) {
    if (clipboard) {
      for (var id in clipboard) {
        var view = clipboard[id];
        if (!view) {
          ibeerror("Specified predefined clipboard for component, but the clipboard content is undefined:" + id);
        } else if (typeof view === "string") {
          this.parserContainer.addStringViewToClipboard(id, view);
        } else if (typeof view.view === "string") {
          this.parserContainer.addComponentViewToClipboard(id, view);
        } else {
          ibewarning("Specified predefined clipboard for component, but the specified clipboard is neither a string or a IBEWebComponentView.");
        }
      }
    }

  };

  this.getNiterState = function(niterName) {
    return this.parserContainer.getNiterState(niterName);
  };

  this.getByteCodeList = function() {
    return this.list;
  };

  this.isDebugMode = function() {
    return this.parserContainer.debugMode;
  };

  this.debugPrint = function(s, s1, s2, s3, s4, s5) {
    // TODO: UGLY! Fix it?
    if (this.isDebugMode()) {
      if (s5) {
        ibelogs(s, s1, s2, s3, s4, s5);
      }
      else if (s4) {
        ibelogs(s, s1, s2, s3, s4);
      }
      else if (s3) {
        ibelogs(s, s1, s2, s3);
      }
      else if (s2) {
        ibelogs(s, s1, s2);
      }
      else if (s1) {
        ibelogs(s, s1);
      }
      else {
        ibelogs(s);
      }

    }
  };

  /**
   *
   * @param view A string containing the view. Not an IBEWebComponentView-object.
   * @param model
   * @param context
   * @param componentList
   * @param args if initStack == false, the model will not be pushed to stack. This is useful for scope push, for if case for example, when the model will remain the same. This prevents us from pushing same model twice.
   */
  this.parse = function(view, model, args) {
    args = $.extend({
                      pushModelToStack:true,
                      childComponents:undefined,
                      context:undefined
                    }, args);

    var context = args.context;
    if (args.pushModelToStack) {
      this.pushContext({
                         model:model,
                         context:context
                       });
    } else {
      this.updateLocalState(); // Must update state of parser
    }
    if (typeof view === "string") {
      this.view = view;
    } else {
      ibeerror("IBEParser.parse() must receive the view as a string.");
      ibelogs("view", view, "typeof view", typeof view);
      ibetrace();
    }
    this.componentList = args.childComponents; // a list of components that can be parsed into the result

    if (args.clipboard) this.applyArgumentClipboard(args.clipboard); // Apply clipboard before interpretation.

    var status = this.buildList();
    if (status) this.interpret();

    this.popAllItemsForParser();

    return this.getResult();
  };

  /**
   * Takes an already parsed token list, ready to be interpreted.
   * @param list
   * @param model
   * @param context
   * @param args
   */
  this.parseList = function(list, model, context, args) {
    args = $.extend({pushModelToStack:true}, args);
    if (args.pushModelToStack) {
      this.pushContext({
                         model:model,
                         context:context
                       });
    } else {
      this.updateLocalState(); // Must update state of parser
    }
    this.list = list;
    this.interpret();

    this.popAllItemsForParser();

    return this.getResult();
  };

  this.popAllItemsForParser = function() {
    while (this.itemsPushedToStack > 0) {
      this.popContext();
    }
  };

  this.dealloc = function() {
  };

  this.interpret = function() {
    this.scriptFailed = false;
    this.pc = 0;
    this.result = '';
    this.resultList = new Array();
    if (this.list !== undefined) {
      while (this.pc < this.list.length && !this.scriptFailed) {
        if (this.debug) ibelog('pc=' + this.pc);
        var e = this.list[this.pc];
        if (e.html) {
          this.addResult(e.html);
        } else {
          if (e.name) {
            this.parseTag();
          } else {
            if (e.html === undefined && e.name === undefined) ibewarning("Empty tag in script syntax list.");
          }
        }
        this.pc++;
      }
    } else {
      ibeerror("Component cannot interpret byte code list, since it is undefined.");
      ibetrace();
    }
  };

  this.parserEval = function(s) {
    return eval(s);
  };

  this.buildList = function() {
    var resi = 0;
    this.list = new Array();
    var view = this.view;
    if (view) {
      for (var i = 0; i < 1000; i++) {
        var i1 = view.indexOf(startTag); // TODO: Replace indexOf with custom find function which works with strings.
        if (i1 < 0) {
          // No more tags
          this.list[resi++] = new IBETag(null, null, view); // HTML code
          break;
        }

        // Sanity check
        var i2 = view.indexOf(endTag);
        if (i2 < i1) {
          ibeerror('Error: End tag found without matching start tag.');
          return false;
        }
        var i1n = view.indexOf(startTag, i1 + 1);
        if (i1n < i2 && i1n != -1) {
          ibeerror('Error: Start tag found inside of other tag.');
          return false;
        }

        // Create HTML tag and script tag.
        var html = view.substring(0, i1);
        if (html) this.list[resi++] = new IBETag(null, null, html); // HTML code
        var tagContent = trim(view.substring(i1 + startTag.length, i2));
        this.list[resi++] = IBEUtil.createTag(tagContent); // Tag

        // Remove the parsed part from the view
        view = view.substring(i2 + endTag.length);

      }
    } else {
      ibeerror("Trying to parse view into byte code list, but the view content is undefined.");
    }

    return true;
  };

  this.printList = function() {
    for (var i = 0; i < this.list.length; i++) {
      var listItem = this.list[i];
      if (listItem.html) ibelog('[' + i + '] html!');
      if (listItem.name) ibelog('[' + i + '] name=' + listItem.name + ' param=' + listItem.param);
    }
  };

  this.hasParserContainer = function() {
    return this.parserContainer ? true : false;
  };

  this.getParserContainer = function() {
    return this.parserContainer;
  };

  this.printStack = function(m) {
    ibelogs(m ? m : "model stack=", this.getContextStack());
  };

  this.addResult = function(r) {
    if (r === undefined || r === null) {
      ibeerror('Error: Rendering result (probably from parser) is undefined.');
    } else {
      this.resultList.push(r);
    }
  };

  this.killExecution = function() {
    this.pc = this.list.length;
  };

  this.getComponent = function() {
    return this.component;
  };

  this.getPane = function() {
    var c = this.getComponent();
    if (c) {
      return c.pane;
    }
    else {
      return undefined;
    }
  };

  this.getPaneId = function() {
    var p = this.getPane();
    if (p) {
      return p._id;
    }
    else {
      return undefined;
    }
  };

  this.getResult = function() {
    return this.resultList.join('');
  };

  this.getContextStack = function() {
    return this.parserContainer.contextStack;
  };

  this.getContextStackSize = function() {
    return this.getContextStack().length;
  };

  this.getModel = function() {
    if (this.getContextStackSize() == 0) return undefined;
    return this.getContextStack().peek().model;
  };

  this.getContext = function() {
    if (this.getContextStackSize() == 0) return {};
    return this.getContextStack().peek().context;
  };

  this.getCustom = function() {
    if (this.getContextStackSize() == 0) return undefined;
    return this.getContextStack().peek().custom;
  };

  this.getParentModel = function() {
    if (this.getContextStack().length > 1) {
      return this.getContextStack()[this.getContextStack().length - 2].model;
    } else {
      return undefined;
    }
  };

  this.getParentContext = function() {
    if (this.getContextStack().length > 1) {
      return this.getContextStack()[this.getContextStack().length - 2].context;
    } else {
      return undefined;
    }
  };

  this.getParentCustom = function() {
    if (this.getContextStack().length > 1) {
      return this.getContextStack()[this.getContextStack().length - 2].custom;
    } else {
      return undefined;
    }
  };

  this.getContextStackItem = function(i) {
    var v = this.getContextStack()[i];
    if (v) {
      return v;
    }
    else {
      return undefined;
    }
  };

  this.getContextStackModel = function(i) {
    var v = this.getContextStack()[i];
    if (v) {
      return v.model;
    }
    else {
      return undefined;
    }
  };

  this.getContextStackItemFromTop = function(i) {
    return this.getContextStack()[this.getContextStackSize() - 1 - i];
  };

  this.getContextStackModelFromTop = function(i) {
    var v = this.getContextStack()[this.getContextStackSize() - 1 - i];
    if (v) {
      return v.model;
    }
    else {
      return undefined;
    }
  };

  this.getContextStackContextFromTop = function(i) {
    var v = this.getContextStack()[this.getContextStackSize() - 1 - i];
    if (v) {
      return v.context;
    }
    else {
      return undefined;
    }
  };

  this.pushModel = function(model) {
    // Push active model to stack, set active model to aModel
    this.pushContext({
                       model:model
                     });
  };

  this.pushContext = function(context) {
    if (!context.custom) context.custom = {};
    if (!context.context) context.context = {};
    this.getContextStack().push(context);
    this.itemsPushedToStack++;
    this.updateLocalState();
  };

  this.peekContext = function() {
    return this.getContextStack().peek();
  };

  this.popContext = function() {
    if (this.getContextStackSize() == 0) {
      ibeerror("Trying to pop from context, but context stack is empty.");
      return undefined;
    }
    var removedModel = this.getContextStack().pop();
    this.itemsPushedToStack--;
    this.updateLocalState();
    return removedModel;
  };

  this.updateLocalState = function() {
    this.model = this.getModel();
    this.parentModel = this.getContextStackModelFromTop(1);
    this.parentParentModel = this.getContextStackModelFromTop(2);
    this.parentParentParentModel = this.getContextStackModelFromTop(3);
    this.parentContext = this.getContextStackContextFromTop(1);
    this.parentParentContext = this.getContextStackContextFromTop(2);
    this.parentParentParentContext = this.getContextStackContextFromTop(3);
    this.context = this.getContext();
    this.custom = this.getCustom();
    this.context.parentModel = this.getParentModel();
    this.context.parentContext = this.getParentContext();
    this.context.parentCustom = this.getParentCustom();
  };

  this.validateTag = function(tag) {
    if (nullOrUndefined(tag)) this.failedValidationResult("Tag is undefined.");
    if (!tag.name) this.failedValidationResult("Tag has no name, tag is empty.");
    if (typeof tag.name !== "string") this.failedValidationResult("Tag name is a number, must be a string.");
    if (typeof tag.name.startsWith !== "function") this.failedValidationResult("Tag name is a string but has no startsWith method, this must be fixed by dev.");
    return {
      result:true
    };
  };

  this.failedValidationResult = function(message) {
    return {
      result:false,
      message:message
    }
  };

  /**
   * Given start index and endTag, this method finds the content between these two tags and endIndex and returns them in an object.
   * @param startIndex
   * @param endTag
   */
  this.findTagContent = function(startIndex, endTag, args) {
    args = $.extend({
                      illegalTags:[],
                      allowNestedTags:true
                    }, args);
    var tag = this.list[startIndex];
    var startTag = tag.name;
    var endIndex = -1;
    var counter = 0;

    for (var i = startIndex + 1, l = this.list.length; i < l; i++) {
      if (this.list[i].name == startTag) {
        if (args.allowNestedTags) {
          counter++;
        } else {
          ibeerror("Found nested tags of type '" + startTag + "' which is illegal. Script parsing cancelled.");
          this.killExecution();
          return {
            error:true,
            endIndex:-1
          };
        }
      }
      if (this.list[i].name == endTag) {
        if (counter == 0) {
          endIndex = i;
          break;
        } else {
          counter--;
        }
      }
      if (args.illegalTags.contains(this.list[i].name)) {
        ibeerror("Found tag of type '" + this.list[i].name + "' in a '" + startTag + "' which is illegal. Script parsing cancelled.");
        this.killExecution();
        return {
          error:true,
          endIndex:-1
        };
      }
    }
    if (counter < 0) {
      ibeerror('"' + startTag + '" statement with missing "' + endTag + '".');
      this.killExecution();
    } else {
      var copiedList = subList(this.list, startIndex + 1, endIndex - startIndex - 1);
      return {
        list:copiedList,
        startIndex:startIndex,
        endIndex:endIndex
      };
    }
    return {
      error:true,
      endIndex:-1
    };
  };

  this.parseTag = function() {
    var v, c, r, tagLowerCase;
    var tag = this.list[this.pc];

    var validation = this.validateTag(tag);
    if (!validation.result) {
      ibewarning("Django tag failed validation: " + validation.message);
      ibewarning("Tag that failed:");
      ibewarning(tag);
      ibewarning("View that failed:");
      ibewarning(this.view.view);
      this.scriptFailed = true;
      return;
    }

    tagLowerCase = tag.name.toLowerCase();

    this.debugPrint("Tag=", tag.name, "Parameter=", tag.param, "Stack=", this.getContextStack());
    this.debugPrint("this.model", this.model);

    switch (tagLowerCase) {

      case 'if':
        // Find endif
        this.debugPrint("If-case, condition=", tag.param);
        var ifIndex = this.pc;
        var endIfIndex = -1;
        var elseIndex = -1;
        var ifCounter = 0;
        for (var i = ifIndex + 1, l = this.list.length; i < l; i++) {
          if (this.list[i].name == 'if') {
            ifCounter++;
          }
          if (this.list[i].name == 'else' && ifCounter == 0) {
            elseIndex = i;
          }
          if (this.list[i].name == 'endif') {
            if (ifCounter == 0) {
              endIfIndex = i;
              break;
            } else {
              ifCounter--;
            }
          }
        }
        if (endIfIndex < 0) {
          ibeerror('Error: "If" statement with missing "endif". ' + "tag name=" + tag.name + "tag param=" + tag.param);
          this.killExecution();
        } else {
          var ifConditionResult = undefined;
          try {
            ifConditionResult = eval(tag.param);
          } catch (e) {
            ibeerror('Error: If condition expression not valid: ' + tag.param + ' Forgot this.model?\nException: ' + e);
          }
          if (ifConditionResult || elseIndex > 0) {
            this.debugPrint("Creating parser for if-case, stack size before=" + this.getContextStackSize());
            var ifParser = new IBEParser(this.component, this.parserContainer);
            var ifList;
            if (ifConditionResult) {
              // Run "if" code
              if (elseIndex > 0) { // has else code
                ifList = subList(this.list, ifIndex + 1, elseIndex - ifIndex - 1);
              } else { // only if, no else
                ifList = subList(this.list, ifIndex + 1, endIfIndex - ifIndex - 1);
              }
            } else {
              if (elseIndex > 0) {
                // Run "else" code
                ifList = subList(this.list, elseIndex + 1, endIfIndex - elseIndex - 1);
              }
            }
            ifParser.parseList(ifList, this.model, this.context, {pushModelToStack:false});
            this.addResult(ifParser.getResult());
            ifParser.dealloc();
            this.debugPrint("Deallocating parser for if-case, stack size after=" + this.getContextStackSize());
          }
          this.pc = endIfIndex;
        }
        this.debugPrint("End of If-case, condition=" + tag.param);
        break;

      case 'else':
        ibeerror('Error: Found "else" with no "if".');
        break;

      case 'endif':
        ibeerror('Error: Found "endif" with no "if".');
        break;

      case 'debug':
        var b = tag.param === "on";
        // If was off, and turning off, display no message in log.
        if (b || this.parserContainer.debugMode) ibelog("Turning " + (b ? "on" : "off") + " debug mode.");
        this.parserContainer.debugMode = b;
        break;

      case 'sort':
        this.debugPrint("Sort, parameter=" + tag.param);
        var s = tag.param.split(" ");
        if (!s || !s.length || s.length !== 2) {
          ibewarning("Sort parameter not valid. First is list to sort, second is name of member to sort by.");
        } else {
          var listToSort = IBEUtil.lookup(s[0], this.model, this.context, this);
          IBESorter.sort(listToSort, s[1]);
        }
        break;

      case 'push':
        this.debugPrint("Push, parameter=" + tag.param);
        var objToPush = IBEUtil.lookup(tag.param, this.model, this.context, this);
        this.pushModel(objToPush);
        // TODO: Do we update context or parentContext?
        break;

      case 'endpush':
        this.debugPrint("End of Push");
        this.popContext();
        break;

      case 'void':
        break;

      case 'throw':
        ibethrow(tag.param);
        break;

      case 'copy':
        // Copy paste
        var clipName = tag.param ? tag.param : "default";
        var f = this.findTagContent(this.pc, "endcopy");
        this.parserContainer.clipboard[clipName] = f.list;
        // XXX: Could remove the tags here, to prevent the copy from happening several times in an iterator.
        // But the current parser is not reused, and the current list is not reused.
        break;

      case 'endcopy':
        break;

      case 'paste':
        clipName = tag.param ? tag.param : "default";
        var list = this.parserContainer.clipboard[clipName];
        var pasteParser = new IBEParser(this.component, this.parserContainer);
        this.addResult(pasteParser.parseList(list, this.model, this.context));
        pasteParser.dealloc();
        break;

      case 'ensurestacksize':
        var size = tag.param;
        var real = this.getContextStackSize();
        if (real != size) {
          ibelogs("Script checked stack size, but size is not matching. Expected=" + size + " Real=" + real);
          ibeerror("Script checked stack size, but size is not matching. Expected=" + size + " Real=" + real);
        }
        break;

      case 'printstack':
        this.printStack(tag.param);
        break;

      case 'global':
        var ps = tag.param.split(" ");
        if (ps.length != 2) ibeerror("global macro takes two parameters, 1st is name of global, 2nd is value to assign.");
        var globalName = ps[0];
        var valueName = removeListHead(ps).join();
        var value;
        if (stringIsNumeric(valueName)) {
          value = valueName;
        } else if (stringContainsAString(valueName)) {
          value = eval(valueName);
        } else {
          value = IBEUtil.lookup(valueName, this.model);
        }
        this.globals[globalName] = value;
        break;

      case "kill":
        this.killExecution();
        break;

      case "iter":
      case "niter":
        var isNiter = tagLowerCase == "niter";
        var hasNiterName = tag.param;

        this.debugPrint("Niter, parameter=" + tag.param);

        var niterName = "";

        if (isNiter && hasNiterName) {
          if (tag.param.indexOf(" ") < 0) {
            // Using top!
            niterName = tag.param;
            tag.param = "";
          } else {
            // Using property on model
            niterName = tag.param.substring(0, tag.param.indexOf(" "));
            tag.param = tag.param.substring(niterName.length + 1);
          }
        }

        f = this.findTagContent(this.pc, isNiter ? "endniter" : "enditer");
        if (f.error) {
          this.killExecution();
          return;
        }

        // Found "endniter", parsing niter body and evaluating everything.
        var iteratorModel;
        if (tag.param) {
          iteratorModel = IBEUtil.lookup(tag.param, this.model, this.context, this);
        } else {
          // If nothing specified, {% iter %} , just use current model
          iteratorModel = this.model;
        }
        var iteratorContext;
        try {
          iteratorContext = IBEUtil.getCloneOfObject(this.context);
        } catch (e) {
          ibeerror("Unable to make clone of iterator context. Object=");
          ibelog(this.context);
          ibelog(e);
        }
        if (iteratorModel) {
          if (typeof iteratorModel.length == "number") {
            var start = 0;
            var end = iteratorModel.length;
            var state = undefined;
            if (isNiter && hasNiterName) {
              state = this.getNiterState(niterName);
            }
            if (state) {
              if (state.usePages) {
                // Example: Display 0-9, 10-19, 20-29, etc.
                state.totalPages = Math.ceil(iteratorModel.length / state.itemsPerPage);
                state.itemsShowing = undefined;
                if (!state.currentPage) state.currentPage = 0;
                start = state.currentPage * state.itemsPerPage;
                end = start + state.itemsPerPage;
              } else {
                // Display 0-9, 0-19, 0-29, etc. 0-n.
                state.currentPage = undefined;
                state.totalPages = undefined;
                if (state.showingAll) {
                  state.itemsShowing = iteratorModel.length;
                }
                end = state.itemsShowing;
              }
              if (end >= iteratorModel.length) {
                if (!state.usePages) state.allItemsAreShowingCallback(this);
                end = iteratorModel.length;
              } else {
                if (!state.usePages) state.notAllItemsAreShowingCallback(this);
              }
            }
            for (i = start; i < end; i++) {
              var iteratorParser = new IBEParser(this.component, this.parserContainer);
              var iteratorListItem = iteratorModel[i];
              iteratorContext = IBEUtil.populateIteratorContext(iteratorContext, iteratorModel, i, this.model, this.context);
              var iteratorResult = iteratorParser.parseList(f.list, iteratorListItem, iteratorContext, {});
              this.addResult(iteratorResult);
              iteratorParser.dealloc();
            }
          } else {
            if (isNiter && hasNiterName) {
              state = this.getNiterState(niterName);
              if (state && !state.usePages) state.allItemsAreShowingCallback(this); // Hide "Show more" buttons.
            }
            ibeerror("Iterator parameter is not a list. typeof=" + typeof iteratorModel);
          }
        } else {
          ibewarning("Specified iterable object is undefined: " + (tag.param ? tag.param : "current model"));
          ibelogs("this.model", this.model, this);
        }
        this.pc = f.endIndex;
        break;

      default:
        // Context stuff
        this.debugPrint("Statement=", tag.name, "parameter=", tag.param);
        if (tag.name.startsWith(contextPrefix)) {
          v = tag.name.substring(1);
          r = IBEUtil.lookupInContextsInContextStack(v, this.getContextStack(), this);
          if (!nullOrUndefined(r)) {
            this.addResult(r);
          } else {
            ibewarning('Trying to use context variable that does not exist: ' + v);
          }
        } else {
          if (tag.name.startsWith(componentPrefix)) {
            v = tag.name.substring(1);
            c = this.findComponent(v);
            if (c && c.resultNode) {
              var node = c.divContent; //resultNode;
              this.addResult(node);
            } else {
              // Component did not exist, allowing this for now. Renders nothing.
            }

            // JS execution
          } else {
            var vs;
            if (tag.name.startsWith('=')) {
              vs = tag.name.substring(1) + (tag.param ? ' ' + tag.param : '');
              try {
                r = this.parserEval(vs);
              } catch (e) {
                ibeerror('Unable to evaluate script tag: ' + vs + '\nException:' + e);
                ibelogs("Model", this.model);
              }
            } else {
              vs = tag.name.substring(1) + (tag.param ? ' ' + tag.param : '');
              if (tag.name.startsWith('@')) {
                if (vs == 'printModel') {
                  if (this.model) this.addResult(nlToBr(printAllToString(this.model)));
                } else {
                  if (vs == 'printContext') {
                    this.addResult(nlToBr(printAllToString(this.context)));
                  } else {
                    if (vs == 'logModel') {
                      ibelog(this.model);
                    } else {
                      if (vs == 'logContext') {
                        ibelog(this.context);
                      } else {
                        if (vs == 'paneId' && this.component && this.component.pane) {
                          this.addResult(this.component.pane._id);
                        } else {
                          if (vs == 'componentId' && this.component) {
                            this.addResult(this.component.id);
                          } else {
                            try {
                              r = this.parserEval(vs);
                              if (r !== undefined) this.addResult(r);
                            } catch (e) {
                              ibeerror('Unable to evaluate script tag: ' + vs + '\nException:' + e);
                              ibelogs("Model", this.model);
                            }
                          }
                        }
                      }
                    }
                  }
                }
                // Lookup
              } else {
                if (this.model == null) {
                  // Allow null models
                  r = "";
                } else {
                  r = IBEUtil.lookup(tag.name, this.model, this.context, this);
                }
                if (r !== undefined) {
                  this.addResult(r);
                } else {
                  ibewarning('Trying to use model variable that does not exist: ' + tag.name);
                  ibelogs("this.model=", this.model);
                }
              }
            }
          }
        }
    }
  };

  this.findComponent = function(id) {
    for (var i = 0, l = this.componentList.length; i < l; i++) {
      var c = this.componentList[i];
      if (c.id == id) return c;
    }
    return null;
  };

  this.init();

};

var IBEUtil = (function() {

  var VAR_SEPARATOR = '.';
  var debugOutput = false;

  return {
    lookup:function(name, model, context, parser) {
      if (debugOutput) ibelogs("lookup", "name", name, "model", model, "context", context, "parser", parser);

      if (typeof name === "object") {
        ibelogs("Trying to value in model, but name of value is an object.");
        ibelogs("name", name);
        ibetrace();
        //return undefined;
      }
      name = "" + name; // Ensure string

      if (this.shouldUseEval(name)) {
        if (name.startsWith("@")) name = name.substring(1);
        return this.lookupByEval(name, parser);
      }

      var result = undefined;
      if (model) result = this.lookupInObject(name, model, {parser:parser});
      if (result === undefined && parser && parser.getContextStackSize()) result = this.lookupInModelsInContextStack(name, parser.getContextStack());
      if (result === undefined) result = this.lookupByEval(name, parser);
      return result;
    },

    lookupInModelsInContextStack:function(name, contextStack, parser) {
      if (contextStack && contextStack.length) {
        for (i = contextStack.length - 1; i >= 0; i--) {
          var r = IBEUtil.lookupInObject(name, contextStack[i].model, {parser:parser});
          if (r !== undefined) return r;
        }
      }
    },

    lookupInContextsInContextStack:function(name, contextStack, parser) {
      if (contextStack && contextStack.length) {
        for (i = contextStack.length - 1; i >= 0; i--) {
          var r = IBEUtil.lookupInObject(name, contextStack[i].context, {parser:parser});
          if (r !== undefined) return r;
        }
      }
    },

    lookupInArray:function(name, modelStack, parser) {
      if (modelStack && modelStack.length) {
        for (i = modelStack.length - 1; i >= 0; i--) {
          var r = IBEUtil.lookupInObject(name, modelStack[i], {parser:parser});
          if (r !== undefined) return r;
        }
      }
    },

    lookupByEval:function(name, parser) {
      try {
        if (parser) {
          return parser.parserEval(name);
        } else {
          return eval(shortName);
        }
      } catch (e) {
        ibewarning('Unable to lookup variable "' + name + '" by eval.');
        ibelog(e);
      }
      return undefined;
    },

    lookupInObject:function(name, model, debugArgs) {
      debugArgs = $.extend({
                             level:0, // Keep track of how many levels down we are. Only for debug purposes.
                             firstName:name, // Keep track of first name specified. Only for debug purposes.
                             firstModel:model, // Keep track of first model specified. Only for debug purposes.
                             currentFullName:"",
                             parser:undefined
                           }, debugArgs);
      if (debugOutput) ibelogs("lookupInObject", "name", name, "model", model);
      if (model === undefined) {
        ibewarning("Trying to lookup variable named '" + name + "' in '" + debugArgs.firstName + "', but '" + debugArgs.currentFullName + "' is undefined. Model=", debugArgs.firstModel);
        if (debugArgs && debugArgs.parser) ibelogs("Stack=", debugArgs.parser.getContextStack());
        return undefined;
      }
      var nnames;
      if (name.indexOf('[') < 0) {
        nnames = name.split(VAR_SEPARATOR);
      } else {
        nnames = splitList(splitList(name.split('['), ']'), VAR_SEPARATOR);
      }
      var names = new Array();
      // Remove "" and '' from keys, and remove empty elements and store new list in names.
      for (var i = 0, l = nnames.length; i < l; i++) if (nnames[i]) names[names.length] = removeFnutts(nnames[i]);

      if (names.length <= 1) {
        // There is only one variable, ex "price"
        return model[name];
      } else {
        // There are recursive variables, ex "price.formattedPrice".
        var newname = implode(removeListHead(names), VAR_SEPARATOR);
        var newmodel = model[names[0]];
        debugArgs.currentFullName += (debugArgs.currentFullName ? "." : "") + names[0];
        debugArgs.level++;
        return this.lookupInObject(newname, newmodel, debugArgs);
      }
    },

    shouldUseEval:function(name) {
      if (!name) {
        ibeerror("Trying evaluate if lookup should use eval, but name is undefined.");
      }
      if (typeof name !== "string") return false;
      if (name.indexOf("(") >= 0 || name.indexOf(")") >= 0) return true;// Contains (), must be a function call! Use eval!
      if (name.startsWith("@")) return true; // Force use of eval
      return false;
    },


    createTag:function(code) {
      var i = code.indexOf(' ');
      var t = new IBETag();
      if (i < 0) {
        t.name = code;
      } else {
        t.name = code.substring(0, i);
        t.param = code.substring(i + 1);
      }
      return t;
    },

    ensureUniqueElementId:function(id) {
      while (getObj(id)) {
        id = id + Math.floor(Math.random() * 10);
      }
      return id;
    },

    getCloneOfObject:function(oldObject, level, name) {
      var tempClone = {};
      for (prop in oldObject) {
        var t = typeof(oldObject[prop]);
        if (t === "string" || t === "number" || t === "undefined" || t === "boolean" || t === "object") {
          // Allow undefined properties, they don't do any harm.
          tempClone[prop] = oldObject[prop];
        } else {
          // No warning message for functions.
          if (t !== "function") ibewarning("Cloning object, skipping property '" + prop + '", invalid type. Typeof=' + t);
        }
      }
      return tempClone;
    },

    getDeepCloneOfObject:function(oldObject, level, name) {
      if (!level) level = 1;
      var levelLimit = 10;
      var tempClone = {};

      if (level > levelLimit) {
        ibewarning("Cloning object is limited to " + levelLimit + " levels.");
        return undefined;
      }

      if (typeof(oldObject) == "object") {
        for (prop in oldObject) {
          if (prop === "__proto__") continue; // Skip prototype stuff
          // for array use private method getCloneOfArray
          if (!nullOrUndefined(oldObject[prop]) && (typeof(oldObject[prop]) == "object") && (oldObject[prop]).__isArray) {
            tempClone[prop] = this.getCloneOfArray(oldObject[prop], level + 1, prop);
            // for object make recursive call to getCloneOfObject
          } else {
            if (typeof(oldObject[prop]) == "object") {
              tempClone[prop] = this.getCloneOfObject(oldObject[prop], level + 1, prop);
              // normal (non-object type) members
            } else {
              var t = typeof(oldObject[prop]);
              if (t === "string" || t === "number" || t === "undefined" || t === "boolean") {
                // Allow undefined properties, they don't do any harm.
                tempClone[prop] = oldObject[prop];
              } else {
                // No warning message for functions.
                if (t !== "function") ibewarning("Cloning object, skipping property '" + prop + '", invalid type. Typeof=' + t);
              }
            }
          }
        }
      }
      return tempClone;
    },

    getCloneOfArray:function(oldArray, level) {
      //private method (to copy array of objects) - getCloneOfObject will use this internally
      var tempClone = [];

      for (var arrIndex = 0, l = oldArray.length; arrIndex <= l; arrIndex++) {
        if (typeof(oldArray[arrIndex]) == "object") {
          tempClone.push(this.getCloneOfObject(oldArray[arrIndex]), level + 1, arrIndex);
        } else {
          tempClone.push(oldArray[arrIndex]);
        }
      }
      return tempClone;
    },

    populateIteratorContext:function(context, list, index, parentModel, parentContext) {
      context.index = index;
      context.count = index + 1;
      context.first = (index == 0);
      context.last = (index == list.length - 1);
      context.even = (index % 2 == 0);
      context.odd = !context.even;
      context.evenEnd = (list.length % 2 == 0 ? context.odd : context.even);
      context.oddEnd = !context.evenEnd;
      context.size = list.length;
      context.parentModel = parentModel;
      context.parentContext = parentContext;
      return context;
    }

  };

})();

var IBEWebComponentManager = (function() {

  var componentList = new Array();

  return {
    addComponent:function(c) {
      var r = this.ensureUniqueId(c.id);
      componentList[componentList.length] = c;
      return r;
    },

    ensureUniqueId:function(id) {
      for (var i = 0, l = componentList.length; i < l; i++) {
        var c = componentList[i];
        if (c.id == id) {
          return this.ensureUniqueId(id + '_r' + Math.floor(Math.random() * 10));
        }
      }
      return id;
    },

    removeComponent:function(c) {
      var i;
      for (i = 0, l = componentList.length; i < l; i++) {
        if (componentList[i] == c) {
          c.remove();
          // Remove hole caused by component removal
          componentList.splice(i, 1);
          break;
        }
      }
    },

    print:function() {
      printAll(componentList);
    },

    getComponentWithId:function(id) {
      for (var i = 0, l = componentList.length; i < l; i++) {
        if (componentList[i].id == id) {
          return componentList[i];
        }
      }
      return null;
    },

    renderMoreOfListWithId:function(id, renderAll) {
      var c = this.getComponentWithId(id);
      c.renderMoreComponents(renderAll);
    }

  };

})();

/**
 * DEPRECATED! DO NOT USE!
 * @param view
 */
function createDynamicIBEWebComponentView(view) {
  ibewarning("You are using createDynamicIBEWebComponentView, it fetches view via AJAX. Please put view in DOM instead.");
  return new IBEWebComponentView("/ajax.component.view.dynamic.action", {view:view});
}
/**
 *
 * @param sourceUrl
 * @param viewParameters format is object literal: {"paramName1":value1, "paramName2": value2 ...}
 */
function IBEWebComponentView(sourceUrl, viewParameters) {
  this.sourceUrl = sourceUrl;
  this.viewParameters = viewParameters;

  if (sourceUrl) ibewarning("Do not use URL's in views. Add them to the DOM using script tags instead.");

  this.view = undefined;

  this.isLoading = false;

  this.componentCallbacks = new Array();

  /**
   * Returns the view as a string containing HTML and Django code.
   */
  this.getView = function() {
    return this.view;
  };

  this.fetchView = function(force) {
    if ((force || this.view === undefined) && this.isLoading === false && this.sourceUrl !== undefined) {
      this.fetchViewFromUrl();
    }
  };

  this.fetchViewFromUrl = function() {
    if (this.isLoading === false && this.sourceUrl !== undefined) {
      this.isLoading = true;
      var url = this.sourceUrl;
      url += url.indexOf('?') == -1 ? '?' : '&';
      if (this.viewParameters) {
        var queryString = [], pName, params = this.viewParameters;
        for (pName in params) {
          if (params.hasOwnProperty(pName)) {
            queryString.push("&"), queryString.push(pName), queryString.push("="), queryString.push(params[pName]);
          }
        }
        url += queryString.join("");
      }

      var that = this;
      $.ajax({
               url:url,
               dataType:'html',
               success:function(response) {
                 that.viewFetchedFromUrl(response);
               },
               error:function(request, status, error) {
                 that.viewFetchedFromUrl("<div></div>");
                 ibeerror("IBEWebComponentView unable to fetch view from URL = " + url);
               }
             });
    }
  };

  /**
   * Decodes important character in script tags that might have been encoded into HTML entities depending on how the view was created in DOM.
   * @param scriptCode
   */
  this.decodeHtmlEntitiesForScriptTags = function(scriptCode) {
    scriptCode = scriptCode.replace(/%7B%%20/g, "{% ");
    scriptCode = scriptCode.replace(/%20%%7D/g, " %}");
    scriptCode = scriptCode.replace(/%7B/g, "{");
    scriptCode = scriptCode.replace(/%7D/g, "}");
    scriptCode = scriptCode.replace(/&amp;/g, "&");
    return scriptCode;
  };

  this.fetchViewFromDOM = function(elementId) {
    this.isLoading = false;
    var e = getObj(elementId);
    if (e) {
      this.view = this.decodeHtmlEntitiesForScriptTags(e.innerHTML);
    } else {
      ibeerror("IBEWebComponentView trying to fetch view from DOM object that is undefined; id = " + elementId);
      ibetrace();
    }
    this.triggerAllCallbacks();
  };

  this.fetchAndRemoveViewFromDOM = function(elementId) {
    this.fetchViewFromDOM(elementId);
    var e = getObj(elementId);
    if (e) {
      var p = e.parentNode;
      if (p) p.removeChild(e);
    }
  };

  this.setViewWithHtml = function(htmlCode) {
    this.view = htmlCode;
    this.triggerAllCallbacks();
  };

  this.addComponentToCallbackList = function(c) {
    this.componentCallbacks[this.componentCallbacks.length] = c;
  };

  this.triggerAllCallbacks = function() {
    if (this.componentCallbacks !== undefined) {
      for (var i = 0, l = this.componentCallbacks.length; i < l; i++) {
        var f = this.componentCallbacks[i];
        if (f) {
          f.render();
        }
      }
    }
    this.componentCallbacks = new Array();
  };

  this.viewFetchedFromUrl = function(view) {
    this.view = view;
    this.triggerAllCallbacks();
  };

}

/**
 * An object that encapsulates iterator commands and state. The iterator state is updated when the component renders.
 * This means, if you update the model (say a list of hotels for example), the getTotalPages() will give you the number of pages for the old list, until you render the component.
 * So, you should updates texts which contains these numbers AFTER you render the component.
 * @param component
 * @param iterName
 * @param args
 */
function IBEIteratorContainer(component, iterName, args) {
  args = $.extend({autoRender:true}, args);
  this.component = component;
  this.iterName = iterName;
  this.state = this.component.getNiterState(iterName);
  this.autoRender = args.autoRender;

  if (!this.state) {
    ibeerror("Trying to get iterator, but there is no iterator with name: " + iterName);
  }

  this._autoRender = function() {
    if (this.autoRender) {
      component.render();
    }
  };

  this.showMoreItems = function() {
    this.state.itemsShowing += this.state.itemsPerPage;
    this._autoRender();
  };

  this.reset = function() {
    this.state.itemsShowing = this.state.itemsPerPage;
    this.state.showingAll = false;
    this.state.currentPage = 0;
    this._autoRender();
  };

  this.showAllItems = function() {
    this.state.showingAll = true;
    this._autoRender();
  };

  this.showNextPage = function() {
    this.state.currentPage++;
    this._autoRender();
  };

  this.showPrevPage = function() {
    this.state.currentPage--;
    this._autoRender();
  };

  this.setItemsPerPage = function(itemsPerPage) {
    if (!this.state.usePages) ibewarning("Trying to set items per page but iterator is not using pages: " + this.iterName);
    this.state.itemsPerPage = itemsPerPage;
    this._autoRender();
  };

  this.setItemsShowing = function(s) {
    if (this.state.usePages) ibewarning("Trying to set items showing, but iterator is using pages: " + this.iterName);
    this.state.itemsShowing = s;
    this._autoRender();
  };

  this.resetItemsShowing = function() {
    if (this.state.usePages) ibewarning("Trying to set items showing, but iterator is using pages: " + this.iterName);
    this.state.itemsShowing = this.state.itemsPerPage;
    this._autoRender();
  };

  this.setCurrentPage = function(p) {
    this.state.currentPage = p;
    this._autoRender();
  };

  this.getCurrentPage = function() {
    return this.state.currentPage;
  };

  this.getTotalPages = function() {
    return this.state.totalPages;
  };

  this.setAllAreShowingCallback = function(f) {
    this.state.allItemsAreShowingCallback = f;
  };

  this.setNotAllAreShowingCallback = function(f) {
    this.state.noAllItemsAreShowingCallback = f;
  };


}

function IBEWebComponent(args) {
  this.args = args = $.extend({
                                id:undefined, // Specify id if you want to
                                view:undefined, // ID of DOM element which innerHTML will become view.
                                viewHtml:undefined, // Specify HTML string to use as view. Overrides view argument.
                                model:undefined,
                                placeHolderId:undefined,
                                renderCallback:undefined,
                                callbackScope:undefined,
                                mode:"insert", // insert, append
                                clipboard:{},
                                beforeRenderAnimation:undefined,
                                beforeRenderAnimationDuration:'slow',
                                afterRenderAnimation:undefined,
                                afterRenderAnimationDuration:'slow',
                                niterConfig:undefined // Settings for the niters
                              }, args);

  var listValues = [args.beforeRenderAnimation, args.afterRenderAnimation], listNames = ['args.beforeRenderAnimation', 'args.afterRenderAnimation'];

  validateArgumentList(listValues, {rules:{allowUndefined:true, requireValueIsOneOf:['fadeIn', 'fadeOut', 'slideDown', 'slideUp', 'hide', 'show']}}, listNames);

  if (typeof args.beforeRenderAnimationDuration === 'string') {
    validateArgument(args.beforeRenderAnimationDuration, {rules:{allowUndefined:false, requireValueIsOneOf:['slow', 'fast', 'normal']}}, 'args.beforeRenderAnimationDuration');
  } else {
    validateArgument(args.beforeRenderAnimationDuration, {rules:{allowUndefined:false, requireNumber:true}}, 'args.beforeRenderAnimationDuration');
  }

  if (typeof args.afterRenderAnimationDuration === 'string') {
    validateArgument(args.afterRenderAnimationDuration, {rules:{allowUndefined:false, requireValueIsOneOf:['slow', 'fast', 'normal']}}, 'args.afterRenderAnimationDuration');
  } else {
    validateArgument(args.afterRenderAnimationDuration, {rules:{allowUndefined:false, requireNumber:true}}, 'args.afterRenderAnimationDuration');
  }

  validateArgument(args.renderCallback, {rules:{allowUndefined:true, requireFunction:true}}, "args.renderCallback");
  validateArgument(args.placeHolderId, {rules:{allowUndefined:true, requireString:true}}, "args.placeHolderId");
  validateArgument(args.mode, {rules:{allowUndefined:false, requireValueIsOneOf:["insert", "append"]}}, "args.mode");
  validateArgument(args.id, {rules:{allowUndefined:true, requireString:true}}, "args.id");

  this.id = args.id;
  this.view = args.view;
  this.viewHtml = args.viewHtml;
  this.model = args.model;
  this.pane = undefined;
  this.placeHolderId = args.placeHolderId;
  this.mode = args.mode;
  this.niterConfig = args.niterConfig;

  var that = this;

  this.shouldRender = false; // If true, applyModel() will run render when done.
  this.isBeingRemoved = false;
  this.isRemoved = false;
  this.hasBeenRendered = false;
  this.storeContentInDivNode = true; // Store in div.innerHTML directly.

  this.context = new Array();
  this.childrenComponents = new Array();
  this.childrenComponentsRendering = undefined;
  this.childrenHasBeenRendered = false;

  this.showLoadingWhileRendering = false;

  this.resultNode = undefined; // A DOM node.

  this.divId = undefined; // Set it when rendering!
  this.divNode = undefined;
  this.divCssClass = undefined;
  this.divContent = undefined;

  this.id = IBEWebComponentManager.addComponent(this);
  this.divCounter = 0;


  this.init = function() {

    // Extend every niter config with default values.
    for (var niterName in this.niterConfig) {
      this.niterConfig[niterName] = $.extend({
                                               itemsShowing:0,
                                               itemsPerPage:10,
                                               usePages:false,
                                               notAllItemsAreShowingCallback:function() {
                                               },
                                               allItemsAreShowingCallback:function() {
                                               }
                                             }, this.niterConfig[niterName]);
      this.niterConfig[niterName].itemsShowing = this.niterConfig[niterName].itemsPerPage;
    }

    // Callbacks

    if (this.viewHtml) {
      this.setViewWithHtml(this.viewHtml);
    } else if (typeof this.view === "string") {
      // Lookup view if typeof == string
      var elementId = this.view;
      this.view = new IBEWebComponentView();
      this.view.fetchViewFromDOM(elementId);
    }
    this.callbackList = new Array();
    this.callbackScopeList = new Array();

    this.addRenderCallback = function(c, s) {
      var i = this.callbackList.length;
      this.callbackList[i] = c;
      this.callbackScopeList[i] = s;
    };

    this.addRenderCallback(args.renderCallback, args.callbackScope);

  };

  this.getGlobals = function() {
    return this.parseContainer.globals;
  };

  this.setModel = function(m) {
    this.model = m;
    this.hasBeenRendered = false;
    this.resultNode = undefined;
  };

  this.getNiterState = function(iterName) {
    return this.niterConfig[iterName];
  };

  this.getModel = function() {
    return this.model;
  };

  this.setViewWithHtml = function(html) {
    this.view = new IBEWebComponentView();
    this.view.setViewWithHtml(html);
  };

  /**
   * Returns an iterator object which allows you to control the iterator and gets its state.
   * @param iterName
   * @param args autoRender, if true, the component will automatically rerender when iterator changes state.
   */
  this.getIterator = function(iterName, args) {
    args = $.extend({autoRender:true}, args);
    return new IBEIteratorContainer(this, iterName, args)
  };

  this.applyModel = function(args) {
    args = $.extend({renderAfter:false}, args);
    if (this.view === undefined) {
      ibeerror('Error: Trying to apply model to null view.');
    } else {
      if (this.view.view === undefined) {
        this.view.addComponentToCallbackList(this);
        this.view.fetchView();
      } else {
        // Create containing div, ensure unique div element id.
        if (this.divId === undefined) {
          this.divId = this.id;
        }

        while (getObj(this.divId)) {
          this.divId = this.divId + Math.floor(Math.random() * 10);
        }

        this.divNode = document.createElement('div');
        this.divNode.id = this.divId;

        // Apply model
        if (this.model || this.model == null) {
          this.parseContainer = new IBEParserContainer({niterConfig:this.niterConfig});
          var parser = new IBEParser(this, this.parseContainer, {clipboard:this.args.clipboard});

          this.divContent = parser.parse(this.view.view, this.model, {
            context:this.context,
            childComponents:this.childrenComponents
          });

          if (this.storeContentInDivNode) {
            this.divNode.innerHTML = this.divContent;
          }
        } else {
          this.divNode.innerHTML = new String(this.view.view);
        }

        // Save result
        if (this.divCssClass) {
          this.divNode.className = this.divCssClass;
        }
        this.resultNode = this.divNode;

        if (this.shouldRender || args.renderAfter) {
          this.shouldRender = false;
          this._render();
        }
      }
    }
  };

  this.renderChildren = function() {
    this.childrenComponentsRendering = this.childrenComponents.length;
    for (var i = 0, l = this.childrenComponents.length; i < l; i++) {
      this.childrenComponents[i].addRenderCallback(this.childRendered, this);
      this.childrenComponents[i].render();
    }
    // Fetch view as well!
    this.view.fetchView();
  };

  this.childRendered = function() {
    this.childrenComponentsRendering--;
    if (this.childrenComponentsRendering == 0) {
      this.allChildrenRendered();
    }
  };

  this.allChildrenRendered = function() {
    this.childrenHasBeenRendered = true;
    // And render this, now that we have all children ready to be parsed into this component
    this.render();
  };

  this.triggerAllCallbacks = function() {
    for (var i = 0, l = this.callbackList.length; i < l; i++) {
      var c = this.callbackList[i];
      if (typeof c == 'function') {
        var s = this.callbackScopeList[i];
        if (s) {
          c.apply(s);
        } else {
          c();
        }
      }
    }
  };

  this.render = function() {
    this.resultNode = undefined;
    this._render();
  };

  this._render = function() {
    if (this.isRemoved) return; // Do nothing!

    var placeHolder = $('#' + this.placeHolderId), that = this;

    if (args.beforeRenderAnimation !== undefined && args.afterRenderAnimation !== undefined) {
      placeHolder[args.beforeRenderAnimation](args.beforeRenderAnimationDuration, function() {
        that.renderComponent();
        placeHolder[args.afterRenderAnimation](args.afterRenderAnimationDuration);
      });
    } else if (args.beforeRenderAnimation !== undefined) {
      placeHolder[args.beforeRenderAnimation](args.beforeRenderAnimationDuration, function() {
        that.renderComponent();
      });
    } else if (args.afterRenderAnimation !== undefined) {
      that.renderComponent();
      placeHolder[args.afterRenderAnimation](args.afterRenderAnimationDuration);
    } else {
      that.renderComponent(); // default behavior is rendering without any animation.
    }
  };

  /**
   * Does the actual rendering of the component
   */
  this.renderComponent = function() {
    if (this.showLoadingWhileRendering === true) {
      setInnerHtmlToLoadingAnimation(this.placeHolderId);
      // Wait 2 ms so that loading screen has been rendered!
      var that = this;
      setTimeout(function() {
        that.actuallyRender();
      }, 2);
    } else {
      // Just render them immediately.
      this.actuallyRender();
    }
  };

  this.actuallyRender = function() {

    if (this.childrenHasBeenRendered === false && this.childrenComponents.length > 0) {
      // We have children, render them first! When rendering is done, this.render will be called again. Via callbacks, so must return after.
      this.renderChildren();
      return;
    }
    if (this.resultNode) {
      //this.applyModel({renderAfter:false}); // Always update!
      //setInnerHTML(this.placeHolderId, ''); // Clear loading animation? DO NOT USER setInnerHTML! DOM SUCKS! :)
      if (this.placeHolderId) {
        var placeHolder = getObj(this.placeHolderId);
        if (placeHolder) {
          if (this.mode === 'append') {
            placeHolder.appendChild(this.resultNode);
          } else {
            // Remove previous children
            while (placeHolder.firstChild) placeHolder.removeChild(placeHolder.firstChild);
            placeHolder.appendChild(this.resultNode);
          }
        } else {
          ibeerror('Error: Rendered component (id=' + this.id + ') but could not find placeholder (id=' + this.placeHolderId + ').');
        }
      }

      this.hasBeenRendered = true;

      this.triggerAllCallbacks();

    } else {
      // Apply model is asyncronously, so must return the function, and not allow more execution!
      this.applyModel({renderAfter:true});
    }
  };

  this.remove = function() {
    var e = getObj(this.divId);
    if (e) {
      if (!this.isBeingRemoved) {
        this.isBeingRemoved = true; // To prevent stack overflow
        e.parentNode.removeChild(e);
        IBEWebComponentManager.removeComponent(this);
      }
    } else {
      // content div does not exist. might not have been rendered yet.
    }
    this.isRemoved = true;
  };

  this.getResultAsHtmlString = function() {
    return this.resultNode.innerHTML;
  };

  this.getResultAsHtmlNode = function() {
    return this.resultNode;
  };

  this.hide = function() {
    var e = getObj(this.divId);
    if (e) {
      setHidden(this.divId, true);
    } else {
      ibeerror('Error: Trying to hide component with missing or incorrect id=' + this.divId);
    }
  };

  this.show = function() {
    var e = getObj(this.divId);
    if (e) {
      setVisible(this.divId, true);
    } else {
      ibeerror('Error: Trying to show component with missing or incorrect id=' + this.divId);
    }
  };

  this.addChildComponent = function(component) {
    // Ensure no rendering to DOM
    component.placeHolderId = undefined;
    this.childrenComponents.push(component);
  };

  this.getChildComponent = function(id) {
    for (var i = 0, l = this.childrenComponents.length; i < l; i++) {
      var c = this.childrenComponents[i];
      if (c.id == id) return c;
    }
    return null;
  };

  this.init();
}

/**
 * id, view, list, placeHolderId, separatorHtml, renderCallback, callbackScope, mode
 *
 * @param args
 */
function IBEWebComponentList(args) {
  args = $.extend({
                    id:undefined,
                    view:undefined,
                    list:undefined,
                    placeHolderId:undefined,
                    separatorHtml:undefined,
                    renderCallback:undefined,
                    callbackScope:undefined,
                    renderMoreCallback:undefined,
                    renderAnythingCallback:undefined,
                    mode:'insert',
                    showLoadingWhileRendering:false,
                    renderPerPass:10,
                    showMoreText:undefined,
                    showMoreDivCssClass:undefined,
                    showAllTextEnabled:true,
                    clipboard:{},
                    beforeRenderAnimation:undefined,
                    beforeRenderAnimationDuration:'slow',
                    afterRenderAnimation:undefined,
                    afterRenderAnimationDuration:'slow'
                  }, args);

  this.validateArguments = function() {
    var listValues = [args.beforeRenderAnimation, args.afterRenderAnimation], listNames = ['args.beforeRenderAnimation', 'args.afterRenderAnimation'];

    validateArgumentList(listValues, {rules:{allowUndefined:true, requireValueIsOneOf:['fadeIn', 'fadeOut', 'slideDown', 'slideUp', 'hide', 'show']}}, listNames);

    if (typeof args.beforeRenderAnimationDuration === 'string') {
      validateArgument(args.beforeRenderAnimationDuration, {rules:{allowUndefined:false, requireValueIsOneOf:['slow', 'fast', 'normal']}}, 'args.beforeRenderAnimationDuration');
    } else {
      validateArgument(args.beforeRenderAnimationDuration, {rules:{allowUndefined:false, requireNumber:true}}, 'args.beforeRenderAnimationDuration');
    }

    if (typeof args.afterRenderAnimationDuration === 'string') {
      validateArgument(args.afterRenderAnimationDuration, {rules:{allowUndefined:false, requireValueIsOneOf:['slow', 'fast', 'normal']}}, 'args.afterRenderAnimationDuration');
    } else {
      validateArgument(args.afterRenderAnimationDuration, {rules:{allowUndefined:false, requireNumber:true}}, 'args.afterRenderAnimationDuration');
    }
  };

  this.validateArguments();

  this.id = args.id;
  this.view = args.view;
  this.list = args.list;
  this.placeHolderId = args.placeHolderId;
  this.renderCallback = args.renderCallback;
  this.renderMoreCallback = args.renderMoreCallback;
  this.renderAnythingCallback = args.renderAnythingCallback;
  this.callbackScope = args.callbackScope;
  this.separatorHtml = args.separatorHtml;
  this.mode = args.mode;
  this.showLoadingWhileRendering = args.showLoadingWhileRendering;

  this.renderAll = false;

  this.componentList = new Array();

  this.resultNode = undefined;

  this.isLoaded = false;
  this.divId = undefined;
  this.divNode = undefined;

  this.showMoreButtonNode = undefined;
  this.showMoreText = args.showMoreText;
  this.showAllText = undefined;
  this.showAllTextEnabled = args.showAllTextEnabled;

  this.listNode = undefined;
  this.divCssClass = undefined;
  this.childrenDivCssClass = undefined;
  this.divContent = undefined;

  // Progressive rendering in passes
  this.rendersPerPass = args.renderPerPass;
  this.alreadyRendered = 0;
  this.showMoreElementId = undefined;
  this.listDivElementId = undefined;
  this.showMoreDivCssClass = args.showMoreDivCssClass;
  this.shouldClearList = false;

  this.isFirstRender = true;

  this.showMoreLinkClass = "";

  this.id = IBEWebComponentManager.addComponent(this);

  this.componentsRendering = 0;

  // Ensure unique id.
  while (getObj(this.id)) {
    this.id = this.id + Math.floor(Math.random() * 10);
  }

  this.setList = function(list) {
    this.isLoaded = false;
    this.list = list;
    this.componentList = [];
  };

  this.getResultAsHtmlNode = function() {
    return this.resultNode;
  };

  this.getListAsHtmlNode = function() {
    return this.listNode;
  };

  this.getComponentById = function(id) {
    return (this.componentList[this.getComponentIndexById(id)]);
  };

  this.getComponentIndexById = function(id) {
    for (var i = 0, l = this.componentList.length; i < l; i++) {
      if (this.componentList[i].id == id)  return i;
    }
    return null;
  };

  this.getComponentByIndex = function(index) {
    return this.componentList[index];
  };

  this.removeComponentById = function(id) {
    var index = this.getComponentIndexById(id);
    this.removeComponentByIndex(index);
  };

  this.removeComponentByIndex = function(index) {
    this.componentList[index].remove();
    for (var i = index, l = this.componentList.length; i < l; i++) {
      this.componentList[i] = this.componentList[i + 1];
    }
  };

  this.loadComponents = function() {
    if (this.isLoaded === false) {
      this.isLoaded = true;
      for (var i = 0, l = this.list.length; i < l; i++) {
        var e = this.list[i];
        var c = new IBEWebComponent({id:this.id + '_item_' + i,
                                      view:this.view,
                                      model:e,
                                      renderCallback:this.componentHasBeenRendered,
                                      callbackScope:this,
                                      clipboard:args.clipboard
                                    });
        c.storeContentInDivNode = false; // Store only strings, we do not want to parse every single component HTML.
        c.divCssClass = this.childrenDivCssClass;
        // Populate context
        c.context = IBEUtil.populateIteratorContext(c.context, this.list, i);
        /*
         c.context.index = i;
         c.context.count = i + 1;
         c.context.first = (i == 0);
         c.context.last = (i == this.list.length - 1);
         c.context.even = (i % 2 == 0);
         c.context.odd = !c.context.even;
         c.context.size = this.list.length;
         */
        this.componentList[i] = c;
      }
    }
  };

  this.render = function() {

    this.alreadyRendered = 0;

    // Create container
    if (this.divId === undefined) {
      this.divId = this.id;
    }
    this.divId = IBEUtil.ensureUniqueElementId(this.divId);
    this.showMoreElementId = IBEUtil.ensureUniqueElementId(this.divId + '_showmore');
    this.listDivElementId = IBEUtil.ensureUniqueElementId(this.divId + '_list');

    // Create container div
    this.divNode = document.createElement('div');
    this.divNode.id = this.divId;
    if (this.divCssClass) {
      this.divNode.className = this.divCssClass;
    }

    // Create list div
    this.listNode = document.createElement('div');
    this.listNode.id = this.listDivElementId;
    this.divNode.appendChild(this.listNode);

    // Create button div
    this.showMoreButtonNode = document.createElement('div');
    this.showMoreButtonNode.id = this.showMoreElementId;
    if (this.showMoreDivCssClass) {
      this.showMoreButtonNode.className = this.showMoreDivCssClass;
    }

    if (!this.showMoreText) {
      this.showMoreText = UiText.get('Hotel.Result.List.ShowMoreButton.Label') + '&nbsp;&raquo;';
    }
    this.showMoreButtonNode.innerHTML = '<a href="javascript:void(0);" class="' + this.showMoreLinkClass + '" onclick="IBEWebComponentManager.renderMoreOfListWithId(\'' + this.id + '\');">' + this.showMoreText + '</a>';

    if (!this.showAllText) {
      this.showAllText = UiText.get('Result.List.ShowAllButton.Label', 'Display All') + '&nbsp;&raquo;';
    }
    // Display "show all" text iff, it's enabled (default is true).
    if (this.showAllTextEnabled) {
      this.showMoreButtonNode.innerHTML += '&nbsp;&nbsp;|&nbsp; &nbsp;<a href="javascript:void(0);" class="' + this.showMoreLinkClass + '" onclick="IBEWebComponentManager.renderMoreOfListWithId(\'' + this.id + '\', true);">' + this.showAllText + '</a>';
    }

    this.divNode.appendChild(this.showMoreButtonNode);

    // Render list from scratch, so clear anything that was there before.
    this.shouldClearList = true;

    var placeHolder = $('#' + this.placeHolderId), that = this;

    if (args.beforeRenderAnimation !== undefined && args.afterRenderAnimation !== undefined) {
      placeHolder[args.beforeRenderAnimation](args.beforeRenderAnimationDuration, function() {
        that.renderComponents();
        placeHolder[args.afterRenderAnimation](args.afterRenderAnimationDuration);
      });
    } else if (args.beforeRenderAnimation !== undefined) {
      placeHolder[args.beforeRenderAnimation](args.beforeRenderAnimationDuration, function() {
        that.renderComponents();
      });
    } else if (args.afterRenderAnimation !== undefined) {
      that.renderComponent();
      placeHolder[args.afterRenderAnimation](args.afterRenderAnimationDuration);
    } else {
      that.renderComponents(); // default behavior is rendering without any animation.
    }
  };

  this.renderComponents = function() {
    // Show loading animation
    if (this.showLoadingWhileRendering === true) {
      setInnerHtmlToLoadingAnimation(this.placeHolderId);
      // Wait 2 ms so that loading screen has been rendered!
      var thisC = this; // Need to do this to keep the scope of the callback execution.
      setTimeout(function() {
        thisC.renderMoreComponents(this.renderAll);
      }, 2);
    } else {
      // Just render them immediately.
      this.renderMoreComponents(this.renderAll);
    }
  };

  this.renderMoreComponents = function(renderAll) {
    // Trigger rendering of components, as many as will be displayed
    if (this.isLoaded === false) {
      this.loadComponents();
    }

    var listSize = this.list.length;
    if (this.renderAll || renderAll) this.rendersPerPass = listSize;
    var max = this.alreadyRendered + this.rendersPerPass;
    if (max > listSize) max = listSize;
    this.componentsRendering = max - this.alreadyRendered;

    for (var i = this.alreadyRendered; i < max; i++) {
      this.componentList[i].render();
    }
  };

  this.componentHasBeenRendered = function() {
    this.componentsRendering--;
    if (this.componentsRendering == 0) {
      this.allComponentsFinishedRendering();
    }
  };

  this.allComponentsFinishedRendering = function() {
    var placeHolder = getObj(this.placeHolderId);

    if (!placeHolder) {
      ibeerror('Error: No placeholder in DOM for component with id=' + this.id);
      return;
    }
    // Clear loading screen
    if (this.showLoadingWhileRendering === true) {
      while (placeHolder.firstChild) placeHolder.removeChild(placeHolder.firstChild);
    }

    if (this.shouldClearList) {
      this.shouldClearList = false;
      clearElement(placeHolder);
    }
    placeHolder.appendChild(this.divNode);

    this.renderMore();
  };

  /**
   * Renders 10 more of the list.
   */
  this.renderMore = function() {
    var htmlArray = new Array();

    var isFirstRenderPass = this.alreadyRendered === 0; // True if this is the first rendering pass, that is for example, the first 20 in list.
    var isFirstElementInCurrentRenderPass = true; // True if currently rendered item is the first item to be rendered in this pass.
    var elementsRenderedInThisRenderPass = 0; // Amount of elements that have been rendered so far in this pass
    var previousAlreadyRendered = this.alreadyRendered;
    var maxElementsAfterThisRenderPass = this.alreadyRendered + this.rendersPerPass;

    for (var i = this.alreadyRendered, l = this.componentList.length; i < maxElementsAfterThisRenderPass && i < l; i++) {
      var c = this.componentList[i];
      var isFirstElementInTotal = this.alreadyRendered === 0; // Only first if first pass

      if (!isFirstElementInTotal) {
        if (this.separatorHtml) htmlArray.push(this.separatorHtml);
      }
      htmlArray.push(c.divContent);

      elementsRenderedInThisRenderPass++;
      this.alreadyRendered++;
      isFirstElementInCurrentRenderPass = false;
    }

    var htmlResult = htmlArray.join('');
    this.divContent += htmlResult;

    var node = document.createElement('div');
    node.innerHTML = htmlResult;

    this.listNode.appendChild(node);


    // Hide/show the "Show more"-button
    if (this.alreadyRendered == this.componentList.length) {
      this.hideShowMoreButton();
    } else {
      this.showShowMoreButton();
    }

    if (isFirstRenderPass === true) {
      if (typeof this.renderCallback === "function") {
        this.renderCallback.apply(this.callbackScope);
      }
      if (typeof this.renderAnythingCallback === "function") {
        this.renderAnythingCallback.apply(this.callbackScope);
      }
      this.isFirstRender = false;
    } else {
      if (this.renderMoreCallback) {
        this.renderMoreCallback.apply(this.callbackScope, [previousAlreadyRendered, elementsRenderedInThisRenderPass]);
      }
      if (typeof this.renderAnythingCallback === "function") {
        this.renderAnythingCallback.apply(this.callbackScope, [previousAlreadyRendered, elementsRenderedInThisRenderPass]);
      }
    }

  };

  this.remove = function() {
    for (var i = 0, l = this.componentList.length; i < l; i++) {
      var c = this.componentList[i];
      c.remove();
    }
    var e = getObj(this.divId);
    if (e) {
      e.parentNode.removeChild(e);
    } else {
      ibeerror('Error: Cannot remove list component with no unique ID.');
    }
  };

  this.hide = function() {
    var e = getObj(this.divId);
    if (e) {
      setHidden(this.divId, true);
    } else {
      ibeerror('Error: Trying to hide component with missing or incorrect id.');
    }
  };

  this.show = function() {
    var e = getObj(this.divId);
    if (e) {
      setVisible(this.divId, true);
    } else {
      ibeerror('Error: Trying to show component with missing or incorrect id.');
    }
  };

  this.showShowMoreButton = function() {
    $('#' + this.showMoreElementId).show();
  };

  this.hideShowMoreButton = function() {
    $('#' + this.showMoreElementId).hide();
  };

}

function createWebComponentFromHtml(id, htmlCode, model, destinationElementId) {
  var v = new IBEWebComponentView();
  v.setViewWithHtml(htmlCode);
  return new IBEWebComponent({id:id, view:v, model:model, placeHolderId:destinationElementId});
}

/**
 * Theory is based on parts of http://phrogz.net/js/classes/OOPinJS2.html
 *
 * USED by WindowComponent
 *      by DynamicBarComponent
 */
function IPane(id, _modelFetchCallback) {
  this._id = id;
  this._type = "IPane";
  this._parent = undefined;
  this._children = new Array();

  /**
   * Model stuff
   */
  this._model = undefined;
  this._modelFetchCallback = _modelFetchCallback;
  this._hasUpdatedModel = true;

  this._placeHolder = undefined;

  this._validMessageTypes = [
  /**
   * Sorts the content of the pane.
   * Parameters:
   * order = the variable name which we order by.
   * desc = true or false. Optional.
   */
    "sort",

  /**
   * Tells the pane to focus on a specific date.
   * Parameters: {timeInformation=..., resultItem=...}
   * timeInformation = JS object
   * resultItem = our general reference object (xs-item) JS object
   */
    "focusOnDate",

  /**
   * Tells the pane to focus on a specific month.
   * Parameters:
   * month = the month, number, 0 - 11
   */
    "focusOnMonth",

  /**
   * Tells the pane to focus on a specific month.
   * Parameters:
   * resultItemId = the id value from what is returned from xtremeController.getResultItem(id). aka id=resultItemId
   */
    "showSearchForm"
    // end of enum
  ];

}

IPane.prototype.getId = function() {
  return this._id;
};

IPane.prototype.getModel = function() {
  return this._model;
};

IPane.prototype.setModel = function(m) {
  if (!equalsObject(m, this._model)) {
    this._hasUpdatedModel = true;
    this._model = m;
  }
};

IPane.prototype.clearModel = function() {
  this._hasUpdatedModel = true;
  this._model = undefined;
};

IPane.prototype.setModelFetchCallback = function(f) {
  this._modelFetchCallback = f;
};

IPane.prototype.addChildPane = function(child) {
  if (child) {
    child._parent = this;
    this.getAllChildPanes().push(child);
  }
};

IPane.prototype.getAllChildPanes = function() {
  if (!this._children) this._children = new Array();
  return this._children;
};

IPane.prototype.getChildPaneAtIndex = function(index) {
  return this.getAllChildPanes()[index];
};

IPane.prototype.getParentPane = function() {
  return this._parent;
};

IPane.prototype.hasParentPane = function() {
  return this._parent !== undefined;
};

IPane.prototype.removeChildPaneAtIndex = function(index) {
  this.getAllChildPanes().splice(index, 1);
};

IPane.prototype.removePaneWithId = function(id) {
  this.getTop()._removePaneWithId(id);
};

IPane.prototype._removePaneWithId = function(id) {
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    if (this.getAllChildPanes()[i]._id == id) {
      this.removeChildPaneAtIndex(i);
    } else {
      this.getAllChildPanes()[i]._removePaneWithId(args);
    }
  }
};

IPane.prototype.removeAllChildPanes = function() {
  this._children = new Array();
};

/**
 * Implementations of update should only update model (by running fetcher callback, setModel(controller.something) or whatever you are using).
 * It must not generate any HTML!
 * @param args
 */
IPane.prototype.update = function(args) {
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    this.getAllChildPanes()[i].update(args);
  }
};

/**
 * Overload this method!
 *
 * Sends a message down the tree which can be received by the leaves.
 * Override this to listen to panes who want to send you messages.
 * When you want to listen to a specific message, check the "message" parameter
 * and string and act on it. Components who use a specific "message" must define what that
 * "message" string means for them.
 *
 * @param message - string, camelcase identifier
 * @param args - any js object which is needed to perform the task
 */
IPane.prototype.send = function(message, args) {
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    this.getAllChildPanes()[i].send(message, args);
  }
};

/**
 * Sends a message to the current group and its children.
 * @param message
 * @param args
 */
IPane.prototype.sendGroup = function(message, args) {
  this._parent.send(message, args);
};

/**
 * Sends a message to a Pane with a specific id. It can be located anywhere in the tree.
 * @param id
 * @param message
 * @param args
 */
IPane.prototype.sendTo = function(id, message, args) {
  this.getTop()._sendTo(id, message, args);
};

IPane.prototype._sendTo = function(id, message, args) {
  if (this._id == id) {
    this.send(message, args);
  }
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    this.getAllChildPanes()[i]._sendTo(id, message, args);
  }
};

IPane.prototype.sendToPanesGroup = function(id, message, args) {
  this.getTop()._sendToPanesGroup(id, message, args);
};

IPane.prototype._sendToPanesGroup = function(id, message, args) {
  if (this._id == id) this.sendGroup(message, args);
  for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
    this.getAllChildPanes()[i]._sendToPanesGroup(id, message, args);
  }
};

IPane.prototype.getTop = function() {
  return this._getTop();
};

IPane.prototype._getTop = function() {
  if (this.hasParentPane()) {
    return this.getParentPane()._getTop();
  }
  else {
    return this;
  }
};

/**
 * Overload this method!
 *
 * Must render the HTML to a node. No HTML should be rendered outside of this function, and this function should do
 * nothing else. It must not update the model in any way, but should use this._model.
 * @param node
 */
IPane.prototype.renderToNode = function(node) {
  if (this.getAllChildPanes() && this.getAllChildPanes().length) {
    for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
      var child = this.getAllChildPanes()[i];
      if (child && child.getHtmlNode && typeof child.getHtmlNode === "function") {
        var childNode = child.getHtmlNode();
        if (childNode) node.appendChild(childNode);
      } else {
        ibewarning("Added Pane that does not extend the IPane interface.");
      }
    }
  }
};

/**
 * Returns true if the Pane has new content (and should be rendered again).
 */
IPane.prototype.needsRender = function() {

  if (this.getAllChildPanes() && this.getAllChildPanes().length) {
    // If it has children, see if any of them has new HTML.
    for (var i = 0, l = this.getAllChildPanes().length; i < l; i++) {
      if (this.getAllChildPanes()[i].needsRender()) return true;
    }
    return false;
  } else {
    // No children, this is a leaf, see if this has new HTML. If we dont have a placeholder, we must render it.
    var b = this._hasUpdatedModel || !this._placeHolder;
    return b;
  }
};

/**
 * Checks if this Pane has a placeholder, if not, it creates one.
 */
IPane.prototype._ensurePlaceHolderExists = function() {
  if (!this._placeHolder) {
    this._placeHolder = document.createElement("div");
    var css = this.getPlaceHolderClassAttribute();
    if (typeof(css) === "string" && this._placeHolder.setAttribute) {
      this._placeHolder["className"] = css;
    }

    this._placeHolder.id = IBEUtil.ensureUniqueElementId(this._id ? this._id : this._type);
  }
};

/**
 * You may override this method.
 */
IPane.prototype.getPlaceHolderClassAttribute = function() {
  return undefined;
};

/**
 * Actually renders the content. Private method, called by IPane when needed.
 */
IPane.prototype._renderHtmlNode = function() {
  $(this._placeHolder).children().remove();
  this._hasUpdatedModel = false;
  this.renderToNode(this._placeHolder);
};

IPane.prototype.redraw = function() {
  if (this._placeHolder) {
    // We have a placeholder in the DOM, just rerender the component to the DOM!
    this._renderHtmlNode();
  }
};


/**
 * This is a public method that can be used to get the HTML node that this Pane creates.
 * Do NOT overload this method!
 */
IPane.prototype.getHtmlNode = function() {
  this._ensurePlaceHolderExists();
  if (this.needsRender()) {
    this._renderHtmlNode();
  }
  return this._placeHolder;
};

IPane.prototype.toString = function() {
  return this._toString(0);
};

IPane.prototype._toString = function(level) {
  var s = "";
  for (var i = 0; i < level; i++) s += "--";
  s += "> ";
  s += this.getId() + " (" + ( this.getAllChildPanes() ? this.getAllChildPanes().length : 0) + " children) parent=" + ( this.getParentPane() ? this.getParentPane().getId() : "N/A");
  s += "\n";
  if (this.getAllChildPanes() && this.getAllChildPanes().length) {
    for (var j = 0; j < this.getAllChildPanes().length; j++) {
      s += this.getAllChildPanes()[j]._toString(level + 1);
    }
  }
  return s;
};


/* *********************
 List pane
 ********************* */

function ListPane(id, _modelFetchCallback, _contentPaneType) {
  IPane.prototype.constructor();
  this._id = id;
  this._type = "ListPane";

  this._contentPaneType = _contentPaneType;

  this._modelModifierCallback = function(obj, index) {
    // Default modifier, does nothing.
    return obj;
  };

  this._paneInitCallback = function(pane, index) {
    // Default pane init, does nothing.
  };
}

ListPane.prototype = new IPane();
ListPane.prototype.constructor = ListPane;

/**
 * Sets the type of pane to use. Pass the constructor as parameter. setContentPaneType(DynamicList)
 * @param f
 */
ListPane.prototype.setContentPaneType = function(f) {
  if (f && typeof f === "function") {
    this._contentPaneType = f;
  }
  else {
    ibeerror("Trying to set PaneList content pane type, but specified type is not a function. Please pass constructor as parameter.");
  }
};

ListPane.prototype.update = function(args) {
  this._fetchModel();
};

ListPane.prototype._fetchModel = function() {
  if (this._modelFetchCallback && typeof this._modelFetchCallback === "function") {
    var m = this._modelFetchCallback();
    if (nullOrUndefined(m)) {
      ibewarning("ListPane component model fetch callback returned null or undefined.");
      this.setModel(new Array());
    } else {
      if (nullOrUndefined(m.length)) {
        ibewarning("ListPane component model fetch callback returned an object, must return an array.");
        this.setModel(new Array());
      } else {
        this.setModel(m);
      }
    }
  } else {
    ibewarning("ListPane component is trying to fetch model, but no model fetch callback has been set.");
  }
  for (var i = 0, l = this.getModel().length; i < l; i++) {
    this.getModel()[i] = this._modelModifierCallback(this.getModel()[i], i);
  }
};

ListPane.prototype._buildChildList = function() {
  var list = this.getModel();
  if (this.list && this.list.length && this._contentPaneType) {
    for (var i = 0, l = this._model.length; i < l; i++) {
      var itemModel = this._model[i];
      var itemPane = new this._contentPaneType();
      this._paneInitCallback(itemPane, i);
      itemPane.setModel(itemModel);
      this.addChildPane(itemPane);
    }
  }
};

ListPane.prototype.renderToNode = function(node) {
  this.removeAllChildPanes();
  this._buildChildList();
  $.each(this.getAllChildPanes(), function(obj, index) {
    node.appendChild(obj.getHtmlNode());
  });
};

/**
 * Fetches a page with url and parameters, then calls the callback function with the response as a parameter.
 */
var AjaxPageGetter = {
  handle:function(o) {
    this.callbackFunction(o.responseText);
  },
  startRequest:function(url, params, callbackFunction, method, scope) {
    method = useDefault(method, 'GET');
    this.callbackFunction = callbackFunction;
    this.scope = scope ? scope : this;
    YAHOO.util.Connect.asyncRequest(method, url, {
      success:this.handle,
      failure:this.handle,
      scope: this.scope
    }, params);
  }
};

/**
 * Posts a page with url and parameters, then calls the callback function with the response as a parameter.
 */
var AjaxPagePoster = {
  startRequest:function(url, params, callbackFunction) {
    AjaxPageGetter.startRequest(url, params, callbackFunction, 'POST');
  }
};

var AjaxJsonObjectRequest = {
  handle:function(o) {
    var json = undefined;
    try {
      json = parseJsonObject(o.responseText);
      if (this.callbackFunction) {
        if (this.scope) {
          this.callbackFunction.apply(this.scope, [json]);
        }
        else {
          this.callbackFunction(json);
        }
      }
    } catch (e) {
      if (this.failedCallbackFunction) {
        if (this.scope) {
          this.failedCallbackFunction.apply(this.scope);
        }
        else {
          this.failedCallbackFunction();
        }
      }
    }
  },
  startRequest:function(url, params, callbackFunction, failedCallbackFunction, method, scope) {
    method = useDefault(method, 'POST');
    this.callbackFunction = callbackFunction;
    this.failedCallbackFunction = failedCallbackFunction;
    this.scope = scope ? scope : this;
    YAHOO.util.Connect.asyncRequest(method, url, {
      success:this.handle,
      failure:this.failedCallbackFunction,
      scope: this.scope
    }, params);
  }
};

/**
 * Submits a form then runs the callback function with the complete response object as parameter.
 * Use response.responseText to get the body of the response.
 */
var AjaxFormSubmitter = {
  startRequest:function(form, callbackFunction, failedCallbackFunction, action) {
    if (!action) action = form.action;
    var postData = formParametersToStringFormat(form);
    AjaxUrlPostRequest.startRequest(action, postData, callbackFunction, failedCallbackFunction);
  }
};

var AjaxRegisterUserFromForm = {
  handle:function(o) {
    this.callbackFunction(parseJsonObject(o.responseText));
  },
  startRequest:function(form, callbackFunction) {
    var url = '/ajax.user.registeruser.do.action';
    this.callbackFunction = callbackFunction;
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope: this
    }, formParametersToStringFormat(form));
  }
};

var AjaxLogoutUser = {
  handle:function(o) {
    this.callbackFunction(parseJsonObject(o.responseText));
  },
  startRequest:function(callbackFunction) {
    this.callbackFunction = callbackFunction;
    var url = '/ajax.user.logoutuser.do.action';
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope: this
    }, undefined);
  }
};

var AjaxForgotPassword = {
  handle:function(o) {
    this.callbackFunction(parseJsonObject(o.responseText));
  },
  startRequest:function(email, callbackFunction) {
    this.callbackFunction = callbackFunction;
    var url = checkAction('ajax.user.forgotpassword.do');
    var parameters = new RequestParameters();
    parameters.addParameter("email", email);
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope: this
    }, parameters.toString());
  }
};

var AjaxChangePassword = {
  handle:function(o) {
    this.callbackFunction(parseJsonObject(o.responseText));
  },
  startRequest:function(password, newPassword, newPasswordRepeat, callbackFunction) {
    this.callbackFunction = callbackFunction;
    var url = checkAction('ajax.user.changepassword.do');
    var parameters = new RequestParameters();
    parameters.addParameter("password", password);
    parameters.addParameter("newPassword", newPassword);
    parameters.addParameter("newPasswordRepeat", newPasswordRepeat);
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope: this
    }, parameters.toString());
  }
};

var AjaxChangePasswordForm = {
  handle:function(o) {
    this.callbackFunction(parseJsonObject(o.responseText));
  },
  startRequest:function(form, callbackFunction) {
    this.callbackFunction = callbackFunction;
    var url = checkAction('ajax.user.changepassword.do');
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope: this
    }, formParametersToStringFormat(form));
  }
};

var AjaxChangeEmailForm = {
  handle:function(o) {
    this.callbackFunction(parseJsonObject(o.responseText));
  },
  startRequest:function(form, callbackFunction) {
    this.callbackFunction = callbackFunction;
    var url = checkAction('ajax.user.changeemail.do');
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope: this
    }, formParametersToStringFormat(form));
  }
};

var AjaxCheckEmail = {
  handle:function(o) {
  },
  startRequest:function(email, callbackFunction, callbackScope, customObject) {
    this.callbackScope = callbackScope;
    this.validatedFunction = callbackFunction;
    this.customObject = customObject;
    var url = '/ajax.user.checkemail.do.action';
    var parameters = new RequestParameters();
    var that = this;
    parameters.addParameter("email", email);
    sendPostRequest(url, parameters.toString(), function(response) {
      that.validatedFunction.apply(that.callbackScope, [response, that.customObject]);
    }, function(response, error) {
      that.validatedFunction.apply(that.callbackScope, [response, that.customObject]);
    });
  }
};

var AjaxVerifyEmail = {
  handle:function(o) {
    this.validatedFunction(parseJsonObject(o.responseText));
  },
  startRequest:function(email, code, callbackFunction) {
    this.validatedFunction = callbackFunction;
    var url = '/ajax.user.verifyemail.do.action';
    var parameters = new RequestParameters();
    parameters.addParameter("email", email);
    parameters.addParameter("code", code);
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope:this
    }, parameters.toString());
  }
};

var AjaxVerifyNewEmail = {
  handle:function(o) {
    this.validatedFunction(parseJsonObject(o.responseText));
  },
  startRequest:function(email, newEmail, code, callbackFunction) {
    this.validatedFunction = callbackFunction;
    var url = '/ajax.user.verifynewemail.do.action';
    var parameters = new RequestParameters();
    parameters.addParameter("email", email);
    parameters.addParameter("newEmail", newEmail);
    parameters.addParameter("code", code);
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope:this
    }, parameters.toString());
  }
};

var AjaxCheckIsPasswordCorrect = {
  handle:function(o) {
    this.validatedFunction.apply(this.callbackScope,
                                 [parseJsonObject(o.responseText), this.customObject]
    );
  },
  startRequest:function(password, callbackFunction, callbackScope, customObject) {
    this.callbackScope = callbackScope;
    this.validatedFunction = callbackFunction;
    this.customObject = customObject;
    var url = '/ajax.user.ispasswordcorrect.do.action';
    var parameters = new RequestParameters();
    parameters.addParameter("password", password);
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope:this
    }, parameters.toString());
  }
};


/** AJAX classes for traveller data requests **/

/**
 * Stores a traveller, returns SUCCESS and the resulting ID if success, otherwise FAILED.
 */
var AjaxStoreTraveller = {
  startRequest:function(form, callbackFunction, failCallback) {
    var url = checkAction('ajax.user.storetraveller.do');
    var params = formParametersToStringFormat(form);
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:function(o) {
        callbackFunction(parseJsonObject(o.responseText));
      },
      failure:failCallback,
      scope: this
    }, params);
  }
};

/**
 * Deletes a traveller.
 */
var AjaxDeleteTraveller = {
  handle:function(o) {
    this.callbackFunction(parseJsonObject(o.responseText));
  },
  startRequest:function(id, callbackFunction) {
    this.callbackFunction = callbackFunction;
    var url = checkAction('ajax.user.deletetraveller.do');
    var parameters = new RequestParameters();
    parameters.addParameter("id", id);
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:this.handle,
      failure:this.handle,
      scope: this
    }, parameters.toString());
  }
};

/** General function that the AJAX classes depend upon **/

function evaluateAllChildrenScripts(element) {
  var scripts = element.getElementsByTagName('script');
  for (var i = 0; i < scripts.length; i++) {
    var script = scripts[i];
    //ibelog('evaluating: ' + script.innerHTML);
    eval(script.innerHTML);
  }
  // Run onLoad function.
  if (typeof onSubContentLoad == 'function') {
    onSubContentLoad();
  }
}


/** These functions are new for the new JSON implementation. They need to be updated for IBE. **/

/**
 * Submits a form then runs the callback function with the complete response object as parameter.
 * Use response.responseText to get the body of the response.
 */
var AjaxUrlGetRequest = {
  startRequest:function(url, callbackFunction, failureCallbackFunction) {
    YAHOO.util.Connect.asyncRequest('GET', url, {
      success:function(o) {
        var response = undefined;
        var success = false;
        try {
          response = parseJsonObject(o.responseText);
          success = true;
        } catch (e) {
          ibeerror('Parsing of JSON failed: ' + exceptionToString(e));
          failureCallbackFunction();
        }
        if (success && callbackFunction && typeof callbackFunction === 'function') {
          try {
            callbackFunction(response);
          } catch (e) {
            ibeerror("Exception when running success callback: " + exceptionToString(e));
          }
        }
      },
      failure:failureCallbackFunction,
      scope: this
    }, undefined);
  }
};

/**
 * Submits a form then runs the callback function with the complete response object as parameter.
 * Use response.responseText to get the body of the response.
 */
var AjaxUrlPostRequest = {
  startRequest:function(url, postData, callbackFunction, failureCallbackFunction) {
    YAHOO.util.Connect.asyncRequest('POST', url, {
      success:function(o) {
        var response = undefined;
        var success = false;
        try {
          response = parseJsonObject(o.responseText);
          success = true;
        } catch (e) {
        }
        if (success) {
          if (callbackFunction && typeof callbackFunction === 'function') callbackFunction(response);
        } else {
          if (failureCallbackFunction && typeof failureCallbackFunction === 'function') failureCallbackFunction();
        }
      },
      failure:failureCallbackFunction,
      scope: this
    }, postData);
  }
};
function getCityCodeSetter(id, combinedCarCostId) {
  return function(text, li) {
    var cityCodeInput = getObj(id);
    cityCodeInput.value = li.getAttribute('iata');
    cityCodeInput.searchCityText = text;

    // -- combined addition
    if (getObj(combinedCarCostId)) {
      displayCostCategoryOptions(text, li);
    }
  };
}
function getCityCodeRemover(id) {
  return function(input) {
    var cityCodeInput = getObj(id);
    if (cityCodeInput.searchCityText != input.value) {
      cityCodeInput.value = '';
    }
  };
}

function clearCitySearchField(f) {
  if (f.value == f.defaultValue) {
    f.value = "";
    var codeElem = f.form[f.name + 'Code'];
    if (codeElem) codeElem.value = "";
  }
}

function checkCitySearchField(f) {
  if (f.value == "") {
    f.value = f.defaultValue;
    var codeElem = f.form[f.name + 'Code'];
    if (codeElem) codeElem.value = codeElem.defaultValue;
  }
}

function showChildAges(num, max, prefix) {
  if (num == 0) {
    getObj(prefix).style.display = "none";
  } else {
    getObj(prefix).style.display = "";
    for (var i = 0; i < num; i++) {
      getObj(prefix + i).style.display = "";
    }
    for (; i < max; i++) {
      getObj(prefix + i).style.display = "none";
    }
  }
}
/**
 * Handles: air stand-alone, air+hotel=combo, air+car=combined_version_1.
 */
function setReturnDateDisplay(form, rowId, owIdPrefix, hotelCheck, hotelDates, combinedCarReturnFields,
                              combinedCarOneWayRental) {
  var ow = [getObj(owIdPrefix + 'true'), getObj(owIdPrefix + 'false')];
  for (var i = 0; i < ow.length; i++) {
    var radio = ow[i];
    if (radio.checked) {
      if (getObj(rowId)) {
        var style = getObj(rowId).style;
        if (radio.value == "true") {
          style.display = "none";
        } else {
          style.display = "";
        }
      }
      var comboToggle = getObj(hotelCheck);
      var combinedToggleDiv = getObj(combinedCarReturnFields);

      if (radio.id === ow[0].id) {
        if (comboToggle) {
          comboToggle.checked = true;
          toggleLayer(hotelDates, true);
        }
        if (combinedToggleDiv) {
          toggleLayer(combinedCarReturnFields, true);
          var checkboxHidden = getObj(combinedCarOneWayRental);
          checkboxHidden.checked = true;
          var visibleReturnInput = getObj("returnCityName");
          if (!visibleReturnInput.value) {// too much of a hack/quick fix this is.
            var hiddenReturnId = getObj("returnCityId");
            hiddenReturnId.value = '';//clear car return location
            visibleReturnInput.value = '';//clear car return location
          }
        }
      } else {
        if (radio.id === ow[1].id) {
          if (comboToggle) {
            comboToggle.checked = false;
            toggleLayer(hotelDates, false);
          }
          if (combinedToggleDiv) {
            toggleLayer(combinedCarReturnFields, false);
            var checkboxHidden = getObj(combinedCarOneWayRental);
            checkboxHidden.checked = false;
          }
        }
      }
    }
  }
}

function isOnewaySearch(returnDateRowId, onewayId, partialHotelId, hotelDates) {
  var onewayCheckbox = getObj(onewayId);
  var partialHotelCheckbox = getObj(partialHotelId);

  if (onewayCheckbox.checked) {
    setHidden(returnDateRowId, true);
    if (partialHotelCheckbox != null) {
      partialHotelCheckbox.checked = true;
      toggleLayer(hotelDates, true);
    }
  } else {
    setVisible(returnDateRowId, true);
    if (partialHotelCheckbox != null) {
      partialHotelCheckbox.checked = false;
      toggleLayer(hotelDates, false);
    }
  }
}
var SelectTrip = {
  d : document,
  formSubmitted : false,
  isOneWay : false,
  DATA : [],
  DATA_SELECTION_KEY : [],
  airFlightData : function (uniqueTripGroupNumber) {
    return this.DATA['a' + uniqueTripGroupNumber];
  },
  airSelectionKey : function (tripId) {
    return this.DATA_SELECTION_KEY['a' + tripId];
  },
  setSelectionKey : function (f, tripId) {
    var i;
    for (i = 0; i < f.elements.length; i++) {
      var e = f.elements[i];
      if (e.name == 'sss.k') {
        e.value = this.airSelectionKey(tripId);
        return;
      }
    }
  },

  airCkTrips : function (form, uniqueTripGroupNumber, tripIdx) {
    var xArr = this.airFlightData(uniqueTripGroupNumber); // Defined in code
    var xxArr = xArr[tripIdx];
    if (xxArr.length > 1) {
      var segs = form['y' + uniqueTripGroupNumber];
      for (var x = 0; x < xxArr.length && x < segs.length; x++) {
        if (xxArr[x] == 0) {
          segs[x].disabled = true;
          segs[x].checked = false;
        } else {
          segs[x].disabled = false;
        }
      }
    }
  },
  clickRd : function (radioId) {
    var radio = this.d.getElementById(radioId);
    if (!radio.checked) radio.click();
  },

  // setting flightcode and submitting
  selectTrip : function (form, uniqueTripGroupNumber, displayIndex, newActionValue) {
    var xArr = this.airFlightData(uniqueTripGroupNumber); // Defined in code
    var i;

    if (newActionValue) {
      form.action = newActionValue;
    }

    if (!this.formSubmitted) {
      var e1 = form['x' + uniqueTripGroupNumber];
      var e2 = form['y' + uniqueTripGroupNumber];
      var xsegs = this.listArr(e1);
      var ysegs = this.isOneWay ? [] : this.listArr(e2);
      var checkedX = -1;
      var checkedY = -1;

      for (i = 0; i < xsegs.length; i += 1) {
        if (xsegs[i].checked) {
          checkedX = parseInt(xsegs[i].value);
          break;
        }
      }

      if (!this.isOneWay) {
        for (i = 0; i < ysegs.length; i += 1) {
          if (ysegs[i].checked) {
            checkedY = parseInt(ysegs[i].value);
          }
        }
      } else {
        checkedY = 0;
      }

      var tripId = -1;
      if (checkedX >= 0 && checkedY >= 0) {
        if (this.isOneWay) {
          tripId = xArr[checkedX][0];
        } else {
          tripId = xArr[checkedX][checkedY];
        }
      } else {
        alert(UiText.get('Air.SelectCons.ChooseBoth'));
        return;
      }

      this.d.location.hash = '#' + (displayIndex > 0 ? ('g' + displayIndex) : '');
      form.tripId.value = tripId;
      this.setSelectionKey(form, tripId);
      form.submit();
    } else {
      alert(UiText.get('Air.SelectCons.ChooseBoth'));
    }
  },
  listArr : function(val) {
    if (!val.length) {
      return new Array(val);
    }
    return val;
  }
}

