<!-- source: www\dw\dw-js.xsp, target: C:\DOCUME~1\acr\LOCALS~1\Temp\dw_js.java , date: 2010-09-01 09:48:01 PDT -->


    

    
    
//****************************************************************************
// GENERIC
//****************************************************************************

var UNDEFINED = "undefined";

/**
 * Generic and useful functions
 */
// Basic Type
function isNull(obj) { return obj === null; }
function isObject(obj) { return typeof(obj) == "object"; }

// Collection types
function isArray(obj) { return obj instanceof Array || typeof(obj) == "array"; }
function isArguments(obj) { return obj.callee != null; }
function isList(obj) { return obj.item != null; }
function isIterable(obj) { return isArray(obj) || isArguments(obj) || isList(obj); }

// Primitives
function isBoolean(obj) { return obj instanceof Boolean || typeof(obj) == "boolean"; }
function isFunction(obj) { return obj instanceof Function || typeof(obj) == "function"; }
function isNumber(obj) { return obj instanceof Number || typeof(obj) == "number"; }
function isString(obj) { return obj instanceof String || typeof(obj) == "string"; }
function isDate(obj) { return obj instanceof Date; }
function isError(obj) { return obj instanceof Error; }
function isRegExp(obj) { return obj instanceof RegExp; }

// Helpers
function isUndefined(obj) { return typeof(obj) == UNDEFINED; }
function isDefined(obj) { return !isUndefined(obj); }
function isEmpty(obj) { return obj == null || (isString(obj) && obj.length === 0) || (isNumber(obj) && isNaN(obj)) || (isArray(obj) && obj.length == 0); }

// Miscellaneous
function isNode(obj) { return obj != null && typeof(obj.nodeType) != UNDEFINED; }

/**
 * Tests wether an object is of a certain type
 */
function isOfType(obj /* Object */, type /* Function */) {
    // Try to get the default case out of the way.  For most cases
    // modern browsers, will recognize this for most types, otherwise
    // the case below will try to check the type in some other way
    if (obj instanceof type) return true;

    // Now iterate
    switch (type) {
        // Basic Types
        case Array:                 return isArray(obj);
        case Boolean:               return isBoolean(obj);
        case Function:              return isFunction(obj);
        case Number:                return isNumber(obj);
        case String:                return isString(obj);
        case Object:                return isObject(obj);
        // Node
        case Node:                  return typeof(obj.nodeType) != UNDEFINED;
        // Node Types
        case Element:               return obj.nodeType == Node.ELEMENT_NODE;
        case Attribute:             return obj.nodeType == Node.ATTRIBUTE_NODE;
        case Text:                  return obj.nodeType == Node.TEXT_NODE;
        case CDATASection:          return obj.nodeType == Node.CDATA_SECTION_NODE;
        case EntityReference:       return obj.nodeType == Node.ENTITY_REFERENCE_NODE;
        case Entity:                return obj.nodeType == Node.ENTITY_NODE;
        case ProcessingInstruction: return obj.nodeType == Node.PROCESSING_INSTRUCTION_NODE;
        case Comment:               return obj.nodeType == Node.COMMENT_NODE;
        case Document:              return obj.nodeType == Node.DOCUMENT_NODE;
        case DocumentType:          return obj.nodeType == Node.DOCUMENT_TYPE_NODE;
        case DocumentFragment:      return obj.nodeType == Node.DOCUMENT_FRAGMENT_NODE;
        case Notation:              return obj.nodeType == Node.NOTATION_NODE;
        default: alert("isOfType - Do not have a case for '"+type+"'"); return false;
    }
}

function enhanceAs(obj, cl) {
    if (obj.__enhanced__) return;
    obj.__enhanced__ = true;

    if (cl == null) return;
    // console.log("Enhacing '"+obj+"' as '"+cl+"'");
    
    // IE: enhance if HTMLElement does not exist
    if (isFunction(HTMLElement)) copyMethods(obj, HTMLElement.prototype);
    
    // Copy target methods
    copyMethods(obj, cl.prototype);
    
    // Build
    cl.call(obj);
}

function copyMethods(dst /* [Function|Object] */, src /* [Function|Object] */, includePrivate /* Boolean */, overwrite /* Boolean */) /* Object */ {
    return _copy(dst, src, isFunction, includePrivate, overwrite);
}

function copyAttributes(dst /* [Function|Object] */, src /* [Function|Object] */, includePrivate /* Boolean */, overwrite /* Boolean */) /* Object */ {
    return _copy(dst, src, function(obj) { return !isFunction(obj); }, includePrivate, overwrite);
}

function _copy(dst /* [Function|Object] */, src /* [Function|Object] */, filter /* Class */, includePrivate /* Boolean */, overwrite /* Boolean */) /* Object */ {
    // console.log("----- dst '"+dst+"'")

    // IE: Ensure 'dst' has a 'hasOwnProperty' function
    if (typeof(dst.hasOwnProperty) == UNDEFINED) {
        dst.hasOwnProperty = function(prop) { return typeof(dst[prop]) != UNDEFINED; }
    }
    
    // Copy all required properties
    for (var prop in src) {
        
        // Do not copy methods that are already in the 'dst' (unless overwritting)
        if (!overwrite && dst.hasOwnProperty(prop)) continue;
        
        // Filter non-desired properties
        if (filter != null && !filter(src[prop])) continue;

        // If not 'includePrivate', try to figure out if we should skip (private methods
        // and attributes start with underscore)
        if (!includePrivate && prop instanceof String && prop.charAt(0) == '_') continue;

        // Copy method
        // console.log("copying '"+prop+"'")
        dst[prop] = src[prop];
    }
    return dst;
}

function firstNonNull() {
    var args = arguments;
    for (var i = 0; i < args.length; i++) if (args[i] != null) return args[i];
    return null;
}


//****************************************************************************
// STACK
//****************************************************************************

function getStack() {
    var stack = null;
    try { throw new Error(); } catch (ex) { stack = parseStack(ex.stack); }
    // Removes the last two which were induced
    stack.removeAt(0);
    stack.removeAt(0);
    stack.toString = function() { return stackToString(this); };
    return stack;
}

/**
 * Parses the stack handled by Mozilla.  It returns an array of objects, where
 * each line has a stack object.
 */
function parseStack(stack) {
    var re = /^\s*(.*)@(.+):(\d+)\s*$/gm;
    var matcher, array = new Array();
    while ((matcher = re.exec(stack))) {
        array.push( { call: matcher[1], source: matcher[2], line: matcher[3] });
    }
    array.toString = function() { return stackToString(array); };
    return array;
}

function stackToString(stack) {
    var buffer = "";
    for (var i = 0; i < stack.length; i++) {
        buffer += (stack[i].call+"@"+stack[i].source+":"+stack[i].line+"\n");
    }
    return buffer;
}


//****************************************************************************
// ASSERT
//****************************************************************************

/**
 * Throws an exception if the assertion fails.  If the asserted condition is true,
 * this method does nothing. If the condition is false, we throw an error with a error message.
 *
 * @param    test  A boolean value, which needs to be true for the assertion to succeed.
 * @param    msg  A string describing the assertion.
 * @throws    Throws an Error if 'test' is false.
 */
function assert(test /* Boolean */, msg /* Boolean */) {
    if (test) return;
    assertReport(msg);
}

/**
 * Specialized assert methods
 */
function assertDefined(value /* Object */, msg /* String */) {
    if (!(typeof(value) == UNDEFINED)) return;
    assertReport(msg, "value was not defined");
}
function assertType(value /* Object */, type /* Function */, msg /* String */) {
    if (isOfType(value, type)) return;
    assertReport(msg, "'"+value+"' is not of type '"+type.typeName+"'");
}
function assertTypeOrNull(value /* Object */, type /* Function */, msg /* String */) {
    if (value == null || isOfType(value, type)) return;
    assertReport(msg, "'"+value+"' is not of type '"+type.typeName+"'");
}

/**
 * Stipulate how the traces behave
 */
var ASSERT_IN_CONSOLE = true;
var ASSERT_WITH_STACK = true;

/**
 * Reports the errors failed assertions
 */
function assertReport(msg /* String */, additionalMsg /* String */) {
    var stack = getStack();
    stack.removeAt(0);
    msg = msg || "assert failed in '"+stack[0].source+"', line '"+stack[0].line+"'";
    if (additionalMsg) msg = msg +" ("+additionalMsg+")";
    if (ASSERT_WITH_STACK) msg += ("\n-----\n"+stackToString(stack));
    // if (ASSERT_IN_CONSOLE && typeof(console) != UNDEFINED) console.error(msg);
    else throw new Error(msg);
}


//****************************************************************************
// OBJECT
//****************************************************************************

function objectValues(obj /* Object */, excludeFunctions /* Boolean */) {
    var values = new Array();
    for (var key in obj) {
        var value = obj[key];
        if (excludeFunctions && isFunction(value)) continue;
        values.push(value);
    }
    return values;
}

function objectKeys(obj /* Object */, excludeFunctions /* Boolean */) {
    var keys = new Array();
    for (var key in obj) {
        var value = obj[key];
        if (excludeFunctions && isFunction(value)) continue;
        values.push(keys);
    }
    return keys;
}


//****************************************************************************
// NUMBER
//****************************************************************************

Number.prototype.format = function(dec, sep) {
	var n = Math.floor(this), arr = new Array();
	while (n > 0) {
		if (sep != null && (arr.length+1) % 4 == 0) arr.push(sep);
		arr.push(n % 10);
		n = Math.floor(n/10);
	}
	// console.log(arr);
	return arr.reverse().join("")+(this%1).toFixed(dec || 0).substring(1);
}


//****************************************************************************
// STRING
//****************************************************************************

String.prototype.contains = function(str) { return (this.indexOf(str) >= 0); }

String.prototype.startsWith = function(prefix) { return (this.indexOf(prefix) === 0); }
String.prototype.endsWith = function(suffix) { return this.length < suffix.length? false : (this.lastIndexOf(suffix) == this.length - suffix.length); }

String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g,""); }
String.prototype.leftTrim = function() { return this.replace(/^\s+/,""); }
String.prototype.rightTrim = function() { return this.replace(/\s+$/,""); }

String.prototype.camelCase = function() { return this.replace(/-\D/g, function(m) { return m.charAt(1).toUpperCase(); }); }
String.prototype.hyphenate = function() { return this.replace(/\w[A-Z]/g, function(m) { return (m.charAt(0) + '-' + m.charAt(1).toLowerCase());  }); }

String.prototype.capitalize = function() { return this.toLowerCase().replace(/(\W\D)|(^\D)/g, function(m) { return m.toUpperCase(); }); }

String.prototype.toInt = function(base) { return parseInt(this, base || 10); }
String.prototype.toFloat = function() { return parseFloat(this); }

String.prototype.padLeft = function(width, pad) {
    var str = this;
    while (str.length < width) str = (pad + str);
    return str;
}    

String.prototype.padRight = function(width, pad) {
    var str = this;
    while (str.length < width) str = (str + pad);
    return str;
}

// String.prototype.isAlpha = function() { return this.match(/^[a-zA-Z]+$/); }
// String.prototype.isAlphanumeric = function() { return this.match(/^[0-9a-zA-Z]+$/); }
// String.prototype.isNumeric = function() { return this.match(/^[0-9]+$/); }

String.prototype.isAlpha = function()        { return (/^[a-zA-Z]+$/).test(this); }
String.prototype.isAlphanumeric = function() { return (/^[0-9a-zA-Z]+$/).test(this); }
String.prototype.isNumeric = function()      { return (/^[0-9]+$/).test(this); }


//****************************************************************************
// DATE
//****************************************************************************

Date.MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ];

Date.prototype.toString = function() {
    var tm = this.getTime();
    
    // Figure out date
    var y = this.getFullYear(), mt = this.getMonth()+1, d = this.getDate();
    var onlyTime = (y == 1970 && mt == 1 && d == 1);
    var dt = onlyTime? "" : (y+"-"+(mt < 10? "0" : "")+mt+"-"+(d < 10? "0" : "")+d);
    
    // Figure out time
    var h = this.getHours(), mn = this.getMinutes(), s = this.getSeconds();
    var onlyDate = (h == 0 && mn == 0 && s == 0);
    var hr = onlyDate? "" : ((h < 10? "0" : "")+h+":"+(mn < 10? "0" : "")+mn+":"+(s < 10? "0" : "")+s);
    
    // Time offset
    // var to = (this.getTimezoneOffset()/60);
    // console.log("Time Offset: "+to);
    
    // Assemble
    return dt+(onlyDate || onlyDate? "" : " ")+hr;
}

Date.prototype.daysInMonth = function() { return Date.daysInMonth(this.getFullYear(), this.getMonth()); }

Date.daysInMonth = function(year, month) {
    return (32 - new Date(year, month, 32).getDate());
}

Date.toString = function(date, format) {
    if (date == null) return null;
    if (format == null) return date.toString();
    
    var yyyy = date.getFullYear(), yy = yyyy.toString().substring(2);
    var M = date.getMonth()+1, MM = M < 10 ? "0" + M : M, MMM = Date.MONTHS[M-1].substring(0, 3);
    var d = date.getDate(), dd = d < 10 ? "0" + d : d;
    var h = date.getHours(), hh = h < 10 ? "0" + h : h;
    var m = date.getMinutes(), mm = m < 10 ? "0" + m : m;
    var s = date.getSeconds(), ss = s < 10 ? "0" + s : s;

    format = format.replace(/yyyy/, yyyy);
    format = format.replace(/yy/,   yy);
    format = format.replace(/MMM/,  MMM);
    format = format.replace(/MM/,   MM);
    format = format.replace(/M/,    M);
    format = format.replace(/dd/,   dd);
    format = format.replace(/d/,    d);
    format = format.replace(/hh/,   hh);
    format = format.replace(/h/,    h);
    format = format.replace(/mm/,   mm);
    format = format.replace(/m/,    m);
    format = format.replace(/ss/,   ss);
    format = format.replace(/s/,    s);
    
    return format;
}

Date.fromString = function(dateString, format) {
    // If there is no format, just use Date.parse
    if (format == null) return Date.parse(dateString);
    
    // First, get order
    var elems = new Array();
    format.replace(/(yyyy)|(yy)|(MMM)|(MM)|(M)|(dd)|(d)|(hh)|(h)|(mm)|(m)|(ss)|(s)/g, function(str) { elems.push(str.charAt(0)); })
    //console.log("Date.fromString - elems: "+elems+", format: "+format);
    
    // Build regular expressions
    format = format.replace(/yyyy/, "([0-9]{4})");
    format = format.replace(/yy/,   "([0-9]{2})");
    format = format.replace(/MMM/,  "([A-Z]{3})");
    format = format.replace(/MM/,   "0?([0-9]{1,2})");
    format = format.replace(/M/,    "([0-9]{1,2})");
    format = format.replace(/dd/,   "0?([0-9]{1,2})");
    format = format.replace(/d/,    "([0-9]{1,2})");
    format = format.replace(/hh/,   "0?([0-9]{1,2})");
    format = format.replace(/h/,    "([0-9]{1,2})");
    format = format.replace(/mm/,   "0?([0-9]{1,2})");
    format = format.replace(/m/,    "([0-9]{1,2})");
    format = format.replace(/ss/,   "0?([0-9]{1,2})");
    format = format.replace(/s/,    "([0-9]{1,2})");

    // Match
    var matches = dateString.match(format);
    if (matches == null || matches.length <= elems) return null;
    
    var dateElems = { year: 0, month: 0, day: 0, hours: 0, minutes: 0, seconds: 0 };
    for (var i = 1; i < matches.length; i++) {
        var value = parseInt(matches[i]);
        // console.log("Date.fromString - elems[i-1]: "+elems[i-1]+", value: "+value+", dateElems: "+dateElems);
        switch (elems[i-1]) {
            case 'y': dateElems.year    = value;   break;
            case 'M': dateElems.month   = value-1; break;
            case 'd': dateElems.day     = value;   break;
            case 'h': dateElems.hours   = value;   break;
            case 'm': dateElems.minutes = value;   break;
            case 's': dateElems.seconds = value;   break;
        }
    }

    var date = new Date(dateElems.year, dateElems.month, dateElems.day, dateElems.hours, dateElems.minutes, dateElems.seconds);
    // console.log("Date.fromString - date: "+date+", dateString: "+dateString+", format: "+format);
    return date;
}


//****************************************************************************
// ARRAYS
//****************************************************************************

Array.prototype.getTypeName = function() { return "Array"; }

// Mozilla 1.8 has support for indexOf, lastIndexOf, forEach, filter, map, some, every

// http://developer-test.mozilla.org/docs/Core_JavaScript_1.5_Reference:Objects:Array:lastIndexOf
if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(obj, fromIndex) {
      if (fromIndex == null) fromIndex = 0;
      else if (fromIndex < 0) fromIndex = Math.max(0, this.length + fromIndex);
      for (var i = fromIndex; i < this.length; i++) if (this[i] === obj) return i;
      return -1;
    }
}

// http://developer-test.mozilla.org/docs/Core_JavaScript_1.5_Reference:Objects:Array:lastIndexOf
if (!Array.prototype.lastIndexOf) {
    Array.prototype.lastIndexOf = function(obj, fromIndex) {
      if (fromIndex == null) fromIndex = this.length - 1;
      else if (fromIndex < 0) fromIndex = Math.max(0, this.length + fromIndex);
      for (var i = fromIndex; i >= 0; i--) if (this[i] === obj) return i;
      return -1;
    }
}

// http://developer-test.mozilla.org/docs/Core_JavaScript_1.5_Reference:Objects:Array:forEach
if (!Array.prototype.forEach) {
    Array.prototype.forEach = function(f, obj) {
      var l = this.length;
      for (var i = 0; i < l; i++) f.call(obj, this[i], i, this);
    }
}

// http://developer-test.mozilla.org/docs/Core_JavaScript_1.5_Reference:Objects:Array:filter
if (!Array.prototype.filter) {
    Array.prototype.filter = function(f, obj) {
      var l = this.length;
      var res = [];
      for (var i = 0; i < l; i++) {
            if (f.call(obj, this[i], i, this)) res.push(this[i]);
      }
      return res;
    }
}

// http://developer-test.mozilla.org/docs/Core_JavaScript_1.5_Reference:Objects:Array:map
if (!Array.prototype.map) {
    Array.prototype.map = function(f, obj) {
      var l = this.length;
      var res = [];
      for (var i = 0; i < l; i++) res.push(f.call(obj, this[i], i, this));
      return res;
    }
}

// http://developer-test.mozilla.org/docs/Core_JavaScript_1.5_Reference:Objects:Array:some
if (!Array.prototype.some) {
    Array.prototype.some = function(f, obj) {
      var l = this.length;
      for (var i = 0; i < l; i++) {
            if (f.call(obj, this[i], i, this)) return true;
      }
      return false;
    }
}

// http://developer-test.mozilla.org/docs/Core_JavaScript_1.5_Reference:Objects:Array:every
if (!Array.prototype.every) {
    Array.prototype.every = function(f, obj) {
      var l = this.length;
      for (var i = 0; i < l; i++) {
        if (!f.call(obj, this[i], i, this)) return false;
      }
      return true;
    }
}

Array.prototype.add = function(obj) { this.push(obj); }
Array.prototype.addAll = function(array) { for (var i = 0; i < array.length; i++) this.push(array[i]); return this; }
Array.prototype.contains = function(obj) {
    for (var i = 0; i < this.length; i++) if (this[i] === obj) return true;
    return false;
}
Array.prototype.copy = function(obj) { return this.concat(); }
Array.prototype.fill = function(obj, n) { for (var i = 0, len = n || this.length; i < len; i++) this[i] = obj; return this; }

// Return the first element that passes a test.  If no test is passed, then
// it's as if a dummy function that always returns true is passed.  If the 
// array contains no elements, or no element passes the test, null is returned
Array.prototype.first = function(f, obj) { 
    if (f == null) return this.length > 0? this[0] : null; 
    for (var i = 0, l = this.length; i < l; i++) {
          if (f.call(obj, this[i], i, this)) return this[i];
    }
    return null;
}

// Return the last element that passes a test.  If no test is passed, then
// it's as if a dummy function that always returns true is passed.  If the 
// array contains no elements, or no element passes the test, null is returned
Array.prototype.last = function(f, obj) { 
    if (f == null) return this.length > 0? this[this.length - 1] : null;
    for (var i = this.length - 1; i >= 0; i--) {
          if (f.call(obj, this[i], i, this)) return this[i];
    }
    return null;
}

Array.prototype.insertAt = function(obj, i) { this.splice(i, 0, obj); }

Array.prototype.removeAt = function(i) { this.splice(i, 1); }
Array.prototype.remove = function(obj) {
    var pos = -1;
    for (var i = 0; i < this.length; i++) if (this[i] === obj) { pos = i; break; }
    if (pos != -1) this.splice(pos, 1);
}

// Static function to flatten an array that should work with other pseudo-array types
Array.flatten = function(arr) {
    var flt = [];
    for (var i = 0, l = arr.length; i < l; i++) {
        flt = flt.concat(isIterable(arr[i])? Array.flatten(arr[i]) : arr[i]);
    }
    return flt;
}

Array.prototype.min = function(filter /* Function */) {
    var m = Number.MAX_VALUE;
    for (var i = 0, len = this.length; i < len; i++) {
        var val = (filter == null? this[i] : filter(this[i]));
        if (val < m) m = val;
    }
    return m;
}

Array.prototype.max = function(filter /* Function */) {
    var m = Number.MIN_VALUE;
    for (var i = 0, len = this.length; i < len; i++) {
        var val = (filter == null? this[i] : filter(this[i]));
        if (val > m) m = val;
    }
    return m;
}

// Swaps two array positions
Array.swap = function(i /* Number */, j /* Number */) {
    var tmp = this[i]; 
    this[i] = this[j]; 
    this[j] = tmp; 
    return arr;
}

// Generates an array of length len, with each element being obj
Array.gen = function(len /* Number */, value /* [Object|Function] */, obj /* Object */) {
    var arr = new Array();
    for (var i = 0; i < len; i++) arr[i] = isFunction(value)? value.call(obj, i, arr) : value;
    return arr;
}


//****************************************************************************
// GENERAL OBJECT COMPARISON
//****************************************************************************

function compare(o1, o2) {
    if (typeof(o1) != typeof(o2)) return false;
    if (isArray(o1) && isArray(o2)) {
        if (o1.length != o2.length) return false;
        for (var i = 0; i < o1.length; i++) {
            if (o1.length != o2.length) return false;
            var c = compare(o1[i], o2[i]);
            if (!c) return false;
        }
    }
    
    // Since there is reliable way of knowing o1 and o2 are objects, 
    // compare them as such anyway
    var props = false;
    for (var p in o1) {
        var c = compare(o1[p], o2[p]);
        if (!c) return false;
        props = true;
    }
    if (props) return true;

    // Finally simple comparison
    return o1 == o2;
}


//****************************************************************************
// MATH
//****************************************************************************

// Extend round function so that it takes a second parameter
Math.decimalRound = function(x, d) {
    var p = Math.pow(10, (d == null? 0 : d));
    return Math.round(x * p) / p;
}

Math.LOGE10 = Math.log(10);

Math.log10 = function(x) { return Math.log(x) / Math.LOGE10; }

Math.genLog = function(x, base) { return Math.log(x) / Math.log(base); }

Math.randInt = function(a,b) {
    if (b == null) { b = a; a = 0; }
    return Math.floor((b-a)*Math.random() + a);
}

Math.randDouble = function(a,b,d) {
    if (b == null) { b = a; a = 0; }
    var r = (b-a)*Math.random() + a;
    return (d == null? r : Math.decimalRound(r, d));
}

Math.randPct = function() { return Math.randInt(0,100); }

Math.normal = function(x, m, s) {
    var s2 = s * s, xmm = (x - m);
    var t1 = 1 / Math.sqrt(2 * Math.PI * s2);
    var t2 = Math.exp( -(xmm*xmm) / (2*s2) );
    return t1 * t2;
}

Math.sampleNormal = function(m, s) {
    var u = Math.random(), v = Math.random();
    var sg = Math.sin(2 * Math.PI * u) * Math.sqrt(-2 * Math.log(1 - v));
    return m + s * sg;
}

Math.logNormal = function(x, m, s) {
    var s2 = s * s, lxmm = (Math.log(x) - m);
    var t1 = 1 / (x * s * Math.sqrt(2 * Math.PI));
    var t2 = Math.exp( -(lxmm*lxmm) / (2*s2) );
    return t1 * t2;
}

Math.permutation = function(arr, reuse) {
    if (!reuse) arr = arr.concat();
    for (var k = arr.length - 1; k > 0; k--) {
        var i = Math.randInt(k+1), j = k;
        var tmp = a[i]; 
        a[i] = a[j]; 
        a[j] = tmp; 
    }
    return arr;
}


//****************************************************************************
// COLOR
//****************************************************************************

function Color() { }

Color.TRANSPARENT = "rgba(0, 0, 0, 0)";

Color.toString = function(value /* Array */, opacity /* Double */) {
    if (opacity === null || opacity === "") return value;
    if (value == "transparent") return "rgba(0,0,0,0.0)";
    if (isString(value)) value = Color.fromString(value);
    if (!isArray(value)) return value;
    if (opacity != null) return "rgba("+value+","+opacity+")";
    if (value.length == 3) return "rgb("+value+")";
    if (value.length == 4) return "rgba("+value+")";
    return value;
};

Color.fromString = function(value /* String */) {
    if (value == null) return null;
    var comps = null;
    if (comps == null) comps = /^\s*rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)\s*$/.exec(value);
    if (comps == null) comps = /^\s*#?(\w{2})(\w{2})(\w{2})\s*$/.exec(value);
    if (comps == null) comps = /^\s*#?(\w{1})(\w{1})(\w{1})\s*$/.exec(value);
    if (comps != null) comps.shift();
    return comps;
};

Color.isTransparent = function(color /* String */) {
    if (color == "none") return true;
    if (color == "transparent") return true;
    if (color == "rgba(0, 0, 0, 0)") return true;
    return false;
};

// Generates a color palette where close elements will be very different
// see http://www.krazydad.com/makecolors.php
Color.gen = function(i, op, c, w, rf, gf, bf, rp, gp, bp) {
    if (i == null) i = 0;
    if (c == null) c = 128;
    if (w == null) w = 127;
 
    var r = Math.round( Math.sin(rf*i+rp) * w + c );
    var g = Math.round( Math.sin(gf*i+gp) * w + c );
    var b = Math.round( Math.sin(bf*i+bp) * w + c );
    return (op == null? "rgb("+r+","+g+","+b+")" : "rgba("+r+","+g+","+b+","+op+")");
}


//****************************************************************************
// GRAPHICS CONTEXT
//****************************************************************************

function Graphics() { }

// Bullet Types
Graphics.TRIANGLE = 0;
Graphics.SQUARE   = 1;
Graphics.DIAMOND  = 2;
Graphics.CIRCLE   = 3;
Graphics.CROSS    = 4;

Graphics.SQRT2 = Math.sqrt(2);
Graphics.SQRT3 = Math.sqrt(3);


// Line function that helps to draw one pixel horizontal and vertical lines
Graphics.line = function(ctx, x1, y1, x2, x2) {
    // Fix Horizontal/Vertical Lines
    if (x1 == x2) x1 = x2 = (Math.round(x2) + 0.5);
    if (y1 == y2) y1 = y2 = (Math.round(y2) + 0.5);
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
}

Graphics.horLine = function(ctx, y, x1, x2, w, s) {
    if (w != null) ctx.lineWidth = w;
    if (s != null) ctx.strokeStyle = s;
    y = Math.round(y) + 0.5;
    ctx.moveTo(x1, y);
    ctx.lineTo(x2, y);
}

Graphics.verLine = function(ctx, x, y1, y2, w, s) {
    if (w != null) ctx.lineWidth = w;
    if (s != null) ctx.strokeStyle = s;
    x = Math.round(x) + 0.5;
    ctx.moveTo(x, y1);
    ctx.lineTo(x, y2);
}

Graphics.rect = function(ctx, x1, y1, x2, y2) {
    ctx.beginPath();
    var x1 = Math.round(x1) - 0.5, y1 = Math.round(y1) - 0.5;
    var x2 = Math.round(x2) + 0.5, y2 = Math.round(y2) + 0.5;
    ctx.moveTo(x1, y1);
    ctx.lineTo(x1, y2);
    ctx.lineTo(x2, y2);
    ctx.lineTo(x2, y1);
    ctx.closePath();
}

Graphics.bullet = function(ctx, x, y, r, type) {
    ctx.beginPath();
    if (type == null) type = Graphics.CIRCLE;
    switch (type) {
        case Graphics.TRIANGLE:
            ctx.lineTo(x, y + r);
            ctx.lineTo(x + r * Graphics.SQRT3 / 2, y - r/2);
            ctx.lineTo(x - r * Graphics.SQRT3 / 2, y - r/2);
            break;
        case Graphics.SQUARE:
            var d = r * Graphics.SQRT2/2;
            ctx.lineTo(x - d, y - d);
            ctx.lineTo(x + d, y - d);
            ctx.lineTo(x + d, y + d);
            ctx.lineTo(x - d, y + d);
            break;
        case Graphics.DIAMOND:
            var d = r * Graphics.SQRT2/2;
            ctx.lineTo(x, y + d);
            ctx.lineTo(x + d, y);
            ctx.lineTo(x, y - d);
            ctx.lineTo(x - d, y);
            break;
        case Graphics.CIRCLE:
            ctx.arc(x, y, r, 0, Math.PI*2, true);
            break;
    }
    ctx.closePath();
    ctx.stroke();
    ctx.fill();
};

Graphics.circle = function(ctx, x, y, r) {
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI*2, true);
    ctx.closePath();
    ctx.stroke();
    ctx.fill();
};

Graphics.ellipse = function(ctx, x, y, rx, ry) {
    var kappa = 0.5522848;
    
    // Control point offset horizontal/vertical, then ends, then middles
    var ox = rx * kappa, oy = ry * kappa;
    var xe = x + 2*rx, ye = y + 2*ry, xm = x + rx, ym = y + ry;

    ctx.beginPath();
    ctx.moveTo(x, ym);
    ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
    ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
    ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
    ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
    ctx.closePath();
    ctx.stroke();
    ctx.fill();
};

Graphics.wedge = function(ctx, x, y, radius, startAngle, endAngle, ccw, w, s, f) {
    if (w != null) ctx.lineWidth = w;
    if (s != null) ctx.strokeStyle = s;
    if (f != null) ctx.fillStyle = f;
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x + radius * Math.cos(startAngle), y + radius * Math.sin(startAngle));
    ctx.arc(x, y, radius, startAngle, endAngle, ccw);
    ctx.lineTo(x, y);
    ctx.closePath();
    ctx.stroke();
    ctx.fill();
};

Graphics.text = function(ctx, text, x, y, angle) {
    ctx.save();
    ctx.translate(x, y);
    if (angle != null) ctx.rotate(angle*Math.PI/180);
    ctx.fillText(text, 0, 0);
    ctx.restore();
};

// Return { width: w, height: height } for a possible rotated  text
Graphics.textDim = function(ctx, text, angle) {
    var w = ctx.measureText(text).width;
    var h = ctx.measureText("M").width;
    if (angle) {
        var sin = Math.abs(Math.sin(angle * Math.PI / 180));
        var cos = Math.abs(Math.cos(angle * Math.PI / 180));
        w = w * cos + h * sin;
        h = w * sin + h * cos;
        
        // Temporary: give more room to the height
        h *= 1.1;
    }
    return { width: w, height: h };
};

Graphics.textHeight = function(ctx) { return ctx.measureText("M").width; }


//****************************************************************************
// REQUEST PARAMETER(S)
//****************************************************************************

function getParameter(name /* String */, def /* Object */) {
    var params = _decodeParameters();
    return (values = params[name]) == null? def : values[0];
}

function getParameterValues(name /* String */) {
    var params = _decodeParameters();
    return (values = params[name]) == null? new Array() : values;
}

function removeParameter(name /* String */) {
    var params = _decodeParameters();
    if (name in params) delete params[name];
}

function setParameter(name /* String */, value /* String */) {
    var params = _decodeParameters();
    if (name in params) params[name][0] = value;
    else params[name] = [value];
    return value;
}

function getQuery() {
    var params = _decodeParameters();
    var qs = "", first = true;
    for (var name in params) {
        var values = params[name];
        for (var i = 0; i < values.length; i++) {
            qs += (first? "?" : "&")+name+"="+escape(values[i]);
        }
        first = false;
    }
    return qs;
}

function _decodeParameters() {
    if (arguments.callee.parameters != null) return arguments.callee.parameters;
    var parameters = new Object();
     
    // The 'search' string is split into its key/value components
    var search = location.search;
    if (search.charAt(0) == "?") search = search.substring(1);
    var pairs = search.split(/[&=?]/);
    
    // Each values is stored in a multi-valued array
    for (var i = 0; i + 1 < pairs.length; i += 2) {
        var key = pairs[i];
        var value = unescape(pairs[i+1]);
        if (parameters[key] == null) parameters[key] = new Array();
        parameters[key].push(value);
    }
    
    arguments.callee.parameters = parameters;
    return parameters;
}


//****************************************************************************
// LOCATION
//****************************************************************************


//****************************************************************************
// FUNCTION FUNCTION(S)
//****************************************************************************

Function.prototype.getArgumentNames = function() {
    var fnSrc = this.toSource();
    var s = fnSrc.indexOf('('), e = fnSrc.indexOf(')');
    return fnSrc.substring(s+1, e).split(/[ ]*,[ ]*/);
};

Function.prototype.bind = function(obj) {
    var __method = this;
    return function() { return __method.apply(obj, arguments); }
};


//****************************************************************************
// CSS FUNCTION(S)
//****************************************************************************

function getCompStyle(elem, prop) {
    if (elem == null) return null;
    if (prop == null) return elem.currentStyle || window.getComputedStyle(elem, null);
    var cs = elem.currentStyle;
    if (cs != null) return prop == "opacity"? cs.filter.alpha.opacity : cs[prop];
    return window.getComputedStyle(elem, null).getPropertyValue(prop);
}

function addClassName(_this /* Node */, className /* String */) {
    if (hasClassName(_this, className)) return;
    if (_this.className.length > 0) _this.className += (" " + className);
    else _this.className = className;
}

function getClassNames(_this /* Node */) {
    if(_this.className == null || _this.className.length == 0) return [];
    return _this.className.split(/\s+/g);
}

function removeClassName(_this /* Node */, className /* String */) {
    var regexp = new RegExp("(^|\\s)" + className + "(\\s|$)");
    _this.className = _this.className.replace(regexp, "$2");
}

function hasClassName(_this /* Node */, className /* String */) {
    var regexp = new RegExp("(^|\\s)" + (isArray(className)? "("+className.join("|")+")" : className) + "(\\s|$)");
    return regexp.test(_this.className);
}


//****************************************************************************
// DW FUNCTION
//****************************************************************************

// Prototype inspired
function dw(elem) {
    if (elem == null) return null;
    
    if (arguments.length > 1) {
        var elems = new Array();
        for (var i = 0, len = arguments.length; i < len; i++) elems.push(dw(arguments[i]));
        return elems;
    }
    
    if (isString(elem)) elem = document.getElementById(elem);
    if (elem == null || elem.__enhanced__) return elem;
    
    // Should we enhance
    var dwType = getDWType(elem);
    // if (dwType != null) enhanceAs(elem, eval(dwType.substr(2)));
    var type = (dwType == null? null : window[dwType.substr(2)]);
    if (type != null) enhanceAs(elem, type);

    return elem;
}

// A DW type can be specified via the class, but ONLY if it is the first
// class that appears on the class attribute declaration
function getDWType(elem /* HTMLElement */) {
    if (elem.nodeType != Node.ELEMENT_NODE) return null;
    var beh = elem.getAttribute(ATTRS.BEHAVIOR);
    if (beh != null && beh.length > 0) return beh;
    var cn = elem.className;
    
    return (cn != null && cn.indexOf("dw") === 0)? cn.split(/\s+/g)[0] : null;
}

// Define '$' based on wether it is already defined or not
var origHash = (typeof($) == UNDEFINED? null : $);

// Simple case
if (origHash == null) $ = dw;

// Convoluted case
else $ = function() {
    var res = origHash.apply(null, arguments);
    return res == null? null : dw.apply(null, isArray(res)? res : [res]);
}


//****************************************************************************
// ELEMENT SYNTACTIC SUGAR
//****************************************************************************

// First descendant element with name 'name'
function $N(nm, context) {
    var elem, cn = context.name || context.getAttribute(ATTRS.NAME);
    if (cn != null && cn == nm) return $(context);
    for (var i = 0, len = context.childNodes.length; i < len; i++) {
        var child = context.childNodes[i];
        if (child.nodeType != Node.ELEMENT_NODE) continue;
        if ( (elem = $N(nm, child)) != null ) return elem;
    }
    return null;
}

// Chrome does not recognize the prototype for elements
Element.prototype.$N = HTMLElement.prototype.$N = function(nm) { return $N(nm, this); }

// First descendant element with tag name 'tag'
function $T(tn, context) {
    var elem, cn = context.nodeName.toLowerCase();
    if (cn != null && cn == tn) return $(context);
    for (var i = 0, len = context.childNodes.length; i < len; i++) {
        var child = context.childNodes[i];
        if (child.nodeType != Node.ELEMENT_NODE) continue;
        if ( (elem = $T(tn, child)) != null ) return elem;
    }
    return null;
}

// Chrome does not recognize the prototype for elements
Element.prototype.$T = HTMLElement.prototype.$T = function(name) { return $T(name, this); }


//****************************************************************************
// ID GENERATION
//****************************************************************************

function genId() { return "_genId_"+(genId.counter++); }
genId.counter = 0;

// Does not work in JavaScript because it wraps long numbers
/*
function nextId() { 
    var time = (new Date()).getTime();
    var rand = Math.floor(0xFFFF*Math.random());
    return (time << 16) + rand; 
}
*/

function nextIdName() {
    var time = (new Date()).getTime();
    var rand = Math.floor(0xFFFF*Math.random());
    return time.toString(16) + rand.toString(16).padLeft(4, " ");
}

var CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

function nextPass(len) {
    var pass = "", cl = CHARS.length;
    for (var i = 0; i < len; i++) pass += CHARS.charAt(Math.randInt(cl));
    return pass;
}


//****************************************************************************
// TEMPLATES
//****************************************************************************

// The idea is that an element can be created into a template by invoking this
// function on it.

function Template(elem /* HTMLElement */) {
    // this.element = elem.cloneNode(true);
    var params = elem.getAttribute("params");
    params = (params !== null? new Array() : params = params.split(/\s*,\s*/));

    // Clean template
    // this.element.removeAttribute("id");
    // this.element.removeAttribute("params");

    // console.log(unescape(this.element.outerHTML));
    
    var ec = "";
    ec += "(function(params) {";
    for (var i = 0; i < this.params.length; i++) {
        var param = this.params[i].trim();
        ec += "    var "+param+" = params[\""+param+"\"];";
    }
    ec += "    return \""+unescape(this.element.outerHTML).replace(/\"/g, "\\\"").replace(/[\n\r]/g, " ").replace(/\{([^}]+)\}/g, function(str, expr) { return "\"+("+expr+")+\""; }) +"\";";
    ec += "})";
    
    // console.log(ec);
    // this.executor = ec;
    
    elem.executor = eval(ec);
    elem.run = Template.prototype.run;
}


// Methods

Template.prototype.run = function(params /* Object */, parent /* HTMLElement */, position /*int*/) {
    var domString = this.executor.call(null, params);
    
    // Make sure temporary node exists
    var tempNode = arguments.callee.tempNode;
    if (tempNode == null) tempNode = (arguments.callee.tempNode = document.createElement(TAGS.DIV));

    tempNode.innerHTML = domString;
    var node = tempNode.firstChild.cloneNode(true);
    if (parent != null) {
    	if (position && parent.children) {
    		parent.insertBefore(node, parent.children[position]);
    	}
    	else parent.appendChild(node);
    }

    return node;
};


//****************************************************************************
// EVENTS
//****************************************************************************

// It's non structured to declare events here, but since many scripts declare
// events directly over the loading of the file (i.e. load), it's a good idea

// For W3C event listener styles (and add inline style events)
var EVENTS = {
    // Window events
    LOAD:"load", UNLOAD:"unload",
    // Mouse events
    CLICK:"click", DBLCLICK:"dblclick", MOUSEDOWN:"mousedown", MOUSEUP:"mouseup", MOUSEOVER:"mouseover", MOUSEMOVE:"mousemove", MOUSEOUT:"mouseout",
    // Focus and Keyboard events
    FOCUS:"focus", BLUR:"blur", KEYPRESS:"keypress", KEYDOWN:"keydown", KEYUP:"keyup",
    // Scrolling
    SCROLL:"scroll", 
    // Form events
    SUBMIT:"submit", RESET:"reset", SELECT:"select", CHANGE:"change",
    // Other events
    CONTEXTMENU: "contextmenu", MESSAGE: "message", 
    // DOM Mutation Events
    DOMATTRMODIFIED: "DOMAttrModified", DOMCHARACTERDATAMODIFIED: "DOMCharacterDataModified", DOMNODEINSERTED: "DOMNodeInserted", DOMNODEINSERTEDINTODOCUMENT: "DOMNodeInsertedIntoDocument", DOMNODEREMOVED: "DOMNodeRemoved", DOMNODEREMOVEDFROMDOCUMENT: "DOMNodeRemovedFromDocument", DOMSUBTREEMODIFIED: "DOMSubtreeModified",
    // Non-Standard events
    DOMCONTENTLOADED: "DOMContentLoaded", DOMFRAMECONTENTLOADED: "DOMFrameContentLoaded",
    // Standard drag events
    DRAG: "drag", DRAGEND: "dragend", DRAGENTER: "dragenter", DRAGLEAVE: "dragleave", DRAGOVER: "dragover", DRAGSTART: "dragstart", DROP: "drop", 
    // Extension Mouse Events (for non IE browsers)
    MOUSEENTER: "mouseenter", MOUSELEAVE: "mouseleave", 
    // Extension Events
    DISPLAY: "display", END: "end", ERROR: "error", FILTER: "filter", HIDE: "hide", INSPECT: "inspect", PROGRESS: "progress", REQUEST: "request", RESPONSE: "response", SORT: "sort", START: "start"
};


//****************************************************************************
// CONSTANT(S)
//****************************************************************************

var TAGS = { A:"a", ABBR:"abbr", ACRONYM:"acronym", ADDRESS:"address", APPLET:"applet", AREA:"area", B:"b", BASE:"base", BASEFONT:"basefont", BDO:"bdo", BIG:"big", BLOCKQUOTE:"blockquote", BODY:"body", BR:"br", BUTTON:"button", CANVAS:"canvas", CAPTION:"caption", CENTER:"center", CITE:"cite", CODE:"code", COL:"col", COLGROUP:"colgroup", DD:"dd", DEL:"del", DFN:"dfn", DIR:"dir", DIV:"div", DL:"dl", DT:"dt", EM:"em", EMBED:"embed", FIELDSET:"fieldset", FONT:"font", FORM:"form", FRAME:"frame", FRAMESET:"frameset", H1:"h1", H2:"h2", H3:"h3", H4:"h4", H5:"h5", H6:"h6", HEAD:"head", HR:"hr", HTML:"html", I:"i", IFRAME:"iframe", IMG:"img", INPUT:"input", INS:"ins", ISINDEX:"isindex", KBD:"kbd", LABEL:"label", LEGEND:"legend", LI:"li", LINK:"link", MAP:"map", MENU:"menu", META:"meta", NOFRAMES:"noframes", NOSCRIPT:"noscript", OBJECT:"object", OL:"ol", OPTGROUP:"optgroup", OPTION:"option", P:"p", PARAM:"param", PRE:"pre", Q:"q", S:"s", SAMP:"samp", SCRIPT:"script", SELECT:"select", SMALL:"small", SPAN:"span", STRIKE:"strike", STRONG:"strong", STYLE:"style", SUB:"sub", SUP:"sup", TABLE:"table", TBODY:"tbody", TD:"td", TEXTAREA:"textarea", TFOOT:"tfoot", TH:"th", THEAD:"thead", TITLE:"title", TR:"tr", TT:"tt", U:"u", UL:"ul", VAR:"var" };

var ATTRS = {
    // HTML Attributes
    ABBR:"abbr", ACCEPT:"accept", ACCESSKEY:"accesskey", ACTION:"action", ALIGN:"align", ALT:"alt", ARCHIVE:"archive", AXIS:"axis", BORDER:"border", CELLPADDING:"cellpadding", CELLSPACING:"cellspacing", CHAR:"char", CHAROFF:"charoff", CHARSET:"charset", CHECKED:"checked", CITE:"cite", CLASS:"class", CLASSID:"classid", CODEBASE:"codebase", CODETYPE:"codetype", COLS:"cols", COLSPAN:"colspan", CONTENT:"content", COORDS:"coords", DATA:"data", DATETIME:"datetime", DECLARE:"declare", DEFER:"defer", DIR:"dir", DISABLED:"disabled", DRAGGABLE:"draggable", DROPABLE:"dropable", ENCTYPE:"enctype", FOR:"for", FRAME:"frame", FRAMEBORDER:"frameborder", HEADERS:"headers", HEIGHT:"height", HREF:"href", HREFLANG:"hreflang", ID:"id", ISMAP:"ismap", LABEL:"label", LANG:"lang", LONGDESC:"longdesc", MARGINHEIGHT:"marginheight", MARGINWIDTH:"marginwidth", MAXLENGTH:"maxlength", MEDIA:"media", METHOD:"method", MULTIPLE:"multiple", NAME:"name", NOHREF:"nohref", NORESIZE:"noresize", PATTERN:"pattern", PROFILE:"profile", READONLY:"readonly", REL:"rel", REV:"rev", ROWS:"rows", ROWSPAN:"rowspan", RULES:"rules", SCHEME:"scheme", SCOPE:"scope", SCROLLING:"scrolling", SELECTED:"selected", SHAPE:"shape", SIZE:"size", SPAN:"span", SRC:"src", STANDBY:"standby", STYLE:"style", SUMMARY:"summary", TABINDEX:"tabindex", TARGET:"target", TITLE:"title", TYPE:"type", USEMAP:"usemap", VALIGN:"valign", VALUE:"value", VALUETYPE:"valuetype", WIDTH:"width",
    // Extension Attributes
    ALL:"all", BEHAVIOR:"behavior", CONTEXT:"context", DELAY: "delay", DIRTY: "dirty", EDITABLE:"editable", EXPANDED:"expanded", FILTER: "filter", FILTERABLE: "filterable", FORMAT: "format", FORMATNAME: "formatName", IMAGE:"image", INSPECTABLE: "inspectable", INVALID:"invalid", MASK:"mask", MAX:"max", MIN: "min", MOVEABLE: "moveable", ORIENT: "orient", POPUP: "popup", POSITION: "position", RESIZABLE: "resizable", SELECTION: "selection", STEP: "step", SORT: "sort", SORTABLE: "sortable", TEXT: "text", TOOLTIP:"tooltip",
    // Data Extension Attributes
    DATADISPLAY:"dataDisplay", DATAFILTER:"dataFilter", DATAID:"dataId", DATALIMIT: "dataLimit", DATASORT: "dataSort", DATASRC: "dataSrc", DATATYPE:"dataType"
};

// Mime Constants
var MIMES = { CSS: "text/css",  HTML: "text/html", JAVASCRIPT: "text/javascript", XHTML: "text/xhtml" };

// Key Codes
var KEYS = {
    // Miscellaneous
    BACKSPACE: 8, TAB: 9, ENTER: 13, ESC: 27, PAGEUP: 33, PAGEDOWN: 34, END: 35, HOME: 36, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40, INS: 45,DEL: 46,
    // Numbers
    N0: 48, N1: 49, N2: 50, N3: 51, N4: 52, N5: 53, N6: 54, N7: 55, N8: 56, N9: 57,
    // Upper Case Letters
    A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90,
    // Function Keys
    F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117, F7: 118, F8: 119, F9: 120, F10: 121, F11: 122, F12: 123
};

function isEditKey(kc) {
    return kc == KEYS.BACKSPACE || kc == KEYS.TAB || (kc >= KEYS.PAGEUP && kc <= KEYS.RIGHT) || kc == KEYS.INS || kc == KEYS.DEL;
}

var UNITS = { EM:"em", EX:"ex", PT:"pt", PX:"px" };

var URNS = { ID:"id:", JAVASCRIPT:"javascript:", URL:"url:" };


//****************************************************************************
// BASIC BROWSER COMPATIBILITY
//****************************************************************************

/*
// Commented out because it's preferable to define it over Element instead of HTMLElement

// Firefox does not define 'innerText' (over HTMLElement)
if (typeof(HTMLElement) != UNDEFINED && typeof(HTMLElement.prototype.innerText) == UNDEFINED) {
    HTMLElement.prototype.__defineGetter__("innerText", function() { return this.textContent; });
    HTMLElement.prototype.__defineSetter__("innerText", function(text) { this.innerHTML = ""; this.appendChild(document.createTextNode(text)); });
}
*/

// Firefox does not define 'innerText' (over Element)
if (typeof(Element) != UNDEFINED && typeof(Element.prototype.innerText) == UNDEFINED) {
    Element.prototype.__defineGetter__("innerText", function() { return this.textContent; });
    Element.prototype.__defineSetter__("innerText", function(text) { this.innerHTML = ""; this.appendChild(document.createTextNode(text)); });
}

if (typeof(Event) != UNDEFINED && typeof(Event.prototype.stop) == UNDEFINED) {
    Event.prototype.stop = function() {
        this.stopPropagation();
        this.preventDefault();
        this.propagationStopped = true;
        if (this.causeEvent != null && this.causeEvent != this) this.causeEvent.stop();
    }
}

// Firefox does not define 'outerHTML'
if (typeof(HTMLElement) != UNDEFINED && typeof(HTMLElement.prototype.outerHTML) == UNDEFINED) {
    HTMLElement.prototype.__defineGetter__("outerHTML", function() { 
        // TODO: I am not sure this works
        var parent = this.parentNode;
        var tempNode = document.createElement(TAGS.DIV);
        tempNode.appendChild(this);
        var html = tempNode.innerHTML;
        parent.appendChild(this);
        return html;
    });
    HTMLElement.prototype.__defineSetter__("outerHTML", function(html) {
        // TODO: I am not sure this works
        var tempNode = document.createElement(TAGS.DIV);
        tempNode.innerHTML = html;
        this.parent.replaceNode(tempNode.firstChild, this);
    });
}

// Firefox does not define 'contains'
if (typeof(Node.prototype.contains) == UNDEFINED) {
    Node.prototype.contains = function(node) {
        for (var n = node; n != null; n = n.parentNode) {
            if (n == this) return true;
        }
        return false;
    }
    // Alternative implementation
    // Node.prototype.contains = function(node) { return !!(this.compareDocumentPosition(node) & 16); }
}

Element.prototype.insertFirst = function(newElem /* Element */) {
    var n = this.firstChild;
    return (n == null? this.appendChild(newElem) : this.insertBefore(newElem, n));
}

//For Chrome
HTMLElement.prototype.insertFirst = Element.prototype.insertFirst;

Element.prototype.insertAfter = function(newElem /* Element */, refElem /* Element */) {
    var n = refElem.nextSibling;
    return (n == null? this.appendChild(newElem) : this.insertBefore(newElem, n));
}

// For Chrome
HTMLElement.prototype.insertAfter = Element.prototype.insertAfter;
	
function contains(a, b){
    return a.contains? a != b && a.contains(b) : !!(a.compareDocumentPosition(arg) & 16);
}

// Firefox does not define 'children'
// The caching is commented out, so one has to be mindful not to use the property directly in a loop
if (typeof(Element) != UNDEFINED  && !Element.prototype.hasOwnProperty("children")) {
    Element.prototype.__defineGetter__("children", function() {
        // If we already have them cached, just return that
        // if (this._cachedChildren != null) return this._cachedChildren;

        // Create the array
        var elems = new Array();
        var j = 0;
        for (var i = 0; i < this.childNodes.length; i++) {
            var node = this.childNodes[i];
            if (node.nodeType == Node.ELEMENT_NODE) {
                elems.push(node);
                // Named children
                if (node.name) {
                    if (elems[node.name] == null) elems[node.name] = new Array();
                    elems[node.name][elems[node.name].length] = node;
                }
                // Child with id
                if (node.id) elems[node.id] = node;
            }
        }

        return elems;
    });
}

// Be able to toggle fixed value attributes like multiple, and returns true if
// at the end of the operation the attribute is present, false otherwise
if (typeof(Element) != UNDEFINED && typeof(Element.prototype.toggleAttribute) == UNDEFINED) {
    Element.prototype.toggleAttribute = function(attrName, attrValue) {
        if (attrValue == null) attrValue = attrName;
        if (!this.getAttribute(attrName)) this.setAttribute(attrName, attrValue);
        else this.removeAttribute(attrName);
    }
    // Chrome does not recognize the prototype for elements
    HTMLElement.prototype.toggleAttribute = Element.prototype.toggleAttribute;
    
}

// IE does not define 'Node', and for some weird reason Safari defines Node,
// but it does not define the node types that are necessary for DOM manipulation
if (typeof(Node) == UNDEFINED || typeof(Node.ELEMENT_NODE) == UNDEFINED) {
    function Node() { };
    Node.ELEMENT_NODE                   = 1;
    Node.ATTRIBUTE_NODE                 = 2;
    Node.TEXT_NODE                      = 3;
    Node.CDATA_SECTION_NODE             = 4;
    Node.ENTITY_REFERENCE_NODE          = 5;
    Node.ENTITY_NODE                    = 6;
    Node.PROCESSING_INSTRUCTION_NODE    = 7;
    Node.COMMENT_NODE                   = 8;
    Node.DOCUMENT_NODE                  = 9;
    Node.DOCUMENT_TYPE_NODE             = 10;
    Node.DOCUMENT_FRAGMENT_NODE         = 11;
    Node.NOTATION_NODE                  = 12;
}

// IE does not define DOM Elements
if (typeof(Element) == UNDEFINED)               { function Element() { }; }
if (typeof(Attribute) == UNDEFINED)             { function Attribute() { }; }
if (typeof(Text) == UNDEFINED)                  { function Text() { }; }
if (typeof(CDATASection) == UNDEFINED)          { function CDATASection() { }; }
if (typeof(EntityReference) == UNDEFINED)       { function EntityReference() { }; }
if (typeof(Entity) == UNDEFINED)                { function Entity() { }; }
if (typeof(ProcessingInstruction) == UNDEFINED) { function ProcessingInstruction() { }; }
if (typeof(Comment) == UNDEFINED)               { function Comment() { }; }
if (typeof(Document) == UNDEFINED)              { function Document() { }; }
if (typeof(DocumentType) == UNDEFINED)          { function DocumentType() { }; }
if (typeof(DocumentFragment) == UNDEFINED)      { function DocumentFragment() { }; }
if (typeof(Notation) == UNDEFINED)              { function Notation() { }; }

// IE does not define 'addEventListener' on the window. Additionally, if the events are 
// mouse related, send them to the document instead of the window
/*
if (typeof(window.addEventListener) == UNDEFINED && typeof(window.attachEvent) != UNDEFINED) {
    window.addEventListener = function(eventType / * String * /, listener / * Function * /, useCapture / * Boolean * /) {
        var et = eventType;
        if (et == EVENTS.CLICK || et == EVENTS.DBLCLICK || et == EVENTS.MOUSEDOWN || et == EVENTS.CLICK || et == EVENTS.MOUSEUP) {
            document.attachEvent("on"+eventType, listener);            
        }
        else {
            window.attachEvent("on"+eventType, listener);
        }
    }
}
*/


//***************************************************************************
// COOKIES
//****************************************************************************

// If we pass a string it's a normal cookie, if we pass an object, it will convert
// it to a JSON string and convert it back to an object on read
function putCookie(name /* String */, value  /* [String|Object] */, days /* Integer */, path /* String */, domain /* String */, secure /* String */) {
    if (isObject(value)) value = "@!"+JSON.encode(value);
    var cookie = name+"="+value;
    if (days != null) {
        var expiration = new Date((new Date()).getTime() + days*86400000);
        cookie += "; expires=" + expiration.toGMTString();
    }
    if (path != null) cookie += "; path=" + path;
    if (domain != null) cookie += "; domain=" + domain;
    if (secure != null) cookie += "; secure";

    // Now store the cookie by setting the magic 'document.cookie' property
    return window.document.cookie = cookie;
}

function getCookie(name /* String */, defaultValue) {
    var allCookies = window.document.cookie;
    if (allCookies == "") return defaultValue;
    var start = allCookies.indexOf(name+"=");
    if (start == -1) return defaultValue;
    start += name.length + 1;
    var end = allCookies.indexOf(";", start);
    if (end == -1) end = allCookies.length;
    var value = allCookies.substring(start, end);
    if (value.startsWith("@!")) value = JSON.decode(value.substring(2));
    return value;
}
 
function removeCookie(name /* String */) { 
    putCookie(name, "", -1);
}


//****************************************************************************
// AJAX
//****************************************************************************

/**
* Read the contents of the specified uri and return those contents.
*
* @param uri  A relative or absolute uri. If absolute, it must be in same domain
* @param asyncHandler  If not specified, load synchronously
*/
function request(uri /* String */, asyncHandler /* Function */, content /* String */, method /* [GET|POST] */, contentType /* String */, xml /* Boolean */) {
    assertType(uri, String);
    assertTypeOrNull(asyncHandler, Function);
    assertTypeOrNull(content, String);
    assertTypeOrNull(method, String);
    assertTypeOrNull(contentType, String);

    var xhr = new XMLHttpRequest();

    // Set asynchronous call
    if (asyncHandler != null) {
        xhr.onreadystatechange = function() {
            if (xhr.readyState == 4 && typeof(xhr.status) != UNDEFINED) {
                asyncHandler(xml? xhr.responseXML :  xhr.responseText);
            }
        }
    }

    // Do request
    // if (content != null) method = "POST";
    // if (method == null || (method != "GET" && method != "POST")) method = "GET";
    if (method == null) method = "GET";

    xhr.open(method, uri, (asyncHandler != null));
    if (contentType != null) xhr.setRequestHeader("Content-type", contentType);

    try { xhr.send(content); }
    catch (ex) { 
    	console.log(ex);
    	throw new Error("Could not perform XMLHttpRequest '" + uri + "'."); 
    }

    // Return depending on the type of request
    return asyncHandler == null ? (xml? xhr.responseXML :  xhr.responseText) : null;
}

/**
 * Style with the contents of the specified uri
 */
function style(uri /* String */, doc /* Document */) {
    assertType(uri, String);
    
    // Default to current document
    if (doc == null) doc = document;

    // Create script
    var link = doc.createElement(TAGS.LINK);
    link.href = uri;
    link.type = MIMES.CSS;
    link.rel = "stylesheet";
    link.onerror = function() { throw new Error("Could not load stylesheet '"+uri+"'."); }

    // Append it to the head
    var head = doc.getElementsByTagName(TAGS.HEAD).item(0);
    head.appendChild(link);
}

/**
 * Adds a CSS rule
 */
function defineStyle(selector, declaration, doc /* Document */) {
    // Default to current document
    if (doc == null) doc = document;
    
    // create the style node for all browsers
    var style = doc.createElement(TAGS.STYLE);
    style.setAttribute(ATTRS.TYPE, MIMES.CSS);
    // style.setAttribute(ATTRS.MEDIA, "screen");

    // Create rule itself
    style.appendChild(doc.createTextNode(selector + " {" + declaration + "}"));
    
    // Append it to the head
    var head = doc.getElementsByTagName(TAGS.HEAD).item(0);
    head.appendChild(style);
}

/**
 * Script the contents of the specified uri
 */
function script(uri /* String */, asyncHandler /* Function */, doc /* Document */) {
    assertType(uri, String);

    // Default to current document
    if (doc == null) doc = document;
    
    // Create script
    var script = doc.createElement(TAGS.SCRIPT);
    script.src = uri;
    script.type = MIMES.JAVASCRIPT;
    script.defer = false;
    // script.onload = asyncHandler;
    script.onerror = function() { throw new Error("Could not load script '"+uri+"'."); }

    // Append it to the head
    var head = doc.getElementsByTagName(TAGS.HEAD).item(0);
    head.appendChild(script);
}

/**
 * Loads an external URL (syncronously) and returns a document fragment containing the results
 */
function load(url /* url */, doc /* Document */) {
    // Default to current document
    if (doc == null) doc = document;
    
    // Ensure loading div
    var ld = arguments.callee._loadDiv;
    if (ld == null) {
        ld = doc.createElement(TAGS.DIV);
        doc.body.appendChild(ld);
        ld = dw(ld);
        arguments.callee._loadDiv = ld;
    }

    // Perform the request.  Since it is a syncronous request, we know it's
    // safe to use the same 'div' for all the dialogs
    var html = request(url);
    ld.innerHTML = html;
    if (typeof(Behavior) != UNDEFINED) Behavior.injectBehavior(ld);
    _executeScripts(_extractScripts(html));
    
    // Return loading div
    var df = doc.createDocumentFragment();
    while (ld.hasChildNodes()) df.appendChild(ld.firstChild);
    return df;
}

/**
 * Execute javascript code (along the lines of eval)
 */
function _executeScripts(text, doc /* Document */) {
    // Default to current document
    if (doc == null) doc = document;
    
    if (text == null || text.length == 0) return;
    // else if (window.eval) window.eval(text);
    else if (window.execScript) window.execScript(text);
    else {
        var script = doc.createElement(TAGS.SCRIPT);
        script.setAttribute("type", MIMES.JAVASCRIPT);
        script.text = text;

        // Append it to the head
        var head = doc.getElementsByTagName(TAGS.HEAD).item(0);
        // console.log("head: "+head);
        head.appendChild(script);
        head.removeChild(script);
    }
}

/**
 * Execute javascript code (along the lines of eval)
 */
function _extractScripts(html) {
    if (html == null) return null;
    
    // Remove comments
    html = html.replace(/<!--[\s\S]*?-->/g, "");
    
    // Extract scripts
    var scripts = '';
    html = html.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, function(){
        scripts += arguments[1] + '\n';
        return '';
    });
    return scripts;
}

 
//****************************************************************************
// FIREBUG PROTECTION
//****************************************************************************

if (typeof(console) == UNDEFINED) console = new Object();
if (typeof(console.log) == UNDEFINED) console.log = function() { }
if (typeof(console.error) == UNDEFINED) console.error = function() { }


//****************************************************************************
// MISCELLANEOUS
//****************************************************************************

// Returns the directory name, or said other way, the parent of the 
// file/url, always ending in '/' to signify that it is a directory
function dirName(path, dirSep) {
    if (dirSep == null) dirSep = "/";
    var ls = path.lastIndexOf(dirSep);
    if (ls == -1) return null;
    if (ls == 0) return dirSep;
    return path.substring(0, ls+1);
}

// Returns the fileName of a path
function fileName(path, dirSep) {
	if (dirSep == null) dirSep = "/";
	var ls = path.lastIndexOf(dirSep);
	return path.substring(ls+1);
}

// Returns the fileName of a path without its suffix
function baseName(path, dirSep, suffixSep) {
    if (dirSep == null) dirSep = "/";
    if (suffixSep == null) suffixSep = ".";
    var ls = path.lastIndexOf(dirSep);
    var lp = path.lastIndexOf(suffixSep);
    return path.substring(ls+1, lp == -1? path.length : lp);
}

function offset(elem) {
	if (!elem || !elem.ownerDocument) return null;
	var box = elem.getBoundingClientRect(), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement;
	var	clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0;
	var x = box.left + (self.pageXOffset || body.scrollLeft) - clientLeft;
	var y = box.top  + (self.pageYOffset || body.scrollTop ) - clientTop;
	return { x: x, y: y };
}

		
//****************************************************************************
// MOUSE TRACKER
//****************************************************************************

// Keeps position of mouse updated in 'window.mouseX' and 'window.mouseY' globals

var _mouseTrackFn = function(ev /* Event */) {
    ev = ev || window.event;
    var doc = ev.target.ownerDocument || document;
    var win = doc.defaultView;
    win.mouseX = ev.pageX || ev.clientX;
    win.mouseY = ev.pageY || ev.clientY;
};

// On standards browsers, we use 'addEventListener', on IEwe use 'attachEvent' and the 'document' instead of the window
if (typeof(window.addEventListener) != UNDEFINED) window.addEventListener(EVENTS.MOUSEMOVE, _mouseTrackFn, true);
else if (typeof(window.attachEvent) != UNDEFINED) document.attachEvent("on"+EVENTS.MOUSEMOVE, _mouseTrackFn);

    
//****************************************************************************
// HTMLELEMENT
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

if (typeof(HTMLElement) == UNDEFINED) HTMLElement = function() { };


//~ GENERIC METHOD(S) --------------------------------------------------------

// Generic HTML

HTMLElement.prototype.center = function(bounds /* Rectangle */) {
    if (bounds == null) {
        var w = document.documentElement.clientWidth;
        var h = document.documentElement.clientHeight;
        bounds = { x: 0, y: 0, width: w, height: h };
    }
    var tb = this.getBounds();
    var x = (bounds.width - tb.width)/2 + bounds.x;
    var y = (bounds.height - tb.height)/2 + bounds.y;
    this.setLocation(x,y);
}

HTMLElement.prototype.getBounds = function() /* Rectangle */ {
    // var cr = this.getBoundingClientRect();
    // return { x: cr.left, y: cr.top, width: this.offsetWidth, height: this.offsetHeight };
    var loc = this.getLocation();
    loc.width = this.offsetWidth;
    loc.height =this.offsetHeight;
    return loc;
}

HTMLElement.prototype.getLocation = function() /* Point */ {
    // var cr = this.getBoundingClientRect();
    // return { x: cr.left, y: cr.top };
    var x = 0, y = 0;
    var elem = this;
    while (elem != null) {
        // console.log("getLocation: ("+x+", "+y+") - ("+elem.offsetLeft+", "+elem.offsetTop+")");
        x += elem.offsetLeft;
        y += elem.offsetTop;
        elem = elem.offsetParent;
    }
    return { x: x, y: y };
}

HTMLElement.prototype.getTooltip = function() {
    return this.getAttribute(ATTRS.TOOLTIP);
}

HTMLElement.prototype.setBounds = function(bounds /* Rectangle */, units /* String */) {
    if (units == null) units = "px";
    this.style.left = bounds.x+units; 
    this.style.top = bounds.y+units; 
    this.style.width = bounds.width+units;
    this.style.height = bounds.height+units;
}

HTMLElement.prototype.setLocation = function(left /* Integer */, top /* Integer */, units /* String */) {
    if (units == null) units = "px";
    this.style.left = left+units;
    this.style.top = top+units;
}

HTMLElement.prototype.setHeight = function(h /* Integer */, units /* String */) { 
    this.style.height = h+(units == null? UNITS.PX : units); 
}

HTMLElement.prototype.setWidth = function(w /* Integer */, units /* String */) { 
    this.style.width = w+(units == null? UNITS.PX : units); 
}


//~ FORM-LIKE METHOD(S) ------------------------------------------------------

HTMLElement.prototype.getValue = function() {
    switch (this.nodeName.toLowerCase()) {
        case TAGS.INPUT:
            var type = this.getAttribute(ATTRS.TYPE) || this.type;
            switch (type.toLowerCase()) {
                case "submit":
                case "hidden":
                case "password":
                case "email":
                case "tel":
                case "number":
                case "text": return this.value;
                case "checkbox":
                case "radio": 
                    var valueAttr = this.getAttribute(ATTRS.VALUE);
                    return valueAttr == null? (this.checked? true : false) : (this.checked? valueAttr : "");
                case "date": return Date.fromString(this.value, this.getAttribute(ATTRS.FORMAT));
                default: return this.value;
            }
            return false;

        case TAGS.SELECT:
            switch (this.type.toLowerCase()) {
                case "select-one":
                    var value = "", opt, i = this.selectedIndex;
                    if (i >= 0) {
                        opt = this.options[i];
                        // console.log("value: "+value+", opt.value: "+opt.value+", text: "+opt.text);
                        value = (opt.value || opt.text);
                        // value = (opt.hasAttribute(ATTRS.VALUE)? opt.value : opt.text);
                    }
                    return value;
                case "select-multiple":
                    var values = new Array();
                    for (var i = 0; i < this.length; i++) {
                        var opt = this.options[i];
                        if (opt.selected) {
                            var value = (opt.value || opt.text);
                            values.push(value);
                        }
                    }
                    return values;
            }
            
        case TAGS.TEXTAREA:            
            return this.value;

        default:
            return this.innerText;
    }
}

HTMLElement.prototype.setValue = function(value /* Object */, html /* Boolean */) {
    switch (this.tagName.toLowerCase()) {
        case TAGS.INPUT:
            switch (this.type.toLowerCase()) {
                case "submit":
                case "hidden":
                case "password":
                case "email":
                case "tel":
                case "number":
                case "text": this.value = (value == null? "" : value.toString()); break;
                case "checkbox":
                case "radio": 
                    var valueAttr = this.getAttribute(ATTRS.VALUE);
                    this.checked = (valueAttr == null? value == true : value == valueAttr); break;
                default:
                    this.value = (value == null? "" : value.toString());
            }
            break;

        case TAGS.SELECT:
            var i, opt = null, optValue = null;
            switch (this.type.toLowerCase()) {
                case "select-one":
                    // Expecting single value
                    value = (value != null? value.id || value.name || value.toString() : value)+"";
                    for (i = 0; i < this.length; i++) {
                        opt = this.options[i];
                        optValue = (opt.value || opt.text);
                        // console.log("value: "+value+", optValue: "+optValue);
                        opt.selected = (value == optValue);
                    }
                    break;
                case "select-multiple":
                    // Expecting single value
                    var values = (value == null? [] : (isArray(value)? value : [value])).map(function(value) {
                        return (value != null? value.id || value.name || value.toString() : value)+"";
                    });
                    for (i = 0; i < this.length; i++) {
                        opt = this.options[i];
                        var optValue = (opt.value || opt.text);
                        opt.selected = values.contains(optValue);
                    }
                    break;
            }
            break;
            
        case TAGS.TEXTAREA:
            if (isObject(value)) {
                var buf = "";
                for (p in value) buf += (p == "__type__"? "" : p+":"+value[p]+"\n");
                value = buf;
            }            
            this.value = value;
            break;
          
        default:
            if (html) this.innerHTML = value;
            else this.innerText = value;
        
    }
}

// Differently from the form version of this function, this function will bind
// all the elements that have a 'name' attribute or the one passed
HTMLElement.prototype.genGetValues = function(obj /* Object */, attrName /* String */) {
    if (obj == null) obj = new Object();
    if (attrName == null) attrName = ATTRS.NAME;
    var elems = DOM.getElementsByAttribute(this, attrName);
    for (var i = 0; i < elems.length; i++) {
        obj[elems[i].getAttribute(ATTRS.NAME)] = elems[i].getValue();
    }
}

// Differently from the form version of this function, this function will bind
// all the elements that have a 'name' attribute or the one passed
HTMLElement.prototype.genSetValues = function(obj /* Object* /, attrName /* String */) {
    this.data = obj;
    if (attrName == null) attrName = ATTRS.NAME;
    var elems = DOM.getElementsByAttribute(this, attrName);
    for (var i = 0; i < elems.length; i++) {
        // console.log("elems[i].name: "+elems[i].name+", elems[i].nodeName: "+elems[i].nodeName);
        var key = elems[i].getAttribute(ATTRS.NAME);
        if (key in obj) elems[i].setValue(obj[key]);
    }
}


//~ NAME METHOD(S) -----------------------------------------------------------

/*
HTMLElement.prototype.bindNames = function(deep / * Boolean * /, origElem / * HTMLElement * /) {
    if (origElem == null) origElem = this;
    for (var i = 0, l = this.childNodes.length; i < l; i++) {
        var childNode = this.childNodes[i];
        if (childNode.nodeType != Node.ELEMENT_NODE) continue;
        var childName = this.childNodes[i].name;
        if (childName != null && childName.length > 0) origElem[childName] = childNode;
        if (deep) childNode.bindNames(deep, origElem);
    }
}
*/


//~ AJAX METHOD(S) -----------------------------------------------------------

/*
HTMLElement.prototype.load = function(url / * url * /) {
    var elem = this;
    var tempFn = function(elem, html) {
        // Extract Scripts, set HTML, inject behavior and execute scripts
        elem.innerHTML = html;
        Behavior.injectBehavior(elem);
        _executeScripts(_extractScripts(html));
        
        // Fire load event
        Event.dispatch(elem, EVENTS.LOAD);
    };
    // if (sync) request(url, function(html) { tempFn(elem, html); });
    // else tempFn(elem, request(url));

    // It always load synchronously
    tempFn(elem, request(url));
}
*/

    
//****************************************************************************
// BEHAVIOR
//****************************************************************************

//~ CONSTRUCTOR(S) -----------------------------------------------------------

function Behavior() { }


//~ CONSTANT(S) --------------------------------------------------------------

/**
 * If 'true', it will print debug statements as the Behavior walks the DOM tree.  
 * It should normally be set to 'false'.
 */
Behavior.DEBUG = false;

/**
 * If 'true', it will invoke the tag handlers inside a throw/catch clause, and only
 * report errors to the console.  It should normally be set to 'true', but during
 * debugging it is useful to disable this flag so that error percolate through the
 * normal channels and show up in the javascript debugging tools.
 */
Behavior.SAFE = false;

/**
 * If 'false', it will disable the Behavior normal mechanism.  Sometimes it is useful
 * to disable the Behavior, to see how the page would look if it never run, and to 
 * understand the original DOM structure the Behavior is working on top off.
 */
Behavior.ENABLED = true;


//~ METHOD(S) ----------------------------------------------------------------

/**
 * Entry function that calls 'parseElement' to do the real work
 */
Behavior.injectBehavior = function(elem /* HTMLElement */) {
    if (!Behavior.ENABLED) return;
    if (Behavior.DEBUG) console.debug("Behavior.injectBehavior("+elem+")");
    
    // Parse element is created so that it can be called recursively
    Behavior._parseElement(elem || document.documentElement, 0);
    
    return elem;
}

Behavior.registerBehavior = function(behavior /* String */, handler /* Function */) {
    assertType(behavior, String);
    assertType(handler, Function);
    
    // Make sure 'Behavior._handlers' is defined and lazily on the 'onload' event.  The
    // reason to add it here is so that it is only invoked if something was added
    // to the handlers, and goes on silently otherwise (and therefore the Behavior event
    // will never exist or fire)
    if (typeof(Behavior._handlers) == UNDEFINED) {
        Behavior._handlers = new Object();
        var fn = function() { if (Behavior.ENABLED) Behavior._parseElement(document.documentElement, 0); }

        // Prefer to call 'Event._attachDomContentLoadedEvent' for IE directly
        if (document.addEventListener) document.addEventListener(EVENTS.DOMCONTENTLOADED, fn, false);
        else Event._attachDomContentLoadedEvent(document, fn);
    }

    this._handlers[behavior] = handler;
}


//~ HELPER(S) ----------------------------------------------------------------

/**
 * This function visit all nodes in the DOM tree, and if there are translators
 * registered, it will attempt calling them.  A translator will make all the changes
 * it needs to.  The translator will return a node where translation should continue.
 * If the returned node has no parent, the current node can be replaced with it.  
 * This has an additional objective: delay the adding of nodes to the real DOM document 
 * until the last possible minute.
 * 
 * Nodes that want to be processed have two ways of doing so: 1) by registering
 * the tag name, or 2) by registering a 'dwType' attribute value.  The reason
 * for the second registration type is that some browsers have trouble with XML 
 * namespaces.
 */
Behavior._parseElement = function(elem /* HTMLElement */, depth /* Integer */) {
    // Comment for efficiency
    // assertType(elem, HTMLElement);
    // assertType(depth, Number);
    
    // Debugging
    // if (Behavior.DEBUG) console.debug("Behavior.parseElement - parsing element '"+elem+"' (tag: "+elem.nodeName+", id: "+elem.id+", behavior: "+behavior+")");
    if (Behavior.DEBUG) console.debug("Behavior.parseElement("+elem+","+depth+")");

    // Parse all the children, it is important to first parse the children and then 
    // apply the behavior so that we go from the inside out
    Behavior._parseNodeList(elem.childNodes, depth);

    // Try getting behavior from the 'behavior' attribute, but if it is not available,
    // then try getting it from the 'class' attribute
    var dwType = getDWType(elem);

    // If there is a behavior handler apply it
    var hnd = (dwType == null? null : Behavior._handlers[dwType]);
    if (hnd != null) {
        if (Behavior.DEBUG) console.debug("Behavior.parseElement - attaching behavior '"+hnd+"'");
        try {
            if (!elem._behavior) {
                elem._behavior = true;
                hnd.call(null, elem);
            }
            // console.debug("Behavior.parseElement - element '"+elem+"' has type '"+elem.dwType+"'");
        }
        catch (ex) {
            if (Behavior.SAFE) console.error(ex.message); 
            else throw ex;
        }
    }
}

Behavior._parseNodeList = function(nl /* NodeList */, depth /* Integer */) {
    // Comment for efficiency
    // assertType(nl, NodeList);
    // assertType(depth, Number);

    if (Behavior.DEBUG) console.debug("Behavior.parseElement("+nl+","+depth+")");
    
    // Parse node list
    for (var i = 0; i < nl.length; i++) {
        if (nl[i].nodeType != Node.ELEMENT_NODE) continue;
        Behavior._parseElement(nl[i], depth+1);
    }
}

    
//****************************************************************************
// EVENT
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

// Protect agains overwriting prototype 'Event'
if (typeof(Event) == UNDEFINED) { function Event() { } }


// DW PROTECTION -------------------------------------------------------------

// If Event.js is being used by itself, make a dummy out of 'assertType'
if (typeof(assertType) == "undefined") assertType = function() { };
if (typeof(assertTypeOrNull) == "undefined") assertTypeOrNull = function() { };


//~ METHOD(S) ----------------------------------------------------------------

Event.addListener = function(obj /* HTMLElement */, eventType /* String */, listener /* Function */, useCapture /* Boolean */) {
    assertType(obj, Object);
    assertType(eventType, String);
    assertType(listener, Function);
    
    // Standard way
    if (typeof(obj.addEventListener) != UNDEFINED) {
        
        // Special cases for 'mouseenter' and 'mouseleave'
        if (eventType == EVENTS.MOUSEENTER || eventType == EVENTS.MOUSELEAVE) {
            var origListener = listener;
            listener = function(ev) {
                var rt = ev.relatedTarget;
                if (this === rt || rt.contains(this)) return;
                origListener.call(this, ev);
            }
            switch (eventType) {
                case EVENTS.MOUSEENTER: eventType = EVENTS.MOUSEOVER;
                case EVENTS.MOUSELEAVE: eventType = EVENTS.MOUSEOUT;
            }
        }
        
        return obj.addEventListener(eventType, listener, useCapture);
    }

    // Ensure generic listener existance
    if (typeof(obj._listeners) == UNDEFINED) obj._listeners = new Object();
    if (typeof(obj._listeners[eventType]) == UNDEFINED) obj._listeners[eventType] = new Array();
    
    // Create wrapper function, save pointer to it from original listener and add it to listeners
    listener._wrapperListener = function(event) { Event._standarizeEvent(event, obj); listener.call(obj, event); };
    obj._listeners[eventType].push(listener._wrapperListener);
    
    // IE
    if (typeof(obj.attachEvent) != UNDEFINED) {
        return obj.attachEvent("on"+eventType, listener._wrapperListener);
    }
}

Event.removeListener = function(obj /* HTMLElement */, eventType /* String */, listener /* Function */, useCapture /* Boolean */) {
    assertType(obj, Object);
    assertType(eventType, String);
    assertType(listener, Function);

    // Standard way
    if (typeof(obj.removeEventListener) != UNDEFINED) {
        return obj.removeEventListener(eventType, listener, useCapture);
    }

    // Ensure generic listener existance
    if (typeof(obj._listeners) == UNDEFINED) return;
    if (typeof(obj._listeners[eventType]) == UNDEFINED) return;

    // Remove listener using pointer from original listener
    obj._listeners[eventType].remove(listener._wrapperListener);
    
    // IE
    if (typeof(obj.detachEvent) != UNDEFINED) {
        return obj.detachEvent("on"+eventType, listener._wrapperListener);
    }
}

Event.dispatch = function(obj /* HTMLElement */, eventType /* String */, causeEvent /* Event */, addProps /* Object */) {
    // console.log("EVENT.dispatchEvent - obj.nodeName: "+obj.nodeName+", typeof(obj): "+typeof(obj)+", listeners: "+obj._listeners);

    // Event creation (if event not supplied)
    var target = obj;
    ev = Event._createEvent(obj);
    ev.initEvent(eventType, true, true);
    ev.causeEvent = causeEvent;
    // console.log("EVENT.dispatchEvent - typeof(ev): "+typeof(ev));

    // Additional Properties
    if (addProps != null) for (var prop in addProps) ev[prop] = addProps[prop];
    
    // Standard way
    // console.log("obj.dispatchEvent: "+obj.dispatchEvent);
    if (typeof(obj.dispatchEvent) != UNDEFINED) return target.dispatchEvent(ev);

    // IE
    ev.pseudoSrcElement = target;
    // console.log("EVENT.dispatchEvent - target.nodeName: "+target.nodeName);
    while (obj != null) {
        // console.log("EVENT.dispatchEvent - obj.nodeName: "+obj.nodeName+", listeners: "+obj._listeners+", ev.pseudoSrcElement: "+ev.pseudoSrcElement);
    
        // logWarn("_dispatchEvent - obj: "+obj.nodeName);
        var hasListeners = (obj._listeners != null && (eventType in obj._listeners));

        // Call listeners if it applies
        if (hasListeners) {
            var listeners = obj._listeners[eventType];
            for (var i = 0; i < listeners.length; i++) listeners[i].call(null, ev);
        }

        // If it can bubble and the bubble has not been canceled, continue
        if (ev.canBubble && !ev.cancelBubble) obj = obj.parentNode;
        else return true;
    }
    return true;
}


//~ HANDLER(S) ---------------------------------------------------------------


//~ HELPER(S) ----------------------------------------------------------------

Event._standarizeEvent = function(ev /* Event */, currentTarget /* HTMLElement */) {
    // console.log("Event: "+ev);
    
    // Standard way
    if (typeof(ev.target) != UNDEFINED) return ev;

    // IE
    // console.log("EVENT.standarizeEvent - ev.srcElement: "+ev.srcElement);
    ev.target = ev.srcElement || ev.pseudoSrcElement;
    ev.preventDefault = function() { ev.returnValue = false; };
    ev.stopPropagation = function() { ev.cancelBubble = true; };
    ev.pageX = ev.clientX + document.body.scrollLeft;
    ev.pageY = ev.clientY + document.body.scrollTop;
    if (currentTarget != null) ev.currentTarget = currentTarget;
    return ev;
};

// Creates the event functions at the object level
Event._createEvent = function(obj /* HTMLElement */) {
    var doc = obj.ownerDocument || window.document;

    // Standard Way
    // console.log("EVENT.dispatchEvent - typeof(doc.createEvent): "+typeof(doc.createEvent));
    if (typeof(doc.createEvent) != UNDEFINED) return doc.createEvent("Events");
    
    // IE
    ev = doc.createEventObject();
    ev.initEvent = function(eventType, canBubble, cancelable) { 
        this.eventType = eventType; 
        this.canBubble = canBubble; 
    }
    return ev;
};

// Specifically for IE
Event._attachDomContentLoadedEvent = function(obj /* HTMLElement */, listener /* Function */) {

    var ev = Event._createEvent(obj);
    ev.initEvent(EVENTS.DOMCONTENTLOADED, false, false);

    // Ensure firing before onload (maybe late but safe also for iframes)
    document.attachEvent("onreadystatechange", function() {
        if (document.readyState === "complete") {
            document.detachEvent("onreadystatechange", arguments.callee);
            listener.call(obj, ev);
        }
    });
    
    // If IE and not an iframe continually check to see if the document is ready
    if (document.documentElement.doScroll && window == window.top) {
        var domContentLoaded = false;
        var fn = function() {
            if (domContentLoaded) return;
            // If IE is used, use trick here: 
            // http://javascript.nwbox.com/IEContentLoaded/
            try { document.documentElement.doScroll("left"); }
            catch (error) { setTimeout(arguments.callee, 10); return; }

            // Execute listener
            listener.call(obj, ev);
        };
        fn.call();
    }

};

    
//****************************************************************************
// DOM
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function DOM() { }
DOM.prototype.className = "DOM";


// DW PROTECTION -------------------------------------------------------------

// If dom.js is being used by itself, make a dummy out of 'assertType'
if (typeof(assertType) == "undefined") assertType = function() { };
if (typeof(assertTypeOrNull) == "undefined") assertTypeOrNull = function() { };


//~ DOCUMENT MANIPULATION METHOD(S) ------------------------------------------

DOM.serialize = function(node, prettyPrint) {
    var ser = (new XMLSerializer()).serializeToString(node);
    return prettyPrint? XML(ser).toXMLString() : ser;
}

DOM.process = function(node, xslUrl) {
    // Acquire document
    var xslDoc = document.implementation.createDocument("", "", null);
    xslDoc.async = false;
    xslDoc.load(xslUrl);
    
    // Process
    var xslProc = new XSLTProcessor();
    xslProc.importStylesheet(xslDoc);
    return xslProc.transformToDocument(node);
}


//~ NODE MANIPULATION ELEMENT METHOD(S) --------------------------------------

/**
 * @param {Node} _this
 * @param {Node} newElem
 * @param {Node} refElem
 */
DOM.insertAfter = function(newElem /* Element */, refElem /* Element */) {
    var _this = refElem.parentNode;
    var ns = refElem.nextSibling;
    newElem = DOM._dom(newElem);
    return (ns == null? _this.appendChild(newElem) : _this.insertBefore(newElem, ns));
}

DOM.insertBefore = function(newElem /* Element */, refElem /* Element */) {
    var _this = refElem.parentNode;
    newElem = DOM._dom(newElem);
    return _this.insertBefore(newElem, refElem);
}

DOM.insert = function(_this /* Element */, newElem /* Element */, position /* Number */) {
    var children = _this.children;
    newElem = DOM._dom(newElem);
    if (position == null) return DOM.insertLast(_this, newElem);
    else if (position >= children.length) return DOM.insertLast(_this, newElem);
    else if (position <= 0) return DOM.insertFirst(_this, newElem);
    else return DOM.insertBefore(newElem, children[position]);
}

DOM.insertFirst = function(_this /* Element */, newElem /* Element */) {
    var fc = _this.firstChild;
    newElem = DOM._dom(newElem);
    return (fc == null? _this.appendChild(newElem) : _this.insertBefore(newElem, fc));
}

DOM.insertLast = function(_this /* Element */, newElem /* Element */) {
    newElem = DOM._dom(newElem);
    _this.appendChild(newElem);
}

DOM.replace = function(newElem, oldElem) {
    var _this = oldElem.parentNode;
    newElem = DOM._dom(newElem);
    _this.insertBefore(newElem, oldElem);
    _this.removeChild(oldElem);
}

DOM.remove = function(_this /* Element */, keepChildren /* Boolean */) {
    if (keepChildren) {
        while (_this.hasChildNodes()) {
            _this.parentNode.insertBefore(_this.firstChild, _this);
        }
    }
    _this.parentNode.removeChild(_this);
}

DOM.removeChildren = function(_this /* Element */) {
    while (_this.hasChildNodes()) _this.removeChild(_this.firstChild);
}

DOM.copyChildren = function(tgtNode /* Element */, srcNode /* Element */) {
    if (srcNode == null) return 0;
    var count = srcNode.childNodes.length;
    for (var i = 0; i < count; i++) {
        tgtNode.appendChild(srcNode.childNodes[i].cloneNode(true));
    }
    return count;
}

DOM.moveChildren = function(tgtNode /* Element */, srcNode /* Element */) {
    if (srcNode == null) return 0;
    if (srcNode == tgtNode) return 0;
    var count = 0;
    while (srcNode.hasChildNodes()) tgtNode.appendChild(srcNode.firstChild);
    return count;
}

DOM.replace = function(newElem, oldElem) {
    var _this = oldElem.parentNode;
    newElem = DOM._dom(newElem);
    _this.insertBefore(newElem, oldElem);
    _this.removeChild(oldElem);
}

DOM.wrap = function(newElem, oldElem) {
    var _this = oldElem.parentNode;
    newElem = DOM._dom(newElem);
    _this.insertBefore(newElem, oldElem);
    newElem.appendChild(oldElem);
}


//~ HELPER(S) ----------------------------------------------------------------

/**
 * Transforms the input into DOM.  If it is already a Node, it leaves it alone,
 * if it is a String, it transforms it into a DocumentFragment.
 */
DOM._dom = function(obj) {
    if (isNode(obj)) return obj;
    if (isString(obj)) {
        var div = arguments.callee.div;
        if (div == null) div = arguments.callee.div = document.createElement(TAGS.DIV);
        div.innerHTML = obj;
        var df = document.createDocumentFragment();
        DOM.moveChildren(df, div);
        return df;
    }
    throw new Error("Input '"+obj+"' is neither a DOM object nor a String");
}

DOM._firstOrNull = function(array) { 
    return array == null || array.length == 0? null : array[0]; 
}

DOM._isElement = function(node) { 
    return node.nodeType == Node.ELEMENT_NODE; 
}

DOM._hasTagName = function(node, tagName) { 
    return node.nodeType == Node.ELEMENT_NODE && node.nodeName.toLowerCase() == tagName; 
}

DOM._hasId = function(node, id) { return DOM._hasAttribute(node, ATTRS.ID, id); }

/*
DOM._hasId = function(node, id) { 
    if (node.nodeType != Node.ELEMENT_NODE) return false;
    return node.id != null && (id == null || node.id == id); 
}
*/

DOM._hasAttribute = function(node, attrName, attrValue) {
    if (node.nodeType != Node.ELEMENT_NODE) return false;
    var av = node.getAttribute(attrName);
    return av != null && (attrValue == null || av == attrValue);
}


//~ GENERIC TEST METHOD(S) ---------------------------------------------------

DOM.hasChild = function(_this /* Node */, child /* Node */) { return child.parentNode = _this; }
DOM.hasDescendant = function(_this /* Node */, desc /* Node */) { return DOM.hasAncestor(desc, _this); }
DOM.hasAncestor = function(_this /* Node */, anc /* Node */) {
    // Iterate over ancestors    
    for (var node = _this; node.parentNode != null; node = node.parentNode) {
        if (node == anc) return true;
    }
    // If it was not found, return false
    return false;
}


//~ GENERAL CHILDREN METHOD(S) -----------------------------------------------

DOM.getChild = function(_this /* Node */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getChildren(_this, 1, 1, reverseOrder)); }
DOM.getChildren = function(_this /* Node */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, DOM._isElement, 1, maxCount, reverseOrder); }

DOM.getChildByTagName = function(_this /* Node */, tn /* String */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getChildrenByTagName(_this, tn, 1, 1, reverseOrder)); }
DOM.getChildrenByTagName = function(_this /* Node */, tn /* String */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return DOM._hasTagName(nd, tn); }, 1, maxCount, reverseOrder); }

DOM.getChildByAttribute = function(_this /* Node */, an /* String */, av /* String */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getChildrenByAttribute(_this, an, av, 1, 1, reverseOrder)); }
DOM.getChildrenByAttribute = function(_this /* Node */, an /* String */, av /* String */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return DOM._hasAttribute(nd, an, av); }, 1, maxCount, reverseOrder); }

DOM.getChildById = function(_this /* Node */, id /* String */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getChildrenById(_this, id, 1, 1, reverseOrder)); }
DOM.getChildrenById = function(_this /* Node */, id /* String */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return DOM._hasId(nd, id); }, 1, maxCount, reverseOrder); }

DOM.getChildByName = function(_this /* Node */, nm /* String */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getChildrenByName(_this, nm, 1, 1, reverseOrder)); }
DOM.getChildrenByName = function(_this /* Node */, nm /* String */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return DOM._hasAttribute(nd, ATTRS.NAME, nm) }, 1, maxCount, reverseOrder); }

DOM.getChildByTest = function(_this /* Node */, testFn /* Function */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getChildrenByTest(_this, testFn, 1, 1, reverseOrder)); }
DOM.getChildrenByTest = function(_this /* Node */, testFn /* Function */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return nd.nodeType == Node.ELEMENT_NODE && testFn.call(null, nd); }, 1, maxCount, reverseOrder); }


//~ GENERAL ELEMENT METHOD(S) ------------------------------------------------

DOM.getElement = function(_this /* Node */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getElements(_this, null, 1, reverseOrder)); }
DOM.getElements = function(_this /* Node */, depth /* Number */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, DOM._isElement, depth, maxCount, reverseOrder); }

DOM.getElementByTagName = function(_this /* Node */, tn /* String */, depth /* Number */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getElementsByTagName(_this, tn, depth, 1, reverseOrder)); }
DOM.getElementsByTagName = function(_this /* Node */, tn /* String */, depth /* Number */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return DOM._hasTagName(nd, tn); }, depth, maxCount, reverseOrder); }

DOM.getElementByAttribute = function(_this /* Node */, an /* String */, av /* String */, depth /* Number */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getElementsByAttribute(_this, an, av, depth, 1, reverseOrder)); }
DOM.getElementsByAttribute = function(_this /* Node */, an /* String */, av /* String */, depth /* Number */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return DOM._hasAttribute(nd, an, av); }, depth, maxCount, reverseOrder); }

DOM.getElementById = function(_this /* Node */, id /* String */, depth /* Number */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getElementsById(_this, id, depth, 1, reverseOrder)); }
DOM.getElementsById = function(_this /* Node */, id /* String */, depth /* Number */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return DOM._hasId(nd, id); }, depth, maxCount, reverseOrder); }

DOM.getElementByName = function(_this /* Node */, nm /* String */, depth /* Number */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getElementsByName(_this, nm, depth, 1, reverseOrder)); }
DOM.getElementsByName = function(_this /* Node */, nm /* String */, depth /* Number */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return DOM._hasAttribute(nd, ATTRS.NAME, nm) }, depth, maxCount, reverseOrder); }

DOM.getElementByTest = function(_this /* Node */, testFn /* Function */, depth /* Number */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getElementsByTest(_this, testFn, depth, 1, reverseOrder)); }
DOM.getElementsByTest = function(_this /* Node */, testFn /* Function */, depth /* Number */, maxCount /* Number */, reverseOrder /* Boolean */) { return DOM.getNodesByTest(_this, function(nd) { return nd.nodeType == Node.ELEMENT_NODE && testFn.call(null, nd); }, depth, maxCount, reverseOrder); }


//~ GENERAL ELEMENT METHOD(S) ------------------------------------------------

DOM.getAncestor = function(_this /* Node */) { return _this.parentNode; }
DOM.getAncestors = function(_this /* Node */, depth /* Number */, maxCount /* Number */) { return DOM.getAncestorsByTest(_this, DOM._isElement, depth, maxCount); }

DOM.getAncestorByTagName = function(_this /* Node */, tn /* String */, depth /* Number */) { return DOM._firstOrNull(DOM.getAncestorsByTagName(_this, tn, depth, 1)); }
DOM.getAncestorsByTagName = function(_this /* Node */, tn /* String */, depth /* Number */, maxCount /* Number */) { return DOM.getAncestorsByTest(_this, function(nd) { return DOM._hasTagName(nd, tn); }, depth, maxCount); }

DOM.getAncestorByAttribute = function(_this /* Node */, an /* String */, av /* String */, depth /* Number */) { return DOM._firstOrNull(DOM.getAncestorsByAttribute(_this, an, av, depth, 1)); }
DOM.getAncestorsByAttribute = function(_this /* Node */, an /* String */, av /* String */, depth /* Number */, maxCount /* Number */) { return DOM.getAncestorsByTest(_this, function(nd) { return DOM._hasAttribute(nd, an, av); }, depth, maxCount); }

DOM.getAncestorById = function(_this /* Node */, id /* String */, depth /* Number */) { return DOM._firstOrNull(DOM.getAncestorsById(_this, id, depth, 1)); }
DOM.getAncestorsById = function(_this /* Node */, id /* String */, depth /* Number */, maxCount /* Number */) { return DOM.getAncestorsByTest(_this, function(nd) { return DOM._hasId(nd, id); }, depth, maxCount); }

DOM.getAncestorByName = function(_this /* Node */, nm /* String */, depth /* Number */) { return DOM._firstOrNull(DOM.getAncestorsByName(_this, nm, depth, 1)); }
DOM.getAncestorsByName = function(_this /* Node */, nm /* String */, depth /* Number */, maxCount /* Number */) { return DOM.getAncestorsByTest(_this, function(nd) { return DOM._hasAttribute(nd, ATTRS.NAME, nm) }, depth, maxCount); }


//~ GENERIC NODE METHOD(S) ---------------------------------------------------

// The function below are the heavy duty one that all the ones above use

DOM.getAncestorByTest = function(_this /* Node */, testFn /* Function */, depth /* Number */) { return DOM._firstOrNull(DOM.getAncestorsByTest(_this, testFn, depth, 1)); }
DOM.getAncestorsByTest = function(_this /* Node */, testFn /* Function */, depth /* Number */, maxCount /* Number */, elems /* Array */) {
    // Commented for efficiency
    assertType(_this, Node);
    assertTypeOrNull(testFn, Function);
    assertTypeOrNull(depth, Number);
    assertTypeOrNull(maxCount, Number);

    // By default go all the way (unrestricted depth) and no maximum count limit
    if (depth == null) depth = -1;
    if (maxCount == null) maxCount = -1;
    if (elems == null) elems = new Array();

    // Iterate over ancestors    
    for (var node = _this; node.parentNode != null; node = node.parentNode) {
        // If this is not an Element node, nothing to do
        if (node.nodeType != Node.ELEMENT_NODE) return elems;
        
        // Have we reached the depth limit?    
        if (depth == 0) return elems;
        
        // Have we reached the count limit?    
        if (elems.length == maxCount) return elems;
        
        // Test current ancestor
        if (testFn == null || testFn.call(null, node)) elems.push(node);

        // console.log("DOM.getAncestorsByTest - nodeName: "+_this.nodeName+", innerText: "+_this.innerText.trim());
    }
    
    // If we get here is because we did not find it
    return null;
}

DOM.countNodesByTest = function(_this /* Node */, testFn /* Function */, depth /* Number */) {
    // If '_this' is 'null', it means I want to look in the entire document
    if (_this == null) _this = document.documentElement;
    
    // Commented for efficiency
    assertType(_this, Node);
    assertTypeOrNull(testFn, Function);
    assertTypeOrNull(depth, Number);
    
    var count = 0;

    // By default go all the way (unrestricted depth)
    if (depth == null) depth = -1;

    // If this is not an Element node, nothing to do
    var nt = _this.nodeType;
    if (nt != Node.ELEMENT_NODE && nt != Node.DOCUMENT_NODE) return count;

    // Have we reached the depth limit?    
    if (depth == 0) return count;
    
    // Iterate over children testing them
    for (var i = 0; i < _this.childNodes.length; i++) {
        var child = _this.childNodes[i];
        if (testFn == null || testFn.call(null, child)) count++;
        count += DOM.countNodesByTest(child, testFn, depth - 1);
    }

    return count;
}

DOM.getNodeByTest = function(_this /* Node */, testFn /* Function */, depth /* Number */, reverseOrder /* Boolean */) { return DOM._firstOrNull(DOM.getNodesByTest(_this, testFn, depth, 1, reverseOrder)); }
DOM.getNodesByTest = function(_this /* Node */, testFn /* Function */, depth /* Number */, maxCount /* Number */, reverseOrder /* Boolean */, elems /* Array */) {
    // If '_this' is 'null', it means I want to look in the entire document
    if (_this == null) _this = document.documentElement;
    
    // Commented for efficiency
    assertType(_this, Node);
    assertTypeOrNull(testFn, Function);
    assertTypeOrNull(depth, Number);
    assertTypeOrNull(maxCount, Number);

    // console.log("DOM.getNodesByTest - nodeName: "+_this.nodeName+", testFn: "+testFn);
    // console.log("DOM.getNodesByTest - nodeName: "+_this.nodeName+", name: "+_this.getAttribute(ATTRS.NAME));

    // By default go all the way (unrestricted depth) and no maximum count limit
    if (depth == null) depth = -1;
    if (maxCount == null) maxCount = -1;
    if (elems == null) elems = new Array();

    // If this is not an Element node, nothing to do
    var nt = _this.nodeType;
    if (nt != Node.ELEMENT_NODE && nt != Node.DOCUMENT_NODE) return elems;
    
    // Have we reached the depth limit?    
    if (depth == 0) return elems;
    
    // Have we reached the count limit?    
    if (elems.length == maxCount) return elems;
        
    // Iterate over children testing them
    if (!reverseOrder) {
        for (var i = 0; i < _this.childNodes.length && (maxCount == -1 || elems.length < maxCount); i++) {
            var child = _this.childNodes[i];
            // console.log("DOM.getNodesByTest - child.nodeName: "+child.nodeName+", child.nodeType: "+child.nodeType);
            if (testFn == null || testFn.call(null, child)) elems.push(child);
            // console.log("DOM.getNodesByTest - nodeName: "+_this.nodeName+", testResult: "+testFn.call(null, child));
            DOM.getNodesByTest(child, testFn, depth - 1, maxCount, reverseOrder, elems);
        }
    }
    else {
        for (var i = _this.childNodes.length - 1; i >= 0 && (maxCount == -1 || elems.length < maxCount); i--) {
            var child = _this.childNodes[i];
            if (testFn == null || testFn.call(null, child)) elems.push(child);
            DOM.getNodesByTest(child, testFn, depth - 1, maxCount, reverseOrder, elems);
        }
    }
    
    return elems;
}

DOM.removeNodesByTest = function(_this /* Node */, testFn /* Function */, depth /* Number */, maxCount /* Number */, reverseOrder /* Boolean */, elems /* Array */) {
    // Commented for efficiency
    assertType(_this, Node);
    assertTypeOrNull(testFn, Function);
    assertTypeOrNull(depth, Number);
    assertTypeOrNull(maxCount, Number);

    // By default go all the way (unrestricted depth) and no maximum count limit
    if (depth == null) depth = -1;
    if (maxCount == null) maxCount = -1;
    if (elems == null) elems = new Array();

    // If this is not an Element node, nothing to do
    var nt = _this.nodeType;
    if (nt != Node.ELEMENT_NODE && nt != Node.DOCUMENT_NODE) return elems;
    
    // Have we reached the depth limit?    
    if (depth == 0) return elems;
    
    // Have we reached the count limit?    
    if (elems.length == maxCount) return elems;

    // Iterate over children testing them
    if (!reverseOrder) {
        for (var i = 0; i < _this.childNodes.length && (maxCount == -1 || elems.length < maxCount); i++) {
            var child = _this.childNodes[i];
            if (testFn == null || testFn.call(null, child)) {
                elems.push(child);
                _this.removeChild(child);
                i = i - 1;
            }
            else DOM.removeNodesByTest(child, testFn, depth - 1, maxCount, reverseOrder, elems);
        }
    }
    else {
        for (i = _this.childNodes.length - 1; i >= 0 && (maxCount == -1 || elems.length < maxCount); i--) {
            child = _this.childNodes[i];
            if (testFn == null || testFn.call(null, child)) {
                elems.push(child);
                _this.removeChild(child);
            }
            else DOM.removeNodesByTest(child, testFn, depth - 1, maxCount, reverseOrder, elems);
        }       
    }
    
    return elems;
}

DOM.nextElement = function(_this /* Node */, circular /* Boolean */) { return DOM.nextSiblingByTest(_this, DOM._isElement, circular); }
DOM.nextSiblingByTest = function(_this /* Node */, testFn /* Function */, circular /* Boolean */) {
    for (var ns = _this.nextSibling; ns != null && ns != _this; ) {
        if (testFn == null || testFn.call(null, ns)) return ns;
        ns = (circular? ns.nextSibling || _this.parentNode.firstChild : ns.nextSibling);
    }
    return null;
}

DOM.previousElement = function(_this /* Node */, circular /* Boolean */) { return DOM.previousSiblingByTest(_this, DOM._isElement, circular); }
DOM.previousSiblingByTest = function(_this /* Node */, testFn /* Function */, circular /* Boolean */) {
    for (var ps = _this.previousSibling; ps != null && ps != _this; ) {
        if (testFn == null || testFn.call(null, ps)) return ps;
        ps = (circular? ps.previousSibling || _this.parentNode.lastChild : ps.previousSibling);
    }
    return null;
}

DOM.nextElementByTest = function(_this /* Node */, testFn /* Function */, top /* Element */, circular /* Boolean */) {
    if (top == null) top == _this.ownerDocument;

    var ne = _this, pne = null;
    while (ne != null) {
        // Get possible next element
        pne = DOM.nextElement(ne);
        while (pne == null && ne != top) { pne = DOM.nextElement(ne = ne.parentNode); }

        // We are at the end of the road
        if (ne == null || ne == top) { ne = null; break; }

        // Follow the next possible element
        ne = pne;
    
        // Test current node
        if (testFn == null || testFn.call(null, ne)) return ne;
        
        // Search children of that node to see if it matches the condition
        pne = DOM.getElementByTest(ne, testFn, null, false);
        if (pne != null) return pne;
    }
    
    // If 'ne' is null and asking for circular, ask for first element of top
    if (ne == null && circular) return DOM.getElementByTest(top, testFn, null, false);
    
    return ne;
}

DOM.previousElementByTest = function(_this /* Node */, testFn /* Function */, top /* Element */, circular /* Boolean */) {
    if (top == null) top == _this.ownerDocument;

    var pe = _this, ppe = null;
    while (pe != null) {
        // Get possible previous element
        ppe = DOM.previousElement(pe);
        while (ppe == null && pe != top) { ppe = DOM.previousElement(pe = pe.parentNode); }

        // We are at the end of the road
        if (pe == null || pe == top) { pe = null; break; }

        // Follow the previous possible element
        pe = ppe;
    
        // Test current node
        if (testFn == null || testFn.call(null, pe)) return pe;
        
        // Search children of that node to see if it matches the condition
        ppe = DOM.getElementByTest(pe, testFn, null, true);
        if (ppe != null) return ppe;
    }
    
    // If 'pe' is null and asking for circular, ask for first element of top
    if (pe == null && circular) return DOM.getElementByTest(top, testFn, null, true);

    return pe;
}

    
//****************************************************************************
// RPC
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function RPC() { }


//~ ATTRIBUTE(S) -------------------------------------------------------------

RPC.METHODS = { SCRIPT:"SCRIPT", GET:"GET", POST:"POST" };

RPC.DEBUG = false;
RPC.SAFE = false;
RPC.METHOD = RPC.METHODS.POST;

// Object to store randomly generated functions
RPC.CALLBACKS = new Object();


//~ METHOD(S) ---------------------------------------------------------------

/*
RPC.define = function(url / * String * /, objectName / * String * /) { 
    var wf = function(methodName, args) {
        assertType(methodName, String);
        args = (new Array()).addAll(args);
        
        // Get last and the one before last
        var beforeLast = (args.length >= 2? args[args.length - 2] : null);
        var last = (args.length >= 1? args[args.length - 1] : null);
        var tm = RPC.METHOD, ah = null;
        if (isFunction(last)) ah = args.pop();
        else if (isFunction(beforeLast) && isString(last)) { tm = args.pop(); ah = args.pop(); }

        // Invoke
        RPC.invoke(url, objectName, methodName, args, tm, function(val) { if (ah != null) ah.call(null, val); });
    }
    
    RPC[objectName] = new Object();
    RPC.invoke(url, "RPC", "listMethods", [objectName], null, function(methods) {
        for (var i = 0; i < methods.length; i++) {
            var mn = methods[i].name;
            var fn = function() { wf(arguments.callee.methodName, arguments); };
            fn.methodName = mn;
            // console.log("Defining 'RPC."+objectName+"."+mn+"'");
            RPC[objectName][mn] = fn;
        }
    });
    return RPC[objectName];
}
*/

RPC.define = function(url /* String */, objectName /* String */) {
    RPC[objectName] = new Object();
    RPC.invoke(url, "RPC", "listMethods", [objectName], null, function(methods) {
        RPC._defineMethods(url, objectName, methods);
    });
    return RPC[objectName];
}

RPC.invoke = function(url /* String */, objectName /* String */, methodName /* String */, args /* Array */, transMethod /* String */, asyncHandler /* Function */) {
    if (transMethod == null) transMethod = RPC.METHOD;
    if (RPC.DEBUG) console.debug("RPC.invoke - invoking: "+(objectName == null? "" : objectName+".")+methodName+"("+args+")");

    // Create custom start event and dispatch it
    Event.dispatch(RPC, EVENTS.START);

    // Create wrapper
    var asyncHandlerWrapper = function(json) {
        
        if (json != null) {
            if (json.startsWith('{')) json = '('+json+')';
            if (RPC.DEBUG) console.debug("RPC.invoke - json: "+json);

            // Replace dates
            json = json.replace(/"@([-+]?\d+)@"/g, "new Date($1)");

            var value = eval(json);
            if (value instanceof Error) {
                RPC._error = value;
                Event.dispatch(RPC, EVENTS.ERROR);
            }
            else {
                try { asyncHandler.call(null, value); }
                catch (ex) {
                    if (RPC.SAFE) console.error(ex.toString());
                    else { console.error(ex); throw ex; }
                }
            }
        }

        // Create custom end event and dispatch it
        Event.dispatch(RPC, EVENTS.END);
    }

    // If there is not asyncHandler, then remove wrapper
    if (asyncHandler == null) asyncHandlerWrapper = null;
    
    // Content
    var content = { object: objectName, method: methodName, params: args };
    // console.log("JSON(content): "+JSON.encode(content));

    // Perform Request
    if (transMethod == null) /* Do nothing */;
    else if (transMethod == "SCRIPT") {
        var cid = genId();
        RPC.CALLBACKS[cid] = asyncHandler;
        content.callback = "RPC.CALLBACKS."+cid;
        script(url+"?"+JSON.encode(content), asyncHandlerWrapper); 
    }
    else if (transMethod == "GET") request(url+"?"+JSON.encode(content), asyncHandlerWrapper, null, RPC.METHOD, "text/plain");
    else if (transMethod == "POST") request(url, asyncHandlerWrapper, JSON.encode(content), RPC.METHOD, "text/plain");
}

RPC.download = function(url /* String */, objectName /* String */, methodName /* String */, args /* Array */) {
    if (RPC.DEBUG) console.debug("RPC.download - invoking: "+(objectName == null? "" : objectName+".")+methodName+"("+args+")");
    
    // Content
    var content = { object: objectName, method: methodName, params: args };

    // Perform Request
    window.location = url+"?"+JSON.encode(content);
}


//~ HELPER(S) ----------------------------------------------------------------

RPC._defineMethods = function(url /* String */, objectName /* String */, methods /* Method[] */) {
    if (typeof(RPC[objectName]) == UNDEFINED) RPC[objectName] = new Object();
    for (var i = 0; i < methods.length; i++) {
        var mn = methods[i].name;
        var fn = function() { RPC._callWrapper(url, objectName, arguments.callee.methodName, arguments); };
        fn.methodName = mn;
        RPC[objectName][mn] = fn;
    }
}

RPC._callWrapper = function(url /* String */, objectName /* String */, methodName /* String */, args /* Array */) {
    assertType(methodName, String);
    args = (new Array()).addAll(args);
    
    // Get last and the one before last
    var beforeLast = (args.length >= 2? args[args.length - 2] : null);
    var last = (args.length >= 1? args[args.length - 1] : null);
    var tm = RPC.METHOD, ah = null;
    if (isFunction(last)) ah = args.pop();
    else if (isFunction(beforeLast) && isString(last)) { tm = args.pop(); ah = args.pop(); }

    // Invoke
    RPC.invoke(url, objectName, methodName, args, tm, function(val) { if (ah != null) ah.call(null, val); });
}

    
//****************************************************************************
// GEO
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function GEO() { }


//~ ATTRIBUTE(S) -------------------------------------------------------------


//~ METHOD(S) ---------------------------------------------------------------

GEO.getBounds = function(locs /* Location[] */) {
    var max = { lat: -Number.MAX_VALUE, lng:  -Number.MAX_VALUE };
    var min = { lat: Number.MAX_VALUE, lng:  Number.MAX_VALUE };
    
    for (var i = 0; i < locs.length; i++) {
        if (locs[i].latitude < min.lat)  min.lat = locs[i].latitude;
        if (locs[i].longitude < min.lng) min.lng = locs[i].longitude;
        if (locs[i].latitude > max.lat)  max.lat = locs[i].latitude;
        if (locs[i].longitude > max.lng) max.lng = locs[i].longitude;
    }
    
    return { lat: min.lat, lng: min.lng, latSpan: (max.lat - min.lat), lngSpan: (max.lng - min.lng) };
}

GEO.getCenter = function(bounds) {
    var lat = bounds.lat + bounds.latSpan / 2;
    var lng = bounds.lng + bounds.lngSpan / 2;
    return new Location(lat,lng);
}


//****************************************************************************
// LOCATION
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Location(lat, lng, nm) { 
    this.latitude = lat;
    this.longitude = lng;
    this.name = nm;
}


//~ ATTRIBUTE(S) -------------------------------------------------------------


//~ METHOD(S) ---------------------------------------------------------------

Location.prototype.toString = function() {
    return (name == null? "" : name)+"["+this.latitude+","+this.longitude+"]";
}

    
//****************************************************************************
// JSON
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function JSON() { }


//~ ATTRIBUTE(S) -------------------------------------------------------------


//~ METHOD(S) ---------------------------------------------------------------

JSON.encode = function(value) {
    var i = 0;
    var text = "null";
    var type = typeof(value);

    if (type == "boolean" || value instanceof Boolean) text = (value) ? "true" : "false";
    else if (!value && value != false) text = "null";
    else if (type == "string" || value instanceof String) {
       // Deal with null type
       if (value.length == 0) text = "null";
       else {
           var data = ['"'];
           for (i = 0; i < value.length; i++) {
               var c = value.charAt(i);
               switch (c) {
                   case '"': data.push('\\"'); break;
                   case '\\':data.push('\\\\'); break;
                   case '\b': data.push('\\b'); break;
                   case '\f': data.push('\\f'); break;
                   case '\n': data.push('\\n'); break;
                   case '\r': data.push('\\r'); break;
                   case '\t': data.push('\\t'); break;
                   default: data.push(c);
               }
            }
            data.push('"');
            text = data.join("");
        }
    }
    else if (type == "number" || value instanceof Number) {
        if (isNaN(value) || value == Number.POSITIVE_INFINITY || value == Number.NEGATIVE_INFINITY) text = '"' + value.toString() + '"';
        else text = value.toString();
    }
    else if (value instanceof Array) {
        var data = [];
        for (var i = 0; i < value.length; i++) data.push(JSON.encode(value[i]));
        text = "[" + data.join(',') + "]";
    }
    else if (value instanceof Date) {
        text = "@" + value.getTime() + "@";
    }
    else if (type == "object") {
        var data = [];
        for (var key in value) data.push(JSON.encode(key) + ':' + JSON.encode(value[key]));
        text = "{" + data.join(',') + "}";
    }
    return text;
}

JSON.decode = function(value) {
    if (/^("(\\.|[^"\\\n\r])*"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.test(value)) {
        // Replace dates
        value = value.replace(/"@(\d+)@"/, "new Date($1)");
        // Eval
        return eval("(" + value + ")");
    }
    throw new Error(1000, "JSON syntax error.");
}

    
//****************************************************************************
// INCLUDE
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Include() { }


//~ PRIVATE METHOD(S) --------------------------------------------------------

Include._insert = function(inc /* HTMLDivElement */) {
    // Get source
    var src = inc.getAttribute(ATTRS.SRC);
    if (src == null) { console.error("Include element does not containt 'src' attribute."); return; }

    // We take advantage of the fact that JS is single threaded
    var ld = arguments.callee._loadDiv;
    if (ld == null) ld = arguments.callee._loadDiv = document.createElement(TAGS.DIV);

    // Get a hold of the parent
    var parent = inc.parentNode;
    
    // Load and insert instead of div
    request(src, function(resp) {
        ld.innerHTML = resp;
        // Transfer children
        while (ld.firstChild != null) parent.insertBefore(ld.firstChild, inc);
        parent.removeChild(inc);
    });
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwInclude", function(inc) {
    Include._insert(inc);
});


    
    
//****************************************************************************
// FX
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Fx() { }


//~ CONSTANTS(S) -------------------------------------------------------------

Fx.SHORT  =  250;
Fx.NORMAL =  500;
Fx.LONG   = 1000;

Fx.LINEAR  = function(p) { return p; };
Fx.QUAD    = function(p) { return p*p; };
Fx.CUBIC   = function(p) { return p*p*p; };
Fx.QUART   = function(p) { return p*p*p*p; }
Fx.QUINT   = function(p) { return p*p*p*p*p; }
Fx.EXP     = function(p) { return Math.pow(2, 8 * (p - 1)); };
Fx.CIRC    = function(p) { return 1 - Math.sin(Math.acos(p)); };
Fx.SIN     = function(p) { return 0.5 - Math.cos(p*Math.PI)/2; };

Fx.POW     = function(p, x) { return Math.pow(p, x[0] || 6); };
Fx.BACK    = function(p, x) { x = x[0] || 1.618; return Math.pow(p, 2) * ((x + 1) * p - x); };
Fx.ELASTIC = function(p, x) { return Math.pow(2, 10 * --p) * Math.cos(20 * p * Math.PI * (x[0] || 1) / 3); };
Fx.BOUNCE  = function(p) {
    var value;
    for (var a = 0, b = 1; 1; a += b, b /= 2) {
        if (p >= (7 - 4 * a) / 11) {
            value = - Math.pow((11 - 6 * a - 11 * p) / 4, 2) + b * b;
            break;
        }
    }
    return value;
};

Fx.LINEAR  = function(p) { return p; };


//~ ATTRIBUTE(S) -------------------------------------------------------------

Fx.parseEl = window.top.document.createElement("div");
Fx.props = ["backgroundColor","borderBottomColor","borderBottomWidth","borderLeftColor","borderLeftWidth","borderRightColor","borderRightWidth","borderSpacing","borderTopColor","borderTopWidth","bottom","color","fontSize","fontWeight","height","left","letterSpacing","lineHeight","marginBottom","marginLeft","marginRight","marginTop","maxHeight","maxWidth","minHeight","minWidth","opacity","outlineColor","outlineOffset","outlineWidth","paddingBottom","paddingLeft","paddingRight","paddingTop","right","textIndent","top","width","wordSpacing","zIndex"];


//~ METHOD(S) ---------------------------------------------------------------

Fx.interpolate = function(src, tgt, pos) { 
	return (src+(tgt-src)*pos).toFixed(3); 
}

Fx.color = function(src, tgt, pos) {
    var s = function(str, p, c) { return str.substr(p,c||1); }
    var i = 2, j, c, t, v = [], r = [];
    while (j = 3, c = arguments[i-1], i--) {
        if (s(c,0) == "r") { 
        	c = c.match(/\d+/g); 
        	while(j--) v.push(~~c[j]); 
        }
        else {
            if (c.length == 4) c = "#"+s(c,1)+s(c,1)+s(c,2)+s(c,2)+s(c,3)+s(c,3);
            while (j--) v.push(parseInt(s(c,1+j*2,2), 16));
        }
    }
    while (j--) { 
    	t = ~~(v[j+3]+(v[j]-v[j+3])*pos); 
    	r.push(t < 0? 0 : t > 255? 255 : t); 
    }
    return "rgb("+r.join(",")+")";
}

Fx.parse = function(prop) {
    var p = parseFloat(prop), q = prop.replace(/^[\-\d\.]+/,"");
    return isNaN(p) ? { v: q, f: Fx.color, u: ""} : { v: p, f: Fx.interpolate, u: q };
};

Fx.normalize = function(style) {
    var css, rules = {}, i = Fx.props.length, v;
    Fx.parseEl.innerHTML = "<div style='"+style+"'></div>";
    css = Fx.parseEl.childNodes[0].style;
    while (i--) if (v = css[Fx.props[i]]) rules[Fx.props[i]] = Fx.parse(v);
    return rules;
};

Fx.mirror = function(eas) {
    if (eas == null) eas = Fx.SIN;
    var fn = function(p) { return p < 0.5? eas(2*p) : eas(1-2*(p-0.5)); }
    return fn;
};

Fx.transform = function(el, style, dur, eas, cb) {
    el = (typeof(el) == "string")? document.getElementById(el) : el;
    if (dur == null) dur = Fx.SHORT;
    if (eas == null) eas = Fx.SIN;
    var target = Fx.normalize(style);
    var comp = el.currentStyle ? el.currentStyle : getComputedStyle(el, null);
    var prop, current = {}, start = +(new Date()), finish = start+dur;
    for (prop in target) current[prop] = Fx.parse(comp[prop]);
    var interval = setInterval(function() {
        var time = +(new Date()), pos = time > finish ? 1 : (time-start)/dur;
        for (prop in target) el.style[prop] = target[prop].f(current[prop].v,target[prop].v,eas(pos))+target[prop].u;
        if (time > finish) { clearInterval(interval); cb && cb(); }
    }, 10);
}


//~ PENNER EASING EQUATION(S) ------------------------------------------------

Fx.easeInQuad     = function(p) { return Math.pow(p, 2); };
Fx.easeOutQuad    = function(p) { return -(Math.pow((p-1), 2) -1); };
Fx.easeInOutQuad  = function(p) { return ((p/=0.5) < 1)? 0.5*Math.pow(p,2) : -0.5 * ((p-=2)*p - 2); };
Fx.easeInCubic    = function(p) { return Math.pow(p, 3); };
Fx.easeOutCubic   = function(p) { return (Math.pow((p-1), 3) +1); };
Fx.easeInOutCubic = function(p) { return ((p/=0.5) < 1)? 0.5*Math.pow(p,3) : 0.5 * (Math.pow((p-2),3) + 2); };
Fx.easeInQuart    = function(p) { return Math.pow(p, 4); };
Fx.easeOutQuart   = function(p) { return -(Math.pow((p-1), 4) -1) };
Fx.easeInOutQuart = function(p) { return ((p/=0.5) < 1)? (0.5*Math.pow(p,4)) : (-0.5 * ((p-=2)*Math.pow(p,3) - 2)); };
Fx.easeInQuint    = function(p) { return Math.pow(p, 5); };
Fx.easeOutQuint   = function(p) { return (Math.pow((p-1), 5) +1); };
Fx.easeInOutQuint = function(p) { return ((p/=0.5) < 1)? 0.5*Math.pow(p,5) : 0.5 * (Math.pow((p-2),5) + 2); };
Fx.easeInSine     = function(p) { return -Math.cos(p * (Math.PI/2)) + 1; };
Fx.easeOutSine    = function(p) { return Math.sin(p * (Math.PI/2)); };
Fx.easeInOutSine  = function(p) { return (-.5 * (Math.cos(Math.PI*p) -1)); };
Fx.easeInExpo     = function(p) { return (p==0) ? 0 : Math.pow(2, 10 * (p - 1)); };
Fx.easeOutExpo    = function(p) { return (p==1) ? 1 : -Math.pow(2, -10 * p) + 1; };
Fx.easeInOutExpo  = function(p) {
    if (p == 0) return 0;
    if (p == 1) return 1;
    return ((p/=0.5) < 1)? 0.5 * Math.pow(2,10 * (p-1)) : 0.5 * (-Math.pow(2, -10 * --p) + 2);
};
Fx.easeInCirc     = function(p) { return -(Math.sqrt(1 - (p*p)) - 1); };
Fx.easeOutCirc    = function(p) { return Math.sqrt(1 - Math.pow((p-1), 2)) };
Fx.easeInOutCirc  = function(p) { return ((p/=0.5) < 1)? -0.5 * (Math.sqrt(1 - p*p) - 1) : 0.5 * (Math.sqrt(1 - (p-=2)*p) + 1); };
Fx.easeOutBounce  = function(p) {
    if ((p) < (1/2.75)) return (7.5625*p*p);
    if (p < (2/2.75)) return (7.5625*(p-=(1.5/2.75))*p + .75);
    if (p < (2.5/2.75)) return (7.5625*(p-=(2.25/2.75))*p + .9375);
    return (7.5625*(p-=(2.625/2.75))*p + .984375);
};
Fx.easeInBack     = function(p) { var s = 1.70158; return (p)*p*((s+1)*p - s); };
Fx.easeOutBack    = function(p) { var s = 1.70158; return (p=p-1)*p*((s+1)*p + s) + 1; };
Fx.easeInOutBack  = function(p) { var s = 1.70158; return ((p/=0.5) < 1)? (0.5*(p*p*(((s*=(1.525))+1)*p -s))) : (0.5*((p-=2)*p*(((s*=(1.525))+1)*p +s) +2)); };
Fx.elastic        = function(p) { return -1 * Math.pow(4,-8*p) * Math.sin((p*6-1)*(2*Math.PI)/2) + 1; };
Fx.swingFromTo    = function(p) { var s = 1.70158; return ((p/=0.5) < 1) ? 0.5*(p*p*(((s*=(1.525))+1)*p - s)) : 0.5*((p-=2)*p*(((s*=(1.525))+1)*p + s) + 2); };
Fx.swingFrom      = function(p) { var s = 1.70158; return p*p*((s+1)*p - s); };
Fx.swingTo        = function(p) { var s = 1.70158; return (p-=1)*p*((s+1)*p + s) + 1; };
Fx.bounce         = function(p) {
    if (p < (1/2.75)) return (7.5625*p*p);
    if (p < (2/2.75)) return (7.5625*(p-=(1.5/2.75))*p + .75);
    if (p < (2.5/2.75)) return (7.5625*(p-=(2.25/2.75))*p + .9375);
    return (7.5625*(p-=(2.625/2.75))*p + .984375);
};
Fx.bouncePast     = function(p) {
    if (p < (1/2.75)) return (7.5625*p*p);
    if (p < (2/2.75)) return 2 - (7.5625*(p-=(1.5/2.75))*p + .75);
    if (p < (2.5/2.75)) return 2 - (7.5625*(p-=(2.25/2.75))*p + .9375);
    return 2 - (7.5625*(p-=(2.625/2.75))*p + .984375);
};
Fx.easeFromTo     = function(p) { return ((p/=0.5) < 1)? (0.5*Math.pow(p,4)) : (-0.5 * ((p-=2)*Math.pow(p,3) - 2)); };
Fx.easeFrom       = function(p) { return Math.pow(p,4); };
Fx.easeTo         = function(p) { return Math.pow(p,0.25); }


    
    
//****************************************************************************
// DRAGGABLE
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Draggable() { 
    this._dragHandle = this;
}


//~ ATTRIBUTE(S) -------------------------------------------------------------

Draggable.NORMAL   = 1;
Draggable.EXTERNAL = 2;
Draggable.GHOST    = 3;
Draggable.CLONE    = 4;

/* Keeps track of last known mouse location */
Draggable._lastMouseLocation = { x: 0, y: 0 };

/* It's a pointer to the external shade that will be moved */
Draggable._dragExternal = null;

/* It's a pointer to the clone that will be moved */
Draggable._dragClone = null;

/* It's a pointer to the drag handle */
Draggable._dragHandle = null;

/* It's a pointer to the drag element */
Draggable._dragElement = null;


//~ METHOD(S) ---------------------------------------------------------------

// The parameter 'ghost' tells the draggable code to make it a ghost while it
// is being dragged.  Very heavy HTML elements will have trouble being dragged
// if they are not made into ghosts
Draggable.makeDraggable = function(elem /* HTMLElement */, handle /* HTMLElement */, dragType /* [NORMAL|GHOST|EXTERNAL|CLONE] */, rect) {
    if (elem._draggable) return;
    elem._draggable = true;

    // Make sure the element has been enhanced
    // enhanceAs(elem, Draggable);
    
    // Set rectangle, drag handle and save drag type
    elem._rectangle = (rect == null? { x: null, y: null, width: null, height: null } : rect);
    elem._dragHandle = (handle == null? elem : handle);
    elem._dragType = dragType;
    
    // Add listener on mouse down
    Event.addListener(elem._dragHandle, EVENTS.MOUSEDOWN, function(ev) { Draggable._mouseDownHandler(ev, elem, dragType); });
}


//~ HELPER(S) ----------------------------------------------------------------

Draggable._startGhost = function(elem /* HTMLElement */) {
    elem._ghosted = true;
    var children = elem.children;
    for (var i = 0; i < children.length; i++) {
        children[i]._originalVisibility = children[i].style.visibility;
        children[i].style.visibility = "hidden";
    }
}

Draggable._stopGhost = function(elem /* HTMLElement */) {
    var children = elem.children;
    for (var i = 0; i < children.length; i++) {
        children[i].style.visibility = children[i]._originalVisibility;
    }
    elem._ghosted = false;
}

// Build an external out of the element
Draggable._startExternal = function(elem /* HTMLElement */) {
    // Build external if it has not been built
    if (Draggable._external == null) {
        Draggable._external = document.createElement(TAGS.DIV);
        Draggable._external.className = "dwDragExternal";
        document.body.appendChild(Draggable._external);
    }

    // Adjust dimension
    // Draggable._external.setBounds(elem.getBounds());
    // Adjust dimension
    var bcr = elem.getBoundingClientRect();
    // console.log(JSON.encode(bcr));
    // console.log(JSON.encode(elem.getBounds()));
    Draggable._external.setBounds({ x: bcr.left, y: bcr.top, width: bcr.width, height: bcr.height });

    // Make it visible
    Draggable._external.style.display = "block";
}

Draggable._stopExternal = function() {
    if (Draggable._external == null) return;
    // var loc = { x: parseInt(Draggable._external.style.left), y: parseInt(Draggable._external.style.top) };
    var bcr = Draggable._external.getBoundingClientRect();
    var loc = { x: bcr.left, y: bcr.top };
    
    // If the element has a relative position, adjust
    var cs = window.getComputedStyle(Draggable._dragElement, null);
    if (cs.position == "relative") {
        var pl = Draggable._dragElement.parentNode.getLocation();
        loc.x -= (pl.x + 11);
        loc.y -= (pl.y + 12);
    }
    
    Draggable._dragElement.setLocation(loc.x, loc.y);
    Draggable._external.style.display = "none";
}

//Build a clone out of the element
Draggable._startClone = function(elem /* HTMLElement */) {
    // Build external if it has not been built
    if (Draggable._clone == null) {
        Draggable._clone = document.createElement(TAGS.DIV);
        Draggable._clone.className = "dwDragClone";
        document.body.appendChild(Draggable._clone);
    }

    // Adjust dimension
    var bcr = elem.getBoundingClientRect();
    Draggable._clone.setLocation(bcr.left, bcr.top);
    
    // Clone
    DOM.removeChildren(Draggable._clone);
    Draggable._clone.appendChild(elem.cloneNode(true));

    // Make it visible
    Draggable._clone.style.display = elem.style.display;
}

Draggable._stopClone = function() {
    if (Draggable._clone == null) return;
    var bcr = Draggable._clone.getBoundingClientRect();
    var loc = { x: bcr.left, y: bcr.top };
    
    // If the element has a relative position, adjust
    var cs = window.getComputedStyle(Draggable._dragElement, null);
    if (cs.position == "relative") {
        var pl = Draggable._dragElement.parentNode.getLocation();
        loc.x -= (pl.x + 11);
        loc.y -= (pl.y + 12);
    }
    
    Draggable._dragElement.setLocation(loc.x, loc.y);
    Draggable._clone.style.display = "none";
}


//~ HANDLER(S) ---------------------------------------------------------------

// We use 'document' instead of 'window' on all the handler to be compatible with IE
Draggable._mouseDownHandler = function(ev /* Event */, elem /* HTMLElement */, dragType) {
    Draggable._dragElement = dw(elem);

    var cs = window.getComputedStyle(elem, null);
    // elem.origPosition = cs.position;
    // elem.style.position = "absolute";

    // Make sure the element has absolute position and save adjustments
    elem._adjustments = { x: parseInt(cs.marginLeft) || 0, y: parseInt(cs.marginTop) || 0 };

    elem._dragType = dragType;
    // console.log("mouseDown - dragType: "+dragType);
    // console.log(ev.target);
    switch (dragType) {
        case Draggable.NORMAL:   break;
        case Draggable.EXTERNAL: Draggable._startExternal(elem); break;
        case Draggable.GHOST:    Draggable._startGhost(elem); break;
        case Draggable.CLONE:    Draggable._startClone(elem); break;
    }

    // Dispatch start event
    Event.dispatch(elem, EVENTS.DRAGSTART, ev);
    
    // Add listeners
    Event.addListener(document, EVENTS.MOUSEMOVE, Draggable._mouseMoveHandler);
    Event.addListener(document, EVENTS.MOUSEUP, Draggable._mouseUpHandler);
    
    // Save last mouse location
    Draggable._lastMouseLocation = { x: window.mouseX, y: window.mouseY };
    
    // Prevent selection
    ev.preventDefault();
}

Draggable._mouseUpHandler = function(ev /* Event */) {
    if (Draggable._dragElement == null) return;
    
    // Get a hold of element
    var elem = Draggable._dragElement;

    switch (elem._dragType) {
        case Draggable.NORMAL:   break;
        case Draggable.EXTERNAL: Draggable._stopExternal(elem); break;
        case Draggable.GHOST:    Draggable._stopGhost(elem); break;
        case Draggable.CLONE:    Draggable._stopClone(elem); break;
    }

    // Remove listeners
    Event.removeListener(document, EVENTS.MOUSEMOVE, Draggable._mouseMoveHandler);
    Event.removeListener(document, EVENTS.MOUSEUP, Draggable._mouseUpHandler);
    
    // Dispatch stop event
    // console.log(elem);
    Event.dispatch(elem, EVENTS.DRAGEND, ev);
    
    // Set current drag element to null
    Draggable._dragElement = null;
}

Draggable._mouseMoveHandler = function(ev /* Event */) {
    if (Draggable._dragElement == null) return;
    
    var elem = Draggable._dragElement, rect = elem._rectangle, adjs = elem._adjustments;
    var cmp  = { x: window.mouseX, y: window.mouseY };
    var dx   = (rect.width  == 0? 0 : (cmp.x - Draggable._lastMouseLocation.x));
    var dy   = (rect.height == 0? 0 : (cmp.y - Draggable._lastMouseLocation.y));
    // console.log("diff: "+JSON.encode(diff));

    // console.log(elem._dragType);
    // console.log(Draggable._external);
    
    // If it is external or clone swap element
    if (elem._dragType == Draggable.EXTERNAL) elem = Draggable._external;
    if (elem._dragType == Draggable.CLONE) elem = Draggable._clone;
    
    // Make sure we are not beyond the bounds
    // var elemLoc = elem.getLocation();
    var elemLoc = { x: parseInt(elem.style.left) || 0, y: parseInt(elem.style.top) || 0 };

    var nl = { x: elemLoc.x + dx, y: elemLoc.y + dy };
    if (rect.width  != null && !(rect.x <= nl.x && nl.x <= rect.x + rect.width )) dx = 0;
    if (rect.height != null && !(rect.y <= nl.y && nl.y <= rect.y + rect.height)) dy = 0;
    elem.setLocation(nl.x - adjs.x, nl.y - adjs.y);

    // Save last mouse location
    Draggable._lastMouseLocation = cmp;

    // Prevent selection
    ev.preventDefault();
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior

// Set up a listener that will automatically make elements draggable if the attribute is set
Event.addListener(window, EVENTS.MOUSEOVER, function (ev) { 
    var elem = ev.target;
    if (elem == null) return;
    var draggable = elem.getAttribute(ATTRS.DRAGGABLE);
    if (draggable == null || draggable.toLowerCase() == "false") return;
    
    // Figure out drag type
    var dragType = Draggable.NORMAL;
    switch (draggable.toLowerCase()) {
        case "normal":   dragType = Draggable.NORMAL; break;
        case "external": dragType = Draggable.EXTERNAL; break;
        case "ghost":    dragType = Draggable.GHOST; break;
        case "clone":    dragType = Draggable.CLONE; break;
        default:         dragType = Draggable.NORMAL;
    }
    
    Draggable.makeDraggable(elem, elem, dragType);
});

    
//****************************************************************************
// RESIZABLE
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Resizable() { }


//~ ATTRIBUTE(S) -------------------------------------------------------------

Resizable.TRANSPARENT = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";

Resizable.HOR =  0;
Resizable.VER =  1;
Resizable.BOTH = 2;

Resizable._typesNames = ["hor", "ver", "both"];

Resizable._specialTags = ["input", "textarea"];

Resizable._resizeElement = null;

Resizable._globalHor  = null;
Resizable._globalVer  = null;
Resizable._globalBoth = null;


//~ METHOD(S) ---------------------------------------------------------------

Resizable.makeResizable = function(elem /* HTMLElement */) {
    if (elem._resizable) return;
    elem._resizable = true;

    var res = elem.getAttribute(ATTRS.RESIZABLE);
    var hor = (res != "ver"), ver = (res != "hor");
    
    var img = null;
    if (hor && ver) img = Resizable._createImage(elem._resizeType = Resizable.BOTH, elem);
    else if (hor)   img = Resizable._createImage(elem._resizeType = Resizable.HOR, elem);
    else if (ver)   img = Resizable._createImage(elem._resizeType = Resizable.VER, elem);

    if (img != null) Event.addListener(img, EVENTS.MOUSEDOWN, function(ev) { Resizable._mouseDownHandler(ev, elem); });
}


//~ HELPER(S) ----------------------------------------------------------------

Resizable._createImage = function(type, elem) {
    // First create the image
    var img = document.createElement(TAGS.IMG);
    img.className = "dwResizable";
    img.setAttribute(ATTRS.TYPE, Resizable._typesNames[type]);
    img.src = Resizable.TRANSPARENT;
    if (elem == null) return img;
    
    // Now attach it
    // img.special = Resizable._specialTags.contains(elem.nodeName.toLowerCase());
    img.special = true;

    // If the tag is special, the images will be floating around, so we have to give them concrete positions
    if (img.special) {
        DOM.insertAfter(img, elem);
        elem._resizableGrip = img;
        img.style.position = "absolute";
        var bcr = elem.getBoundingClientRect();
        var x = (type == Resizable.VER? bcr.left + bcr.width/2  : bcr.right  - 18);
        var y = (type == Resizable.HOR? bcr.top  + bcr.height/2 : bcr.bottom - 18);
        img._location = { x: x, y: y };
        img.setLocation(x,y);
    }
    else {
        elem.appendChild(img);
    }        
    
    return img;
}


//~ HANDLER(S) ---------------------------------------------------------------

Resizable._mouseDownHandler = function(ev /* Event */, elem /* HTMLElement */) {
    if (ev.target.className != "dwResizable") return;
    Resizable._resizeElement = dw(elem);
    Event.addListener(document, EVENTS.MOUSEMOVE, Resizable._mouseMoveHandler);
    Event.addListener(document, EVENTS.MOUSEUP, Resizable._mouseUpHandler);
    Resizable._lastMouseLocation = { x: window.mouseX, y: window.mouseY };
    ev.preventDefault();
}

Resizable._mouseUpHandler = function(ev /* Event */) {
    Event.removeListener(document, EVENTS.MOUSEMOVE, Resizable._mouseMoveHandler);
    Event.removeListener(document, EVENTS.MOUSEUP, Resizable._mouseUpHandler);
    Resizable._resizeElement = null;
}

Resizable._mouseMoveHandler = function(ev /* Event */) {
    if (Resizable._resizeElement == null) return;
    var elem = Resizable._resizeElement, rt = elem._resizeType;
    var cmp  = { x: window.mouseX, y: window.mouseY };
    var dx   = (rt == Resizable.VER? 0 : (cmp.x - Resizable._lastMouseLocation.x));
    var dy   = (rt == Resizable.HOR? 0 : (cmp.y - Resizable._lastMouseLocation.y));
    elem.style.width = ""+(elem.offsetWidth + dx)+"px";
    elem.style.height = ""+(elem.offsetHeight + dy)+"px";

    var img = elem._resizableGrip;
    if (img != null) img.setLocation(img._location.x += dx, img._location.y += dy);
    
    Resizable._lastMouseLocation = cmp;
    ev.preventDefault();
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Set up a listener that will automatically make elements resizable if the attribute is set
Event.addListener(window, EVENTS.MOUSEOVER, function (ev) {
    var elem = ev.target;
    if (elem == null) return;
    var resElem = DOM.getAncestorByAttribute(elem, ATTRS.RESIZABLE);
    if (resElem != null) Resizable.makeResizable(resElem);
});


    
    
//****************************************************************************
// WIDGET
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Widget() { }


//~ ATTRIBUTES(S) ------------------------------------------------------------

Widget._charSets = {
    "alpha"        : /[a-zA-Z]/,
    "alphanumeric" : /[a-zA-Z\d]/,
    "boolean"      : /[truefalsTRUEFALS]/,
    "date"         : /[\d\/\-]/,
    "decimal"      : /[\-\d\.,]/,
    "digit"        : /[\d]/,
    "email"        : /[\w@\.]/,
    "hostname"     : /[\dA-Za-z\.]/,
    "identifier"   : /[-_a-zA-Z\d]/,
    "ipAddress"    : /[\d\.]/,
    "integer"      : /[\-\d]/,
    "punctuation"  : /[\W_]/,
    "real"         : /[\d\.,\-+E]/,
    "time"         : /[\d:]/,
    "whitespace"   : /[\s]/,
    "phone"        : /[\d]/
};


//~ METHOD(S) ----------------------------------------------------------------

// Make something 'spurious' means wiring so that it dissapears when typing ESC
// or clicking somewhere else.  This is somewhat tricky because there might be
// other clicked elements that should not make it dissapear (for example the
// button that started a pop-up menu)

Widget._makeSpurious = function(elem, preservingElemsOrFilter, preservingEvent) {
    if (elem._spurious) return;
    var pf = preservingElemsOrFilter;
    if (preservingElemsOrFilter == null) preservingFilter = null;
    else if (isFunction(preservingElemsOrFilter)) preservingFilter = preservingElemsOrFilter;
    else if (isArray(preservingElemsOrFilter)) preservingFilter = function(elem) { return preservingElemsOrFilter.some(function(pe) { return pe.contains(elem) } ) };
    var tempListener = function(ev) {
        if (ev == preservingEvent) return;
        if (ev.type == EVENTS.KEYDOWN && ev.keyCode != KEYS.ESC && ev.keyCode != KEYS.TAB) return;
        if (ev.type == EVENTS.CLICK && preservingFilter != null && preservingFilter(ev.target)) return;
        elem.style.display = "none";
        Event.removeListener(document, EVENTS.CLICK, tempListener);
        Event.removeListener(document, EVENTS.KEYDOWN, tempListener);
        elem._spurious = false;
    };
    Event.addListener(document, EVENTS.CLICK, tempListener);
    Event.addListener(document, EVENTS.KEYDOWN, tempListener);
    elem._spurious = true;
}

// Returns a displayValue for objects
Widget.displayValue = function(obj, p) {
    try {
        if (obj == null)   return null;
        if (isString(obj)) return obj;
        if (isNumber(obj)) return p == null? obj.toString() : obj.toFixed(p);
        if (isArray(obj))  return obj.map(Widget.displayValue).toString();
        if (isObject(obj)) return firstNonNull(obj.name, obj.id, obj.toString());
        return obj.toString();
    }
    catch (ex) { console.log(ex); }
    return obj;
}

Widget.validateKeyPress = function(ev, validator) {
    if (validator == null) return true;
    if (isEditKey(ev.keyCode)) return true;
    return validator.test(String.fromCharCode(ev.charCode));
}

Widget.showMask = function(busy) {
    var mask = Widget._mask;
    if (mask == null) {
        mask = document.createElement(TAGS.DIV);
        mask.className = "dwWidgetMask";
        document.body.appendChild(Widget._mask = mask);
        mask.busy = document.createElement(TAGS.IMG);
        document.body.appendChild(mask.busy);
    }

    // Hide applets
    var applets = document.getElementsByTagName(TAGS.APPLET);
    for (var i = 0; i < applets.length; i++) applets[i].style.visibility = "hidden";
    
    mask.style.display = "block";
    if (busy) mask.busy.style.display = "inline";
}

Widget.hideMask = function() {
    var mask = Widget._mask;
    if (mask == null) return;
    
    // Show applets
    var applets = document.getElementsByTagName(TAGS.APPLET);
    for (var i = 0; i < applets.length; i++) applets[i].style.visibility = "visible";
    
    mask.style.display = "none";
    mask.busy.style.display = "none";
}


//~ COMPUTED ATTRIBUTE METHOD(S) ---------------------------------------------

// Expression elements consist of four attributes, the HTML element in question,
// the attribute that should be changed, the function to determine the value and
// whether it is a CSS attribute or not
// 
// An example could be: 
// 
// { elem: $("some-id"), attr: "enabled", expr: function() { return true; }, css: false }

Widget._expressionElements = new Array();

Widget.setComputedAttr = function(elem /* HTMLElem */, attr /* [String|AttributeNode] */, expr /* Function */, css /* Boolean */) {
    if (elem == null || attr == null) return;
    var ee = { elem: elem, attr: attr, expr: expr, css: css };
    Widget._expressionElements.push(ee);
}

Widget.removeComputedAttr = function(elem /* HTMLElem */, attr /* [String|AttributeNode] */) {
    if (elem == null || attr == null) return;
    var fn = function(ee) { return ee.elem == elem && ee.attr == attr; };
    Widget._expressionElements = Widget._expressionElements.filter(fn);
}

// If you want to compute on all elements and all attributes this function should be
// called with no parameters
Widget._computeAttrs = function(elem /* HTMLElem */, attr /* [String|AttributeNode] */) {
    var ees = Widget._expressionElements;
    for (var i = 0; i < ees.length; i++) {
        var ee = ees[i];
        if (elem != null && ee.elem != elem) continue;
        if (attr != null && ee.attr != attr) continue;

        // CSS
        if (ee.css) ee.elem.style = ee.expr.call(null, ee.elem);
        
        // NON-CSS
        else {
            var value = ee.expr.call(null, ee.elem);
            if (!value || isEmpty(value)) ee.elem.removeAttribute(ee.attr);
            else ee.elem.setAttribute(ee.attr, value);
        }
    }
}

// Use capturing events, because 'blur', 'focus' and 'change' do not bubble up
// When using IE, one would have to use 'focusin'/'focusout' type of functions
window.addEventListener(EVENTS.CHANGE, function(ev) {}, true);

    
//****************************************************************************
// SELECTION
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Selection() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

Selection.prototype.getCaretRange = function(textElem /* HTMLTextElement */) {
    if (textElem.setSelectionRange) return { begin: textElem.selectionStart, end: textElem.selectionEnd };
    else if (document.selection && document.selection.createRange) {
        var range = document.selection.createRange();
        var b = 0 - range.duplicate().moveStart('character', -100000);
        var e = b + range.text.length;
        return { begin: b, end: e };
    }
}

Selection.prototype.clearSelection = function() {
    if (this.selectedItems == null) return;
    for (var i = 0; i < this.selectedItems.length; i++) {
        this.selectedItems[i].removeAttribute(ATTRS.SELECTED);
    }
    this.selectedItems.length = 0;
}

Selection.prototype.hasSelection = function() {
    return this.selectedItems != null && this.selectedItems.length > 0;
}

Selection.prototype.selectionCount = function() {
    return (this.selectedItems == null? 0 : this.selectedItems.length);
}

Selection.prototype.getSelectedItem = function() {
    return (this.selectedItems == null || this.selectedItems.length == 0? null : this.selectedItems.first());
}

Selection.prototype.getSelectedItems = function() {
    return (this.selectedItems == null? new Array() : this.selectedItems.copy());
}

Selection.prototype.select = function(contItem /* HTMLElement */, ev /* Event */) {
    if (contItem == null) return;

    // Clear selection if it not multiple, mark contItem as selected and add it to collection
    if (this.getAttribute(ATTRS.MULTIPLE) == null) this.clearSelection();
    
    // Continue selecting
    contItem.setAttribute(ATTRS.SELECTED, ATTRS.SELECTED);
    if (this.selectedItems == null) this.selectedItems = new Array();
    if (!this.selectedItems.contains(contItem)) this.selectedItems.push(contItem);

	// Create custom event and dispatch it
    if (ev != null && ev.propagationStopped) return;
    Event.dispatch(contItem, EVENTS.SELECT, ev);
	
    // var ev = cont.ownerDocument.createEvent("Events");
    // ev.initEvent(EVENTS.SELECT, true, true);
    // contItem.dispatchEvent(ev);
}

// Trivial Implementations, classes up the hierarchy should implement better versions according to their needs
Selection.prototype.firstItem = function() {
    var impl = this;
    if (impl.hasItem) return DOM.getElementByTest(impl, function(n) { return impl.hasItem(n); }, null, false);
    else return DOM.getElement(this, false);
}

Selection.prototype.lastItem = function() {
    var impl = this;
    if (impl.hasItem) return DOM.getElementByTest(impl, function(n) { return impl.hasItem(n); }, null, true);
    else return DOM.getElement(this, true);
}

Selection.prototype.nextItem = function(contItem /*  HTMLElement  */, circular /*  Boolean  */) {
    var impl = this;
    if (impl.hasItem) return DOM.nextElementByTest(contItem, function(n) { return impl.hasItem(n); }, this, circular);
    else return DOM.nextSiblingByTest(contItem, DOM._isElement, circular);
}

Selection.prototype.previousItem = function(contItem /*  HTMLElement  */, circular /*  Boolean  */) {
    var impl = this;
    if (impl.hasItem) return DOM.previousElementByTest(contItem, function(n) { return impl.hasItem(n); }, this, circular);
    else return DOM.previousSiblingByTest(contItem, DOM._isElement, circular);
}

Selection.prototype.selectNextItem = function(contItem /* HTMLElement */, circular /*  Boolean  */) {
    var impl = this;
    var nextItem = (contItem == null? impl.lastItem() : impl.nextItem(contItem, circular));
    if (nextItem != null) impl.select(nextItem);
}

Selection.prototype.selectPreviousItem = function(contItem /* HTMLElement */, circular /*  Boolean  */) {
    var impl = this;
    var previousItem = (contItem == null? impl.lastItem() : impl.previousItem(contItem, circular));
    if (previousItem != null) impl.select(previousItem);
}


//~ PRIVATE METHOD(S) --------------------------------------------------------

/**
 * Given a container 'cont' and a clicked element 'clickedElem' (that can be deep down the
 * DOM hierarchy), the function will return the closest ancestor for which the function
 * 'hasItem' returns true.  
 * 
 * The main use of this function is to find the owning composed widget
 * (such as a grid or a tree) that contains the an element that was clicked.
 */
Selection.prototype._itemAncestor = function(clickedElem /* HTMLElement */) {
    var cont = this;
    return DOM.getAncestorByTest(clickedElem, function(n) { return cont.hasItem(n); });
}


    
    
//****************************************************************************
// BUTTON
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Button() { }


//~ PRIVATE METHOD(S) --------------------------------------------------------


//~ METHOD(S) ----------------------------------------------------------------

// Tells whether the button is checked
Button.prototype.checked = function() {
    return this.getAttribute(ATTRS.CHECKED) != null;
};

// Tells whether the current event happened on the arrow (for drop down buttons)
Button.prototype.onArrow = function(ev /* Event */) {
    var loc = this.getLocation();
    return (loc.x + this.clientWidth - ev.pageX < 16);
};

// Toggles check buttons and returns current value
Button.prototype.toggle = function(ev /* Event */) {
    // Being kind to IE
    // console.log("toggle");
    this.checked = Element.prototype.toggleAttribute.call(this, ATTRS.CHECKED);
    return this.checked;
};


//~ HANDLER(S) ---------------------------------------------------------------

Button._clickHandler = function(ev /* Event */, button /* HTMLElement */) {
    if (button == null) button = ev.currentTarget;
    button = dw(button);
    
    var tp = button.getAttribute(ATTRS.TYPE);
    if (tp == null) return;
    
    // Make the type lowercase
    tp = tp.toLowerCase();

    if (tp == "check") {
        // Toggle button
        button.toggle();
    }
    
    if (tp == "menu") {
        // If the button has a 'onclick' only continue if the click was at the arrow
        if (button.onclick != null && !button.onArrow(ev)) return;
        
        // Get a hold of the popup
        var popup = dw(button.getAttribute(ATTRS.POPUP));
        var bounds = button.getBounds();
        popup.setLocation(bounds.x, bounds.y+bounds.height);
        popup.style.display = "inline-block";
    
        // Make spurious
        Widget._makeSpurious(popup, [ button ], ev);
    }
}


//~ STYLE --------------------------------------------------------------------

Button._displayAccessKey = function(button /* HTMLButtonElement */) {        
    // Check the access key is there
    var ak = button.getAttribute(ATTRS.ACCESSKEY);
    if (ak == null || ak.length == 0) return;    
    ak = ak.charAt(0);
    
    // Find the first text node with some text
    var textNode = DOM.getNodeByTest(button, function(node) {
        return (node.nodeType == Node.TEXT_NODE && node.nodeValue.indexOf(ak) != -1);
    });
    if (textNode == null) return;
    
    // Find accessKey position
    var str = textNode.nodeValue;
    var i = str.indexOf(ak);
    
    // Create document fragment with appropriate pieces
    var df = document.createDocumentFragment();
    var prefix = document.createTextNode(str.substr(0,i));
    var span = document.createElement(TAGS.SPAN);
    span.className = "accesskey";
    span.appendChild(document.createTextNode(ak));
    var suffix = document.createTextNode(str.substr(i+1));
    df.appendChild(prefix);
    df.appendChild(span);
    df.appendChild(suffix);

    // Replace 'textNode' with a new DocumentFragment
    DOM.replace(df, textNode);
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register Behavior
Behavior.registerBehavior("dwButton", function(button) {
    Button._displayAccessKey(button);
    Event.addListener(button, EVENTS.CLICK, Button._clickHandler);
});

    
//****************************************************************************
// CALENDAR
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Calendar() { }
copyMethods(Calendar.prototype, Selection.prototype);


//~ CONSTANTS ----------------------------------------------------------------

Calendar.HEAD_TEMPLATE = 
    "<thead>"+
    "  <tr><th colspan='5'><span name='banner'>Month YYYY</span></th></tr>"+
    "  <tr>"+
    "    <td style='text-align: left;'><button name='farleft'>&laquo;</button>&nbsp;<button name='left'>&lt;</button></td>"+
    "    <td style='text-align: center;'><button name='today' style='padding: 0px 4px;'>Today</button></td>"+
    "    <td style='text-align: right;'><button name='right'>&gt;</button>&nbsp;<button name='farright'>&raquo;</button></td>"+
    "  </tr>"+
    "</thead>";

Calendar.BODY_TEMPLATE = 
    "<tbody>"+
    "  <tr>"+
    "    <td colspan='5'>"+
    "      <table name='days' cellpadding='0' cellspacing='0' tabindex='0'>"+
    "        <thead><tr><th class='weekend'>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th class='weekend'>S</th></tr></thead>"+
    "        <tbody>"+
    "          <tr><td style='padding: 2px;' class='weekend'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;' class='weekend'></td></tr>"+
    "          <tr><td style='padding: 2px;' class='weekend'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;' class='weekend'></td></tr>"+
    "          <tr><td style='padding: 2px;' class='weekend'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;' class='weekend'></td></tr>"+
    "          <tr><td style='padding: 2px;' class='weekend'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;' class='weekend'></td></tr>"+
    "          <tr><td style='padding: 2px;' class='weekend'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;' class='weekend'></td></tr>"+
    "          <tr><td style='padding: 2px;' class='weekend'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;'></td><td style='padding: 2px;' class='weekend'></td></tr>"+
    "        </tbody>"+
    "      </table>"+
    "    </td>"+
    "  </tr>"+
    "</tbody>";
    

//~ PUBLIC METHOD(S) ---------------------------------------------------------

Calendar.prototype.hasItem = function(calItem /* HTMLElement */) {
    if (calItem.parentNode == null) return false;
    if (calItem.parentNode.parentNode == null) return false;
    var days = this._daysNode();
    if (calItem.parentNode.parentNode.parentNode != days) return false;
    if (calItem.tagName.toLowerCase() != TAGS.TD) return false;
    if (calItem.innerText.trim().length == 0) return false;
    return true;
}

Calendar.prototype.getValue = function() {
    return this._value;
}

Calendar.prototype.nextMonth = function() {
    var date = this._monthDate;
    var m = (date.getMonth() + 1) % 12;
	var y = (date.getFullYear()+(m == 0? 1 : 0));
    this.setMonth(new Date(y,m,1));
}

Calendar.prototype.nextYear = function() {
    var date = this._monthDate;
    this.setMonth(new Date(date.getFullYear()+1, date.getMonth(), 1));
}

Calendar.prototype.previousMonth = function() {
    var date = this._monthDate;
    var m = (date.getMonth() + 11) % 12;
    var y = (date.getFullYear()-(m == 12? 1 : 0));
    this.setMonth(new Date(y,m,1));
}

Calendar.prototype.previousYear = function() {
    var date = this._monthDate;
    this.setMonth(new Date(date.getFullYear()-1, date.getMonth(), 1));
}

Calendar.prototype.setMonth = function(date) {
    // It's the same
    var year = date.getFullYear(), month = date.getMonth();
    var oldMonthDate = this._monthDate;
    if (oldMonthDate != null && oldMonthDate.getFullYear() == year && oldMonthDate.getMonth() == month) return;
    
    // Set it
    this._monthDate = new Date(year, month, 1);

    // Set banner and remove today cell's style
    this._bannerNode().innerHTML = Date.MONTHS[month]+" "+year;
    if (this._todayNode != null) { removeClassName(this._todayNode, "today"); this._todayNode = null; }
    this.clearSelection();

    // Grab days node
    var days = this._daysNode();
    
    // Get important variables
    var daysInMonth = 32 - new Date(year, month, 32).getDate();
    var sc = 1 - this._monthDate.getDay();

    // Is it today's month?
    var today = new Date();
    var currentMonth = today.getFullYear() == year && today.getMonth() == month;
    var currentDate = (currentMonth? today.getDate() : null);

    // Is it the value month?
    var valueMonth = (this._value != null && this._value.getFullYear() == year && this._value.getMonth() == month);
    var valueDate = (valueMonth? this._value.getDate() : null);

    // The 'startingCount' is a number representing the (negative) number of days that are left out
    // of week when the first of the month hits.  So for example, if the 1st of the month is
    // a Sunday, then 'startingCount' would be 1, if it is Wednesday, 'startingCount' would be -2, since
    // we have to add 1 three times to -2 to reach 1
    for (var weekOfMonth = 0; weekOfMonth < 6; weekOfMonth++) {
        for (var dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++, sc++) {
            var value = (sc >= 1 && sc <= daysInMonth? sc : '\u00A0');
            var cell = days.tBodies[0].rows[weekOfMonth].cells[dayOfWeek];
            cell.innerHTML = value;
            if (sc == currentDate) addClassName(this._todayNode = cell, "today");
            if (sc == valueDate) this.select(cell, { propagationStopped: true });
        }
    }
}

Calendar.prototype.setValue = function(date /* Date */, fireEvent /* Boolean */) {
    this._value = date;
    // console.log(getStack());
    // console.log("value: "+date);
    if (this._value == null) {
        this.clearSelection();
        return;
    }
    
    // Set month
    this.setMonth(date);

    // Select (pos is the row-wise offset in the calendar matrix)
    var pos = date.getDate() + this._monthDate.getDay() - 1;
    var w = Math.floor(pos / 7), dow = pos % 7;
    this.select(this._daysNode().tBodies[0].rows[w].cells[dow], { propagationStopped: !fireEvent });
}


//~ PRIVATE METHOD(S) --------------------------------------------------------

// The banner node is the first (and only) SPAN tag inside the calendar head
Calendar.prototype._bannerNode = function() {
    return this.bannerNode || (this.bannerNode = DOM.getElementByTagName(this.tHead, TAGS.SPAN));
}

// The days node is the first (and only) TABLE tag inside the calendar body
Calendar.prototype._daysNode = function() {
    return this.daysNode ||  (this.daysNode = DOM.getElementByTagName(this, TAGS.TABLE));
}


//~ STYLE --------------------------------------------------------------------

Calendar._display = function(cal /* HTMLTableElement */) {
    if (cal._displayed) return;
    cal._displayed = true;
    
    cal = dw(cal);
    console.log(cal);
    cal.innerHTML = Calendar.HEAD_TEMPLATE + Calendar.BODY_TEMPLATE;
    // cal.setMonth(new Date());
    try { cal.setMonth(new Date()); }
    catch (ex) { console.log(ex); }
}


//~ HANDLER(S) ---------------------------------------------------------------

Calendar._clickHandler = function(ev /* Event */, cal /* HTMLElement */) {
    if (cal == null) cal = ev.currentTarget;
    var clickedElem = ev.target;

    // Check it is not disabled
    // if (!this.isEnabled()) return;

    // Get out of the way the buttons
    if (clickedElem == null);
    else if (clickedElem.name == "farleft") cal.previousYear();
    else if (clickedElem.name == "left") cal.previousMonth();
    else if (clickedElem.name == "today") cal.setValue(new Date());
    else if (clickedElem.name == "right") cal.nextMonth();
    else if (clickedElem.name == "farright") cal.nextYear();
    else if (this.hasItem(clickedElem)) {
        var date = new Date(this._monthDate.getFullYear(), this._monthDate.getMonth(), clickedElem.innerText);
        cal.setValue(date, true);
    }
}

Calendar._keyDownHandler = function(ev /* Event */, cal /* HTMLElement */) {
    // The Grid is the current target of the event
    if (cal == null) cal = ev.currentTarget;

    // Check it is not disabled
    // if (!this.isEnabled()) return;

    // Get selected item
    var calItem = cal.getSelectedItem();
    
    // Assume we are going to catch event here
    var days = cal._daysNode();
    var propagate = false;
    switch(ev.keyCode) {
        // case KEYS.PAGEDOWN: cal.nextMonth(); break;
        // case KEYS.PAGEUP: cal.previousMonth(); break;
        case KEYS.DOWN:
            var r = calItem.parentNode.sectionRowIndex, c = calItem.cellIndex;
            var rows = days.tBodies[0].rows;
            if (r < rows.length - 1) {
                var pci = rows[r+1].cells[c];
                if (cal.hasItem(pci)) cal.select(pci);
            }
            break;
        case KEYS.UP: 
            var r = calItem.parentNode.sectionRowIndex, c = calItem.cellIndex;
            var rows = days.tBodies[0].rows;
            if (r > 0) {
                var pci = rows[r-1].cells[c];
                if (cal.hasItem(pci)) cal.select(pci);
            }
            break;
        case KEYS.LEFT: this.selectPreviousItem(calItem); break;
        case KEYS.RIGHT: this.selectNextItem(calItem); break;
        default: propagate = true;
    }

    // Do not propagate afterwards
    if (!propagate) ev.stopPropagation();
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwCalendar", function(cal) {
    Event.addListener(cal, EVENTS.KEYDOWN, Calendar._keyDownHandler);
    Event.addListener(cal, EVENTS.CLICK, Calendar._clickHandler);
    Calendar._display(cal);
});

    
//****************************************************************************
// CHECKBOX
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function CheckBox() { }

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

// CHART
//****************************************************************************

/**
 * A chart is built on top of a canvas
 */

//~ CONSTRUCTOR --------------------------------------------------------------

function Chart() {
    this.transform = { m11: 1, m12: 0, m21: 0, m22: 1, dx: 0, dy: 0 };
    this.steps = { hor: null, ver: 5 };
    this.shift = { hor: 0, ver: 0 };
    // this.palette = Chart.PALETTE.copy();
    this.tx = this.ty = null;
}


//~ CONSTANT(S) --------------------------------------------------------------

Chart.PALETTE = [[116,148,205],[225,153,77],[133,186,94],[212,93,95],[148,152,155],[203,194,115],[139,99,161]];
Chart.BULLETS = ["o", "x", "+", "*", "$", "#", "&", "=", "^", "-", "~" ];

Chart.LOG_2 = Math.genLog(2, 10);
Chart.LOG_5 = Math.genLog(5, 10);

// This is the judged to be too small to tell the difference
Chart.EPSILON = 10e-10;

Chart.MAX_VALUE = +Number.MAX_VALUE;
Chart.MIN_VALUE = -Number.MAX_VALUE;

ATTRS.ANGLE     = "angle";
ATTRS.MIN       = "min";
ATTRS.MAX       = "max";
ATTRS.STEP      = "step";
ATTRS.PRECISION = "precision";
ATTRS.GRID      = "grid";
ATTRS.SMOOTH    = "smooth";

Chart.AREA    = "area";
Chart.BARS    = "bars";
Chart.BUBBLE  = "bubble";
Chart.LINE    = "line";
Chart.PIE     = "pie";
Chart.SCATTER = "scatter";
Chart.SPARK   = "spark";


//~ METHOD(S) ----------------------------------------------------------------

// These functions assume no rotation
Chart.prototype.fx = function(x) { var t = this.transform; return x * t.m11 + t.dx; }
Chart.prototype.fy = function(y) { var t = this.transform; return y * t.m22 + t.dy; }

// Function to calculate distance
Chart.prototype.xdist = function(x) { return x * this.transform.m11; }
Chart.prototype.ydist = function(y) { return y * this.transform.m22; }

// These functions may include rotation
Chart.prototype.fxy = function(x,y) { var t = this.transform; return x * t.m11 + y * t.m12 + t.dx; }
Chart.prototype.fyx = function(y,x) { var t = this.transform; return x * t.m21 + y * t.m22 + t.dy; }


//~ DATASET METHOD(S) --------------------------------------------------------

Chart.prototype.setDataSets = function(dataSets /* Object[] */, render /* Boolean */) {
    this.dataSets = new Array();
    for (var i = 0; i < dataSets.length; i++) {
        this.addDataSet(dataSets[i]);
    }
    if (render) this.render();
};

// Each data set has the form: { label: "label", data: [0,1,2,3,4,5,6,7,8,9] }
Chart.prototype.addDataSet = function(dataSet /* Object */, render /* Boolean */) {
    // Normalize name
    if (dataSet.name != null) dataSet.name = dataSet.name.replace(/ /g, "-");
    
    // Normalize data
    if (isArray(dataSet)) dataSet = { data: dataSet };
    dataSet.data = Chart._normalizeData(dataSet.data, this.tx, this.ty, this.getAttribute(ATTRS.ORIENT) == "horizontal");
    
    // If the dataSet is 'multi', the entire chart will be multi
    if (dataSet.data.multi) this._multi = true;

    // Save the dataSet
    if (this.dataSets == null) this.dataSets = new Array();
    if (typeof(dataSet.label) == UNDEFINED) dataSet.label = dataSet.name || (this.dataSets.length).toString();
    this.dataSets.push(dataSet);
    
    if (render) this.render();
};

Chart.prototype.getDataSet = function(dataSetName /* String */) {
    dataSetName = dataSetName.replace(/ /g, "-");
    if (this.dataSets == null) return null;
    for (var i = 0; i < this.dataSets.length; i++) {
        if (this.dataSets[i].name == dataSetName) return this.dataSets[i];
    }
    return null;
}

Chart.prototype.getLength = function() {
    if (this.dataSets == null) return 0;
    var length = 0;
    for (var i = 0; i < this.dataSets.length; i++) {
        var data = this.dataSets[i].data;
        if (data.length > length) length = data.length;
    }
    return length;
}

// Gets the labels for the horizonal axis
Chart.prototype.getHorLabels = function() {
    if (this._hlabels != null) return this._hlabels;
    this._hlabels = new Array();
    var b = this.dataBounds;
    for (var i = b.xstart; i <= b.xmax; i += b.xstep) this._hlabels.push(i.toFixed());
    return this._hlabels;
};

// Sets the labels for the horizonal axis
Chart.prototype.setHorLabels = function(hl) { this._hlabels = hl.copy(); };

//Gets the labels for the horizonal axis
Chart.prototype.getVerLabels = function() {
    if (this._vlabels != null) return this._vlabels;
    this._vlabels = new Array();
    var b = this.dataBounds;
    for (var i = b.ystart; i <= b.ymax; i += b.ystep) this._vlabels.push(i.toFixed(0));
    return this._vlabels;
};

// Sets the labels for the horizonal axis
Chart.prototype.setVerLabels = function(vl) { this._vlabels = vl.copy(); };

// Gets the labels for the legend
Chart.prototype.getLegendLabels = function() {
    var labels = new Array();
    for (var i = 0, len = this.dataSets.length; i < len; i++) {
        var ds = this.dataSets[i], label = ds.label || ds.name || i.toString();
        labels.push(label);
    }
    return labels;
};

Chart._normalizeData = function(data /* Object[] */, tx, ty, transp) {
    // console.log("trans: "+trans);
    var normData = new Array();
    for (var i = 0; i < data.length; i++) {
        var v = data[i];
        var x = isArray(v)? v[0] : (isNumber(v)? i : v.x);
        var y = isArray(v)? v[1] : (isNumber(v)? v : v.y);
        if (tx != null) x = tx(x);
        if (ty != null) y = ty(x);
        normData[i] = transp? { x: y, y: x } : { x: x, y: y };
        
        // Should add the Z coordinate?
        if (data[i].z != undefined) normData[i].z = data[i].z;
        
        // Mark the data as unidimensional if x is always equal to 
        if (x != i) normData.multi = true;
    }
    
    return normData;
};


//~ RENDERING METHOD(S) ------------------------------------------------------

// Calculates all the bounds inside the chart
Chart.prototype.bounds = function(ctx) {
	// Ensure parts
    Chart._ensureParts(this);
	
    // Get data bounds
    this.dataBounds = this._getDataBounds(this.getAttribute(ATTRS.POSITION) == "stacked");

    // Find out constraining values
    var xth = this._getXTitleHeight(ctx), ytw = this._getYTitleWidth(ctx);
    var xah = this._getXAxisHeight(ctx), yaw = this._getYAxisWidth(ctx);
    var lgw = this._getLegendWidth(ctx);
    
    // Get styles and calculate bounds
    var cs = getCompStyle(this.plotArea);
    var margins = { top: parseInt(cs.marginTop), right: parseInt(cs.marginRight), bottom: parseInt(cs.marginBottom), left: parseInt(cs.marginLeft) };
    this.plotArea.bounds = { x: ytw + yaw + margins.left, y: margins.top, w: this.width - ytw - yaw - lgw - margins.left - margins.right, h: this.height - xth - xah - margins.top - margins.bottom };
    this.xaxis.bounds =    { x: this.plotArea.bounds.x, y: this.height - xth - xah, w: this.plotArea.bounds.w, h: xah };
    this.yaxis.bounds =    { x: ytw, y: margins.top, w: yaw, h: this.plotArea.bounds.h };
    
    // Optional parts
    if (this.xtitle) this.xtitle.bounds =   { x: this.plotArea.bounds.x, y: this.height - xth, w: this.plotArea.bounds.w, h: xth };
    if (this.ytitle) this.ytitle.bounds =   { x: 0, y: margins.top, w: ytw, h: this.plotArea.bounds.h };
    if (this.legend) this.legend.bounds =   { x: this.width - lgw, y: margins.top, w: lgw, h: this.plotArea.bounds.h };
    
    var b = this.dataBounds;
    var xdiff = (b.xmax - b.xmin), ydiff = (b.ymax - b.ymin), xshift = 0, yshift = 0;
    
    if (this.type == Chart.BARS) { xdiff += 1.5; xshift = -0.75; }
    
    // Calculate transform
    // TODO: I am not sure about yshift in the equations below
    // console.log("xshift:  "+xshift);
    this.transform.m11 = (this.plotArea.bounds.w / xdiff);
    this.transform.dx  = -this.xdist(b.xmin + xshift);
    this.transform.m22 = (-this.plotArea.bounds.h / ydiff);
    this.transform.dy  = this.plotArea.bounds.h - this.ydist(b.ymin - yshift);
}

Chart.prototype.render = function() {
    // Make sure we can get a context
    if (!this.getContext) return;

    // Get type, and copy appropiate methods
    var type = this.type = this.getAttribute(ATTRS.TYPE) || "line";
    switch (type) {
        case Chart.AREA:    copyMethods(this, ChartArea.prototype);    break;
        case Chart.BARS:    copyMethods(this, ChartBars.prototype);    break;
        case Chart.BUBBLE:  copyMethods(this, ChartBubble.prototype);  break;
        case Chart.LINE:    copyMethods(this, ChartLine.prototype);    break;
        case Chart.PIE:     copyMethods(this, ChartPie.prototype);     break;
        case Chart.SCATTER: copyMethods(this, ChartScatter.prototype); break;
        case Chart.SPARK:   copyMethods(this, ChartSpark.prototype);   break;
    }

    // this._isScatter = (type == Chart.SCATTER);
    
    // Get context
    var ctx = this.getContext("2d");

    // Clear space
    ctx.clearRect(0, 0, this.width, this.height);

    // Ensure bounds
    this.bounds(ctx);

    // Render chart
    this._renderChart(ctx);
};

Chart.prototype._renderChart = function(ctx) {
    // Render Plot Area
    this._renderPlotArea(ctx);
    
    // Render Axis
    this._renderYAxis(ctx);
    this._renderXAxis(ctx);
    if (this.xtitle) this._renderXTitle(ctx);
    if (this.ytitle) this._renderYTitle(ctx);
    
    // Render legend
    if (this.legend) this._renderLegend(ctx);
}

Chart.prototype._renderPlotArea = function(ctx) {
    var cs = getCompStyle(this.plotArea), b = this.plotArea.bounds, db = this.dataBounds;

    ctx.save();
    ctx.translate(b.x, b.y);
    Chart._rect(ctx, b.w, b.h, cs.backgroundColor);

    // Clip
    ctx.rect(0,0,b.w+1,b.h+1);
    ctx.clip();
    
    // Draw grid lines
    var forceGrid = (this.getAttribute(ATTRS.GRID) != null);
    this._renderGrid(ctx, forceGrid);
    
    var stacked = (this.getAttribute(ATTRS.POSITION) == "stacked");
    ctx.beginPath();
    var cum = stacked? (new Array()).fill(0, this.getLength()) : null;
    for (var i = 0; i < this.dataSets.length; i++) {
        var ds = this.dataSets[i];
        if (ds.disabled) continue;
        var dse = this.querySelector(".dataSet[name="+ds.name+"], .dataSet:not([name])"), dss = getCompStyle(dse);
        
        // Render Data
        var smooth = (dse.getAttribute(ATTRS.SMOOTH) != null);
        if (smooth) this._renderSplines(ctx, i, dss);
        else this._renderData(ctx, i, dss, cum);
        
        // Are there bullets?
        if (dss.listStyleType == "none") continue;
        // console.log("dss: "+dss);
        var fill = (dss.listStyleType == "disc"? ctx.strokeStyle : null);
        this._renderBullets(ctx, ds.data, 2, ctx.strokeStyle, fill);
    }
    
    // Draw border
    ctx.beginPath();
    ctx.shadowColor = Color.TRANSPARENT;
    if (cs.borderTopStyle != "none")    Graphics.horLine(ctx, 0,   0, b.w, parseInt(cs.borderTopWidth), cs.borderTopColor);
    if (cs.borderBottomStyle != "none") Graphics.horLine(ctx, b.h, 0, b.w, parseInt(cs.borderBottomWidth), cs.borderBottomColor);
    if (cs.borderLeftStyle != "none")   Graphics.verLine(ctx, 0,   0, b.h, parseInt(cs.borderLeftWidth), cs.borderLeftColor);
    if (cs.borderRightStyle != "none")  Graphics.verLine(ctx, b.w, 0, b.h, parseInt(cs.borderRightWidth), cs.borderRightColor);
    ctx.stroke();
    
    ctx.restore();
}

// Draw grid lines. The color of grid lines is given by the outline CSS property of each axis
Chart.prototype._renderGrid = function(ctx, forceGrid) {
    var xs = getCompStyle(this.xaxis), ys = getCompStyle(this.yaxis), b = this.plotArea.bounds, db = this.dataBounds;
    
    if (forceGrid || xs.outlineStyle != "none") {
        ctx.lineWidth = parseInt(xs.outlineWidth);
        ctx.strokeStyle = xs.outlineColor;
        ctx.beginPath();
        for (var i = db.xstart; i <= db.xmax; i += db.xstep) Graphics.verLine(ctx, this.fx(i), 0, b.h);
        ctx.stroke();
    }
    
    if (forceGrid || ys.outlineStyle != "none") {
        ctx.lineWidth = parseInt(ys.outlineWidth);
        ctx.strokeStyle = ys.outlineColor;
        ctx.beginPath();
        for (var i = db.ystart; i <= db.ymax; i += db.ystep) Graphics.horLine(ctx, this.fy(i), 0, b.w);
        ctx.stroke();
    }
}

Chart.prototype._renderXTitle = function(ctx) {
    var cs = getCompStyle(this.xtitle), b = this.xtitle.bounds, text = this.xtitle.innerText;
    if (cs.display == "none" || isEmpty(text)) return;

    ctx.save();
    ctx.translate(b.x, b.y);
    Chart._rect(ctx, b.w, b.h, cs.backgroundColor);

    // Draw text
    Chart._inheritTextProps(ctx, cs);
    ctx.translate(b.w/2, 0);
    ctx.fillText(text, 0, 0);
    
    ctx.restore();    
};

Chart.prototype._renderXAxis = function(ctx) {
    var cs = getCompStyle(this.xaxis), b = this.xaxis.bounds, db = this.dataBounds;
    if (cs.display == "none") return;
    
    ctx.save();
    ctx.translate(b.x, b.y);
    Chart._rect(ctx, b.w, b.h, cs.backgroundColor);

    // Text properties
    var angle = this.xaxis.getAttribute(ATTRS.ANGLE);
    Chart._inheritTextProps(ctx, cs);

    // Render labels
    var hl = this.getHorLabels(), fixed = 0;
    // console.log(JSON.encode(hl));
    for (var i = db.xstart, c = 0; i <= db.xmax; i += db.xstep) {
        Graphics.text(ctx, hl[c++], this.fx(i), fixed, angle);
    }
    
    ctx.restore();
};

Chart.prototype._renderYTitle = function(ctx) {
    var cs = getCompStyle(this.ytitle), b = this.ytitle.bounds, text = this.ytitle.innerText;
    if (cs.display == "none" || isEmpty(text)) return;

    ctx.save();
    ctx.translate(b.x, b.y);
    Chart._rect(ctx, b.w, b.h, cs.backgroundColor);

    // Draw text
    Chart._inheritTextProps(ctx, cs);
    ctx.translate(0, b.h/2);
    ctx.rotate(270*Math.PI/180);
    ctx.fillText(text, 0, 0);
    
    ctx.restore();    
};

Chart.prototype._renderYAxis = function(ctx, step) {
    var cs = getCompStyle(this.yaxis), b = this.yaxis.bounds, db = this.dataBounds;
    if (cs.display == "none") return;

    ctx.save();
    ctx.translate(b.x, b.y);
    Chart._rect(ctx, b.w, b.h, cs.backgroundColor);

    // Text properties
    var angle = this.xaxis.getAttribute(ATTRS.ANGLE);
    Chart._inheritTextProps(ctx, cs);

    // Render labels
    var fixed = b.w;
    for (var i = db.ystart; i <= db.ymax; i += db.ystep) {
        Graphics.text(ctx, i.toFixed(), fixed, this.fy(i));
    }
    
    ctx.restore();
};

Chart.prototype._renderLegend = function(ctx) {
    var cs = getCompStyle(this.legend), b = this.legend.bounds;
    if (cs.display == "none") return;

    ctx.save();
    ctx.translate(b.x, b.y);
    
    ctx.fillStyle = cs.backgroundColor; 
    ctx.fillRect(0, 0, b.w, b.h);

    // Text properties
    Chart._inheritTextProps(ctx, cs);

    // Draw legends
    var ll = this.getLegendLabels();
    var h = Graphics.textHeight(ctx) + parseInt(cs.borderSpacing);
    for (var i = 0; i < ll.length; i++) {
        var y = i * h;
        ctx.fillStyle = ctx.strokeStyle = Chart._color(i);
        Graphics.bullet(ctx, h/2, y + h/2, h/2, Graphics.SQUARE);
        ctx.fillStyle = cs.color;
        ctx.fillText(ll[i], 4*h/3, y);
    }

    ctx.restore();    
};

Chart.prototype._renderBullets = function(ctx, data, radius, stroke, fill) {
    if (radius == null) radius = 5;
    if (stroke) ctx.strokeStyle = stroke;
    if (fill) ctx.fillStyle = fill;
    for (var i = 0; i < data.length; i++) {
        ctx.beginPath();
        ctx.arc(this.fx(data[i].x), this.fy(data[i].y), radius, 0, Math.PI*2, true);
        ctx.closePath();
        if (stroke) ctx.stroke();
        if (fill) ctx.fill();
    }
};


//~ HELPER(S) ----------------------------------------------------------------

// Returns an object of the type { xmin: -2, ymin: -10, xmax: 20, ymax: 12 }
Chart.prototype._getDataBounds = function(stacked) {
    var bounds = { xmin: Chart.MAX_VALUE, xmax: Chart.MIN_VALUE, ymin: Chart.MAX_VALUE, ymax: Chart.MIN_VALUE };
 
    // First get bounds based on data
    var cum = stacked? (new Array()).fill(0, this.getLength()) : null;
    for (var i = 0; i < this.dataSets.length; i++) {
        var data = this.dataSets[i].data;
        if (data == null) continue;
        for (var j = 0; j < data.length; j++) {
            // If each element in data is a number, then x is the index and y the value,
            // otherwise, we are in a scatter plot diagram and x and y coordinates will
            // extracted from the dataSet
            var x = data[j].x, y = data[j].y;
            if (stacked) y = (cum[j] += y);
            if (x < bounds.xmin) bounds.xmin = x;
            if (x > bounds.xmax) bounds.xmax = x;
            if (y < bounds.ymin) bounds.ymin = y;
            if (y > bounds.ymax) bounds.ymax = y;
        }
    }

    // Now see if the attributes are overriding the found min/max values
    var xmin = this.xaxis.getAttribute(ATTRS.MIN); if (xmin != null) bounds.xmin = parseInt(xmin);
    var xmax = this.xaxis.getAttribute(ATTRS.MAX); if (xmax != null) bounds.xmax = parseInt(xmax);
    var ymin = this.yaxis.getAttribute(ATTRS.MIN); if (ymin != null) bounds.ymin = parseInt(ymin);
    var ymax = this.yaxis.getAttribute(ATTRS.MAX); if (ymax != null) bounds.ymax = parseInt(ymax);

    // If they were not touched, make them a box of 10 units
    if (bounds.xmin == Chart.MAX_VALUE) bounds.xmin = -5;
    if (bounds.xmax == Chart.MIN_VALUE) bounds.xmax = +5;
    if (bounds.ymin == Chart.MAX_VALUE) bounds.ymin = -5;
    if (bounds.ymax == Chart.MIN_VALUE) bounds.ymax = +5;
    
    // If they are equal, increase the top one
    if (bounds.xmin == bounds.xmax) bounds.xmax += 5;
    if (bounds.ymin == bounds.ymax) bounds.ymax += 5;
    
    // Leave a little breathing room
    bounds.ymax *= 1.1;
    
    // Only for bars, the starting minimum should be at 0
    if (this.getAttribute(ATTRS.TYPE) == Chart.BARS) bounds.ymin = 0;
    
    // Set steps
    var xstep = this.xaxis.getAttribute(ATTRS.STEP), ystep = this.xaxis.getAttribute(ATTRS.STEP);
    bounds.xstep = xstep = (xstep == null? (this._multi? Chart._stepSize(bounds.xmin, bounds.xmax, 10) : 1) : parseInt(xstep));
    bounds.ystep = ystep = (ystep == null? Chart._stepSize(bounds.ymin, bounds.ymax, 8) : parseInt(ystep));

    // Set starts
    bounds.xstart = xstep * Math.ceil(bounds.xmin/xstep);
    bounds.ystart = ystep * Math.ceil(bounds.ymin/ystep);

    // Set precisions
    bounds.xprec = this._multi? 0 : (parseInt(this.xaxis.getAttribute(ATTRS.PRECISION) || "0"));
    bounds.yprec = parseInt(this.xaxis.getAttribute(ATTRS.PRECISION) || "0");
    
    // Return data bounds
    return bounds;
}

// Find out how high should the X axis be
Chart.prototype._getXAxisHeight = function(ctx) {
    var cs = getCompStyle(this.xaxis);
    if (cs.display == "none") return 0;
    if (cs.height != "auto") return parseInt(cs.height);

    var max = 0;
    var angle = parseInt(this.xaxis.getAttribute(ATTRS.ANGLE));
    var hl = this.getHorLabels();
    ctx.font = Chart._font(cs);
    for (var i = 0; i < hl.length; i++) {
        var dim = Graphics.textDim(ctx, hl[i], angle);
        if (dim.height > max) max = dim.height;
    }

    return (max+parseInt(cs.paddingTop)+parseInt(cs.paddingBottom));
}

// Find out how wide should the Y axis be
Chart.prototype._getYAxisWidth = function(ctx) {
    var cs = getCompStyle(this.yaxis);
    if (cs.display == "none") return 0;
    if (cs.width != "auto") return parseInt(cs.width);

    var max = 0;
    var angle = parseInt(this.xaxis.getAttribute(ATTRS.ANGLE));
    var vl = this.getVerLabels();
    ctx.font = Chart._font(cs);
    for (var i = 0; i < vl.length; i++) {
        var dim = Graphics.textDim(ctx, vl[i], angle);
        if (dim.width > max) max = dim.width;
    }

    return (max+parseInt(cs.paddingLeft)+parseInt(cs.paddingRight));
}

// Find out how wide should the legend axis be
Chart.prototype._getLegendWidth = function(ctx) {
    if (this.legend == null) return 0;
    var cs = getCompStyle(this.legend);
    if (cs.display == "none") return 0;
    if (cs.width != "auto") return parseInt(cs.width);

    var max = 0;
    ctx.font = Chart._font(cs);
    var ll = this.getLegendLabels();
    ctx.font = Chart._font(cs);
    for (var i = 0; i < ll.length; i++) {
        var dim = Graphics.textDim(ctx, ll[i]);
        if (dim.width > max) max = dim.width;
    }

    // Add space for squares
    max += 2 * Graphics.textHeight(ctx);
    
    return (max+parseInt(cs.paddingLeft)+parseInt(cs.paddingRight));
}

// Find out how high should the X title should be
Chart.prototype._getXTitleHeight = function(ctx) {
    if (this.xtitle == null) return 0;
    var cs = getCompStyle(this.xtitle);
    if (cs.display == "none") return 0;
    if (cs.height != "auto") return parseInt(cs.height);
    ctx.font = Chart._font(cs);
    return (ctx.measureText("M").width+2+parseInt(cs.paddingTop)+parseInt(cs.paddingBottom));
}

// Find out how width should the Y title should be
Chart.prototype._getYTitleWidth = function(ctx) {
    if (this.ytitle == null) return 0;
    var cs = getCompStyle(this.ytitle);
    if (cs.display == "none") return 0;
    if (cs.width != "auto") return parseInt(cs.width);
    ctx.font = Chart._font(cs);
    return (ctx.measureText("M").width+2+parseInt(cs.paddingLeft)+parseInt(cs.paddingRight));
}

Chart._inheritTextProps = function(ctx, cs) {
    // Get text properties
    ctx.textAlign    = cs.textAlign;
    ctx.textBaseline = (cs.verticalAlign == "baseline"? "alphabetic" : cs.verticalAlign);
    ctx.fillStyle    = cs.color;
    ctx.font         = Chart._font(cs);
}

Chart._inheritDrawProps = function(ctx, cs, stroke, fill, op) {
    // Get stroke properties
    ctx.strokeStyle = (!Color.isTransparent(cs.color)? cs.color : (stroke != null? stroke : Color.TRANSPARENT));

    // Get fill properties
    ctx.fillStyle = (!Color.isTransparent(cs.backgroundColor)? cs.backgroundColor : (fill != null? fill : Color.TRANSPARENT));

    // Get shadow properties
    if (cs.textShadow != "none") {
        var shadow = Chart._shadow(cs.textShadow);
        ctx.shadowColor   = shadow.color;
        ctx.shadowOffsetX = shadow.x;
        ctx.shadowOffsetY = shadow.y;
        ctx.shadowBlur    = shadow.blur;
    }
    
    // Get line properties
    ctx.lineWidth = parseInt(cs.width);
}

// Parses a shadow string, and returns an object like { y: 3, y: 1, blur: 4, color: "grey" }
// Assuming canonical order is the one reflected in the examples (at least in Firefox it is)
Chart._shadow = function(shadow) {
    // Remove spaces between RGB parameters
    shadow = shadow.replace(", ",",");

    // Split
    var vals = shadow.replace(", ",",").split(/\s+/);
    if (vals.length < 2) return null;
    
    // CASE A: First parameter is color
    if (/^[A-Za-z]/.test(vals[0])) {
        switch (vals.length) {
            case 2: return null;
            case 3: return { x: parseInt(vals[1]), y: parseInt(vals[2]), blur: 0, color: vals[0] };
            case 4: return { x: parseInt(vals[1]), y: parseInt(vals[2]), blur: parseInt(vals[3]), color: vals[0] };
        }
    }
    else {
        switch (vals.length) {
            case 2: return { x: parseInt(vals[0]), y: parseInt(vals[1]), blur: 0, color: "grey" };
            case 3: return { x: parseInt(vals[0]), y: parseInt(vals[1]), blur: parseInt(vals[2]), color: "grey" };
            case 4: return { x: parseInt(vals[0]), y: parseInt(vals[1]), blur: parseInt(vals[2]), color: vals[3] };
        }
    }
    return null;
}

// If the elements are not inside, create them
Chart._ensureParts = function(chart) {
    chart.plotArea = Chart._ensurePart(chart, "plotArea", true);
    chart.xaxis    = Chart._ensurePart(chart, "xaxis", true);
    chart.yaxis    = Chart._ensurePart(chart, "yaxis", true);
    chart.xtitle   = Chart._ensurePart(chart, "xtitle");
    chart.ytitle   = Chart._ensurePart(chart, "ytitle");
    chart.legend   = Chart._ensurePart(chart, "legend");
    
    // Default data set
    chart.dataSet  = Chart._ensurePart(chart, "dataSet", true);
}

Chart._ensurePart = function(chart, className, create) {
    var elems = chart.getElementsByClassName(className);
    if (elems.length > 0) return elems[0];
    if (elems.length == 0 && !create) return null;
    var elem = document.createElement(TAGS.DIV);
    chart.appendChild(elem); 
    elem.className = className;
    return elem;
}

Chart._color = function(i, op) {
    var len = Chart.PALETTE.length;
    if (i < len) return Color.toString(Chart.PALETTE[i], op);
    else return Color.gen(i - len, op, 128, 127, 1.666, 2.666, 3.666, 0, 0, 0); 
}

Chart._font = function(cs /* ComputedCSSStyleDeclaration */) { return cs.fontStyle + " " + cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily; }

Chart._rect = function(ctx, w, h, backgroundColor) {
    if (isEmpty(backgroundColor) || Color.isTransparent(backgroundColor)) return;
    ctx.fillStyle = backgroundColor; 
    ctx.fillRect(0, 0, w, h); 
}

// Calculates the control points as distances to the original points
// Code inspired by: http://www.ibiblio.org/e-notes/Splines/Bint.htm
Chart._controlPoints = function(pts) {
    var n = pts.length, d = [], Ax = [], Ay = [], Bi = [];
    d[0] = { x: (pts[1].x - pts[0].x) / 3, y: (pts[1].y - pts[0].y) / 3 };
    d[n-1] = { x: (pts[n-1].x - pts[n-2].x) / 3, y: (pts[n-1].y - pts[n-2].y) / 3 };
    Bi[1] = -0.25;
    Ax[1] = (pts[2].x - pts[0].x - d[0].x) / 4;
    Ay[1] = (pts[2].y - pts[0].y - d[0].y) / 4;
    for (var i = 2; i < n-1; i++) {
        Bi[i] = -1 / (4 + Bi[i - 1]);
        Ax[i] = -(pts[i+1].x - pts[i-1].x - Ax[i-1]) * Bi[i];
        Ay[i] = -(pts[i+1].y - pts[i-1].y - Ay[i-1]) * Bi[i];
    }
    for (var i = n - 2; i > 0; i--) {
        d[i] = { x: Ax[i] + d[i+1].x * Bi[i], y: Ay[i] + d[i+1].y * Bi[i] };
    }
    
    return d;
}

Chart._stepSize = function(min, max, steps) {
    var ss = (max - min) / steps;
    var log = Math.genLog(ss, 10), dec = log - Math.floor(log);
    var tens = Math.pow(10, Math.floor(log));
    
    // Return approriate step
    if (dec < 0.10) return 1 * tens;
    if (dec < 0.45) return 2 * tens;
    if (dec < 0.80) return 4 * tens;
    return 10 * tens;
}


//~ HANDLER(S) ---------------------------------------------------------------


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwChart", function(chart) {
    Chart._ensureParts(chart);
    
    
});


//****************************************************************************
// CHARTAREA
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function ChartArea() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

ChartArea.prototype._renderData = function(ctx, index, cs, cum) {
    var data = this.dataSets[index].data, b = this.plotArea.bounds;
    Chart._inheritDrawProps(ctx, cs, Chart._color(index), Chart._color(index, 0.50));
    
    ctx.beginPath();
    for (var i = 0; i < data.length; i++) {
        var x = data[i].x, y = data[i].y;
        if (cum != null) y = (cum[i] += y);
        if (i == 0) ctx.lineTo(0, b.h);
        // ctx.lineTo(this.fx(x), this.fy(y));
        var fx = this.fx(x), fy = this.fy(y);
        ctx.lineTo(fx, fy);
        if (i == data.length-1) ctx.lineTo(b.w, b.h);
    }
    ctx.fill();
    ctx.stroke();
};


//****************************************************************************
// CHARTBARS
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function ChartBars() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

ChartBars.prototype._renderData = function(ctx, index, cs, cum) {
    var data = this.dataSets[index].data;
    Chart._inheritDrawProps(ctx, cs, Chart._color(index), Chart._color(index, 0.50));

    // The available width for all the bars is 2/3 of the distance between data points, and then
    // we have to divide that up by the number of datasets.  Half that distance will be the radius
    var stacked = (cum != null);
    var halfAvail = this.xdist(1)/3, radius = halfAvail/(stacked? 1 : this.dataSets.length);
 
    // Where to start depends on the dataSet index, the first one will start at 
    var shift = -halfAvail + (stacked? 0 : 2*index*radius);

    // The base depends on the orientation
    ctx.beginPath();
    for (var i = 0; i < data.length; i++) {
        var x = data[i].x, y = data[i].y, fh = this.ydist(y);
        if (cum != null) y = (cum[i] += y);
        var fx = this.fx(x), fy = this.fy(y);
        ctx.fillRect(fx + shift + 1, fy, 2*radius - 1, -fh-1);
        ctx.strokeRect(fx + shift + 1, fy, 2*radius - 1, -fh-1);
    }
};


//****************************************************************************
// CHARTBUBBLE
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function ChartBubble() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

ChartBubble.prototype._renderData = function(ctx, index, cs) {
    var data = this.dataSets[index].data;
    Chart._inheritDrawProps(ctx, cs, Chart._color(index));

    ctx.beginPath();
    for (var i = 0; i < data.length; i++) {
        var x = data[i].x, y = data[i].y, z = data[i].z;
        Graphics.circle(ctx, this.fx(x), this.fy(y), z*z);
    }
};


//****************************************************************************
// CHARTLINE
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function ChartLine() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

ChartLine.prototype._renderData = function(ctx, index, cs, cum) {
    var data = this.dataSets[index].data;
    Chart._inheritDrawProps(ctx, cs, Chart._color(index));
    
    ctx.beginPath();
    for (var i = 0; i < data.length; i++) {
        var x = data[i].x, y = data[i].y;
        if (cum != null) y = (cum[i] += y);
        ctx.lineTo(this.fx(x), this.fy(y));
    }
    if (!Color.isTransparent(ctx.fillStyle)) ctx.fill();
    if (!Color.isTransparent(ctx.strokeStyle)) ctx.stroke();
};

ChartLine.prototype._renderSplines = function(ctx, index, cs) {
    var data = this.dataSets[index].data;
    Chart._inheritDrawProps(ctx, cs, Chart._color(index));

    // Find control points
    var d = Chart._controlPoints(data);
    
    ctx.beginPath();
    ctx.moveTo(this.fx(data[0].x), this.fy(data[0].y));
    for (var i = 1; i < data.length; i++) {
        var cp1x = this.fx(data[i-1].x+d[i-1].x), cp1y = this.fy(data[i-1].y+d[i-1].y);
        var cp2x = this.fx(data[i].x-d[i].x), cp2y = this.fy(data[i].y-d[i].y);
        ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, this.fx(data[i].x), this.fy(data[i].y));
    }
    ctx.fill();
    ctx.stroke();
};


//****************************************************************************
// CHARTPIE
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function ChartPie() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

ChartPie.prototype._renderData = function(ctx, index, cs) {
    var data = this.dataSets[index].data, b = this.plotArea.bounds;
    Chart._inheritDrawProps(ctx, cs);

    // Find total
    var total = 0;
    var values = data.map(function(v) { v = v.y; total += v; return v; });
 
    // Find center and radius, and then draw all wedges
    ctx.beginPath();
    var cx = b.w/2, cy = b.h/2, r = 0.9 * Math.min(b.w, b.h) / 2, startAngle = 0, endAngle = 0;
    for (var i = 0; i < values.length; i++) {
        endAngle = startAngle + (2 * Math.PI * values[i] / total);
        Graphics.wedge(ctx, cx, cy, r, startAngle, endAngle, false, null, "black", Chart._color(i, 0.50));
        startAngle = endAngle;
    }
};


//****************************************************************************
// CHARTSCATTER
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function ChartScatter() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

ChartScatter.prototype._renderData = function(ctx, index, cs) {
    var data = this.dataSets[index].data;
    Chart._inheritDrawProps(ctx, cs, Chart._color(index), Chart._color(index));
    
    // ctx.globalAlpha = 0.5;
    var r = 2, d = 2*r;
    var bullet = (cs.listStyleType == "bullet");
    for (var i = 0; i < data.length; i++) {
        var x = data[i].x, y = data[i].y;
        // console.log("conv["+i+"] "+x+" -> "+this.fx(x));
        if (bullet) Graphics.bullet(ctx, this.fx(x), this.fy(y), 4, index);
        else ctx.fillRect(this.fx(x)-r, this.fy(y)-r, d, d);
    }
    ctx.fill();
    ctx.stroke();
};


//****************************************************************************
// CHARTSPARK
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function ChartSpark() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

ChartSpark.prototype._renderData = ChartLine.prototype._renderData;
ChartSpark.prototype._renderSplines = ChartLine.prototype._renderSplines;

    
//****************************************************************************
// Combo
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Combo() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

Combo.prototype.addOption = function(obj /* Object */) {
    return this.insertOption(obj);
}

Combo.prototype.addOptions = function(objs /* Object[] */) {
    for (var i = 0; i < objs.length; i++) {
        this.addOption(objs[i]);
    }
}

Combo.prototype.clear = function() { this.length = 0; }

Combo.prototype.insertOption = function(obj /* Object */, position /* Number */) {
    var comboItem = document.createElement(TAGS.OPTION);
    comboItem.data = obj;
    comboItem.value = firstNonNull(obj.value, obj.id, ""+obj);
    comboItem.appendChild(document.createTextNode(firstNonNull(obj.name, obj.id, ""+obj)));
    if (position== null) this.appendChild(comboItem);
    else this.insertBefore(comboItem, this.options[position]);
    return comboItem;
}

Combo.prototype.setOptions = function(objs /* Object[] */) {
    this.options.length = 0;
    this.addOptions(objs);
}

Combo.prototype.getSelectedName = function() {
    var si = this.selectedIndex;
    return si < 0? null : this.options[si].text;
}

Combo.prototype.getSelectedValue = function() {
    var si = this.selectedIndex;
    return si < 0? null : this.options[si].value;
}


//~ PRIVATE METHOD(S) --------------------------------------------------------


//~ HANDLER(S) ---------------------------------------------------------------


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
/*
Behavior.registerBehavior("dwCombo", function(elem) {
    Combo.loadData(elem, elem.getAttribute(ATTRS.DATASRC));
});
*/

    
//****************************************************************************
// DIALOG
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Dialog() { }


//~ CONSTANT(S) --------------------------------------------------------------

Dialog.CLOSE_BUTTON = false;
Dialog.ESC_CLOSES = true;


//~ ATTRIBUTE(S) -------------------------------------------------------------

Dialog._mask = null;


//~ METHOD(S) ----------------------------------------------------------------

Dialog.load = function(url /* url */, id /* String */, forceLoad /* Boolean */) {
    // If it already exists, just return it
    var dialog = dw(id);
    if (dialog != null && !forceLoad) return dialog;
    
    // Synchronous request to get dialog
    if (dialog != null) DOM.remove(dialog);
    var df = load(url);
    if (df != null) document.body.appendChild(df);

    // Append to body if it is there
    // if (dialog != null) document.body.appendChild(dialog);
    
    // Return enhanced dialog
    return dialog = dw(id);
}

Dialog.prototype.open = function(openFn /* Function */, closeFn /* Function */) {
    // Dialog._showMask();
    Widget.showMask();
    if (this.getAttribute(ATTRS.DRAGGABLE)) Draggable.makeDraggable(this, this._titleBar);
    this._openFn = openFn;
    this._closeFn = closeFn;
    this.style.display = "block";

    // Bind 'dialog' with a global variable
    window.dialog = this;
    
    // Dispatch load event for dialog
    Event.dispatch(this, EVENTS.LOAD);
    
    // Fire open function
    if (this._openFn != null) this._openFn();
    
    this.center();
}

Dialog.prototype.close = function() {
    this.style.display = "none";
    // Dialog._hideMask();
    Widget.hideMask();
    // console.log("this._closeFn: "+this._closeFn);
    if (this._closeFn != null) this._closeFn.apply(null, arguments);
    // console.log("closing");
    // console.log("arguments: "+JSON.encode(arguments));

    // Unbind 'dialog' from global variable
    window.dialog = null;
    
    // Dispatch unload event for dialog
    Event.dispatch(this, EVENTS.UNLOAD);
}

Dialog.prototype.setTitle = function(title) {
    this.$N("title").innerText = title;
}


//~ HELPER(S) ----------------------------------------------------------------

/*
Dialog._showMask = function() {
    if (Dialog._mask == null) {
        Dialog._mask = document.createElement(TAGS.DIV);
        Dialog._mask.className = "dwDialogMask";
        document.body.appendChild(Dialog._mask);
    }

    // Hide applets
    var applets = document.getElementsByTagName(TAGS.APPLET);
    for (var i = 0; i < applets.length; i++) applets[i].style.visibility = "hidden";
    
    Dialog._mask.style.display = "block";
}

Dialog._hideMask = function() {
    if (Dialog._mask == null) return;
    
    // Show applets
    var applets = document.getElementsByTagName(TAGS.APPLET);
    for (var i = 0; i < applets.length; i++) applets[i].style.visibility = "visible";
    
    Dialog._mask.style.display = "none";
}
*/


//~ HANDLER(S) ---------------------------------------------------------------


//~ STYLE --------------------------------------------------------------------

Dialog._displayTitleBar = function(dialog /* HTMLDivElement */) {
    // Check the access key is there
    var title = dialog._title || dialog.getAttribute(ATTRS.TITLE);
    if (title == null || title.length == 0) return;

    // In order to avoid default tooltip, remove attribute
    dialog._title = title;
    dialog.removeAttribute(ATTRS.TITLE);
    
    // Simply, insert a div as the first element
    var div = document.createElement(TAGS.DIV);
    div.appendChild(document.createTextNode(title));
    div.name = "title";
    div.className = "title";
    dialog.insertBefore(div, dialog.firstChild);
    
    // Attach close behavior
    if (Dialog.CLOSE_BUTTON) {
        var img = document.createElement(TAGS.IMG);
        img.src = "images/close.png";
        div.appendChild(img);
        Event.addListener(img, EVENTS.CLICK, function() { dialog.close(null); });
    }
    /*
    if (Dialog.ESC_CLOSES) {
        Event.addListener(dialog, EVENTS.KEYPRESS, function(ev) { if (ev.keyCode == KEYS.ESC) dialog.close(null); });
    }
    */
    
    // Ensure title bar
    dialog._titleBar = div;
}


//~ DEFAULT KEYDOWN ----------------------------------------------------------

Event.addListener(window, EVENTS.LOAD, function() {
    Event.addListener(document, EVENTS.KEYDOWN, function(ev) { 
        if (!Dialog.ESC_CLOSES) return;
        if (window.dialog == null) return;
        if (ev.keyCode == KEYS.ESC) window.dialog.close(null);
    });
});


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register Behavior
Behavior.registerBehavior("dwDialog", function(dialog) {
    Dialog._displayTitleBar(dialog);
});

    
//****************************************************************************
// Editor
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------
 
function Editor() { }

/*
Editor.newInstance = function() {
    var editor = document.createElement(TAGS.INPUT);
    editor.className = "dwEditor";
    document.body.appendChild(editor);
    
    // Register events
    Behavior.injectBehavior(editor);

    return editor;
}
*/


//~ CONSTANT(S) --------------------------------------------------------------

Editor.FORMAT =
	"<ul>"+
	"<li name='format' value='h1'>Heading 1</li>"+
	"<li name='format' value='h2'>Heading 2</li>"+
	"<li name='format' value='h3'>Heading 3</li>"+
	"<li name='format' value='h4'>Heading 4</li>"+
	"<li name='format' value='p'>Paragraph</li>"+
	"<li name='format' value='pre'>Preformatted</li>"+
	"<li name='format' value='code'>Code</li>"+
	"</ul>";

Editor.STYLE =
	"<ul>"+
	"<li name='style' value='style1'>style1</li>"+
	"<li name='style' value='style2'>style2</li>"+
	"<li name='style' value='style3'>style3</li>"+
	"</ul>";

Editor.COLOR =
	"<table cellspacing='4' cellpadding='0'>"+
	"<tr>"+
	"<td title='Black' style='background-color: rgb(0,0,0);'></td>"+
	"<td title='Maroon' style='background-color: rgb(128,0,0);'></td>"+
	"<td title='Saddle Brown' style='background-color: rgb(139,69,19);'></td>"+
	"<td title='Dark Slate Gray' style='background-color: rgb(47,79,79);'></td>"+
	"<td title='Teal' style='background-color: rgb(0,128,128);'></td>"+
	"<td title='Navy' style='background-color: rgb(0,0,128);'></td>"+
	"<td title='Indigo' style='background-color: rgb(75,0,130);'></td>"+
	"<td title='Dim Gray' style='background-color: rgb(105,105,105);'> </td>"+
	"</tr>"+
	"<tr>"+
	"<td title='Fire Brick' style='background-color: rgb(178,34,34);'></td>"+
	"<td title='Brown' style='background-color: rgb(165,42,42);'></td>"+
	"<td title='Golden Rod' style='background-color: rgb(218,165,32);'></td>"+
	"<td title='Dark Green' style='background-color: rgb(0,100,0);'></td>"+
	"<td title='Turquoise' style='background-color: rgb(64,224,208);'></td>"+
	"<td title='Medium Blue' style='background-color: rgb(0,0,205);'></td>"+
	"<td title='Purple' style='background-color: rgb(128,0,128);'></td>"+
	"<td title='Gray' style='background-color: rgb(128,128,128);'></td>"+
	"</tr>"+
	"<tr>"+
	"<td title='Red' style='background-color: rgb(255,0,0);'></td>"+
	"<td title='Dark Orange' style='background-color: rgb(255,140,0);'></td>"+
	"<td title='Gold' style='background-color: rgb(255,215,0);'></td>"+
	"<td title='Green' style='background-color: rgb(0,128,0);'></td>"+
	"<td title='Cyan' style='background-color: rgb(0,255,255);'></td>"+
	"<td title='Blue' style='background-color: rgb(0,0,255);'></td>"+
	"<td title='Violet' style='background-color: rgb(238,130,238);'></td>"+
	"<td title='Dark Gray' style='background-color: rgb(169,169,169);'></td>"+
	"</tr>"+
	"<tr>"+
	"<td title='Light Salmon' style='background-color: rgb(255,160,122);'></td>"+
	"<td title='Orange' style='background-color: rgb(255,165,0);'></td>"+
	"<td title='Yellow' style='background-color: rgb(255,255,0);'></td>"+
	"<td title='Lime' style='background-color: rgb(0,255,0);'></td>"+
	"<td title='Pale Turquoise' style='background-color: rgb(175,238,238);'></td>"+
	"<td title='Light Blue' style='background-color: rgb(173,216,230);'></td>"+
	"<td title='Plum' style='background-color: rgb(221,160,221);'></td>"+
	"<td title='Light Grey' style='background-color: rgb(211,211,211);'></td>"+
	"</tr>"+
	"<tr>"+
	"<td title='Lavender Blush' style='background-color: rgb(255,240,245);'></td>"+
	"<td title='Antique White' style='background-color: rgb(250,235,215);'></td>"+
	"<td title='Light Yellow' style='background-color: rgb(255,255,224);'></td>"+
	"<td title='Honeydew' style='background-color: rgb(240,255,240);'></td>"+
	"<td title='Azure' style='background-color: rgb(240,255,255);'></td>"+
	"<td title='Alice Blue' style='background-color: rgb(240,248,255);'></td>"+
	"<td title='Lavender' style='background-color: rgb(230,230,250);'></td>"+
	"<td title='White' style='background-color: rgb(255,255,255);'></td>"+
	"</tr>"+
	"</table>";

Editor.BAR =
	"<div class='dwEditorBar' contentEditable='false'>"+
	"<span name='format'    title='Format'>"+Editor.FORMAT+"</span>"+
	"<span name='style'     title='Style'>"+Editor.STYLE+"</span>"+
	"<span name='bold'      title='Bold (Ctrl+B)'></span>"+
	"<span name='italic'    title='Italic (Ctrl+I)'></span>"+
	"<span name='underline' title='Underline (Ctrl+U)'></span>"+
	"<span name='strike'    title='Strikethrough'></span>"+
	"<span name='fore'      title='Text Color'>"+Editor.COLOR+"</span>"+
	"<span name='back'      title='Background Color'>"+Editor.COLOR+"</span>"+
	"<span name='left'      title='Align Left (Ctrl+L)'></span>"+
	"<span name='center'    title='Align Center (Ctrl+E)'></span>"+
	"<span name='right'     title='Align Right (Ctrl+R)'></span>"+
	"<span name='link'      title='Insert Link (Ctrl+K)'></span>"+
	"<span name='unlink'    title='Unlink'></span>"+
	"<span name='image'     title='Insert Image'></span>"+
	"<span name='table'     title='Insert Table'></span>"+
	"<span name='ul'        title='Bullet List (Ctrl+.)'></span>"+
	"<span name='ol'        title='Number List (Ctrl+0)'></span>"+
	"<span name='html'      title='Insert HTML (Ctrl+H)'></span>"+
	"<span name='sub'       title='Subscript (Ctrl+-)'></span>"+
	"<span name='sup'       title='Superscript (Ctrl++)'></span>"+
	"<span name='indent'    title='Indent (Ctrl+&gt;)'></span>"+
	"<span name='outdent'   title='Outdent (Ctrl+&lt;)'></span>"+
	"<span name='unformat'  title='Remove Format'></span>"+
	"</div>";

Editor.CMDS = {
    bold:      "bold",
    italic:    "italic",
    underline: "underline",
    strike:    "strikeThrough",
    sub:       "subscript",
    sup:       "superscript",
    left:      "justifyLeft",
    center:    "justifyCenter",
    right:     "justifyRight",
    link:      "createLink",
    unlink:    "unlink",
    table:     "table",
    ul:        "insertUnorderedList",
    ol:        "insertOrderedList",
    image:     "insertImage",
    html:      "insertHTML",
    back:      "backColor",
    fore:      "foreColor",
    indent:    "indent",
    outdent:   "outdent",
    format:    "formatBlock",
    unformat:  "removeFormat"
};

	  
//~ PUBLIC METHOD(S) ---------------------------------------------------------

/*
Editor.prototype.edit = function(elem / * HTMLElement * /, focusElem / * HTMLElement * /) {
    // Establish back pointers
    this.element = elem;
    this.focusElement = focusElem;

    // Match style
    // var cs = document.defaultView.getComputedStyle(elem, null);
    // editor.style.fontFamily = cs.fontFamily;
    // editor.style.fontSize = cs.fontSize;
    
    // Match Position and value
    this.setBounds(elem.getBounds());
    this.show(elem.getValue());
}

Editor.prototype.hide = function() {
    // Disable and dissapear editor
    this.style.display = "none";
    this.disabled = true;
    
    // Send focus back to element
    if (this.focusElement != null) this.focusElement.focus();
}

Editor.prototype.show = function(text / * String * /) {
    // Display and enable
    this.style.display = "inline";
    this.disabled = false;
    this.setValue(text);
    
    // Give editor the focus
    this.focus();
}
*/

Editor.prototype.editing = function() {
	return this.getAttribute("contentEditable") == "true";
}

Editor.prototype.command = function(cmd /* String */, val /* String */) { 
	document.execCommand(cmd, false, val); 
}

Editor.prototype.link = function() {
    var url = prompt("Enter Link URL:", "http://");
    if (url == null || url.length == 0) return;
    document.execCommand(Editor.CMDS.link, false, url);
};

Editor.prototype.image = function() {
    var url = prompt("Enter Image URL:", "http://");
    if (url == null || url.length == 0) return;
    document.execCommand(Editor.CMDS.image, false, url);
};

Editor.prototype.html = function() {
    var html = prompt("Enter HTML:", "<span style='color: red;'>something</span>");
    if (html == null || html.length == 0) return;
    document.execCommand(Editor.CMDS.html, false, html);
};

Editor.prototype.inline = function(tag) {
    var range = window.getSelection().getRangeAt(0);
    var elem = document.createElement(tag);
    elem.appendChild(range.extractContents());
    range.insertNode(elem);
};


//~ HELPER(S) ----------------------------------------------------------------

Editor._ensureBar = function(editor /* HTMLElement */) {
	if (editor._bar != null) return editor._bar;
	var div = document.createElement(TAGS.DIV);
	div.innerHTML = Editor.BAR;
	
	// Add bar to page and wire
	var bar = editor._bar = div.firstChild;
	bar._editor = editor;
	editor.parentNode.insertBefore(bar, editor);

	// Make it same width
    // bar.style.width = editor.offsetWidth;
    // bar.style.left = offset(editor).x;

    // Add behavior
    Event.addListener(bar, EVENTS.MOUSEDOWN, Editor._mouseDownHandler);
    Event.addListener(bar, EVENTS.MOUSEUP, function() { editor.focus(); });
    
    return bar;
}


//~ HANDLER(S) ---------------------------------------------------------------

Editor._keyDownHandler = function(ev /* Event */, editor /* HTMLElement */) {
	if (editor == null) editor = $(ev.currentTarget);
	if (!editor.editing()) return true;

	// Only take over commands that have a Ctrl in the keys
    if (!ev.ctrlKey) return true;
	
    // The editor is the current target of the event
    // if (editor == null) editor = ev.currentTarget;
    
    switch (ev.keyCode) {
	    case KEYS.O:     editor.inline(TAGS.CODE); break;
	    case KEYS.B:     editor.command(Editor.CMDS.bold); break;
	    case KEYS.I:     editor.command(Editor.CMDS.italic); break;
	    case KEYS.U:     editor.command(Editor.CMDS.underline); break;
	    case KEYS.L:     editor.command(Editor.CMDS.left); break;
	    case KEYS.E:     editor.command(Editor.CMDS.center); break;
	    case KEYS.R:     editor.command(Editor.CMDS.right); break;
	    case KEYS.K:     Editor.link(); break;
	    case KEYS.H:     Editor.html(); break;
	    case KEYS.DOT:   editor.command(Editor.CMDS.ul); break;
	    case KEYS.N0:    editor.command(Editor.CMDS.ol); break;
	    case KEYS.MINUS: editor.command(Editor.CMDS.sub); break;
	    case KEYS.PLUS:  editor.command(Editor.CMDS.sup); break;
	    case KEYS.GT:    editor.command(Editor.CMDS.indent); break;
	    case KEYS.LT:    editor.command(Editor.CMDS.outdent); break;
	    default: return true;
    }
    
    // We called one of the functions, stop normal behavior
    ev.preventDefault();
    ev.stopPropagation();
    return false;
}

Editor._mouseDownHandler = function(ev /* Event */, editor /* HTMLElement */) {
	if (editor == null) editor = ev.currentTarget._editor;
	
	// Get the name of the closest ancestor before the bar
	var elem = ev.target, name;
	while ( (name = elem.getAttribute(ATTRS.NAME)) == null && elem !=  editor._bar) {
		elem = elem.parentNode;
	}
	
	var cmd = Editor.CMDS[name];
	if (cmd != null) {
		// Tricky: we give 'val' a value that will not be used
		var val = "none";
		
		// For colors fetch value
		if (name == "fore" || name == "back") val = ev.target.style.backgroundColor;
		if (name == "format" || name == "style") val = ev.target.getAttribute(ATTRS.VALUE);
		else if (name == "link")  return editor.link();
		else if (name == "image") return editor.image();
		else if (name == "html")  return editor.html();
		else if (name == "table") return editor.table();

		// If there is no value, return
		if (val == null || val.length == 0) return;
		
		editor.command(cmd, val);
	}
}

Editor._scrollHandler = function(ev /* Event */, editor /* HTMLElement */) {
	if (!editor.editing()) return true;
	var y = offset(editor).y;
	var fix = window.pageYOffset > y;
	editor._bar.style.position = (fix? "fixed" : "absolute");
	editor._bar.style.top = (fix? "0px" : y+"px");
}

Editor._dropHandler = function(ev /* Event */, editor /* HTMLElement */) {
	console.log(ev);
//    if (!file.type.match(/image.*/)) return true;
//	
//	ev.stopPropagation();
//	ev.preventDefault();
//	var file = ev.dataTransfer.files[0];
//    
//    var img = document.createElement("img");
//    img.classList.add("obj");
//    img.file = file;
//    preview.appendChild(img);
//    
//    var reader = new FileReader();
//    reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; }; })(img);
//    reader.readAsDataURL(file);
	
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with BEHAVIOR
Behavior.registerBehavior("dwEditor", function(editor) {
	editor = dw(editor);
	Event.addListener(editor, EVENTS.DOMATTRMODIFIED, function(ev) {
		if (ev.attrName.toLowerCase() != "contenteditable") return;
		var bar = Editor._ensureBar(editor);
		bar.style.display = (editor.editing()? "" : "none");
	});
	if (editor.editing()) Editor._ensureBar(editor);
	
	Event.addListener(editor, EVENTS.KEYDOWN, Editor._keyDownHandler);
	if (editor.getAttribute(ATTRS.SCROLLING)) {
	    Event.addListener(document, EVENTS.SCROLL, function(ev) { 
	    	Editor._scrollHandler(ev, editor); 
	    });
	}
	if (editor.getAttribute(ATTRS.DROPABLE)) {
		// console.log("Adding hooks!")
	    Event.addListener(editor, EVENTS.DRAGENTER, function(ev) { ev.stopPropagation(); ev.preventDefault(); });
	    Event.addListener(editor, EVENTS.DRAGOVER, function(ev) { ev.stopPropagation(); ev.preventDefault(); });
	    // Event.addListener(editor, EVENTS.DROP, function(ev) { console.log(ev); });
	}
});

    
//****************************************************************************
// FORM
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

// Protect agains overwriting prototype 'Form'
var origForm = (typeof(Form) == UNDEFINED? null : Form);
// console.log("Form: "+origForm+" type: "+typeof(origForm));

// This form is preferred because otherwise the function is define before
// we have the chance to test for existance of the original Form
var Form = function() { }

// Copy prototype known functions
if (origForm) {
    Form.Element = origForm.Element;
}
// console.log("Are they equal? "+(Form == origForm));
// if (origForm != null && ) _copy(Form, origForm, null, true);
    

//~ CONSTANT(S) --------------------------------------------------------------


//~ PUBLIC METHOD(S) ---------------------------------------------------------

Form.prototype.setEnabled = function(enabled /* Boolean */, focusFirst /* Boolean */) {
    // Set attribute (HTML extension)
    if (enabled) this.removeAttribute(ATTRS.DISABLED);
    else this.setAttribute(ATTRS.DISABLED, ATTRS.DISABLED);

    var elems = this.elements;
    for (var i = 0; i < elems.length; i++) {
        elems[i].disabled = !enabled;
        if (focusFirst && !elems[i].readOnly) {
            elems[i].focus();
            focusFirst = false;
        }
    }
}

Form.prototype.setError = function(obj /* Object */) {
    if (obj == null) obj = new Object();
    var elems = this.elements;
    
    // Get the values this way to avoid interfering with Prototype
    var fn = HTMLElement.prototype.getValue;
    for (var i = 0; i < elems.length; i++) {
        obj[elems[i].name] = fn.call(elems[i]);
        // console.log(elems[i].name+": "+obj[elems[i].name]);
    }
    
    return obj;
}

Form.prototype.getValues = function(obj /* Object */, proc /* Function */) {
    if (obj == null) obj = new Object();
    var elems = this.elements;
    
    // Get the values this way to avoid interfering with Prototype
    for (var i = 0; i < elems.length; i++) {
        var key = elems[i].name;
        if (key == null || key.length == 0) continue;
        var value = HTMLElement.prototype.getValue.call(elems[i]);
        try { value = (proc == null? value : proc.call(this, key, value)); }
        catch (ex) { console.log(ex); }
        obj[key] = value;
    }
    
    return obj;
}

Form.prototype.setValues = function(obj /* Object */, proc /* Function */) {
    this.data = obj;
    var elems = this.elements;
    for (var i = 0; i < elems.length; i++) {
        var key = elems[i].name;
        if (key in obj) {
            var value = obj[key];
            try { value = (proc == null? value : proc.call(obj, key, value)); }
            catch (ex) { console.log(ex); }
            elems[i].setValue(value);
        }
    }
}

Form.prototype.focusFirst = function() {
    var elems = this.elements;
    for (var i = 0; i < elems.length; i++) {
        if (elems[i].disabled) continue;
        elems[i].focus();
        return;
    }
}

Form.prototype.clear = function(obj /* Object */) {
    this.reset();
    var elems = this.elements;
    for (var i = 0; i < elems.length; i++) {
        this.setInvalid(elems[i], false);
    }
}

Form.prototype.setInvalid = function(elem, invalid, text) {
    if (invalid && elem.flag == null ) {
        elem.setAttribute(ATTRS.INVALID, ATTRS.INVALID);
        var img = elem.flag = document.createElement(TAGS.IMG);
        img.className = "invalid";
        img.src = "images/flag.png";
        if (text != null) img.title = text;
        DOM.insertAfter(img, elem);
        
    }
    else if(!invalid && elem.flag != null) {
        elem.removeAttribute(ATTRS.INVALID);
        DOM.remove(elem.flag);
        elem.flag = null;
    }
}


//~ HELPER(S) ----------------------------------------------------------------


//~ HANDLER(S) ---------------------------------------------------------------

Form.prototype.loadData = function() {
    var src = this.getAttribute(ATTRS.DATASRC);
    var type = this.getAttribute(ATTRS.DATATYPE);
    var id = this.getAttribute(ATTRS.DATAID);
    var loader = null;

    if (src.startsWith("javascript:")) loader = eval(src.substring(11));
    else { /* TODO: build a data source out of a URL */ }

    var form = this;
    Event.dispatch(form, EVENTS.REQUEST);
    loader.call(null, type, id, function(obj) {
        form.setValues(obj);
        Event.dispatch(form, EVENTS.RESPONSE);
    });
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwForm", function(form) {
    if (form.getAttribute(ATTRS.DATASRC) != null) {
        Event.addListener(window, EVENTS.LOAD, function() { form.loadData(); });
    }
});

    
//****************************************************************************
// GRID
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Grid() { }
copyMethods(Grid.prototype, Selection.prototype);


//~ CONSTANT(S) --------------------------------------------------------------

Grid.ROW_LIMIT = 20;
Grid.BEGINNING = "beginning";
Grid.END = "end";
Grid.NEXT = "next";
Grid.PREVIOUS = "previous";

// Wether we should replace boolean values by ticks and crosses
Grid.BOOLEAN_IMAGES = true;

// Types of selection
Grid.ROW  = "row";
Grid.CELL = "cell";

// Types of sorting
Grid.ASCENDING  = "asc";
Grid.DESCENDING = "desc";

// Wether to enable ellipsis
Grid.ELLIPIS_TOLERANCE = 5;


//~ PUBLIC METHOD(S) ---------------------------------------------------------

Grid.prototype.itemCount = function() { return this.tBodies[0].rows.length; }

Grid.prototype.hasItem = function(gridItem /* HTMLElement */) {
    var cellSelection = this.getAttribute(ATTRS.SELECTION) == Grid.CELL;
    return (cellSelection? this.hasCellItem(gridItem) : this.hasRowItem(gridItem));
}

Grid.prototype.hasRowItem = function(gridItem /* HTMLElement */) {
    if (gridItem.parentNode == null) return false;
    if (gridItem.parentNode == this.tHead || gridItem.parentNode == this.tFoot) return false;
    if (gridItem.parentNode.parentNode != this) return false;
    if (gridItem.nodeName.toLowerCase() != TAGS.TR) return false;
    return true;
}

Grid.prototype.hasCellItem = function(gridItem /* HTMLElement */) {
    if (gridItem.parentNode == null) return false;
    if (gridItem.parentNode.parentNode == null) return false;
    if (gridItem.parentNode.parentNode.parentNode != this) return false;
    if (gridItem.tagName.toLowerCase() != TAGS.TD) return false;
    return true;
}

Grid.prototype.clear = function() {
    DOM.removeChildren(this.tBodies[0]);
    this.data = null;
}

// Returns an array with the names of all the columns
Grid.prototype.getColumns = function() {
    // If it already exists, just return it
    if (this._columns != null) return this._columns;
 
    // Gather headers
    var ths = DOM.getElementsByTagName(this.tHead.rows[0], TAGS.TH);
    for (var i = 0; i < ths.length; i++) {
        ths[i].name = ths[i].getAttribute(ATTRS.NAME);
    }
    
    // Return
    return this._columns = ths;
}

Grid.prototype.getColumnNames = function() {
    return this.getColumns().map(function(th) { return th.getAttribute(ATTRS.NAME); });
}

// Returns an array with the names of all the columns
Grid.prototype.setColumns = function(colNames) {
    this._columns = null;
 
    // Create headers
    var tr = this.tHead.rows[0];
    if (tr != null) DOM.removeChildren(tr);
    else this.tHead.appendChild(tr = document.createElement(TAGS.TR));
    for (var i = 0; i < colNames.length; i++) {
        var th = document.createElement(TAGS.TH);
        th.innerHTML = colNames[i];
        tr.appendChild(th);
        th.setAttribute(ATTRS.NAME, colNames[i]);
    }
}

// Returns the index of a certain column
Grid.prototype.columnIndex = function(name) {
    var cs = this.getColumns();
    for (var i = 0; i < cs.length; i++) {
        if (cs[i].name == name) return i;
    }
    return -1;
}

/**
 * Moves the current selection in the UP, DOWN, LEFT, RIGHT, PAGEUP or PAGEDOWN direction.
 */
Grid.prototype.moveSelection = function(dir /* Integer */) {
    // Get selected item
    var gridItem = this.getSelectedItem();
    
    if (cellSelection) {
        var row = gridItem.parentNode;
        switch(dir) {
            case KEYS.LEFT: this.selectPreviousItem(gridItem); break;
            case KEYS.RIGHT: this.selectNextItem(gridItem); break;
            case KEYS.UP: 
                if (DOM.previousElement(row) != null) {
                    this.select(this.rows[row.rowIndex-1].cells[gridItem.cellIndex]); 
                }
                break;
            case KEYS.DOWN: 
                if (DOM.nextElement(row) != null) {
                    this.select(this.rows[row.rowIndex+1].cells[gridItem.cellIndex]); 
                }
                break;
        }
    }
    else {
        switch(dir) {
            case KEYS.UP: this.selectPreviousItem(gridItem); break;
            case KEYS.DOWN: this.selectNextItem(gridItem); break;
        }
    }
}

Grid.prototype.editSelection = function() {
    // Make sure there is a cell to edit
    var cell = this.getSelectedItem();
    if (cell == null || cell.getAttribute(ATTRS.EDITABLE) == null) return;
    
    // Make sure the grid has an editor
    this.editor = this.editor || Editor.newInstance();
    
    // Ensure the editor is enhanced
    this.editor = dw(this.editor);
    
    // Edit selection
    this.editor.edit(cell, this);
}

Grid.prototype.appendValue = function(obj /* Object */, proc /* Function */, alt /* String */) {
    var doc = this.ownerDocument;
    if (obj == null) return;

    // Create row and cells
    var gridItem = doc.createElement(TAGS.TR);
    if (alt != null) gridItem.setAttribute(ATTRS.ALT, alt);
    var cs = this.getColumns();
    for (var i = 0; i < cs.length; i++) gridItem.appendChild(doc.createElement(TAGS.TD));
    if (gridItem.className != "hidden") this.setValue(gridItem, obj, proc);
    this.tBodies[0].appendChild(gridItem);
}

Grid.prototype.setValue = function(gridItem /* HTMLTableRowElement */, obj /* Object */, proc /* Function */) {
    if (gridItem == null || obj == null) return;

    // Grab column names
    var cs = this.getColumns();
    var ia = isArray(obj);

    // Set values
    gridItem.data = obj;
    obj.view = gridItem;
    
    // Create cells
    for (var i = 0; i < cs.length; i++) {
        var td = gridItem.cells[i];

        // Grab value
        var value = ia? obj[i] : obj[cs[i].name];
        if (value == null) value = "";

        // Set cell value
        if (isBoolean(value) && cs[i].className == "boolean") td.setAttribute(ATTRS.VALUE, ""+value);
        else {
            // try { value = (proc == null? value : proc.call(obj, ia? i : cs[i].name, value, td)); }
            try { value = (proc == null? value : proc.call(obj, cs[i].name, value, td)); }
            catch (ex) { console.log(ex); }
            
            // Filter special number values
            // console.log("value: "+value+", isNumber: "+isNumber(value)+", className: "+cs[i].className);
            if (isNumber(value)) {
                switch (cs[i].className) {
                    case "integer": value = value.toFixed(0); break;
                    case "decimal": value = value.toFixed(2); break;
                    case "currency": value = value.toFixed(2); break;
                    case "percent": value = (100 * value).toFixed(2)+"%"; break;
                }
            }

            td.innerHTML = Widget.displayValue(value);
        }

        // Copy style
        if (!isEmpty(cs[i].className)) addClassName(td, cs[i].className);
    }
}

Grid.prototype.setValues = function(objs /* Object[] */, proc /* Function */) {
    // Remove all previous items
    DOM.removeChildren(this.tBodies[0]);

    // Set the global data
    this.data = objs;
    
    // Add all the rows coming from the server
    for (var i = 0; i < objs.length; i++) {
        this.appendValue(objs[i], proc, (i % 2) == 0? null : "alt");
    }
}

Grid.prototype.loadData = function(cb /* Function */, proc /* Function */) {
    // Ensure there is a cursor and a max value
    this._cursor  = this._cursor || 0;
    this._limit = this._limit || parseInt(this.getAttribute(ATTRS.DATALIMIT) || Grid.ROW_LIMIT);

    var src = this.getAttribute(ATTRS.DATASRC);
    var type = this.getAttribute(ATTRS.DATATYPE);
    var orderBy = this.getAttribute(ATTRS.DATASORT);
    // var where = this.getAttribute(ATTRS.DATAFILTER);
    var where = this.getFilter();
    var loader = this.dataLoader;

    if (src != null && src.startsWith(URNS.JAVASCRIPT)) loader = eval(src.substring(URNS.JAVASCRIPT.length));
    else { /* TODO: build a data source out of a URL */ }

    var grid = this;
    Event.dispatch(grid, EVENTS.REQUEST);
    loader.call(null, type, where, orderBy, this._cursor, this._limit, function(data) {

    	try { grid.setValues(data, proc); }
        catch (ex) { console.log(ex); }

        // Do a check for height, it should not be an issue if pages are paginated
        var maxHeight = grid.tBodies[0].style.maxHeight;
        maxHeight = (maxHeight != null && maxHeight.length > 0? parseInt(maxHeight) : null);
        if (maxHeight != null && grid.tBodies[0].clientHeight > maxHeight) grid.tBodies[0].style.height = maxHeight;
        
        // Load footer only if there is a total named element in there
        var total = grid.tFoot.querySelector("[name=total]");
        if (total) grid.loadFooter(grid.tFoot, grid._cursor, grid._cursor + grid._limit - 1, where);
        
        Event.dispatch(grid, EVENTS.RESPONSE);
        
        if (cb != null) cb.call(this);
    });
}

Grid.prototype.loadFooter = function(footer, min, max, where) {
    footer = footer || this.tFoot;
    if (footer == null) return;
    var type = this.getAttribute(ATTRS.DATATYPE);
    RPC.API.countAll(type, where || this.getFilter(), function(total) {
        var minSpan = footer.querySelector("[name=min]"), maxSpan = footer.querySelector("[name=max]")
        if (minSpan != null) minSpan.innerText = (min+1).toString();
        if (maxSpan != null) {
            if (max <= 0)  maxSpan.innerText = " - "+total;
            else maxSpan.innerText = total < (max+1) ? total : (max+1).toString();
        }
        var totalSpans = footer.querySelectorAll("[name=total]");
        if (totalSpans.length == 0) return;
        for (var i = 0; i < totalSpans.length; i++) totalSpans[i].innerText = total; 
    });
}

Grid.prototype.next = function(n) {
	if (n == null) n = 1;
	var c = this._cursor + (n * this._limit);
	// if (c > max) return;
    this._cursor = c;
    this.loadData();
}

Grid.prototype.prev = function(n) {
	if (n == null) n = 1;
    var c = this._cursor - (n * this._limit);
    if (c < 0) return;
    this._cursor = c;
    this.loadData();
}

// The sort specification is simply a list of variables names followed by a +/-
// sign depending if the sort is ascending or descending (+ is asc, - is desc)
Grid.prototype.setSort = function(sort) {
    // Ensure that if the spec brings ASC/DESC we replace it by +/-  and remove commas
    var sort = sort.replace(/\s+ASC/ig, "+").replace(/\s+DESC/ig, "-");
    console.log("SORT: "+sort);
    this.setAttribute(ATTRS.DATASORT, sort);
    var sorts = sort.trim().split(/\s+/);
    var fields = new Object();
    for (var i = 0; i < sorts.length; i++) {
        var s = sorts[i], l = s.length;
        var field = s.substring(0,l-1), ascDesc = s.charAt(l-1);
        fields[field] = ascDesc;
    }
    
    var ths = this.tHead.rows[0].cells;
    for (var i = 0; i < ths.length; i++) {
        var th = ths[i], nm = th.getAttribute(ATTRS.NAME);
        if (nm == null || fields[nm] == null) continue;
        // var sort = (th.getAttribute(ATTRS.SORT) == Grid.ASCENDING? Grid.DESCENDING : Grid.ASCENDING);
        th.setAttribute(ATTRS.SORT, fields[nm] == '+'? Grid.ASCENDING : Grid.DESCENDING);
    }
}

// The sort speficication is simply a list of variables names followed by a +/-
// sign depending if the sort is ascending or descending (+ is asc, - is desc)
Grid.prototype.addSort = function(dataName /* String */, ascDesc /* String */, sticky /* Boolean */) {
    
    // Update column attributes
    var ths = this.tHead.rows[0].cells;
    for (var i = 0; i < ths.length; i++) {
        var th = ths[i], nm = th.getAttribute(ATTRS.NAME);
        if (nm == null) continue;
        if (nm == dataName) {
            var sort = (th.getAttribute(ATTRS.SORT) == Grid.ASCENDING? Grid.DESCENDING : Grid.ASCENDING);
            th.setAttribute(ATTRS.SORT, sort);
        }
        else if (!sticky) th.removeAttribute(ATTRS.SORT);
    }

    // Remove sort, and add it at the end
    if (sticky) {
        var dataSort = this.getAttribute(ATTRS.DATASORT) || "";
        var regexp = new RegExp("(^|\\s)"+dataName+"[-+]"+"(\\s|$)");
        dataSort = dataSort.replace(regexp, "$2");
        dataSort += (dataSort.length > 0? " " : "")+dataName+ascDesc;
    }
    else dataSort = dataName+ascDesc;
    
    // Set sort
    this.setAttribute(ATTRS.DATASORT, dataSort);
    // console.log(dataSort);
    // this.loadData();
    this.sort(dataSort);

    Event.dispatch(this, EVENTS.SORT);
}

Grid.prototype.sort = function(dataSort) { this.remoteSort(); }

Grid.prototype.remoteSort = function(dataSort) {
	// Reset cursor
	this._cursor = 0;
	
	// Load data
    this.loadData();
}

Grid.prototype.localSort = function(dataSort) {
    if (dataSort == null) dataSort = this.getAttribute(ATTRS.DATASORT);
    var sorts = dataSort.split(/\|/), names = new Array(), descs = new Array();

	// Reset cursor
	// this._cursor = 0;
    
    // Extract names and asc/desc characters
    for (var i = 0; i < sorts.length; i++) {
        var s = sorts[i];
        names.push(s.substring(0, s.length - 1));
        descs.push((s.substring(s.length - 1) == '-'));
    }
    
    var rows = this.tBodies[0].rows;
    var cmpFn = function(a, b) {
        for (var i = 0; i < names.length; i++) {
            var af = a[names[i]], bf = b[names[i]];
            var cmp = (af < bf) - (af > bf);
            if (cmp != 0) return descs[i]? cmp : -cmp;
        }
    };
    
    this.data.sort(cmpFn);
    this.setValues(this.data);

    // Reappend rows in sort order
    // for (var i = 0; i < this.data.length; i++) this.tBodies[0].appendChild(this.data[i].view);
    /*
    DOM.removeChildren(this.tBodies[0]);
    for (var i = this._cursor, len = i + this._limit; i < len && i < this.data.length; i++) {
    	// this.tBodies[0].appendChild(this.data[i].view);
    	this.appendValue(this.data[i]);
    }
    */
    
}

Grid.prototype.getFilter = function() {
    var ths = this.querySelectorAll("th[filter]");
    var dataFilter = (this.getAttribute(ATTRS.DATAFILTER) || "");
    var add = Array.prototype.map.call(ths, function(nd) { return nd.getAttribute(ATTRS.FILTER); }).join(" AND ");
    dataFilter += add != null && add.length > 0 ? (" AND " + add) : "";
    return dataFilter;
}

Grid.prototype.removeAllFilters = function() {
    this.removeAttribute(ATTRS.DATAFILTER);
    var ths = this.querySelectorAll("th[filter]");
    for (var i = 0; i < ths.length; i++) ths[i].removeAttribute(ATTRS.FILTER);
    // Event.dispatch(this, EVENTS.FILTER);
}


//~ HELPER(S) ----------------------------------------------------------------

// Returns the width of a string inside an HTML element (used for style)
Grid._stringWidth = function(str, fontStyle) {
    // Make sure we have a span
    var measuringSpan = arguments.callee.measuringSpan;
    if (measuringSpan == null) {
        arguments.callee.measuringSpan = measuringSpan = document.createElement(TAGS.SPAN);
        measuringSpan.style.whiteSpace = "nowrap";
        measuringSpan.style.visibility = "hidden";
        document.body.appendChild(measuringSpan);
    }
    if (fontStyle != null) measuringSpan.style.font = fontStyle;
    measuringSpan.innerText = str;
    return measuringSpan.offsetWidth;
}

Grid.prototype._countAll = function(fn) {
    var type = this.getAttribute(ATTRS.DATATYPE);
    RPC.API.countAll(type, this.getFilter(), fn);
}


//~ HANDLER(S) ---------------------------------------------------------------

Grid._clickHandler = function(ev /* Event */, grid /* HTMLElement */) {
    // console.log("Grid._clickHandler");
    
    if (grid == null) grid = ev.currentTarget;
    grid = dw(grid);
    var clickedElem = ev.target;
    
    // If the click was on the header, either sort or filter
    if (grid.tHead.contains(clickedElem)) {
        // Get current header
        var th = clickedElem, sticky = ev.ctrlKey;
        
        // If the ALT key was pressed and it is filterable, filter
        if (ev.altKey && (grid.getAttribute(ATTRS.FILTERABLE) || th.getAttribute(ATTRS.FILTERABLE))) {
            // grid.addFilter(th.getAttribute(ATTRS.NAME), null, sticky);
            if (!sticky) {
                var ths = this.querySelectorAll("th[filter]");
                for (var i = 0; i < ths.length; i++) ths[i].removeAttribute(ATTRS.FILTER);
            }
            Event.dispatch(grid, EVENTS.FILTER, ev);
        }
        
        // Else, try sorting
        else if (grid.getAttribute(ATTRS.SORTABLE) || th.getAttribute(ATTRS.SORTABLE)) {
            var ascDesc = (th.getAttribute(ATTRS.SORT) == Grid.ASCENDING? '-' : '+');
            grid.addSort(th.getAttribute(ATTRS.NAME), ascDesc, sticky);
        }
    }

    // If the click was on the body, select
    if (grid.tBodies[0].contains(clickedElem)) grid.select(grid._itemAncestor(clickedElem));
}

Grid._dblclickHandler = function(ev /* Event */, grid /* HTMLElement */) {
    if (grid == null) grid = ev.currentTarget;
    grid = dw(grid);
    var clickedElem = ev.target;

    // If the double click was on the body, inspect
    if (grid.tBodies[0].contains(clickedElem)) Event.dispatch(grid._itemAncestor(clickedElem), EVENTS.INSPECT);
}

Grid._keyDownHandler = function(ev /* Event */, grid /* HTMLElement */) {
	// The Grid is the current target of the event
    if (grid == null) grid = ev.currentTarget;
    grid = dw(grid);

    // console.log("this.hasFocus(): "+this.hasFocus());
    
    // Check it is not disabled
    // if (!this.isEnabled()) return;

    // Figure out what type of selection we want

    // Assume we are going to catch event here
    var propagate = false;

    switch(ev.keyCode) {
        case KEYS.LEFT:  grid.moveSelection(ev.keyCode); break;
        case KEYS.RIGHT: grid.moveSelection(ev.keyCode); break;
        case KEYS.UP:    grid.moveSelection(ev.keyCode); break;
        case KEYS.DOWN:  grid.moveSelection(ev.keyCode); break;
        case KEYS.F2:    grid.editSelection(); break;
        default: propagate = true;
    }

    // Do not propagate afterwards
    if (!propagate) ev.stopPropagation();
}

Grid._mouseOverHandler = function(ev /* Event */, grid /* HTMLElement */) {
    if (grid == null) grid = ev.currentTarget;
    grid = dw(grid);
    
    var mousedElem = ev.target;
    if (mousedElem.tagName.toLowerCase() != TAGS.TD) return;

    // Only show longer text
    var sw = Grid._stringWidth(mousedElem.innerText, mousedElem.style.font);
    // console.log("offsetWidth: "+mousedElem.offsetWidth);
    // console.log("stringWidth: "+sw);
    if (sw < mousedElem.offsetWidth) return;
    
    // Set location and display
    var loc = mousedElem.getLocation();
    // var et = dw(ToolTip.OVERFLOW);
    // et.show(mousedElem.innerText, loc.x, loc.y);
}

Grid._mouseOutHandler = function(ev /* Event */) {
    // var et = dw(ToolTip.OVERFLOW);
    // if (et == null || ev.relatedTarget == et) return;
    // et.hide();
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwGrid", function(grid) {
    Event.addListener(grid, EVENTS.CLICK, Grid._clickHandler);
    Event.addListener(grid, EVENTS.KEYDOWN, Grid._keyDownHandler);

    // Set data limit in case it is not set
    if (grid.getAttribute(ATTRS.DATALIMIT) == null) this._limit = Grid.ROW_LIMIT;

    // Retrieve data if there is a data source
    if (grid.getAttribute(ATTRS.DATASRC) != null) {
        grid = $(grid);
        Event.addListener(window, EVENTS.LOAD, function() { grid.loadData(); });
    }

    // Inspectable elements
    if (grid.getAttribute(ATTRS.INSPECTABLE) != null) {
    	Event.addListener(grid, EVENTS.DBLCLICK, Grid._dblclickHandler);
    }
    
    // Hidden content
    Event.addListener(grid, EVENTS.MOUSEOVER, function (ev) { Grid._mouseOverHandler(ev, grid); });
    Event.addListener(grid, EVENTS.MOUSEOUT, function (ev) { Grid._mouseOutHandler(ev); });
});

    
//****************************************************************************
// LIST
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function List() { }
copyMethods(List.prototype, Selection.prototype);


//~ CONSTANT(S) --------------------------------------------------------------


//~ PUBLIC METHOD(S) ---------------------------------------------------------

List.prototype.itemCount = function() {
    return this.children.length;
}

List.prototype.hasItem = function(listItem /* HTMLElement */) {
    if (listItem.parentNode != this) return false;
    if (listItem.nodeName.toLowerCase() != TAGS.LI) return false;
    return true;
}


//~ HELPER(S) ----------------------------------------------------------------


//~ HANDLER(S) ---------------------------------------------------------------

List._clickHandler = function(ev /* Event */, list /* HTMLElement */) {
    // console.log("List._clickHandler - target: "+ev.target+", ev.propagationStopped: "+ev.propagationStopped);

    if (ev.propagationStopped) return;
    if (list == null) list = ev.currentTarget;
    list = dw(list);
    var clickedElem = ev.target;
    list.select(list._itemAncestor(clickedElem), ev);
}

List._keyDownHandler = function(ev /* Event */, list /* HTMLElement */) {
    // The List is the current target of the event
    if (list == null) list = ev.currentTarget;
    list = dw(list);

    // console.log("this.hasFocus(): "+this.hasFocus());
    
    // Check it is not disabled
    // if (!this.isEnabled()) return;

    // Figure out what type of selection we want

    // Assume we are going to catch event here
    var propagate = false;

    switch(ev.keyCode) {
        case KEYS.LEFT:   list.moveSelection(ev.keyCode); break;
        case KEYS.RIGHT:  list.moveSelection(ev.keyCode); break;
        case KEYS.UP:     list.moveSelection(ev.keyCode); break;
        case KEYS.DOWN:   list.moveSelection(ev.keyCode); break;
        case KEYS.F2:     list.editSelection(); break;
        default: propagate = true;
    }

    // Do not propagate afterwards
    if (!propagate) ev.stopPropagation();
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwList", function(list) {
    Event.addListener(list, EVENTS.CLICK, List._clickHandler);
});

    
//****************************************************************************
// MAP
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Map() { }



//~ CONSTANT(S) --------------------------------------------------------------

Map.GW_LAT = 51.47778;
Map.GW_LNG =  0.00000;

// Color to draw from for polylines
Map.COLORS = ["#edc240", "#00a8f0", "#c0d800", "#cb4b4b", "#4da74d", "#9440ed"];
Map.ICONS = [null, null, null, null, null, null];


//~ METHOD(S) ----------------------------------------------------------------

Map.prototype.init = function(center) {
    this.googleMap = new GMap2(this);
    this.googleMap.addControl(new GSmallMapControl());
    if (center == null) center = new GLatLng(Map.GW_LAT, Map.GW_LNG);
    this.googleMap.setCenter(center, 13);
}

Map.getSmallIcon = function(index) {
    if (index == null) index = 0;
    if (Map.ICONS[index] != null) return Map.ICONS[index];
    var icon = Map.ICONS[index] = new GIcon();
    icon.image = "http://bluemessaging.net/images/maps/"+Map.COLORS[index].substring(1)+".png";
    icon.shadow = "http://bluemessaging.net/images/maps/shadow.png";
    icon.iconSize = new GSize(12, 20);
    icon.shadowSize = new GSize(22, 20);
    icon.iconAnchor = new GPoint(6, 20);
    icon.infoWindowAnchor = new GPoint(5, 1);
    return icon;
}


//~ HANDLER(S) ---------------------------------------------------------------


//~ STYLE --------------------------------------------------------------------


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register Behavior
Behavior.registerBehavior("dwMap", function(map) {
    // console.log("Establishing Google Map");
    // map.googleMap = new GMap2(map);
    // map.googleMap.addControl(new GSmallMapControl());
    // map.googleMap.setCenter(new GLatLng(Map.GW_LAT, Map.GW_LNG), 13);
    // Event.addListener(window, EVENTS.LOAD, function() { });
});

    
//****************************************************************************
// MENU
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Menu() { }


//~ PUBLIC METHOD(S) ---------------------------------------------------------

Menu.prototype.show = function(x, y) {
    this.setLocation(x || window.mouseX, y || window.mouseY);
    this.style.display = "block";
    
    var popup = this;
    var tempListener = function(ev) {
        popup.style.display = "none";
        Event.removeListener(window, EVENTS.CLICK, tempListener);
    };
    Event.addListener(window, EVENTS.CLICK, tempListener);
}


//~ PRIVATE METHOD(S) --------------------------------------------------------

// This is necessary if we want to have a fluid width menu, all of the pure CSS
// menus published on the internet are fixed width, making unnecessary the code below
Menu._adjustWidth = function(menuItem /* HTMLLIEelement */) {
    // Verify if the child width has bee set before and to what
    if (menuItem._itemWidthSet != null && menuItem._itemWidthSet == menuItem.clientWidth) return;
    
    // Iterate over all UL child elements, making sure the width is ok
    for (var i = 0, l = menuItem.childNodes.length; i < l; i++) {
        var childMenu = menuItem.childNodes[i];
        if (childMenu.nodeType != Node.ELEMENT_NODE) continue;
        if (childMenu.nodeName.toLowerCase() != TAGS.UL) continue;
        
        // Set width
        childMenu.style.left = menuItem.clientWidth+"px";
    }
    
    menuItem._itemWidthSet = menuItem.clientWidth;
    
}


//~ HANDLER(S) ---------------------------------------------------------------

Menu._clickHandler = function(ev /* Event */) {
    var target = ev.target;
    var type = target.getAttribute(ATTRS.TYPE);
    if (type == "check") target.toggleAttribute(ATTRS.CHECKED);
    if (type == "radio") {
        if (target.getAttribute(ATTRS.CHECKED) != null) return;
        var name = target.getAttribute(ATTRS.NAME);
        if (name == null) return;
        var siblings = DOM.getElementsByAttribute(target.parentNode, ATTRS.NAME, name);
        for (var i = 0; i < siblings.length; i++) siblings[i].removeAttribute(ATTRS.CHECKED);
        target.setAttribute(ATTRS.CHECKED, ATTRS.CHECKED);
    }
}

Menu._contextMenuHandler = function(ev /* Event */) {
    var target = ev.target;
    if (ev.button != 2) return;
    
    // Show the context menu
    var context = dw(target.getAttribute(ATTRS.CONTEXT));
    if (context == null) return;
    enhanceAs(context, Menu);
    context.htmlFor = ev.target;
    context.show();
    
    // Do not show default menu
    ev.preventDefault();
}

Menu._mouseOverHandler = function(ev /* Event */) {
    var menuItem = ev.target;
    if (menuItem.nodeName.toLowerCase() != TAGS.LI) return;
    
    // Adjust width
    if (menuItem.getAttribute(ATTRS.TYPE) == "cascade") Menu._adjustWidth(menuItem);
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register Behavior
Behavior.registerBehavior("dwMenu", function(menu) {
    Event.addListener(menu, EVENTS.CLICK, Menu._clickHandler);
    Event.addListener(menu, EVENTS.MOUSEOVER, Menu._mouseOverHandler);
});

// Register a click event so that we can show context menus
Event.addListener(window, EVENTS.CONTEXTMENU, Menu._contextMenuHandler);

    
//****************************************************************************
// OVERLAY
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

/**
 * The overlay item functions similarly to XUL's overlay.  But, since ID cannot be
 * duplicated in the same HTML document, we use a target attribute in the overlay
 * element to indicate what to do with the overlay children.
 */
function Overlay() { }


//~ CONSTANT(S) --------------------------------------------------------------

/**
 * Content can be either 'elements' or 'nodes'.  If it is 'elements', then only the
 * element nodes will be moved, if it is 'nodes', then all nodes wwill be moved along
 */
Overlay.DEFAULT_CONTENT = "nodes";

/**
 * Appends all the overlay children to the target
 */
Overlay.APPEND_CHILDREN = "append-children";

/**
 * Adds all the overlay children after the target
 */
Overlay.AFTER = "after";

/**
 * Adds all the overlay children before the target
 */
Overlay.BEFORE = "before";

/**
 * Removes the target itself
 */
Overlay.REMOVE = "remove";

/**
 * Removes all the children at target (and ignores any children the overlay might have)
 */
Overlay.REMOVE_CHILDREN = "remove-children";

/**
 * Replaces the target with a document fragment containing all the overlay children.  Leaving
 * the overlay empty is equivalent to 'remove-children'
 * 
 */
Overlay.REPLACE = "replace";

/**
 * Removes all the children at the target and replaces them with the overlay children
 */
Overlay.REPLACE_CHILDREN = "replace-children";


//~ PRIVATE METHOD(S) --------------------------------------------------------

/**
 * An overlay can specify a target, an action (add or replace) and a position,
 * which can be an absolute number, or an id of the element before which we
 * want to insert the overlay.  The position has no real meaning if the action
 * is replace.
 */
Overlay._merge = function(overlay /* HTMLDivElement */) {
    var target = document.getElementById(overlay.getAttribute(ATTRS.TARGET));
    // if (target == null) throw new Error("Element with ID '"+overlay.getAttribute(ATTRS.TARGET)+"' does not exist.");
    if (target == null) { console.error("Element with ID '"+overlay.getAttribute(ATTRS.TARGET)+"' does not exist."); return; }

    // Get action (APPEND_CHILDREN by default)
    var action = overlay.getAttribute(ATTRS.ACTION).toLowerCase() || Overlay.APPEND_CHILDREN;

    // Get position (if it exists)
    // var pos = overlay.hasAttribute(ATTRS.POSITION)? parseInt(overlay.getAttribute(ATTRS.POSITION)) : null;
    var pos = overlay.getAttribute(ATTRS.POSITION);
    if (pos != null) pos = parseInt(pos);

    // Get content (if it exists)
    var content = overlay.hasAttribute(ATTRS.CONTENT)? overlay.getAttribute(ATTRS.CONTENT) : Overlay.DEFAULT_CONTENT;
    var elementsOnly = (content != "nodes");

    // Build a document fragment with all the overlay children
    var df = document.createDocumentFragment();
    while (overlay.hasChildNodes()) {
        var child = overlay.firstChild;
        if (elementsOnly && child.nodeType != Node.ELEMENT_NODE) {
            overlay.removeChild(child);
            continue;
        }
        // console.log("Child: "+child.id);
        df.appendChild(child);
    }

    // Remove overlay itself
    overlay.parentNode.removeChild(overlay);

    // Depending on the action, execute DOM surgery
    switch (action) {
        case Overlay.APPEND_CHILDREN:     DOM.insert(target, df, pos); break;
        case Overlay.AFTER:               DOM.insertAfter(df, target); break;
        case Overlay.BEFORE:              DOM.insertBefore(df, target); break;
        case Overlay.REMOVE:              DOM.remove(target); break;
        case Overlay.REMOVE_CHILDREN:     DOM.removeChildren(target); break;
        case Overlay.REPLACE:             DOM.replace(df, target); break;
        case Overlay.REPLACE_CHILDREN:    DOM.removeChildren(target); target.appendChild(df); break;
        default: throw new Error("Overlay.js: Unknown action '"+action+"' does not exist.");
    }
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwOverlay", function(overlay) {
    Overlay._merge(overlay);
});

    
//****************************************************************************
// PROGRESSBAR
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function ProgressBar() { }


//~ CONSTANT(S) --------------------------------------------------------------


//~ ATTRIBUTE(S) -------------------------------------------------------------


//~ METHOD(S) ----------------------------------------------------------------

ProgressBar.prototype.getValue = function() {
    return parseInt(this.getAttribute(ATTRS.VALUE) || "0");
}

ProgressBar.prototype.setValue = function(value /* String */) {
    var label = DOM.getElementByTagName(this, TAGS.LABEL);
    var div = DOM.getElementByTagName(this, TAGS.DIV);
    label.innerText = value+"%";
    div.style.width = value+"%";
    this.setAttribute(ATTRS.VALUE, ""+value);
}


//~ HELPER(S) ----------------------------------------------------------------


//~ HANDLER(S) ---------------------------------------------------------------


//~ STYLE --------------------------------------------------------------------


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register Behavior
Behavior.registerBehavior("dwProgressBar", function(progressBar) {    
    DOM.removeChildren(progressBar);
    progressBar.appendChild(document.createElement(TAGS.LABEL));
    progressBar.appendChild(document.createElement(TAGS.DIV));
    var value = progressBar.getAttribute(ATTRS.VALUE) || "0";
    ProgressBar.prototype.setValue.call(progressBar, value);
});

    
//****************************************************************************
// SEPARATOR
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Separator() { }

    
//****************************************************************************
// SLIDER
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Slider() { }


//~ CONSTANT(S) --------------------------------------------------------------


//~ ATTRIBUTE(S) -------------------------------------------------------------


//~ METHOD(S) ----------------------------------------------------------------

Slider.prototype.setValue = function(value /* String */) {
    var div = DOM.getElementByTagName(this, TAGS.DIV);
    div.style.width = value+"%";
}


//~ HELPER(S) ----------------------------------------------------------------


//~ HANDLER(S) ---------------------------------------------------------------


//~ STYLE --------------------------------------------------------------------


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register Behavior
Behavior.registerBehavior("dwSlider", function(slider) {
    DOM.removeChildren(slider);
    var handle = document.createElement(TAGS.DIV);
    // handle.setAttribute(ATTRS.DRAGGABLE, "true");
    Draggable.makeDraggable(handle, handle, Draggable.NORMAL, { x: 0, y: 0, width: 90, height: 0 });
    slider.appendChild(handle);
    // var value = slider.getAttribute(ATTRS.VALUE) || "0";
    // Slider.prototype.setValue.call(slider, value);
});

    
//****************************************************************************
// SPLITTER
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function Splitter() { }


//~ CONSTANT(S) --------------------------------------------------------------

Splitter.HORIZONTAL = 1;
Splitter.VERTICAL   = 2;


//~ ATTRIBUTE(S) -------------------------------------------------------------


//~ METHOD(S) ----------------------------------------------------------------

Splitter.prototype.getPrevious = function() {
    return this._previous || (this._previous = DOM.previousElement(this));
}

Splitter.prototype.getNext = function() {
    return this._next || (this._next = DOM.nextElement(this));
}


//~ HELPER(S) ----------------------------------------------------------------


//~ HANDLER(S) ---------------------------------------------------------------

Splitter._dragStartHandler = function(ev /* Event */, splitter /* HTMLElement */) {
    splitter._startLocation = { x: window.mouseX, y: window.mouseY };
}

Splitter._dragEndHandler = function(ev /* Event */, splitter /* HTMLElement */) {
    splitter = dw(splitter);
    var p = splitter.getPrevious();
    var n = splitter.getNext();
    var pb = p.getBoundingClientRect(), nb = n.getBoundingClientRect();
    switch (splitter._orient) {
        case Splitter.HORIZONTAL:
            var d = window.mouseY - splitter._startLocation.y;
            p.style.height = ""+(pb.height + d)+"px";
            n.style.height = ""+(nb.height - d)+"px";
            break;
        case Splitter.VERTICAL:
            var d = window.mouseX - splitter._startLocation.x;
            p.style.width = ""+(pb.width + d)+"px";
            n.style.width = ""+(nb.width - d)+"px";
            break;
    }
}


//~ STYLE --------------------------------------------------------------------


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register Behavior
Behavior.registerBehavior("dwSplitter", function(splitter) {
    var orient = splitter.getAttribute(ATTRS.ORIENT) || "horizontal";

    var rect = null;
    switch (orient.toLowerCase()) {
        case "horizontal": 
            splitter._orient = Splitter.HORIZONTAL;
            rect = { x: 0, y: null, width: 0, height: null }; 
            break;
        case "vertical":   
            splitter._orient = Splitter.VERTICAL;
            rect = { x: null, y: 0, width: null, height: 0 }; 
            break;
    }

    Draggable.makeDraggable(splitter, splitter, Draggable.EXTERNAL, rect);
    Event.addListener(splitter, EVENTS.DRAGSTART, function (ev) { Splitter._dragStartHandler(ev, splitter); });
    Event.addListener(splitter, EVENTS.DRAGEND, function (ev) { Splitter._dragEndHandler(ev, splitter); });
});

    
//****************************************************************************
// STATUSBAR
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function StatusBar() { 
    // Private attributes
    this._pendingTimeout = null;
    
    // Public attributes
    this.defaultMessage = "Ready!";        
    
    // Reusable effect
    this._curColor = "#DCDCDC";
    // this._tween = new Fx.Tween(this, { duration: 250 });
    
    this._popup = (this.getAttribute(ATTRS.POPUP) != null);
    
}


//~ CONSTANT(S) --------------------------------------------------------------

/**
 * If 'true', it will beep (provided there is a beep object in the page) each time 
 * it warns or errors.
 */
StatusBar.BEEP = false;

/**
 * If 'true', it will use effects
 */
StatusBar.EFFECTS = false;


//~ METHOD(S) ----------------------------------------------------------------

StatusBar.prototype.blank = function() {
    this.removeAttribute(ATTRS.TYPE);
    if (this._popup) this.style.display = "none";
    if (StatusBar.EFFECTS) this._tween.start("background-color", this._curColor, this._curColor = "#DCDCDC");
    else this.style.backgroundColor = "#DCDCDC";
    this.innerHTML = this.defaultMessage;
    this._pendingTimeout = null;
}

// Color: #DCDCDC
StatusBar.prototype.info = function(text /* String */, append /* Boolean */) {
    this._set("info", text, append, "#DCDCDC");
}

// Color: #78BA91
StatusBar.prototype.note = function(text /* String */, append /* Boolean */) {
    this._set("info", text, append, "#78BA91");
}

// Color: #F0CF3C
StatusBar.prototype.warn = function(text /* String */, append /* Boolean */) {
    if (StatusBar.BEEP) beep();
    this._set("warning", text, append, "#F0CF3C");
}

// Color: #F5807E
StatusBar.prototype.error = function(text /* String */, append /* Boolean */) {
    if (StatusBar.BEEP) beep();
    this._set("error", text, append, "#F5807E", -1);
}


//~ PRIVATE METHOD(S) --------------------------------------------------------

StatusBar.prototype._set = function(type /* String */, text /* String */, append /* Boolean */, color /* String */, delay /* Number */) {
    if (delay == null) delay = 2000;
    
    // If there is a pending timeout, get rid of it
    if (this._pendingTimeout != null) clearTimeout(this._pendingTimeout);

    // Proceeed 
    this.setAttribute(ATTRS.TYPE, type);
    if (this._popup) this.style.display = "block";
    if (StatusBar.EFFECTS) this._tween.start("background-color", this._curColor, this._curColor = color);
    else this.style.backgroundColor = color;
    this.innerText = (append? this.innerText + text : text + " ");
    var self = this;
    if (delay >= 0) this.pendingTimeout = setTimeout(function() { self.blank(); }, delay);
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwStatusBar", function(statusbar) { 
});

    
//****************************************************************************
// TABBOX
//****************************************************************************

// Since a tab item has really two items, the label and the box below, the
// selection is complicated.  We are assuming that for all purposes, the
// selection is the label, and there will be a 'box' attribute from there
// that will point to the box itself.

//~ CONSTRUCTOR --------------------------------------------------------------

function TabBox() { }
copyMethods(TabBox.prototype, Selection.prototype);


//~ PUBLIC METHOD(S) ---------------------------------------------------------

TabBox.prototype.hasItem = function(tbItem /* HTMLElement */) {
    if (tbItem.parentNode == null) return false;
    if (tbItem.parentNode.parentNode != this) return false;
    if (tbItem.tagName.toLowerCase() != TAGS.LI) return false;
    return true;
}

// This function has to be redefined with respect to the one in Selection, 
// since selecting an item is selecting both itself and the tab bar item
TabBox.prototype.select = function(tbItem /* HTMLElement */) {
    if (tbItem == null) return;
    Selection.prototype.select.call(this, tbItem);

    // Select tab also
    var lis = DOM.getElementsByTagName(this._barNode(), TAGS.LI, 1);
    var divs = DOM.getElementsByTagName(this, TAGS.DIV, 1);
    for (var i = 0; i < lis.length; i++) {
        var sel = lis[i].getAttribute(ATTRS.SELECTED);
        if (sel == null) divs[i].removeAttribute(ATTRS.SELECTED);
        else divs[i].setAttribute(ATTRS.SELECTED, ATTRS.SELECTED);
        
        // Make sure the pointer is in place
        lis[i].box = divs[i];
    }
    
    // If tab has a 'src' element load it
    var src = tbItem.box.getAttribute(ATTRS.SRC);
    if (src != null) tbItem.box.innerHTML = request(src);
}


//~ PRIVATE METHOD(S) --------------------------------------------------------

// The bar node is the first (and only) UL tag inside the calendar
TabBox.prototype._barNode = function() {
    if (this.barNode == null) this.barNode = DOM.getElementByTagName(this, TAGS.UL, 1);
    return this.barNode;
}


//~ HANDLER(S) ---------------------------------------------------------------

TabBox._clickHandler = function(ev /* Event */, tb /* HTMLDivElement */) {
    if (tb == null) tb = ev.currentTarget;
    tb = dw(tb);
    var clickedElem = ev.target;
    
    // Check it is not disabled
    // if (!this.isEnabled()) return;

    tb.select(tb._itemAncestor(clickedElem));
}

TabBox._keyDownHandler = function(ev /* Event */, tb /* HTMLElement */) {
    if (tb == null) tb = ev.currentTarget;

    // Check it is not disabled
    // if (!this.isEnabled()) return;

    // Get selected item
    var tbItem = tb.getSelectedItem();
    
    // Assume we are going to catch event here
    var propagate = false;
    switch(ev.keyCode) {
        case KEYS.RIGHT:
            // Right and Left are only allowed when focus is on the tabBar
            if (ev.target.contains(tb._barNode())) tb.selectNextItem(tbItem);
            else propagate = true;
            break;
        case KEYS.LEFT:
            // Right and Left are only allowed when focus is on the tabBar
            if (ev.target.contains(tb._barNode())) tb.selectPreviousItem(tbItem);
            else propagate = true;
            break;
        case KEYS.TAB:
            if (ev.ctrlKey) {
                if (ev.shiftKey) tb.selectPreviousItem(tbItem, true);
                else tb.selectNextItem(tbItem, true);
            }
            else propagate = true;
            break;
        default: propagate = true;
    }

    // Do not propagate afterwards
    if (!propagate) ev.stopPropagation();
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register Behavior
Behavior.registerBehavior("dwTabBox", function(tb) {
    Event.addListener(tb, EVENTS.CLICK, TabBox._clickHandler);
    Event.addListener(tb, EVENTS.KEYDOWN, TabBox._keyDownHandler);
    
    // Build selection for tabbox
    tb = dw(tb);
    var lis = DOM.getElementsByTagName(tb._barNode(), TAGS.LI, 1);
    tb.selectedItems = lis.filter(function(li) { return li.getAttribute(ATTRS.SELECTED) != null; });
    
    // If there are no selected items, select first
    if (tb.selectionCount() == 0 && lis.length > 0) {
    	tb.select(lis[0]);
    }
    else {
        // TODO: Ensure DIV is visible
    }     
});

    
//****************************************************************************
// TEXTAREA
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function TextArea() { }


//~ METHOD(S) ----------------------------------------------------------------


//~ HANDLER(S) ---------------------------------------------------------------


    
//****************************************************************************
// TEXTBOX
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function TextBox() { }


//~ METHOD(S) ----------------------------------------------------------------

TextBox.prototype.setPattern = function(pattern) {
    if (isEmpty(pattern)) return;
    var validator = Widget._charSets[pattern];
    if (validator == null) validator = new RegExp(pattern);
    Event.addListener(this, EVENTS.KEYPRESS, function(ev) {
        var validKey = Widget.validateKeyPress(ev, validator);
        if (!validKey) ev.stop();
        return validKey;
    });
}


//~ HELPER(S) ----------------------------------------------------------------

TextBox._calendar = null;

TextBox.getCalendar = function() {
    if (TextBox._calendar != null) return TextBox._calendar;
    var cal = TextBox._calendar = document.createElement(TAGS.TABLE);
    cal.id = "textbox-calendar";
    cal.className = "dwCalendar";
    Behavior.injectBehavior(cal);
    document.body.appendChild(cal);
    
    Event.addListener(cal, EVENTS.SELECT, function(ev) {
        if (cal._textBox != null) {
            var format = cal._textBox.getAttribute(ATTRS.FORMAT);
            cal._textBox.value = Date.toString(cal.getValue(), format);
        }
        cal.style.display = "none";
        cal.setValue(null);
    });
    
    return cal;
}


//~ HANDLER(S) ---------------------------------------------------------------

TextBox._focusHandler = function(ev /* Event */, textBox /* HTMLInputElement */) {
    if (textBox == null) textBox = ev.currentTarget;
    var cal = TextBox.getCalendar();
    var loc = textBox.getLocation();
    cal._textBox = textBox;
    cal.setLocation(loc.x, loc.y + textBox.offsetHeight + 4);
    // cal.setValue(textBox.getValue());
    cal.style.display = "table";
    
    // Make spurious
    var preservingFilter = function(elem) {
        if (elem == textBox) return true;
        if (cal.contains(elem)) return !cal._daysNode().contains(elem);
        return false;
    }
    // Widget._makeSpurious(cal, [ textBox ], ev);
    Widget._makeSpurious(cal, preservingFilter, ev);
    
    // Attach a listener such that any other click will a) dissapear menu, and
    // b) remove the listener itself.
    /*
    var tempListener = function(ev) {
        if (ev.keyCode != null && ev.keyCode != KEYS.ESC) return;
        if (ev.which != null && (ev.target == textBox || cal.contains(ev.target))) return;
        cal.style.display = "none";
        Event.removeListener(document, EVENTS.MOUSEDOWN, tempListener);
        Event.removeListener(document, EVENTS.KEYDOWN, tempListener);
    };
    Event.addListener(document, EVENTS.MOUSEDOWN, tempListener);
    Event.addListener(document, EVENTS.KEYDOWN, tempListener);
    */
}


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwTextBox", function(textBox) {
    var type = textBox.getAttribute(ATTRS.TYPE);
    if (type == null) { }
    else if (type == "date") {
        Event.addListener(textBox, EVENTS.FOCUS, TextBox._focusHandler);
        textBox.value = textBox.getAttribute(ATTRS.FORMAT) || "yyyy-MM-dd";
    }
    else if (type == "datetime") {
        Event.addListener(textBox, EVENTS.FOCUS, TextBox._focusHandler);
        textBox.value = textBox.getAttribute(ATTRS.FORMAT) || "yyyy-MM-dd hh:mm:ss";
    }
    else if (type == "time") {
        Event.addListener(textBox, EVENTS.FOCUS, TextBox._focusHandler);
        textBox.value = textBox.getAttribute(ATTRS.FORMAT) || "hh:mm:ss";
    }

    // Character set
    var charSet = textBox.getAttribute(ATTRS.CHARSET);
    if (charSet != null) dw(textBox).setPattern(charSet);
});

    
//****************************************************************************
// TOOLBAR
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

function TOOLBAR() { }


//~ METHOD(S) ----------------------------------------------------------------


//~ HANDLER(S) ---------------------------------------------------------------


    
//****************************************************************************
// TOOLTIP
//****************************************************************************

//~ CONSTRUCTOR --------------------------------------------------------------

// This constructor is only called by the 'enhanceAs' function
function ToolTip() {
    // Private attributes
    this._postFn = null;
    this._last = { x: null, y: null };
    this._delay = this.getAttribute(ATTRS.DELAY) || 0;
    console.log("delay: "+this._delay);
    this._delay = parseInt(this._delay);
    if (isNaN(this._delay)) this._delay = 0;
    
    // Public attributes
    this.displacement = { x: 0, y: 22 };

    // Start thread
    if (this._delay > 0) ToolTip._thread(this);
}

ToolTip.newInstance = function(id, delay, doc) {
    if (doc == null) doc = document;
    
    // Create element
    var tooltip = doc.createElement(TAGS.DIV);
    tooltip.appendChild(document.createTextNode(" "));
    tooltip.id = id;
    tooltip.className = "dwToolTip";
    if (delay != null) tooltip.setAttribute(ATTRS.DELAY, delay);
    
    // Append it to the body
    doc.body.appendChild(tooltip);
    
    // Inject behavior (event handlers basically), only for default tooltip
    Behavior.injectBehavior(tooltip);
    
    // Return element
    return tooltip;
}


//~ CONSTANT(S) --------------------------------------------------------------

ToolTip.DEFAULT = "default-tooltip";
ToolTip.SINGLETON = null;


//~ ATTRIBUTE(S) -------------------------------------------------------------


//~ CUSTOMIZATION METHOD(S) --------------------------------------------------

// This function can be overriden in order to process a custom tooltip.
// The parameters are: the element that triggered the tooltip, the tooltip 
// itself and the tooltip value, which can be text, a DOM object, or a function.
ToolTip.set = function(tooltip, elem, value) { return tooltip.innerHTML = value; }

ToolTip.getValue = function(elem /* Element */) {
	// If it is not an element, return it unchanged
	if (!isNode(elem)) return elem;
	
    // Obtain attribute
    var value = elem.getAttribute(ATTRS.TOOLTIP);
    
	// Nothing to do
    if (isEmpty(value)) return null;
    
    // Trim value
    value = value.trim();
    
    // Replace ID references
    if (value.startsWith(URNS.ID)) return dw(value.substring(URNS.ID.length).trim());

    // Replace URL references
    if (value.startsWith(URNS.URL)) return load(value.substring(URNS.URL.length).trim());

    // Replace JAVASCRIPT references
    if (value.startsWith(URNS.JAVASCRIPT)) return eval("(function() { return "+value.substring(URNS.JAVASCRIPT.length).trim()+"; })");

    return value;
}


//~ PUBLIC METHOD(S) ---------------------------------------------------------

ToolTip.prototype.hide = function() {
    this._postFn = null;
    this.style.display = "none";
}

ToolTip.prototype.show = function(elem /* Element */, x /* Integer */, y /* Integer */) {
    // if (text == null || text.length == 0) return;
    
    // console.log("text: "+text);
    if (this._delay == 0) this.display(elem, x, y);
    else {
        var tooltip = this;
        this._postFn = function() { tooltip.display(elem, x, y); }
    }
}

ToolTip.prototype.display = function(elem, value, x /* Integer */, y /* Integer */) {
    // console.log(this.displacement.toSource());
    // console.log(this.displacement.toSource());
	
    var win = this.ownerDocument.defaultView;
    var disp = this.displacement;
    
    if (x == null) x = win.mouseX + disp.x;
    if (y == null) y = win.mouseY + disp.y;

    // console.log("tooltip - (x,y): ("+x+","+y+")");

    // Obtain value from element
    if (value == null) value = ToolTip.getValue(elem);

    /*
    var text = elem.getAttribute(ATTRS.TOOLTIP);
    
	// Nothing to do
    if (text == null) return;
    
    // Replace ID references
    else if (text.startsWith(URNS.ID)) {
        var id = text.substring(URNS.ID.length).trim();
        text = dw(id).innerHTML;
    }

    // Replace URL references
    else if (text.startsWith(URNS.URL)) {
        var url = text.substring(URNS.URL.length).trim();
        text = load(url).innerHTML;
    }

    // Replace JAVASCRIPT references
    else if (text.startsWith(URNS.JAVASCRIPT)) {
        var js = eval("(function() { return "+text.substring(URNS.JAVASCRIPT.length).trim()+"; })");
        text = js.call(elem);
    }
    */
    
	// if (!text) return;

	// Set tooltip
	if (!ToolTip.set(this, elem, value)) return;

	// Should we move it to the right?
	// var w = parseInt(getCompStyle(this).width);
	// if (x + w > dimx) x = (dimx - w - 32);
	
	// Calculate position
	// var w = this.offsetWidth;
	// var h = this.offsetHeight;
	// if (x + w > dimx) x -= (x + w - dimx + 10);
	// if (y + h > dimy) y -= (y + h - dimy + 10);
	
    // console.log("(x,y): ("+x+","+y+")");
    // console.log("y: : "+y+", h: : "+h+", y + h: : "+(y + h)+", dim.y: "+dim.y);
	// console.log("this.offsetWidth: "+this.offsetWidth+", this.offsetHeight: "+this.offsetHeight);
	
	// Move to the right position, insert HTML and make it visible
	// this.setLocation(x, y);
    this.style.left = x+"px";
    this.style.top = y+"px";
    Event.dispatch(this, EVENTS.RESET);
    this.style.display = "block";
	
	// Get inner dimension
	var dimx = window.innerWidth || window.document.body.clientWidth;
	var dimy = window.innerHeight || window.document.body.clientHeight;
    
    // Adjust if not well placed
	// console.log("this.offsetWidth: "+this.offsetWidth);
	if (x + this.offsetWidth > dimx) {
		var left = dimx - this.offsetWidth - 16;
		this.style.left = left+"px";
	}
	// console.log("this.offsetHeight: "+this.offsetHeight);
	/*
	if (y + this.offsetHeight > window.pageYOffset + dimy) {
		var top = (window.pageYOffset + dimy - this.offsetHeight - 16);
		this.style.top = (window.pageYOffset > top? window.pageYOffset + 16 : top)+"px";
	}
	*/
}


//~ PRIVATE METHOD(S) --------------------------------------------------------

ToolTip._thread = function(tooltip) {
    // console.log("ToolTip._thread - postFn: "+tooltip._postFn);
    
	// If the mouse moved has not moved since last time around do any posted tooltips
	// console.log("last: "+JSON.encode(tooltip._last));
	// console.log("current: ("+window.mouseX+","+window.mouseY+")");
	// console.log("tooltip._postFn: "+tooltip._postFn);
	tooltip._visible = (tooltip._last.x == window.mouseX && tooltip._last.y == window.mouseY);
	if (tooltip._visible && tooltip._postFn != null) {
		tooltip._postFn.call();
		tooltip._postFn = null;
	}
    
    // Set the last seen mouse positions
	tooltip._last.x = window.mouseX;
	tooltip._last.y = window.mouseY;
    
    // Call myself in 'delay' milliseconds
    if (tooltip._delay > 0) setTimeout(function() { ToolTip._thread(tooltip); }, tooltip._delay);
}

// if (ToolTip._delay > 0) ToolTip._thread(this);


//~ HANDLER(S) ---------------------------------------------------------------

ToolTip._mouseOverHandler = function(ev /* Event */, tooltip /* HTMLDivElement */) {
    // console.log("ToolTip._mouseOverHandler - tooltip: "+tooltip);
    tooltip = dw(tooltip);

    // If the target is *inside* the tooltip do nothing
    // if (tooltip.contains(ev.target)) return;
    
    // Look for ancestor with tooltip
    var text = null;
    for (var elem = ev.target; elem.nodeType == Node.ELEMENT_NODE; elem = elem.parentNode) {
        if (elem.getAttribute(ATTRS.TOOLTIP) != null) {
            tooltip.show(elem);
            break;
        }
    }
}

ToolTip._mouseOutHandler = function(ev /* Event */, tooltip /* HTMLDivElement */) {
    // console.log("ToolTip._mouseOutHandler - tooltip: "+tooltip);
    tooltip = dw(tooltip);
    
    // If the relatedTarget is *inside* the tooltip do nothing
    if (tooltip.contains(ev.relatedTarget)) return;
    
    tooltip.hide();
}


//~ DEFAULT TOOLTIP ----------------------------------------------------------

// <div id="tooltip" class="dwToolTip" delay="500">&amp;nbsp;</div>
Event.addListener(window, EVENTS.LOAD, function() {
    // console.log("DW - Executing ToolTip event on ONLOAD");
    var dt = dw(ToolTip.DEFAULT);
    if (dt == null) dt = ToolTip.newInstance(ToolTip.DEFAULT, 250);
    ToolTip.SINGLETON = dt;
});


//~ BEHAVIOR(S) --------------------------------------------------------------

// Register tag with Behavior
Behavior.registerBehavior("dwToolTip", function(tooltip) {
    var tooltip = dw(tooltip);
    if (tooltip.id != ToolTip.DEFAULT) return;
    var win = tooltip.ownerDocument.defaultView;
    Event.addListener(win, EVENTS.MOUSEOVER, function (ev) { ToolTip._mouseOverHandler(ev, tooltip); });
    Event.addListener(win, EVENTS.MOUSEOUT, function (ev) { ToolTip._mouseOutHandler(ev, tooltip); });
});

