/***

    wForms 3.0.alpha
    a javascript extension to web forms. 

    Build $Sun, 21 Oct 2007 20:46:09 UTC$

    THIS FILE IS AUTOMATICALLY GENERATED.  If creating patches, please
    diff against the source tree, not this file.

    Copyright (c) 2005-2007 Cedric Savarese <cedric@veerwest.com> and contributors.
    This software is licensed under the CC-GNU LGPL <http://creativecommons.org/licenses/LGPL/2.1/>
    For more information, visit: http://www.formassembly.com/wForms 

    Build script by Troels Knak-Nielsen <troelskn@gmail.com>
    wForms version 3.0 by Demid Nikitin.
    wForms 3.0 uses base2 - copyright 2007, Dean Edwards 
***/

// timestamp: Fri, 10 Aug 2007 20:00:50
/*
  base2 - copyright 2007, Dean Edwards
  http://code.google.com/p/base2/
  http://www.opensource.org/licenses/mit-license
  
  Contributors:
    Doeke Zanstra
*/

var base2 = {
  name:    "base2",
  version: "0.9 (alpha)",
  exports:
    "Base, Namespace, Abstract, Module, Enumerable, Hash, Collection, RegGrp, " +
    "Array2, Date2, String2, " +
    "assert, assertArity, assertType, " +
    "assignID, copy, detect, extend, forEach, format, instanceOf, match, rescape, slice, trim, " +
    "I, K, Undefined, Null, True, False, bind, delegate, flip, not, partial, returns, unbind",
  
  global: this, // the window object in a browser environment
  namespace: "var global=base2.global;function base(o,a){return o.base.apply(o,a)};",
    
  // this is defined here because it must be defined in the global scope
  detect: new function(_) {  
    // Two types of detection:
    //  1. Object detection
    //     e.g. detect("(java)");
    //     e.g. detect("!(document.addEventListener)");
    //  2. Platform detection (browser sniffing)
    //     e.g. detect("MSIE");
    //     e.g. detect("MSIE|opera");
        
    var global = _;
    var jscript/*@cc_on=@_jscript_version@*/; // http://dean.edwards.name/weblog/2007/03/sniff/#comment85164
    var java = _.java;
    
    if (_.navigator) {
      var element = document.createElement("span");
      var platform = navigator.platform + " " + navigator.userAgent;
      // Fix opera's (and others) user agent string.
      if (!jscript) platform = platform.replace(/MSIE\s[\d.]+/, "");
      // Close up the space between name and version number.
      //  e.g. MSIE 6 -> MSIE6
      platform = platform.replace(/([a-z])[\s\/](\d)/gi, "$1$2");
      java = navigator.javaEnabled() && java;
    }
    
    return function(test) {
      var r = false;
      var not = test.charAt(0) == "!";
      if (not) test = test.slice(1);
      test = test.replace(/^([^\(].*)$/, "/($1)/i.test(platform)");
      try {
        eval("r=!!" + test);
      } catch (error) {
        // the test failed
      }
      return !!(not ^ r);
    };
  }(this)
};

new function(_) { ////////////////////  BEGIN: CLOSURE  ////////////////////

// =========================================================================
// base2/lang/header.js
// =========================================================================

var detect = base2.detect;
var slice = Array.slice || function(array) {
  // Slice an array-like object.
  return _slice.apply(array, _slice.call(arguments, 1));
};

var Undefined = K(), Null = K(null), True = K(true), False = K(false);

// private
var _ID = 1;
var _PRIVATE = /^[_$]/; //-dean: get rid of this?
var _FORMAT = /%([1-9])/g;
var _LTRIM = /^\s\s*/;
var _RTRIM = /\s\s*$/;
var _RESCAPE = /([\/()[\]{}|*+-.,^$?\\])/g;           // safe regular expressions
var _BASE = /eval/.test(detect) ? /\bbase\b/ : /./;   // some platforms don't allow decompilation
var _HIDDEN = ["constructor", "toString", "valueOf"]; // only override these when prototyping
var _REGEXP_STRING = String(new RegExp);
var _slice = Array.prototype.slice;
var _Function_forEach = _get_Function_forEach();      // curse you Safari!

eval(base2.namespace);

// =========================================================================
// base2/Base.js
// =========================================================================

// http://dean.edwards.name/weblog/2006/03/base/

var _subclass = function(_instance, _static) {
  // Build the prototype.
  base2.__prototyping = true;
  var _prototype = new this;
  extend(_prototype, _instance);
  delete base2.__prototyping;
  
  // Create the wrapper for the constructor function.
  var _constructor = _prototype.constructor;
  function klass() {
    // Don't call the constructor function when prototyping.
    if (!base2.__prototyping) {
      if (this.constructor == arguments.callee || this.__constructing) {
        // Instantiation.
        this.__constructing = true;
        _constructor.apply(this, arguments);
        delete this.__constructing;
      } else {
        // Cast.
        return extend(arguments[0], _prototype);
      }
    }
  };
  _prototype.constructor = klass;
  
  // Build the static interface.
  for (var i in Base) klass[i] = this[i];
  klass.ancestor = this;
  klass.base = Undefined;
  klass.init = Undefined;
  klass.prototype = _prototype;
  extend(klass, _static);
  klass.init();
  
  // reflection (removed when packed)
  ;;; klass["#implements"] = [];
  ;;; klass["#implemented_by"] = [];
  
  return klass;
};

var Base = _subclass.call(Object, {
  constructor: function() {
    if (arguments.length > 0) {
      this.extend(arguments[0]);
    }
  },
  
  base: function() {
    // Call this method from any other method to invoke the current method's ancestor (super).
  },
  
  extend: delegate(extend)  
}, Base = {
  ancestorOf: delegate(_ancestorOf),
  
  extend: _subclass,
    
  forEach: delegate(_Function_forEach),
  
  implement: function(source) {
    if (instanceOf(source, Function)) {
      // If we are implementing another classs/module then we can use
      // casting to apply the interface.
      if (Base.ancestorOf(source)) {
        source(this.prototype); // cast
        // reflection (removed when packed)
        ;;; this["#implements"].push(source);
        ;;; source["#implemented_by"].push(this);
      }
    } else {
      // Add the interface using the extend() function.
      extend(this.prototype, source);
    }
    return this;
  }
});

// =========================================================================
// base2/Namespace.js
// =========================================================================

var Namespace = Base.extend({
  constructor: function(_private, _public) {
    this.extend(_public);
    
    // Initialise.
    if (typeof this.init == "function") this.init();
    
    if (this.name != "base2") {
      base2.addName(this.name, this);
      this.namespace = format("var %1=base2.%1;", this.name);
    }
    
    var LIST = /[^\s,]+/g; // pattern for comma separated list
    
    // This string should be evaluated immediately after creating a Namespace object.
    _private.imports = Array2.reduce(this.imports.match(LIST), function(namespace, name) {
      assert(base2[name], format("Namespace not found: '%1'.", name));
      return namespace += base2[name].namespace;
    }, base2.namespace);
    
    // This string should be evaluated after you have created all of the objects
    // that are being exported.
    _private.exports = Array2.reduce(this.exports.match(LIST), function(namespace, name) {
      this.namespace += format("var %2=%1.%2;", this.name, name);
      return namespace += format("if(!%1.%2)%1.%2=%2;", this.name, name);
    }, "", this);
  },

  exports: "",
  imports: "",
  namespace: "",
  name: "",
  
  addName: function(name, value) {
    this[name] = value;
    this.exports += ", " + name;
    this.namespace += format("var %1=%2.%1;", name, this.name);
  }
});

// =========================================================================
// base2/Abstract.js
// =========================================================================

var Abstract = Base.extend({
  constructor: function() {
    throw new TypeError("Class cannot be instantiated.");
  }
});

// =========================================================================
// base2/Module.js
// =========================================================================

var Module = Abstract.extend(null, {
  extend: function(_interface, _static) {
    // Extend a module to create a new module.
    var module = this.base();
    // Inherit class methods.
    forEach (this, function(method, name) {
      if (!Module[name] && instanceOf(method, Function) && !_PRIVATE.test(name)) {
        extend(module, name, method);
      }
    });
    // Implement module (instance AND static) methods.
    module.implement(_interface);
    // Implement static properties and methods.
    extend(module, _static);
    // Make the submarine noises Larry!
    module.init();
    return module;
  },
  
  implement: function(_interface) {
    // Implement an interface on BOTH the instance and static interfaces.
    var module = this;
    if (typeof _interface == "function") {
      module.base(_interface);
      // If we are implementing another Module then add its static methods.
      if (Module.ancestorOf(_interface)) {
        forEach (_interface, function(method, name) {
          if (!Module[name] && instanceOf(method, Function) && !_PRIVATE.test(name)) {
            extend(module, name, method);
          }
        });
      }
    } else {
      // Create the instance interface.
      _Function_forEach (Object, _interface, function(source, name) {
        if (name.charAt(0) == "@") { // object detection
          if (detect(name.slice(1))) {
            forEach (source, arguments.callee);
          }
        } else if (!Module[name] && instanceOf(source, Function)) {
          function _module() { // Late binding.
            return module[name].apply(module, [this].concat(slice(arguments)));
          };
          _module._module = module;
          _module._base = _BASE.test(source);
          extend(module.prototype, name, _module);
        }
      });
      // Add the static interface.
      extend(module, _interface);
    }
    return module;
  }
});

// =========================================================================
// base2/Enumerable.js
// =========================================================================

var Enumerable = Module.extend({
  every: function(object, test, context) {
    var result = true;
    try {
      this.forEach (object, function(value, key) {
        result = test.call(context, value, key, object);
        if (!result) throw StopIteration;
      });
    } catch (error) {
      if (error != StopIteration) throw error;
    }
    return !!result; // cast to boolean
  },
  
  filter: function(object, test, context) {
    var i = 0;
    return this.reduce(object, function(result, value, key) {
      if (test.call(context, value, key, object)) {
        result[i++] = value;
      }
      return result;
    }, []);
  },
  
  invoke: function(object, method) {
    // Apply a method to each item in the enumerated object.
    var args = slice(arguments, 2);
    return this.map(object, (typeof method == "function") ? function(item) {
      if (item != null) return method.apply(item, args);
    } : function(item) {
      if (item != null) return item[method].apply(item, args);
    });
  },
  
  map: function(object, block, context) {
    var result = [], i = 0;
    this.forEach (object, function(value, key) {
      result[i++] = block.call(context, value, key, object);
    });
    return result;
  },
  
  pluck: function(object, key) {
    return this.map(object, function(item) {
      if (item != null) return item[key];
    });
  },
  
  reduce: function(object, block, result, context) {
    var initialised = arguments.length > 2;
    this.forEach (object, function(value, key) {
      if (initialised) { 
        result = block.call(context, result, value, key, object);
      } else { 
        result = value;
        initialised = true;
      }
    });
    return result;
  },
  
  some: function(object, test, context) {
    return !this.every(object, not(test), context);
  }
}, {
  forEach: forEach
});

// =========================================================================
// base2/Hash.js
// =========================================================================

var _HASH = "#";

var Hash = Base.extend({
  constructor: function(values) {
    this.merge(values);
  },

  copy: delegate(copy),

  // Ancient browsers throw an error when we use "in" as an operator.
  exists: function(key) {
    /*@cc_on @*/
    /*@if (@_jscript_version < 5.5)
      return $Legacy.exists(this, _HASH + key);
    @else @*/
      return _HASH + key in this;
    /*@end @*/
  },

  fetch: function(key) {
    return this[_HASH + key];
  },

  forEach: function(block, context) {
    for (var key in this) if (key.charAt(0) == _HASH) {
      block.call(context, this[key], key.slice(1), this);
    }
  },

  merge: function(values) {
    var store = flip(this.store);
    forEach (arguments, function(values) {
      forEach (values, store, this);
    }, this);
    return this;
  },

  remove: function(key) {
    var value = this[_HASH + key];
    delete this[_HASH + key];
    return value;
  },

  store: function(key, value) {
    if (arguments.length == 1) value = key;
    // Create the new entry (or overwrite the old entry).
    return this[_HASH + key] = value;
  },

  union: function(values) {
    return this.merge.apply(this.copy(), arguments);
  }
});

Hash.implement(Enumerable);

// =========================================================================
// base2/Collection.js
// =========================================================================

// A Hash that is more array-like (accessible by index).

// Collection classes have a special (optional) property: Item
// The Item property points to a constructor function.
// Members of the collection must be an instance of Item.

// The static create() method is responsible for all construction of collection items.
// Instance methods that add new items (add, store, insertAt, storeAt) pass *all* of their arguments
// to the static create() method. If you want to modify the way collection items are 
// created then you only need to override this method for custom collections.

var _KEYS = "~";

var Collection = Hash.extend({
  constructor: function(values) {
    this[_KEYS] = new Array2;
    this.base(values);
  },
  
  add: function(key, item) {
    // Duplicates not allowed using add().
    // But you can still overwrite entries using store().
    assert(!this.exists(key), "Duplicate key '" + key + "'.");
    return this.store.apply(this, arguments);
  },

  copy: function() {
    var copy = this.base();
    copy[_KEYS] = this[_KEYS].copy();
    return copy;
  },

  count: function() {
    return this[_KEYS].length;
  },

  fetchAt: function(index) { // optimised (refers to _HASH)
    if (index < 0) index += this[_KEYS].length; // starting from the end
    var key = this[_KEYS][index];
    if (key !== undefined) return this[_HASH + key];
  },

  forEach: function(block, context) { // optimised (refers to _HASH)
    var keys = this[_KEYS];
    var length = keys.length;
    for (var i = 0; i < length; i++) {
      block.call(context, this[_HASH + keys[i]], keys[i], this);
    }
  },

  indexOf: function(key) {
    return this[_KEYS].indexOf(String(key));
  },

  insertAt: function(index, key, item) {
    assert(Math.abs(index) < this[_KEYS].length, "Index out of bounds.");
    assert(!this.exists(key), "Duplicate key '" + key + "'.");
    this[_KEYS].insertAt(index, String(key));
    return this.store.apply(this, arguments);
  },
  
  item: Undefined, // alias of fetchAt (defined when the class is initialised)

  keys: function(index, length) {
    switch (arguments.length) {
      case 0:  return this[_KEYS].copy();
      case 1:  return this[_KEYS].item(index);
      default: return this[_KEYS].slice(index, length);
    }
  },

  remove: function(key) {
    // The remove() method of the Array object can be slow so check if the key exists first.
    var keyDeleted = arguments[1];
    if (keyDeleted || this.exists(key)) {
      if (!keyDeleted) {                   // The key has already been deleted by removeAt().
        this[_KEYS].remove(String(key));   // We still have to delete the value though.
      }
      return this.base(key);
    }
  },

  removeAt: function(index) {
    var key = this[_KEYS].removeAt(index);
    if (key !== undefined) return this.remove(key, true);
  },

  reverse: function() {
    this[_KEYS].reverse();
    return this;
  },

  sort: function(compare) { // optimised (refers to _HASH)
    if (compare) {
      var self = this;
      this[_KEYS].sort(function(key1, key2) {
        return compare(self[_HASH + key1], self[_HASH + key2], key1, key2);
      });
    } else this[_KEYS].sort();
    return this;
  },

  store: function(key, item) {
    if (arguments.length == 1) item = key;
    if (!this.exists(key)) {
      this[_KEYS].push(String(key));
    }
    var klass = this.constructor;
    if (klass.Item && !instanceOf(item, klass.Item)) {
      item = klass.create.apply(klass, arguments);
    }
    return this[_HASH + key] = item;
  },

  storeAt: function(index, item) {
    assert(Math.abs(index) < this[_KEYS].length, "Index out of bounds.");
    arguments[0] = this[_KEYS].item(index);
    return this.store.apply(this, arguments);
  },

  toString: function() {
    return String(this[_KEYS]);
  }
}, {
  Item: null, // If specified, all members of the collection must be instances of Item.
  
  init: function() {
    this.prototype.item = this.prototype.fetchAt;
  },
  
  create: function(key, item) {
    if (this.Item) return new this.Item(key, item);
  },
  
  extend: function(_instance, _static) {
    var klass = this.base(_instance);
    klass.create = this.create;
    extend(klass, _static);
    if (!klass.Item) {
      klass.Item = this.Item;
    } else if (typeof klass.Item != "function") {
      klass.Item = (this.Item || Base).extend(klass.Item);
    }
    klass.init();
    return klass;
  }
});

// =========================================================================
// base2/RegGrp.js
// =========================================================================

// A collection of regular expressions and their associated replacement values.
// A Base class for creating parsers.

var _RG_BACK_REF        = /\\(\d+)/g;
var _RG_ESCAPE_CHARS    = /\\./g;
var _RG_ESCAPE_BRACKETS = /\(\?[:=!]|\[[^\]]+\]/g;
var _RG_BRACKETS        = /\(/g;

var RegGrp = Collection.extend({
  constructor: function(values, flags) {
    this.base(values);
    if (typeof flags == "string") {
      this.global = /g/.test(flags);
      this.ignoreCase = /i/.test(flags);
    }
  },

  global: true, // global is the default setting
  ignoreCase: false,

  exec: function(string, replacement) { // optimised (refers to _HASH/_KEYS)
    string += ''; // type-safe
    if (arguments.length == 1) {
      var self = this;
      var keys = this[_KEYS];
      replacement = function(match) {
        if (!match) return "";
        var item, offset = 1, i = 0;
        // Loop through the RegGrp items.
        while (item = self[_HASH + keys[i++]]) {
          var next = offset + item.length + 1;
          if (arguments[offset]) { // do we have a result?
            var replacement = item.replacement;
            switch (typeof replacement) {
              case "function":
                var args = slice(arguments, offset, next);
                var index = arguments[arguments.length - 2];
                return replacement.apply(self, args.concat(index, string));
              case "number":
                return arguments[offset + replacement];
              default:
                return replacement;
            }
          }
          offset = next;
        }
      };
    }
    return string.replace(this.valueOf(), replacement);
  },

  test: function(string) {
    return this.exec(string) != string;
  },
  
  toString: function() {
    var length = 0;
    return "(" + this.map(function(item) {
      // Fix back references.
      var ref = String(item).replace(_RG_BACK_REF, function(match, index) {
        return "\\" + (1 + Number(index) + length);
      });
      length += item.length + 1;
      return ref;
    }).join(")|(") + ")";
  },
  
  valueOf: function(type) {
    if (type == "object") return this;
    var flags = (this.global ? "g" : "") + (this.ignoreCase ? "i" : "");
    return new RegExp(this, flags);
  }
}, {
  IGNORE: "$0",
  
  init: function() {
    forEach ("add,exists,fetch,remove,store".split(","), function(name) {
      extend(this, name, function(expression) {
        if (instanceOf(expression, RegExp)) {
          expression = expression.source;
        }
        return base(this, arguments);
      });
    }, this.prototype);
  },
  
  Item: {
    constructor: function(expression, replacement) {
      expression = instanceOf(expression, RegExp) ? expression.source : String(expression);
      
      if (typeof replacement == "number") replacement = String(replacement);
      else if (replacement == null) replacement = "";    
      
      // does the pattern use sub-expressions?
      if (typeof replacement == "string" && /\$(\d+)/.test(replacement)) {
        // a simple lookup? (e.g. "$2")
        if (/^\$\d+$/.test(replacement)) {
          // store the index (used for fast retrieval of matched strings)
          replacement = parseInt(replacement.slice(1));
        } else { // a complicated lookup (e.g. "Hello $2 $1")
          // build a function to do the lookup
          var Q = /'/.test(replacement.replace(/\\./g, "")) ? '"' : "'";
          replacement = replacement.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\$(\d+)/g, Q +
            "+(arguments[$1]||" + Q+Q + ")+" + Q);
          replacement = new Function("return " + Q + replacement.replace(/(['"])\1\+(.*)\+\1\1$/, "$1") + Q);
        }
      }
      
      this.length = RegGrp.count(expression);
      this.replacement = replacement;
      this.toString = returns(expression);
    },
    
    length: 0,
    replacement: ""
  },
  
  count: function(expression) {
    // Count the number of sub-expressions in a RegExp/RegGrp.Item.
    expression = String(expression).replace(_RG_ESCAPE_CHARS, "").replace(_RG_ESCAPE_BRACKETS, "");
    return match(expression, _RG_BRACKETS).length;
  }
});

// =========================================================================
// JavaScript/~/Function.js
// =========================================================================

// some browsers don't define this

Function.prototype.prototype = {};

// =========================================================================
// JavaScript/~/String.js
// =========================================================================

// fix String.replace (Safari/IE5.0)
if ("".replace(/^/, String)) {
  var _GLOBAL = /(g|gi)$/;
  extend(String.prototype, "replace", function(expression, replacement) {
    if (typeof replacement == "function") { // Safari doesn't like functions
      if (instanceOf(expression, RegExp)) {
        var regexp = expression;
        var global = regexp.global;
        if (global == null) global = _GLOBAL.test(regexp);
        // we have to convert global RexpExps for exec() to work consistently
        if (global) regexp = new RegExp(regexp.source); // non-global
      } else {
        regexp = new RegExp(rescape(expression));
      }
      var match, string = this, result = "";
      while (string && (match = regexp.exec(string))) {
        result += string.slice(0, match.index) + replacement.apply(this, match);
        string = string.slice(match.index + match[0].length);
        if (!global) break;
      }
      return result + string;
    }
    return this.base(expression, replacement);
  });
}

// =========================================================================
// JavaScript/Array2.js
// =========================================================================

var Array2 = _createObject2(
  Array,
  "concat,join,pop,push,reverse,shift,slice,sort,splice,unshift", // generics
  [Enumerable, {
    combine: function(keys, values) {
      // Combine two arrays to make a hash.
      if (!values) values = keys;
      return this.reduce(keys, function(object, key, index) {
        object[key] = values[index];
        return object;
      }, {});
    },

    contains: function(array, item) {
      return this.indexOf(array, item) != -1;
    },

    copy: function(array) {
      var copy = this.slice(array);
      if (!copy.swap) this(copy);  // cast to Array2
      return copy;
    },

    flatten: function(array) {
      var i = 0;
      return this.reduce(array, function(result, item) {
        if (this.like(item)) {
          this.reduce(item, arguments.callee, result, this);
        } else {
          result[i++] = item;
        }
        return result;
      }, [], this);
    },
    
    forEach: _Array_forEach,
    
    indexOf: function(array, item, fromIndex) {
      var length = array.length;
      if (fromIndex == null) {
        fromIndex = 0;
      } else if (fromIndex < 0) {
        fromIndex = Math.max(0, length + fromIndex);
      }
      for (var i = fromIndex; i < length; i++) {
        if (array[i] === item) return i;
      }
      return -1;
    },
    
    insertAt: function(array, item, index) {
      this.splice(array, index, 0, item);
      return item;
    },
    
    item: function(array, index) {
      if (index < 0) index += array.length; // starting from the end
      return array[index];
    },
    
    lastIndexOf: function(array, item, fromIndex) {
      var length = array.length;
      if (fromIndex == null) {
        fromIndex = length - 1;
      } else if (from < 0) {
        fromIndex = Math.max(0, length + fromIndex);
      }
      for (var i = fromIndex; i >= 0; i--) {
        if (array[i] === item) return i;
      }
      return -1;
    },
  
    map: function(array, block, context) {
      var result = [];
      this.forEach (array, function(item, index) {
        result[index] = block.call(context, item, index, array);
      });
      return result;
    },
    
    remove: function(array, item) {
      var index = this.indexOf(array, item);
      if (index != -1) this.removeAt(array, index);
      return item;
    },

    removeAt: function(array, index) {
      return this.splice(array, index, 1);
    },

    swap: function(array, index1, index2) {
      var temp = array[index1];
      array[index1] = array[index2];
      array[index2] = temp;
      return array;
    }
  }]
);

Array2.reduce = Enumerable.reduce; // Mozilla does not implement the thisObj argument
Array2.prototype.forEach = delegate(_Array_forEach);

Array2.like = function(object) {
  // is the object like an array?
  return !!(object && typeof object == "object" && typeof object.length == "number");
};

// =========================================================================
// JavaScript/Date2.js
// =========================================================================

// http://developer.mozilla.org/es4/proposals/date_and_time.html

// big, ugly, regular expression
var _DATE_PATTERN = /^((-\d+|\d{4,})(-(\d{2})(-(\d{2}))?)?)?T((\d{2})(:(\d{2})(:(\d{2})(\.(\d{1,3})(\d)?\d*)?)?)?)?(([+-])(\d{2})(:(\d{2}))?|Z)?$/;  
var _DATE_PARTS = { // indexes to the sub-expressions of the RegExp above
  FullYear: 2,
  Month: 4,
  Date: 6,
  Hours: 8,
  Minutes: 10,
  Seconds: 12,
  Milliseconds: 14
};
var _TIMEZONE_PARTS = { // idem, but without the getter/setter usage on Date object
  Hectomicroseconds: 15, // :-P
  UTC: 16,
  Sign: 17,
  Hours: 18,
  Minutes: 20
};

var _TRIM_ZEROES   = /(((00)?:0+)?:0+)?\.0+$/;
var _TRIM_TIMEZONE = /(T[0-9:.]+)$/;

var Date2 = _createObject2(
  Date, "", [{
    toISOString: function(date) {
      var string = "####-##-##T##:##:##.###";
      for (var part in _DATE_PARTS) {
        string = string.replace(/#+/, function(digits) {
          var value = date["getUTC" + part]();
          if (part == "Month") value++; // js month starts at zero
          return ("000" + value).slice(-digits.length); // pad
        });
      }
      // remove trailing zeroes, and remove UTC timezone, when time's absent
      return string.replace(_TRIM_ZEROES, "").replace(_TRIM_TIMEZONE, "$1Z");
    }
  }]
);

Date2.now = function() {
  return (new Date).valueOf(); // milliseconds since the epoch
};

Date2.parse = function(string, defaultDate) {
  if (arguments.length > 1) {
    assertType(defaultDate, "number", "defaultDate should be of type 'number'.")
  }
  // parse ISO date
  var match = String(string).match(_DATE_PATTERN);
  if (match) {
    if (match[_DATE_PARTS.Month]) match[_DATE_PARTS.Month]--; // js months start at zero
    // round milliseconds on 3 digits
    if (match[_TIMEZONE_PARTS.Hectomicroseconds] >= 5) match[_DATE_PARTS.Milliseconds]++;
    var date = new Date(defaultDate || 0);
    var prefix = match[_TIMEZONE_PARTS.UTC] || match[_TIMEZONE_PARTS.Hours] ? "UTC" : "";
    for (var part in _DATE_PARTS) {
      var value = match[_DATE_PARTS[part]];
      if (!value) continue; // empty value
      // set a date part
      date["set" + prefix + part](value);
      // make sure that this setting does not overflow
      if (date["get" + prefix + part]() != match[_DATE_PARTS[part]]) {
        return new Date(NaN)
      }
    }
    // timezone can be set, without time being available
    // without a timezone, local timezone is respected
    if (match[_TIMEZONE_PARTS.Hours]) {
      var Hours = Number(match[_TIMEZONE_PARTS.Sign] + match[_TIMEZONE_PARTS.Hours]);
      var Minutes = Number(match[_TIMEZONE_PARTS.Sign] + (match[_TIMEZONE_PARTS.Minutes] || 0));
      date.setUTCMinutes(date.getUTCMinutes() + (Hours * 60) + Minutes);
    } 
    return date.valueOf();
  } else {
    return Date.parse(string);
  }
};

// =========================================================================
// JavaScript/String2.js
// =========================================================================

var String2 = _createObject2(
  String,
  "charAt,charCodeAt,concat,indexOf,lastIndexOf,match,replace,search,slice,split,substr,substring,toLowerCase,toUpperCase",
  [{trim: trim}]
);

// =========================================================================
// JavaScript/functions.js
// =========================================================================

function _createObject2(Native, generics, extensions) {
  // Clone native objects and extend them.
  
  // Create a Module that will contain all the new methods.
  var INative = Module.extend();
  // http://developer.mozilla.org/en/docs/New_in_JavaScript_1.6#Array_and_String_generics
  forEach (generics.split(","), function(name) {
    INative[name] = unbind(Native.prototype[name]);
  });
  forEach (extensions, INative.implement, INative);
  
  // create a faux constructor that augments the native object
  var Native2 = function() {
    if (arguments[0] == Native.prototype) { // casting
      extend(Native, Native2);
    }
    return INative(this.constructor == INative ? Native.apply(Native, arguments) : arguments[0]);
  };
  Native2.prototype = INative.prototype;
  
  // Remove methods that are already implemented.
  forEach (INative, function(method, name) {
    if (Native[name]) {
      INative[name] = Native[name];
      delete INative.prototype[name];
    }
    Native2[name] = INative[name];
  });
  Native2.ancestor = Object;
  delete Native2.extend;
  
  return Native2;
};

// =========================================================================
// lang/extend.js
// =========================================================================

function extend(object, source) { // or extend(object, key, value)
  var extend = arguments.callee;
  if (object != null) {
    if (arguments.length > 2) { // Extending with a key/value pair.
      var key = String(source);
      var value = arguments[2];
      // Object detection.
      if (key.charAt(0) == "@") {
        return detect(key.slice(1)) ? extend(object, value) : object;
      }
      // Protect certain objects.
      if (object.extend == extend && /^(base|extend)$/.test(key)) {
        return object;
      }
      // Check for method overriding.
      var ancestor = object[key];
      if (ancestor && instanceOf(value, Function)) {
        if (value != ancestor && !_ancestorOf(value, ancestor)) {
          if (value._base || _BASE.test(value)) {
            // Override the existing method.
            var method = value;
            function _base() {
              var previous = this.base;
              this.base = ancestor;
              var returnValue = method.apply(this, arguments);
              this.base = previous;
              return returnValue;
            };
            value = _base;
            value.method = method;
            value.ancestor = ancestor;
          }
          object[key] = value;
        }
      } else {
        object[key] = value;
      }
    } else if (source) { // Extending with an object literal.
      var Type = instanceOf(source, Function) ? Function : Object;
      if (base2.__prototyping) {
        // Add constructor, toString etc if we are prototyping.
        forEach (_HIDDEN, function(key) {
          if (source[key] != Type.prototype[key]) {
            extend(object, key, source[key]);
          }
        });
      } else {
        // Does the target object have a custom extend() method?
        if (typeof object.extend == "function" && typeof object != "function" && object.extend != extend) {
          extend = unbind(object.extend);
        }
      }
      // Copy each of the source object's properties to the target object.
      _Function_forEach (Type, source, function(value, key) {
        extend(object, key, value);
      });
    }
  }
  return object;
};

function _ancestorOf(ancestor, fn) {
  // Check if a function is in another function's inheritance chain.
  while (fn && fn.ancestor != ancestor) fn = fn.ancestor;
  return !!fn;
};

// =========================================================================
// lang/forEach.js
// =========================================================================

// http://dean.edwards.name/weblog/2006/07/enum/

if (typeof StopIteration == "undefined") {
  StopIteration = new Error("StopIteration");
}

function forEach(object, block, context, fn) {
  if (object == null) return;
  if (!fn) {
    if (instanceOf(object, Function)) {
      // Functions are a special case.
      fn = Function;
    } else if (typeof object.forEach == "function" && object.forEach != arguments.callee) {
      // The object implements a custom forEach method.
      object.forEach(block, context);
      return;
    } else if (typeof object.length == "number") {
      // The object is array-like.
      _Array_forEach(object, block, context);
      return;
    }
  }
  _Function_forEach(fn || Object, object, block, context);
};

// These are the two core enumeration methods. All other forEach methods
//  eventually call one of these two.

function _Array_forEach(array, block, context) {
  if (array == null) return;
  var length = array.length, i; // preserve length
  if (typeof array == "string") {
    for (i = 0; i < length; i++) {
      block.call(context, array.charAt(i), i, array);
    }
  } else {
    // Cater for sparse arrays.
    for (i = 0; i < length; i++) {    
      // Ignore undefined values. This is contrary to standard behaviour
      //  but it's what Internet Explorer does. We want consistent behaviour
      //  so we do this on all platforms.
      if (array[i] !== undefined) {
        block.call(context, array[i], i, array);
      }
    }
  }
};

function _get_Function_forEach() {
  // http://code.google.com/p/base2/issues/detail?id=10
  
  // run the test for Safari's buggy enumeration
  var Temp = function(){this.i=1};
  Temp.prototype = {i:1};
  var count = 0;
  for (var i in new Temp) count++;
  
  return (count > 1) ? function(fn, object, block, context) {
    ///////////////////////////////////////
    //    Safari fix (pre version 3)     //
    ///////////////////////////////////////    
    var processed = {};
    for (var key in object) {
      if (!processed[key] && fn.prototype[key] === undefined) {
        processed[key] = true;
        block.call(context, object[key], key, object);
      }
    }
  } : function(fn, object, block, context) {
    // Enumerate an object and compare its keys with fn's prototype.
    for (var key in object) {
      if (fn.prototype[key] === undefined) {
        block.call(context, object[key], key, object);
      }
    }
  };
};

// =========================================================================
// lang/instanceOf.js
// =========================================================================

function instanceOf(object, klass) {
  // Handle exceptions where the target object originates from another frame.
  // This is handy for JSON parsing (amongst other things).
  
  assertType(klass, "function", "Invalid 'instanceOf' operand.");
  
  /*@cc_on @*/
  /*@if (@_jscript_version < 5.1)
    if ($Legacy.instanceOf(object, klass)) return true;
  @else @*/
    if (object instanceof klass) return true;
  /*@end @*/

  if (object == null) return false;

  // If the class is a Base class then it would have passed the test above.
  if (_isBaseClass(klass)) return false;

  // Base objects can only be instances of Object.
  if (_isBaseClass(object.constructor)) return klass == Object;
  
  switch (klass) {
    case Array: // This is the only troublesome one.
      return !!(typeof object == "object" && object.join && object.splice);
    case Function:
      return !!(typeof object == "function" && object.call);
    case RegExp:
      return object.constructor.prototype.toString() == _REGEXP_STRING;
    case Date:
      return !!object.getTimezoneOffset;
    case String:
    case Number:  // These are bullet-proof.
    case Boolean:
      return typeof object == typeof klass.prototype.valueOf();
    case Object:
      // Only JavaScript objects allowed.
      // COM objects do not have a constructor.
      return typeof object == "object" && typeof object.constructor == "function";
  }
  return false;
};

function _isBaseClass(klass) {
  return klass == Base || _ancestorOf(Base, klass);
};

// =========================================================================
// lang/assert.js
// =========================================================================

function assert(condition, message, Err) {
  if (!condition) {
    throw new (Err || Error)(message || "Assertion failed.");
  }
};

function assertArity(args, arity, message) {
  if (arity == null) arity = args.callee.length;
  if (args.length < arity) {
    throw new SyntaxError(message || "Not enough arguments.");
  }
};

function assertType(object, type, message) {
  if (type && (typeof type == "function" ? !instanceOf(object, type) : typeof object != type)) {
    throw new TypeError(message || "Invalid type.");
  }
};

// =========================================================================
// lang/core.js
// =========================================================================

function assignID(object) {
  // Assign a unique ID to an object.
  if (!object.base2ID) object.base2ID = "b2_" + _ID++;
  return object.base2ID;
};

function copy(object) {
  var fn = function(){};
  fn.prototype = object;
  return new fn;
};

// String/RegExp.

function format(string) {
  // Replace %n with arguments[n].
  // e.g. format("%1 %2%3 %2a %1%3", "she", "se", "lls");
  // ==> "she sells sea shells"
  // Only %1 - %9 supported.
  var args = arguments;
  var _FORMAT = new RegExp("%([1-" + arguments.length + "])", "g");
  return String(string).replace(_FORMAT, function(match, index) {
    return index < args.length ? args[index] : match;
  });
};

function match(string, expression) {
  // Same as String.match() except that this function will return an empty 
  // array if there is no match.
  return String(string).match(expression) || [];
};

function rescape(string) {
  // Make a string safe for creating a RegExp.
  return String(string).replace(_RESCAPE, "\\$1");
};

// http://blog.stevenlevithan.com/archives/faster-trim-javascript
function trim(string) {
  return String(string).replace(_LTRIM, "").replace(_RTRIM, "");
};

// =========================================================================
// lang/functional.js
// =========================================================================

function I(i) {
    return i;
};

function K(k) {
  return function() {
    return k;
  };
};

var returns = K; // alias of K

function bind(fn, context) {
  var args = slice(arguments, 2);
  var bound = function() {
    return fn.apply(context, args.concat(slice(arguments)));
  };
  bound._cloneID = assignID(fn);
  return bound;
};

function delegate(fn, context) {
  return function() {
    //Array2.unshift(arguments, this);
    //return fn.apply(context, arguments);
    return fn.apply(context, [this].concat(slice(arguments)));
  };
};

function flip(fn) {
  return function() {
    return fn.apply(this, Array2.swap(arguments, 0, 1));
  };
};

function not(fn) {
  return function() {
    return !fn.apply(this, arguments);
  };
};

function partial(fn) {
  var args = slice.call(arguments, 1);
  return function() {
      return fn.apply(this, args.concat(slice(arguments)));
  };
};

function unbind(fn) {
  return function(context) {
    return fn.apply(context, slice(arguments, 1));
  };
};

// =========================================================================
// base2/init.js
// =========================================================================

base2 = new Namespace(this, base2);
eval(this.exports);

base2.extend = extend;

forEach (Enumerable, function(method, name) {
  if (!Module[name]) base2.addName(name, bind(method, Enumerable));
});

// =========================================================================
// base2/utils/namespace.js
// =========================================================================

var utils = new base2.Namespace(this, {name: "utils"});

}; ////////////////////  END: CLOSURE  /////////////////////////////////////

// timestamp: Tue, 01 May 2007 19:13:00

new function(_) { ////////////////////  BEGIN: CLOSURE  ////////////////////

// =========================================================================
// BOM/object.js
// =========================================================================

// browser specific code

var element = document.createElement("span");
var jscript/*@cc_on=@_jscript_version@*/; // http://dean.edwards.name/weblog/2007/03/sniff/#comment85164

var BOM = {
	userAgent: "",

	init: function() {
		var MSIE/*@cc_on=true@*/;
		// initialise the user agent string
		var userAgent = navigator.userAgent;
		// fix opera's (and others) user agent string
		if (!MSIE) userAgent = userAgent.replace(/MSIE\s[\d.]+/, "");
		// close up the space between name and version number
		//  e.g. MSIE 6 -> MSIE6
		userAgent = userAgent.replace(/([a-z])[\s\/](\d)/gi, "$1$2");
		this.userAgent = navigator.platform + " " + userAgent;
	},

	detect: function(test) {
		var r = false;
		var not = test.charAt(0) == "!";
		test = test
			.replace(/^\!?(if\s*|platform\s+)?/, "")
			.replace(/^(["']?)([^\(].*)(\1)$/, "/($2)/i.test(BOM.userAgent)");
		try {
			eval("r=!!" + test);
		} catch (error) {
			// the test failed
		}
		return Boolean(not ^ r);
	}
};

// =========================================================================
// BOM/namespace.js
// =========================================================================

// browser specific code
base2.extend(BOM, {
	name:    "BOM",
	version: "0.9",
	exports: "detect,Window"
});
BOM = new base2.Namespace(this, BOM);

eval(this.imports);

// =========================================================================
// BOM/Base.js
// =========================================================================

var _extend = Base.prototype.extend;
Base.prototype.extend = function(source, value) {
	if (typeof source == "string" && source.charAt(0) == "@") {
		return BOM.detect(source.slice(1)) ? _extend.call(this, value) : this;
	}
	return _extend.apply(this, arguments);
};

// =========================================================================
// BOM/MSIE.js
// =========================================================================

// avoid memory leaks

if (BOM.detect("MSIE.+win")) {
	var $closures = {}; // all closures stored here
	
	BOM.$bind = function(method, element) {
		if (!element || element.nodeType != 1) {
			return method;
		}
		
		// unique id's for element and function
		var $element = element.uniqueID;
		var $method = assignID(method);
		
		// store the closure in a manageable scope
		$closures[$method] = method;			
		if (!$closures[$element]) $closures[$element] = {};		
		var closure = $closures[$element][$method];
		if (closure) return closure; // already stored
		
		// reset pointers
		element = null;
		method = null;
		
		// return a new closure with a manageable scope 
		var bound = function() {
			var element = document.all[$element];
			if (element) return $closures[$method].apply(element, arguments);
		};
		bound.cloneID = $method;
		$closures[$element][$method] = bound;
		return bound;
	};
	
	attachEvent("onunload", function() {
		$closures = null; // closures are destroyed when the page is unloaded
	});
}

// =========================================================================
// BOM/Window.js
// =========================================================================

var Window = Module.extend(null, {
	verify: function(window) {
		return (window && window.Infinity) ? window : null;
	},
	
	"@MSIE": {
		verify: function(window) {
			// A very weird bug...
			return (window == self) ? self : this.base();
		}
	}
});

eval(this.exports);

}; ////////////////////  END: CLOSURE  /////////////////////////////////////

// timestamp: Fri, 10 Aug 2007 20:00:50

new function(_) { ////////////////////  BEGIN: CLOSURE  ////////////////////

// =========================================================================
// DOM/namespace.js
// =========================================================================

var DOM = new base2.Namespace(this, {
  name:    "DOM",
  version: "0.9 (alpha)",
  exports:
    "Interface, Binding, AbstractView, Event, EventTarget, NodeSelector, DocumentEvent, DocumentSelector, ElementSelector, " +
    "StaticNodeList, ViewCSS, Node, Document, Element, HTMLDocument, HTMLElement, Selector, Traversal, XPathParser",
  
  bind: function(node) {
    // apply a base2 DOM Binding to a native DOM node
    if (node && node.nodeType) {
      var uid = assignID(node);
      if (!arguments.callee[uid]) {
        switch (node.nodeType) {
          case 1: // Element
            if (typeof node.className == "string") {
              // it's an HTML element, use bindings based on tag name
              (HTMLElement.bindings[node.tagName] || HTMLElement).bind(node);
            } else {
              Element.bind(node);
            }
            break;
          case 9: // Document
            if (node.links) {
              HTMLDocument.bind(node);
            } else {
              Document.bind(node);
            }
            break;
          default:
            Node.bind(node);
        }
        arguments.callee[uid] = true;
      }
    }
    return node;
  }
});

eval(this.imports);

// =========================================================================
// DOM/plumbing.js
// =========================================================================

// avoid memory leaks

if (detect("MSIE[56].+win") && !detect("SV1")) {
  var closures = {}; // all closures stored here
  
  extend(base2, "bind", function(method, element) {
    if (!element || element.nodeType != 1) {
      return this.base(method, element);
    }
    
    // unique id's for element and function
    var elementID = element.uniqueID;
    var methodID = assignID(method);
    
    // store the closure in a manageable scope
    closures[methodID] = method;
    
    // reset pointers
    method = null;
    element = null;
    
    if (!closures[elementID]) closures[elementID] = {};
    var closure = closures[elementID][methodID];
    if (closure) return closure; // already stored
    
    var bound = function() {
      var element = document.all[elementID];
      if (element) return closures[methodID].apply(element, arguments);
    };
    bound._cloneID = methodID;
    closures[elementID][methodID] = bound;
    
    return bound;
  });
  
  attachEvent("onunload", function() {
    closures = null; // closures are destroyed when the page is unloaded
  });
}

// =========================================================================
// DOM/Interface.js
// =========================================================================

// The Interface module is the base module for defining DOM interfaces.
// Interfaces are defined with reference to the original W3C IDL.
// e.g. http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-1950641247

var Interface = Module.extend(null, {
  implement: function(_interface) {    
    if (typeof _interface == "object") {
      forEach (_interface, function(source, name) {
        if (name.charAt(0) == "@") {
          forEach (source, arguments.callee, this);
        } else if (!this[name] && typeof source == "function") {
          this.createDelegate(name, source.length);
        }
      }, this);
    }
    return this.base(_interface);
  },
  
  createDelegate: function(name, length) {
    // delegate a static method to the bound object
    //  e.g. for most browsers:
    //    EventTarget.addEventListener(element, type, listener, capture) 
    //  forwards to:
    //    element.addEventListener(type, listener, capture)
    if (!this[name]) {
      var FN = "var fn=function _%1(%2){%3.base=%3.%1.ancestor;var m=%3.base?'base':'%1';return %3[m](%4)}";
      var args = "abcdefghij".split("").slice(-length);
      eval(format(FN, name, args, args[0], args.slice(1)));
      fn._delegate = name;
      this[name] = fn;
    }
  }
});

// =========================================================================
// DOM/Binding.js
// =========================================================================

var Binding = Interface.extend(null, {
  bind: function(object) {
    return this(object); // cast
  }
});

// =========================================================================
// DOM/Traversal.js
// =========================================================================

// DOM Traversal. Just the basics.

// Loosely based on this:
// http://www.w3.org/TR/2007/WD-ElementTraversal-20070727/

var Traversal = Module.extend({
  getDefaultView: function(node) {
    return this.getDocument(node).defaultView;
  },
  
  getNextElementSibling: function(node) {
    // return the next element to the supplied element
    //  nextSibling is not good enough as it might return a text or comment node
    while (node && (node = node.nextSibling) && !this.isElement(node)) continue;
    return node;
  },

  getNodeIndex: function(node) {
    var index = 0;
    while (node && (node = node.previousSibling)) index++;
    return index;
  },
  
  getOwnerDocument: function(node) {
    // return the node's containing document
    return node.ownerDocument;
  },
  
  getPreviousElementSibling: function(node) {
    // return the previous element to the supplied element
    while (node && (node = node.previousSibling) && !this.isElement(node)) continue;
    return node;
  },

  getTextContent: function(node) {
    return node[Traversal.$TEXT];
  },

  isEmpty: function(node) {
    node = node.firstChild;
    while (node) {
      if (node.nodeType == 3 || this.isElement(node)) return false;
      node = node.nextSibling;
    }
    return true;
  },

  setTextContent: function(node, text) {
    return node[Traversal.$TEXT] = text;
  },
  
  "@MSIE": {
    getDefaultView: function(node) {
      return this.getDocument(node).parentWindow;
    },
  
    "@MSIE5": {
      // return the node's containing document
      getOwnerDocument: function(node) {
        return node.ownerDocument || node.document;
      }
    }
  }
}, {
  $TEXT: "textContent",
  
  contains: function(node, target) {
    while (target && (target = target.parentNode) && node != target) continue;
    return !!target;
  },
  
  getDocument: function(node) {
    // return the document object
    return this.isDocument(node) ? node : this.getOwnerDocument(node);
  },
  
  isDocument: function(node) {
    return !!(node && node.documentElement);
  },
  
  isElement: function(node) {
    return !!(node && node.nodeType == 1);
  },
  
  "@(element.contains)": {  
    contains: function(node, target) {
      return node != target && this.isDocument(node) ? node == this.getOwnerDocument(target) : node.contains(target);
    }
  },
  
  "@MSIE": {
    $TEXT: "innerText"
  },
  
  "@MSIE5": {
    isElement: function(node) {
      return !!(node && node.nodeType == 1 && node.tagName != "!");
    }
  }
});

// =========================================================================
// DOM/views/AbstractView.js
// =========================================================================

// This is just fluff for now.

var AbstractView = Binding.extend();

// =========================================================================
// DOM/events/Event.js
// =========================================================================

// http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Event

var Event = Binding.extend({  
  "@!(document.createEvent)": {
    initEvent: function(event, type, bubbles, cancelable) {
      event.timeStamp = new Date().valueOf();
      event.type = type;
      event.bubbles = bubbles;
      event.cancelable = cancelable;
    },
    
    "@MSIE": {
      initEvent: function(event, type, bubbles, cancelable) {
        this.base(event, type, bubbles, cancelable);
        event.cancelBubble = !event.bubbles;
      },
      
      preventDefault: function(event) {
        if (event.cancelable !== false) {
          event.returnValue = false;
        }
      },
    
      stopPropagation: function(event) {
        event.cancelBubble = true;
      }
    }
  }
}, {
  BUBBLES: "abort,error,select,change,resize,scroll", // + Event.CANCELABLE
  CANCELABLE: "click,mousedown,mouseup,mouseover,mousemove,mouseout,keydown,keyup,submit,reset",
  
  init: function() {
    this.BUBBLES = Array2.combine((this.BUBBLES + "," + this.CANCELABLE).split(","));
    this.CANCELABLE = Array2.combine(this.CANCELABLE.split(","));
  },
  
  "@MSIE": {
    "@Mac": {
      bind: function(event) {
        // Mac IE5 does not allow expando properties on the event object so
        //  we copy the object instead.
        return this.base(extend({
          preventDefault: function() {
            if (this.cancelable !== false) {
              this.returnValue = false;
            }
          }
        }, event));
      }
    },
    
    "@Windows": {
      bind: function(event) {
        this.base(event);
        if (!event.timeStamp) {
          event.bubbles = !!this.BUBBLES[event.type];
          event.cancelable = !!this.CANCELABLE[event.type];
          event.timeStamp = new Date().valueOf();
        }
        if (!event.target) {
          event.target = event.srcElement;
        }
        event.relatedTarget = event.fromElement || null;
        return event;
      }
    }
  }
});

// =========================================================================
// DOM/events/EventTarget.js
// =========================================================================

// http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Registration-interfaces

// TO DO: event capture

var EventTarget = Interface.extend({
  "@!(element.addEventListener)": {
    addEventListener: function(target, type, listener, capture) {
      // assign a unique id to both objects
      var targetID = assignID(target);
      var listenerID = listener._cloneID || assignID(listener);
      // create a hash table of event types for the target object
      var events = EventTarget.$all[targetID];
      if (!events) events = EventTarget.$all[targetID] = {};
      // create a hash table of event listeners for each object/event pair
      var listeners = events[type];
      var current = target["on" + type];
      if (!listeners) {
        listeners = events[type] = {};
        // store the existing event listener (if there is one)
        if (current) listeners[0] = current;
      }
      // store the event listener in the hash table
      listeners[listenerID] = listener;
      if (current !== undefined) {
        target["on" + type] = delegate(EventTarget.$handleEvent);
      }
    },
  
    dispatchEvent: function(target, event) {
      return EventTarget.$handleEvent(target, event);
    },
  
    removeEventListener: function(target, type, listener, capture) {
      // delete the event listener from the hash table
      var events = EventTarget.$all[target.base2ID];
      if (events && events[type]) {
        delete events[type][listener.base2ID];
      }
    },
    
    "@MSIE.+win": {
      addEventListener: function(target, type, listener, capture) {
        // avoid memory leaks
        if (typeof listener == "function") {
          listener = bind(listener, target);
        }
        this.base(target, type, listener, capture);
      },
      
      dispatchEvent: function(target, event) {
        event.target = target;
        try {
          return target.fireEvent(event.type, event);
        } catch (error) {
          // the event type is not supported
          return this.base(target, event);
        }
      }
    }
  }
}, {  
  dispatchEvent: function(target, event) {
    // a little sugar
    if (typeof event == "string") {
      var type = event;
      event = DocumentEvent.createEvent(target, "Events");
      Event.initEvent(event, type, false, false);
    }
    this.base(target, event);
  },
  
  "@!(element.addEventListener)": {
    $all : {},
  
    $handleEvent: function(target, event) {
      var returnValue = true;
      // get a reference to the hash table of event listeners
      var events = EventTarget.$all[target.base2ID];
      if (events) {
        event = Event.bind(event); // fix the event object
        var listeners = events[event.type];
        // execute each event listener
        for (var i in listeners) {
          var listener = listeners[i];
          // support the EventListener interface
          if (listener.handleEvent) {
            returnValue = listener.handleEvent(event);
          } else {
            returnValue = listener.call(target, event);
          }
          if (event.returnValue === false) returnValue = false;
          if (returnValue === false) break;
        }
      }
      return returnValue;
    },
    
    "@MSIE": {  
      $handleEvent: function(target, event) {
        if (target.Infinity) {
          target = target.document.parentWindow;
          if (!event) event = target.event;
        }
        return this.base(target, event || Traversal.getDefaultView(target).event);
      }
    }
  }
});

// =========================================================================
// DOM/events/DocumentEvent.js
// =========================================================================

// http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-DocumentEvent

var DocumentEvent = Interface.extend({  
  "@!(document.createEvent)": {
    createEvent: function(document, type) {
      return Event.bind({});
    },
  
    "@(document.createEventObject)": {
      createEvent: function(document, type) {
        return Event.bind(document.createEventObject());
      }
    }
  },
  
  "@(document.createEvent)": {
    "@!(document.createEvent('Events'))": { // before Safari 3
      createEvent: function(document, type) {
        // a type of "Events" throws an error on Safari
        return this.base(document, type == "Events" ? "UIEvents" : type);
      }
    }
  }
});

// =========================================================================
// DOM/events/DOMContentLoaded.js
// =========================================================================

// http://dean.edwards.name/weblog/2006/06/again

var DOMContentLoaded = Module.extend(null, {
  fired: false,
  
  fire: function() {
    if (!DOMContentLoaded.fired) {
      DOMContentLoaded.fired = true;
      // this function will be called from another event handler so we'll user a timer
      //  to drop out of any current event
      // use a string for old browsers
      setTimeout("base2.DOM.EventTarget.dispatchEvent(document,'DOMContentLoaded')", 0);
    }
  },
  
  init: function() {
    // use the real event for browsers that support it (opera & firefox)
    EventTarget.addEventListener(document, "DOMContentLoaded", function() {
      DOMContentLoaded.fired = true;
    }, false);
    // if all else fails fall back on window.onload
    EventTarget.addEventListener(window, "load", this.fire, false);
  },

  "@(addEventListener)": {
    init: function() {
      this.base();
      addEventListener("load", this.fire, false);
    }
  },

  "@(attachEvent)": {
    init: function() {
      this.base();
      attachEvent("onload", this.fire);
    }
  },

  "@MSIE.+win": {
    init: function() {
      this.base();
      // Matthias Miller/Mark Wubben/Paul Sowden/Me
      document.write("<script id=__ready defer src=//:><\/script>");
      document.all.__ready.onreadystatechange = function() {
        if (this.readyState == "complete") {
          this.removeNode(); // tidy
          DOMContentLoaded.fire();
        }
      };
    }
  },
  
  "@KHTML": {
    init: function() {
      this.base();
      // John Resig
      var timer = setInterval(function() {
        if (/loaded|complete/.test(document.readyState)) {
          clearInterval(timer);
          DOMContentLoaded.fire();
        }
      }, 100);
    }
  }
});

// =========================================================================
// DOM/style/ViewCSS.js
// =========================================================================

// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-ViewCSS

var ViewCSS = Interface.extend({
  "@!(document.defaultView.getComputedStyle)": {
    "@MSIE": {
      getComputedStyle: function(view, element, pseudoElement) {
        var METRICS = /(width|height|top|bottom|left|right|fontSize)$/;
        var COLOR = /^(color|backgroundColor)$/;
        // pseudoElement parameter is not supported
        var computedStyle = {};
        var currentStyle = element.currentStyle;
        for (var i in currentStyle) {
          if (METRICS.test(i)) {
            computedStyle[i] = this.$getPixelValue(element, computedStyle[i]) + "px";
          } else if (COLOR.test(i)) {
            computedStyle[i] = this.$getColorValue(element, i == "color" ? "ForeColor" : "BackColor");
          } else {
            computedStyle[i] = currentStyle[i];
          }
        }
        return computedStyle;
      }
    }
  }
}, {
  toCamelCase: function(string) {
    return String(string).replace(/\-([a-z])/g, function(match, chr) {
      return chr.toUpperCase();
    });
  },
  
  "@MSIE": {
    $getPixelValue: function(element, value) {
      var PIXEL = /^\d+(px)?$/i;
      if (PIXEL.test(value)) return parseInt(value);
      var styleLeft = element.style.left;
      var runtimeStyleLeft = element.runtimeStyle.left;
      element.runtimeStyle.left = element.currentStyle.left;
      element.style.left = value || 0;
      value = element.style.pixelLeft;
      element.style.left = styleLeft;
      element.runtimeStyle.left = runtimeStyleLeft;
      return value;
    },
    
    $getColorValue: function(element, value) {
      var range = element.document.body.createTextRange();
      range.moveToElementText(element);
      var color = range.queryCommandValue(value);
      return format("rgb(%1,%2,%3)", color & 0xff, (color & 0xff00) >> 8,  (color & 0xff0000) >> 16);
    }
  }
});

// =========================================================================
// DOM/selectors-api/NodeSelector.js
// =========================================================================

// http://www.w3.org/TR/selectors-api/
// http://www.whatwg.org/specs/web-apps/current-work/#getelementsbyclassname

var NodeSelector = Interface.extend({  
  "@!(element.getElementsByClassName)": { // firefox3?
    getElementsByClassName: function(node, className) {
      if (instanceOf(className, Array)) {
        className = className.join(".");
      }
      return this.matchAll(node, "." + className);
    }
  },
  
  "@!(element.matchSingle)": { // future-proof
    matchAll: function(node, selector) {
      return new Selector(selector).exec(node);
    },
    
    matchSingle: function(node, selector) {
      return new Selector(selector).exec(node, 1);
    }
  }
});

// automatically bind objects retrieved using the Selectors API

extend(NodeSelector.prototype, {
  matchAll: function(selector) {
    return extend(this.base(selector), "item", function(index) {
      return DOM.bind(this.base(index));
    });
  },
  
  matchSingle: function(selector) {
    return DOM.bind(this.base(selector));
  }
});

// =========================================================================
// DOM/selectors-api/DocumentSelector.js
// =========================================================================

// http://www.w3.org/TR/selectors-api/#documentselector

var DocumentSelector = NodeSelector.extend();

// =========================================================================
// DOM/selectors-api/ElementSelector.js
// =========================================================================

// more Selectors API sensibleness

var ElementSelector = NodeSelector.extend({
  "@!(element.matchesSelector)": { // future-proof
    matchesSelector: function(element, selector) {
      return new Selector(selector).test(element);
    }
  }
});


// =========================================================================
// DOM/selectors-api/StaticNodeList.js
// =========================================================================

// http://www.w3.org/TR/selectors-api/#staticnodelist

// A wrapper for an array of elements or an XPathResult.
// The item() method provides access to elements.
// Implements Enumerable so you can forEach() to your heart's content... :-)

var StaticNodeList = Base.extend({
  constructor: function(nodes) {
    nodes = nodes || [];
    this.length = nodes.length;
    this.item = function(index) {
      return nodes[index];
    };
  },
  
  length: 0,
  
  forEach: function(block, context) {
    var length = this.length; // preserve
    for (var i = 0; i < length; i++) {
      block.call(context, this.item(i), i, this);
    }
  },
  
  item: Undefined, // defined in the constructor function
  
  "@(XPathResult)": {
    constructor: function(nodes) {
  //- if (nodes instanceof XPathResult) { // doesn't work in Safari
      if (nodes && nodes.snapshotItem) {
        this.length = nodes.snapshotLength;
        this.item = function(index) {
          return nodes.snapshotItem(index);
        };
      } else this.base(nodes);
    }
  }
});

StaticNodeList.implement(Enumerable);

// =========================================================================
// DOM/selectors-api/Selector.js
// =========================================================================

// This object can be instantiated, however it is probably better to use
// the matchAll/matchSingle methods on DOM nodes.

// There is no public standard for this object. It just separates the NodeSelector
//  interface from the complexity of the Selector parsers.

var Selector = Base.extend({
  constructor: function(selector) {
    this.toString = returns(trim(selector));
  },
  
  exec: function(context, single) {
    try {
      var result = this.$evaluate(context || document, single);
    } catch (error) { // probably an invalid selector =)
      throw new SyntaxError(format("'%1' is not a valid CSS selector.", this));
    }
    return single ? result : new StaticNodeList(result);
  },
  
  test: function(element) {
    //-dean: improve this for simple selectors
    element.setAttribute("b2_test", true);
    var selector = new Selector(this + "[b2_test]");
    var result = selector.exec(Traversal.getOwnerDocument(element), true);
    element.removeAttribute("b2_test");
    return result == element;
  },
  
  $evaluate: function(context, single) {
    return Selector.parse(this)(context, single);
  }
});

// =========================================================================
// DOM/selectors-api/Parser.js
// =========================================================================
  
var Parser = RegGrp.extend({
  constructor: function(items) {
    this.base(items);
    this.cache = {};
    this.sorter = new RegGrp;
    this.sorter.add(/:not\([^)]*\)/, RegGrp.IGNORE);
    this.sorter.add(/([ >](\*|[\w-]+))([^: >+~]*)(:\w+-child(\([^)]+\))?)([^: >+~]*)/, "$1$3$6$4");
  },
  
  cache: null,
  ignoreCase: true,
  
  escape: function(selector) {
    // remove strings
    var QUOTE = /'/g;
    var strings = this._strings = [];
    return this.optimise(this.format(String(selector).replace(Parser.ESCAPE, function(string) {
      strings.push(string.slice(1, -1).replace(QUOTE, "\\'"));
      return "\x01" + strings.length;
    })));
  },
  
  format: function(selector) {
    return selector
      .replace(Parser.WHITESPACE, "$1")
      .replace(Parser.IMPLIED_SPACE, "$1 $2")
      .replace(Parser.IMPLIED_ASTERISK, "$1*$2");
  },
  
  optimise: function(selector) {
    // optimise wild card descendant selectors
    return this.sorter.exec(selector.replace(Parser.WILD_CARD, ">* "));
  },
  
  parse: function(selector) {
    return this.cache[selector] ||
      (this.cache[selector] = this.unescape(this.exec(this.escape(selector))));
  },
  
  unescape: function(selector) {
    // put string values back
    var strings = this._strings;
    return selector.replace(/\x01(\d+)/g, function(match, index) {
      return strings[index - 1];
    });
  }
}, {
  ESCAPE:           /(["'])[^\1]*\1/g,
  IMPLIED_ASTERISK: /([\s>+~,]|[^(]\+|^)([#.:@])/g,
  IMPLIED_SPACE:    /(^|,)([^\s>+~])/g,
  WHITESPACE:       /\s*([\s>+~(),]|^|$)\s*/g,
  WILD_CARD:        /\s\*\s/g,
  
  _nthChild: function(match, args, position, last, not, and, mod, equals) {
    // ugly but it works for both CSS and XPath
    last = /last/i.test(match) ? last + "+1-" : "";
    if (!isNaN(args)) args = "0n+" + args;
    else if (args == "even") args = "2n";
    else if (args == "odd") args = "2n+1";
    args = args.split(/n\+?/);
    var a = args[0] ? (args[0] == "-") ? -1 : parseInt(args[0]) : 1;
    var b = parseInt(args[1]) || 0;
    var not = a < 0;
    if (not) {
      a = -a;
      if (a == 1) b++;
    }
    var query = format(a == 0 ? "%3%7" + (last + b) : "(%4%3-%2)%6%1%70%5%4%3>=%2", a, b, position, last, and, mod, equals);
    if (not) query = not + "(" + query + ")";
    return query;
  }
});

// =========================================================================
// DOM/selectors-api/Selector/operators.js
// =========================================================================

Selector.operators = {
  "=":  "%1=='%2'",
  "!=": "%1!='%2'", //  not standard but other libraries support it
  "~=": /(^| )%1( |$)/,
  "|=": /^%1(-|$)/,
  "^=": /^%1/,
  "$=": /%1$/,
  "*=": /%1/
};

Selector.operators[""] = "%1!=null";

// =========================================================================
// DOM/selectors-api/Selector/pseudoClasses.js
// =========================================================================

Selector.pseudoClasses = { //-dean: lang()
  "checked":     "e%1.checked",
  "contains":    "e%1[Traversal.$TEXT].indexOf('%2')!=-1",
  "disabled":    "e%1.disabled",
  "empty":       "Traversal.isEmpty(e%1)",
  "enabled":     "e%1.disabled===false",
  "first-child": "!Traversal.getPreviousElementSibling(e%1)",
  "last-child":  "!Traversal.getNextElementSibling(e%1)",
  "only-child":  "!Traversal.getPreviousElementSibling(e%1)&&!Traversal.getNextElementSibling(e%1)",
  "root":        "e%1==Traversal.getDocument(e%1).documentElement"
/*  "lang": function(element, lang) {
    while (element && !element.getAttribute("lang")) {
      element = element.parentNode;
    }
    return element && lang.indexOf(element.getAttribute("lang")) == 0;
  }, */
};

// =========================================================================
// DOM/selectors-api/Selector/parse.js
// =========================================================================

// CSS parser - converts CSS selectors to DOM queries.

// Hideous code but it produces fast DOM queries.
// Respect due to Alex Russell and Jack Slocum for inspiration.

// TO DO:
// * sort nodes into document order (comma separated queries only)

new function(_) {
  // some constants
  var _MSIE = detect("MSIE");
  var _MSIE5 = detect("MSIE5");
  var _INDEXED = detect("(element.sourceIndex)") ;
  var _VAR = "var p%2=0,i%2,e%2,n%2=e%1.";
  var _ID = _INDEXED ? "e%1.sourceIndex" : "assignID(e%1)";
  var _TEST = "var g=" + _ID + ";if(!p[g]){p[g]=1;";
  var _STORE = "r[r.length]=e%1;if(s)return e%1;";
  var _FN = "fn=function(e0,s){indexed++;var r=[],p={},reg=[%1]," +
    "d=Traversal.getDocument(e0),c=d.body?'toUpperCase':'toString';";
  
  // IE confuses the name attribute with id for form elements,
  // use document.all to retrieve all elements with name/id instead
  var byId = _MSIE ? function(document, id) {
    var result = document.all[id] || null;
    // returns a single element or a collection
    if (!result || result.id == id) return result;
    // document.all has returned a collection of elements with name/id
    for (var i = 0; i < result.length; i++) {
      if (result[i].id == id) return result[i];
    }
    return null;
  } : function(document, id) {
    return document.getElementById(id);
  };
  
  // register a node and _index its children
  var indexed = 1;
  function register(element) {
    if (element.b2_indexed != indexed) {
      var _index = 0;
      var child = element.firstChild;
      while (child) {
        if (child.nodeType == 1 && child.tagName != "!") {
          child.b2_index = ++_index;
        }
        child = child.nextSibling;
      }
      element.b2_length = _index;
      element.b2_indexed = indexed;
    }
    return element;
  };
  
  // variables used by the parser
  var fn;
  var reg; // a store for RexExp objects
  var _index;
  var _wild; // need to flag certain _wild card selectors as _MSIE includes comment nodes
  var _list; // are we processing a node _list?
  var _duplicate; // possible duplicates?
  var _cache = {}; // store parsed selectors
  
  // a hideous parser
  var parser = new Parser({
    "^ \\*:root": function(match) { // :root pseudo class
      _wild = false;
      var replacement = "e%2=d.documentElement;if(Traversal.contains(e%1,e%2)){";
      return format(replacement, _index++, _index);
    },
    " (\\*|[\\w-]+)#([\\w-]+)": function(match, tagName, id) { // descendant selector followed by _ID
      _wild = false;
      var replacement = "var e%2=byId(d,'%4');if(e%2&&";
      if (tagName != "*") replacement += "e%2.nodeName=='%3'[c]()&&";
      replacement += "Traversal.contains(e%1,e%2)){";
      if (_list) replacement += format("i%1=n%1.length;", _list);
      return format(replacement, _index++, _index, tagName, id);
    },
    " (\\*|[\\w-]+)": function(match, tagName) { // descendant selector
      _duplicate++; // this selector may produce duplicates
      _wild = tagName == "*";
      var replacement = _VAR;
      // IE5.x does not support getElementsByTagName("*");
      replacement += (_wild && _MSIE5) ? "all" : "getElementsByTagName('%3')";
      replacement += ";for(i%2=0;(e%2=n%2[i%2]);i%2++){";
      return format(replacement, _index++, _list = _index, tagName);
    },
    ">(\\*|[\\w-]+)": function(match, tagName) { // child selector
      var children = _MSIE && _list;
      _wild = tagName == "*";
      var replacement = _VAR;
      // use the children property for _MSIE as it does not contain text nodes
      //  (but the children collection still includes comments).
      // the document object does not have a children collection
      replacement += children ? "children": "childNodes";
      if (!_wild && children) replacement += ".tags('%3')";
      replacement += ";for(i%2=0;(e%2=n%2[i%2]);i%2++){";
      if (_wild) {
        replacement += "if(e%2.nodeType==1){";
        _wild = _MSIE5;
      } else {
        if (!children) replacement += "if(e%2.nodeName=='%3'[c]()){";
      }
      return format(replacement, _index++, _list = _index, tagName);
    },
    "\\+(\\*|[\\w-]+)": function(match, tagName) { // direct adjacent selector
      var replacement = "";
      if (_wild && _MSIE) replacement += "if(e%1.tagName!='!'){";
      _wild = false;
      replacement += "e%1=Traversal.getNextElementSibling(e%1);if(e%1";
      if (tagName != "*") replacement += "&&e%1.nodeName=='%2'[c]()";
      replacement += "){";
      return format(replacement, _index, tagName);
    },
    "~(\\*|[\\w-]+)": function(match, tagName) { // indirect adjacent selector
      var replacement = "";
      if (_wild && _MSIE) replacement += "if(e%1.tagName!='!'){";
      _wild = false;
      _duplicate = 2; // this selector may produce duplicates
      replacement += "while(e%1=e%1.nextSibling){if(e%1.b2_adjacent==indexed)break;e%1.b2_adjacent=indexed;if(";
      if (tagName == "*") {
        replacement += "e%1.nodeType==1";
        if (_MSIE5) replacement += "&&e%1.tagName!='!'";
      } else replacement += "e%1.nodeName=='%2'[c]()";
      replacement += "){";
      return format(replacement, _index, tagName);
    },
    "#([\\w-]+)": function(match, id) { // _ID selector
      _wild = false;
      var replacement = "if(e%1.id=='%2'){";
      if (_list) replacement += format("i%1=n%1.length;", _list);
      return format(replacement, _index, id);
    },
    "\\.([\\w-]+)": function(match, className) { // class selector
      _wild = false;
      // store RegExp objects - slightly faster on IE
      reg.push(new RegExp("(^|\\s)" + rescape(className) + "(\\s|$)"));
      return format("if(reg[%2].test(e%1.className)){", _index, reg.length - 1);
    },
    ":not\\((\\*|[\\w-]+)?([^)]*)\\)": function(match, tagName, filters) { // :not pseudo class
      var replacement = (tagName && tagName != "*") ? format("if(e%1.nodeName=='%2'[c]()){", _index, tagName) : "";
      replacement += parser.exec(filters);
      return "if(!" + replacement.slice(2, -1).replace(/\)\{if\(/g, "&&") + "){";
    },
    ":nth(-last)?-child\\(([^)]+)\\)": function(match, last, args) { // :nth-child pseudo classes
      _wild = false;
      last = format("e%1.parentNode.b2_length", _index);
      var replacement = "if(p%1!==e%1.parentNode)";
      replacement += "p%1=register(e%1.parentNode);var i=e%1.b2_index;if(";
      return format(replacement, _index) + Parser._nthChild(match, args, "i", last, "!", "&&", "%", "==") + "){";
    },
    ":([\\w-]+)(\\(([^)]+)\\))?": function(match, pseudoClass, $2, args) { // other pseudo class selectors
      return "if(" + format(Selector.pseudoClasses[pseudoClass], _index, args || "") + "){";
    },
    "\\[([\\w-]+)\\s*([^=]?=)?\\s*([^\\]]*)\\]": function(match, attr, operator, value) { // attribute selectors
      var alias = Element.$attributes[attr] || attr;
      if (attr == "class") alias = "className";
      else if (attr == "for") alias = "htmlFor";
      if (operator) {
        attr = format("(e%1.%3||e%1.getAttribute('%2'))", _index, attr, alias);
      } else {
        attr = format("Element.getAttribute(e%1,'%2')", _index, attr);
      }
      var replacement = Selector.operators[operator || ""];
      if (instanceOf(replacement, RegExp)) {
        reg.push(new RegExp(format(replacement.source, rescape(parser.unescape(value)))));
        replacement = "reg[%2].test(%1)";
        value = reg.length - 1;
      }
      return "if(" + format(replacement, attr, value) + "){";
    }
  });
  
  // return the parse() function
  Selector.parse = function(selector) {
    if (!_cache[selector]) {
      reg = []; // store for RegExp objects
      fn = "";
      var selectors = parser.escape(selector).split(",");
      for (var i = 0; i < selectors.length; i++) {
        _wild = _index = _list = 0; // reset
        _duplicate = selectors.length > 1 ? 2 : 0; // reset
        var block = parser.exec(selectors[i]) || "throw;";
        if (_wild && _MSIE) { // IE's pesky comment nodes
          block += format("if(e%1.tagName!='!'){", _index);
        }
        // check for duplicates before storing results
        var store = (_duplicate > 1) ? _TEST : "";
        block += format(store + _STORE, _index);
        // add closing braces
        block += Array(match(block, /\{/g).length + 1).join("}");
        fn += block;
      }
      eval(format(_FN, reg) + parser.unescape(fn) + "return s?null:r}");
      _cache[selector] = fn;
    }
    return _cache[selector];
  };
};

// =========================================================================
// DOM/selectors-api/xpath/XPathParser.js
// =========================================================================

// XPath parser
// converts CSS expressions to *optimised* XPath queries

// This code used to be quite readable until I added code to optimise *-child selectors. 

var XPathParser = Parser.extend({
  constructor: function() {
    this.base(XPathParser.rules);
    // The sorter sorts child selectors to the end because they are slow.
    // For XPath we need the child selectors to be sorted to the beginning,
    // so we reverse the sort order. That's what this line does:
    this.sorter.storeAt(1, "$1$4$3$6");
  },
  
  escape: function(selector) {
    return this.base(selector).replace(/,/g, "\x02");
  },
  
  unescape: function(selector) {
    return this.base(selector
      .replace(/\[self::\*\]/g, "")   // remove redundant wild cards
      .replace(/(^|\x02)\//g, "$1./") // context
      .replace(/\x02/g, " | ")        // put commas back
    );
  },
  
  "@opera": {
    unescape: function(selector) {
      // opera does not seem to support last() but I can't find any 
      //  documentation to confirm this
      return this.base(selector.replace(/last\(\)/g, "count(preceding-sibling::*)+count(following-sibling::*)+1"));
    }
  }
}, {
  init: function() {
    // build the prototype
    this.values.attributes[""] = "[@$1]";
    forEach (this.types, function(add, type) {
      forEach (this.values[type], add, this.rules);
    }, this);
  },
  
  optimised: {    
    pseudoClasses: {
      "first-child": "[1]",
      "last-child":  "[last()]",
      "only-child":  "[last()=1]"
    }
  },
  
  rules: extend({}, {
    "@!KHTML": { // these optimisations do not work on Safari
      // fast id() search
      "(^|\\x02) (\\*|[\\w-]+)#([\\w-]+)": "$1id('$3')[self::$2]",
      // optimise positional searches
      "([ >])(\\*|[\\w-]+):([\\w-]+-child(\\(([^)]+)\\))?)": function(match, token, tagName, pseudoClass, $4, args) {
        var replacement = (token == " ") ? "//*" : "/*";
        if (/^nth/i.test(pseudoClass)) {
          replacement += _nthChild(pseudoClass, args, "position()");
        } else {
          replacement += XPathParser.optimised.pseudoClasses[pseudoClass];
        }
        return replacement + "[self::" + tagName + "]";
      }
    }
  }),
  
  types: {
    identifiers: function(replacement, token) {
      this[rescape(token) + "([\\w-]+)"] = replacement;
    },
    
    combinators: function(replacement, combinator) {
      this[rescape(combinator) + "(\\*|[\\w-]+)"] = replacement;
    },
    
    attributes: function(replacement, operator) {
      this["\\[([\\w-]+)\\s*" + rescape(operator) +  "\\s*([^\\]]*)\\]"] = replacement;
    },
    
    pseudoClasses: function(replacement, pseudoClass) {
      this[":" + pseudoClass.replace(/\(\)$/, "\\(([^)]+)\\)")] = replacement;
    }
  },
  
  values: {
    identifiers: {
      "#": "[@id='$1'][1]", // ID selector
      ".": "[contains(concat(' ',@class,' '),' $1 ')]" // class selector
    },
    
    combinators: {
      " ": "/descendant::$1", // descendant selector
      ">": "/child::$1", // child selector
      "+": "/following-sibling::*[1][self::$1]", // direct adjacent selector
      "~": "/following-sibling::$1" // indirect adjacent selector
    },
    
    attributes: { // attribute selectors
      "*=": "[contains(@$1,'$2')]",
      "^=": "[starts-with(@$1,'$2')]",
      "$=": "[substring(@$1,string-length(@$1)-string-length('$2')+1)='$2']",
      "~=": "[contains(concat(' ',@$1,' '),' $2 ')]",
      "|=": "[contains(concat('-',@$1,'-'),'-$2-')]",
      "!=": "[not(@$1='$2')]",
      "=":  "[@$1='$2']"
    },
    
    pseudoClasses: { // pseudo class selectors
      "empty":            "[not(child::*) and not(text())]",
//-   "lang()":           "[boolean(lang('$1') or boolean(ancestor-or-self::*[@lang][1][starts-with(@lang,'$1')]))]",
      "first-child":      "[not(preceding-sibling::*)]",
      "last-child":       "[not(following-sibling::*)]",
      "not()":            _not,
      "nth-child()":      _nthChild,
      "nth-last-child()": _nthChild,
      "only-child":       "[not(preceding-sibling::*) and not(following-sibling::*)]",
      "root":             "[not(parent::*)]"
    }
  },
  
  "@opera": {  
    init: function() {
      this.optimised.pseudoClasses["last-child"] = this.values.pseudoClasses["last-child"];
      this.optimised.pseudoClasses["only-child"] = this.values.pseudoClasses["only-child"];
      this.base();
    }
  }
});

// these functions defined here to make the code more readable
var _notParser = new XPathParser;
function _not(match, args) {
  return "[not(" + _notParser.exec(trim(args))
    .replace(/\[1\]/g, "") // remove the "[1]" introduced by ID selectors
    .replace(/^(\*|[\w-]+)/, "[self::$1]") // tagName test
    .replace(/\]\[/g, " and ") // merge predicates
    .slice(1, -1)
  + ")]";
};

function _nthChild(match, args, position) {
  return "[" + Parser._nthChild(match, args, position || "count(preceding-sibling::*)+1", "last()", "not", " and ", " mod ", "=") + "]";
};

// =========================================================================
// DOM/selectors-api/xpath/Selector.js
// =========================================================================

// If the browser supports XPath then the CSS selector is converted to an XPath query instead.

Selector.implement({
  toXPath: function() {
    return Selector.toXPath(this);
  },
  
  "@(XPathResult)": {
    $evaluate: function(context, single) {
      // use DOM methods if the XPath engine can't be used
      if (Selector.$NOT_XPATH.test(this)) {
        return this.base(context, single);
      }
      var document = Traversal.getDocument(context);
      var type = single
        ? 9 /* FIRST_ORDERED_NODE_TYPE */
        : 7 /* ORDERED_NODE_SNAPSHOT_TYPE */;
      var result = document.evaluate(this.toXPath(), context, null, type, null);
      return single ? result.singleNodeValue : result;
    }
  },
  
  "@MSIE": {
    $evaluate: function(context, single) {
      if (typeof context.selectNodes != "undefined" && !Selector.$NOT_XPATH.test(this)) { // xml
        var method = single ? "selectSingleNode" : "selectNodes";
        return context[method](this.toXPath());
      }
      return this.base(context, single);
    }
  }
});

extend(Selector, {
  xpathParser: null,
  
  toXPath: function(selector) {
    if (!this.xpathParser) this.xpathParser = new XPathParser;
    return this.xpathParser.parse(selector);
  },
  
  $NOT_XPATH: /:(checked|disabled|enabled|contains)|^(#[\w-]+\s*)?\w+$/,
  
  "@KHTML": { // XPath is just too buggy on earlier versions of Safari  
    $NOT_XPATH: /:(checked|disabled|enabled|contains)|^(#[\w-]+\s*)?\w+$|nth\-|,/,
    
    "@!WebKit5": {
      $NOT_XPATH: /./
    }
  }
});

// =========================================================================
// DOM/core/Node.js
// =========================================================================

// http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-1950641247

var Node = Binding.extend({  
  "@!(element.compareDocumentPosition)" : {
    compareDocumentPosition: function(node, other) {
      // http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition
      
      if (Traversal.contains(node, other)) {
        return 4|16; // following|contained_by
      } else if (Traversal.contains(other, node)) {
        return 2|8;  // preceding|contains
      }
      
      var nodeIndex = Node.$getSourceIndex(node);
      var otherIndex = Node.$getSourceIndex(other);
      
      if (nodeIndex < otherIndex) {
        return 4; // following
      } else if (nodeIndex > otherIndex) {
        return 2; // preceding
      }      
      return 0;
    }
  }
}, {
  $getSourceIndex: function(node) {
    // return a key suitable for comparing nodes
    var key = 0;
    while (node) {
      key = Traversal.getNodeIndex(node) + "." + key;
      node = node.parentNode;
    }
    return key;
  },
  
  "@(element.sourceIndex)": {  
   $getSourceIndex: function(node) {
      return node.sourceIndex;
    }
  }
});

// =========================================================================
// DOM/core/Document.js
// =========================================================================

var Document = Node.extend(null, {
  bind: function(document) {
    this.base(document);
    extend(document, "createElement", function(tagName) { //-dean- test this!
      return DOM.bind(this.base(tagName));
    });
    AbstractView.bind(document.defaultView);
    return document;
  },
  
  "@!(document.defaultView)": {
    bind: function(document) {
      document.defaultView = Traversal.getDefaultView(document);
      return this.base(document);
    }
  }
});

// provide these as pass-through methods
Document.createDelegate("createElement", 2);

// =========================================================================
// DOM/core/Element.js
// =========================================================================

// http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-745549614

// I'm going to fix getAttribute() for IE here instead of HTMLElement.

// getAttribute() will return null if the attribute is not specified. This is
//  contrary to the specification but has become the de facto standard.

var Element = Node.extend({
  "@MSIE[67]": {
    getAttribute: function(element, name, iFlags) {
      if (element.className === undefined || name == "href" || name == "src") {
        return this.base(element, name, 2);
      }
      var attribute = element.getAttributeNode(name);
      return attribute && attribute.specified ? attribute.nodeValue : null;
    }
  },
  
  "@MSIE5.+win": {
    getAttribute: function(element, name, iFlags) {
      if (element.className === undefined || name == "href" || name == "src") {
        return this.base(element, name, 2);
      }
      var attribute = element.attributes[this.$attributes[name.toLowerCase()] || name];
      return attribute ? attribute.specified ? attribute.nodeValue : null : this.base(element, name);
    }
  }
}, {
  $attributes: {},
  
  "@MSIE5.+win": {
    init: function() {
      // these are the attributes that IE is case-sensitive about
      // convert the list of strings to a hash, mapping the lowercase name to the camelCase name.
      var attributes = "colSpan,rowSpan,vAlign,dateTime,accessKey,tabIndex,encType,maxLength,readOnly,longDesc";
      // combine two arrays to make a hash
      var keys = attributes.toLowerCase().split(",");
      var values = attributes.split(",");
      this.$attributes = Array2.combine(keys, values);
    }
  }
});

Element.createDelegate("setAttribute", 3);

// remove the base2ID for clones
extend(Element.prototype, "cloneNode", function(deep) {
  var clone = this.base(deep || false);
  clone.base2ID = undefined;
  return clone;
});

// =========================================================================
// DOM/implementations.js
// =========================================================================

AbstractView.implement(ViewCSS);

Document.implement(DocumentSelector);
Document.implement(DocumentEvent);
Document.implement(EventTarget);

Element.implement(ElementSelector);
Element.implement(EventTarget);

// =========================================================================
// DOM/html/HTMLDocument.js
// =========================================================================

// http://www.whatwg.org/specs/web-apps/current-work/#htmldocument

var HTMLDocument = Document.extend(null, {
  // http://www.whatwg.org/specs/web-apps/current-work/#activeelement  
  "@(document.activeElement===undefined)": {
    bind: function(document) {
      this.base(document);
      document.activeElement = null;
      document.addEventListener("focus", function(event) { //-dean: is onfocus good enough?
        document.activeElement = event.target;
      }, false);
      return document;
    }
  }
});

// =========================================================================
// DOM/html/HTMLElement.js
// =========================================================================

// The className methods are not standard but are extremely handy. :-)

var HTMLElement = Element.extend({
  addClass: function(element, className) {
    if (!this.hasClass(element, className)) {
      element.className += (element.className ? " " : "") + className;
    }
  },
  
  hasClass: function(element, className) {
    var regexp = new RegExp("(^|\\s)" + className + "(\\s|$)");
    return regexp.test(element.className);
  },

  removeClass: function(element, className) {
    var regexp = new RegExp("(^|\\s)" + className + "(\\s|$)", "g");
    element.className = element.className.replace(regexp, "$2");
  },

  toggleClass: function(element, className) {
    if (this.hasClass(element, className)) {
      this.removeClass(element, className);
    } else {
      this.addClass(element, className);
    }
  }
}, {
  bindings: {},
  tags: "*",
  
  extend: function() {
    // Maintain HTML element bindings.
    // This allows us to map specific interfaces to elements by reference
    // to tag name.
    var binding = base(this, arguments);
    var tags = (binding.tags || "").toUpperCase().split(",");
    forEach (tags, function(tagName) {
      HTMLElement.bindings[tagName] = binding;
    });
    return binding;
  },
  
  "@!(element.ownerDocument)": {
    bind: function(element) {
      this.base(element);
      element.ownerDocument = Traversal.getOwnerDocument(element);
      return element;
    }
  }
});

// =========================================================================
// DOM/init.js
// =========================================================================

// all other libraries allow this handy shortcut so base2 will too :-)

DOM.$ = function(selector, context) {
  return new Selector(selector).exec(context, 1);
};

DOM.$$ = function(selector, context) {
  return new Selector(selector).exec(context);
};

eval(this.exports);

}; ////////////////////  END: CLOSURE  /////////////////////////////////////


if (typeof(base2) == "undefined") {
	throw new Error("Base2 not found. wForms 3.0 depends on the base2 library.");
}
base2.DOM.bind(document);

if (typeof(wFORMS) == "undefined") {
	wFORMS = {};
}
wFORMS.NAME 	= "wFORMS";
wFORMS.VERSION 	= "3.0.alpha";
wFORMS.__repr__ = function () {
	return "[" + this.NAME + " " + this.VERSION + "]";
};
wFORMS.toString = function () {
	return this.__repr__();
};

wFORMS.behaviors = {};
wFORMS.helpers   = {}
wFORMS.instances = []; // keeps track of behavior instances

/**
 * Helper method.
 * @return {string} A randomly generated id (with very high probability of uniqueness). 
 */	
wFORMS.helpers.randomId = function () {
	var seed = (new Date()).getTime();
	seed = seed.toString().substr(6);
	for (var i=0; i<6;i++)
		seed += String.fromCharCode(48 + Math.floor((Math.random()*10)));
	return "id_" + seed;
}

/**
 * getFieldValue 
 * @param {domElement} element 
 * @returns {string} the value of the field. 
 */
wFORMS.helpers.getFieldValue = function(element) {
	switch(element.tagName) {
		case "INPUT":
			if(element.type=='checkbox')
				return element.checked?element.value:null;
			if(element.type=='radio')
				return element.checked?element.value:null;
			return element.value;
			break;
		case "SELECT":		
			if(element.selectedIndex==-1) {					
				return null; 
			} 
			if(element.getAttribute('multiple')) {
				var v=[];
				for(var i=0;i<element.options.length;i++) {
					if(element.options[i].selected) {
						v.push(element.options[i].value);
					}
				}
				return v;
			}											
			return element.options[element.selectedIndex].value;
			break;
		case "TEXTAREA":
			// TODO: fix this
			return element.value;
			break;
		default:
			return null; 
			break;
	} 	 
}

/**
 * Returns computed style from the element by style name
 * @param	{HTMLElement}	element
 * @param	{String}	styleName
 * @return	{String} or false
 */
wFORMS.helpers.getComputedStyle = function(element, styleName){
	if(document.defaultView && document.defaultView.getComputedStyle){
		return document.defaultView.getComputedStyle(element, "")[styleName];
	} else if(element.currentStyle){	
		return element.currentStyle[styleName];
	}
	return false;
}

/**
 * Returns left position of the element
 * @params	{HTMLElement}	elem	Source element 
 */
wFORMS.helpers.getLeft = function(elem){
	var pos = 0;
	while(elem.offsetParent) {
		if(wFORMS.helpers.getComputedStyle(elem, 'position') == 'relative'){
			return pos;
		}
		if(pos > 0 && wFORMS.helpers.getComputedStyle(elem, 'position') == 'absolute'){
			return pos;
		}
		pos += elem.offsetLeft;
		elem = elem.offsetParent;
	}
	return pos;
}

/**
 * Returns top position of the element
 * @params	{HTMLElement}	elem	Source element 
 */
wFORMS.helpers.getTop = function(elem){
	var pos = 0;
	while(elem.offsetParent) {
		if(wFORMS.helpers.getComputedStyle(elem, 'position') == 'relative'){
			return pos;
		}
		if(pos > 0 && wFORMS.helpers.getComputedStyle(elem, 'position') == 'absolute'){
			return pos;
		}
		pos += elem.offsetTop;
		elem = elem.offsetParent;
	}
	return pos;
}

/**
 * Activating an Alternate Stylesheet (thx to: http://www.howtocreate.co.uk/tutorials/index.php?tut=0&part=27)
 * Use this to activate a CSS Stylesheet that shouldn't be used if javascript is turned off.
 * The stylesheet rel attribute should be 'alternate stylesheet'. The title attribute MUST be set.
 */
wFORMS.helpers.activateStylesheet = function(sheetref) {
	if(document.getElementsByTagName) {
		var ss=document.getElementsByTagName('link');
	} else if (document.styleSheets) {
		var ss = document.styleSheets;
	}
	for(var i=0;ss[i];i++ ) {
		if(ss[i].href.indexOf(sheetref) != -1) {
			ss[i].disabled = true;
			ss[i].disabled = false;			
		}
	}
}

/**
 * Initialization routine. Automatically applies the behaviors to all web forms in the document.  
 */	
wFORMS.onLoadHandler = function() {
	document.matchAll("form").forEach(function(f){
		wFORMS.applyBehaviors(f);
	});	
}

/**
 * Initialization routine. Automatically applies all behaviors to the given element.
 * @param {domElement} A form element, or any of its children.
 * TODO: Kill existing instances before applying the behavior to the same element. 
 */	
wFORMS.applyBehaviors = function(f) {
	if(!f.matchAll)
		base2.DOM.bind(f);
		
// hack [don] {{{
// This hack is done due to switch reflects on the Targets state depends on triggers
// state. I.e. if checkbox is checked its target should be on state
// and inside the paging while applying it calls switch behavior (static method)
// to check if Page is on or Off. So if paging will be loaded before switch it would be
// initialized with possible fake values
	if(wFORMS.behaviors['switch']){
		var b = wFORMS.behaviors['switch'].applyTo(f);
		if(!wFORMS.instances['switch']) {
			wFORMS.instances['switch'] = [b];
		} else {
			wFORMS.removeBehavior(f, 'switch');
			wFORMS.instances['switch'].push(b);
		}		
	}
// hack [don] }}}

	for(var behaviorName in wFORMS.behaviors) {
// hack [don] {{{
		if(behaviorName == 'switch'){
			continue;
		}
// hack [don] }}}
		var b = wFORMS.behaviors[behaviorName].applyTo(f);
		// behaviors may create several instances
		// if single instance returned, convert it to an array
		if(b && b.constructor.toString().indexOf("Array") == -1) {
			b=[b];			
		} 
		for(var i=0;b && i<b.length;i++) {
			if(!wFORMS.instances[behaviorName]) {
				wFORMS.instances[behaviorName] = [b[i]];
			} else {
				wFORMS.removeBehavior(f, behaviorName);
				wFORMS.instances[behaviorName].push(b[i]);
			}
		}
	}
}

wFORMS.removeBehavior = function(f, behaviorName) {
	
	return null;
	
	if(!wFORMS.instances[behaviorName]) 
		return null;

	for(var i=0; i < wFORMS.instances[behaviorName].length; i++) {
		if(wFORMS.instances[behaviorName][i].target==f) {
			
			// TODO: call a remove method for each behavior to cleanly remove any event handler
			wFORMS.instances[behaviorName][i] = null;
		}	
	}
	return null;
}

/**
 * Returns the behavior instance associated to the given form/behavior pair.
 * @param	{domElement}	a HTML element (often the form element itself)
 * @param	{string}		the name of the behavior 
 * @return	{object}		the instance of the behavior 
 * TODO: Returns an array if more than one instance for the given form
 */
wFORMS.getBehaviorInstance = function(f, behaviorName) {
	if(!wFORMS.instances[behaviorName]) 
		return null;

	for(var i=0; i < wFORMS.instances[behaviorName].length; i++) {
		if(wFORMS.instances[behaviorName][i].target==f) {
			return wFORMS.instances[behaviorName][i];
		}	
	}
	return null;
}

document.addEventListener('DOMContentLoaded',wFORMS.onLoadHandler,false);
// Attach JS only stylesheet.
wFORMS.helpers.activateStylesheet('wforms-jsonly.css');


if (typeof(wFORMS) == "undefined") {
	throw new Error("wFORMS core not found. This behavior depends on the wFORMS core.");
}
/**
 * wForms hint behavior. Show/highlight an HTML element when the associated input gets the focus.
 */
wFORMS.behaviors.hint  = { 
	
	/**
	 * Inactive CSS class for the element
     * @final
	 */
	CSS_INACTIVE : 'field-hint-inactive',

	/**
	 * Active CSS class for the element
     * @final
	 */
	CSS_ACTIVE : 'field-hint',

	/**
	 * Selector expression for the hint elements
     * @final
     * @see	http://www.w3.org/TR/css3-selectors/
	 */
	HINT_SELECTOR : '*[id$="-H"]',

	/**
	 * Suffix of the ID for the hint element
     * @final
	 */
	HINT_SUFFIX : '-H',

	/**
	 * Creates new instance of the behavior
     * @constructor
	 */
	instance : function(f) {
		this.behavior = wFORMS.behaviors.hint; 
		this.target = f;
	}
}

/**
 * Factory Method.
 * Applies the behavior to the given HTML element by setting the appropriate event handlers.
 * @param {domElement} f An HTML element, either nested inside a FORM element or (preferably) the FORM element itself.
 * @return {object} an instance of the behavior 
 */	
wFORMS.behaviors.hint.applyTo = function(f) {
	var b = new wFORMS.behaviors.hint.instance(f);
	// Selects all hints elements using predefined selector and attaches
	// event listeners to related HTML elements for each hint
	f.matchAll(wFORMS.behaviors.hint.HINT_SELECTOR).forEach(
		function(elem){
			
			// ID attribute is not checked here because of selector already contains it
			// if selector is changed, ID check should also exists
			// if(!elem.id) { return ; }
			var e = b.getElementByHintId(elem.id);
			if(e){
				if(!e.addEventListener) base2.DOM.bind(e);
				if(e.tagName.match(/(select)|(input)|(textarea)/i)){		
					if(e.attachEvent) {
						// Weird behavior in IE when using Base2 addEventListener implementation
						// all focus/blur events on each instance of a repeated input are triggered. 
						// with attachEvent, only the hints on the master and the copy are triggered
						// .. maybe a side-effect of using cloneNode in repeat behavior.  
						e.attachEvent('onfocus', function() { b.run(window.event, e)});
						e.attachEvent('onblur',  function() { b.run(window.event, e)});
					} else {
						e.addEventListener('focus', function(event) { b.run(event, e)}, false);
						e.addEventListener('blur', function(event) { b.run(event, e)}, false);
					}			
				} else {
					e.addEventListener('mouseover', function(event) { b.run(event, e)}, false);
					e.addEventListener('mouseout', function(event) { b.run(event, e)}, false);
				}
			}
		}
	);

	return b;
}

/**
 * Executes the behavior
 * @param {event} event
 * @param {domElement} elem
 */
wFORMS.behaviors.hint.instance.prototype.run = function(event, element) { 	
	var hint = this.getHintElement(element);
	if(!hint) return;
	if(event.type == 'focus' || event.type == 'mouseover'){
		hint.removeClass(wFORMS.behaviors.hint.CSS_INACTIVE)
		hint.addClass(wFORMS.behaviors.hint.CSS_ACTIVE);
		this.setup(hint, element);
	} else{
		hint.addClass(wFORMS.behaviors.hint.CSS_INACTIVE);
		hint.removeClass(wFORMS.behaviors.hint.CSS_ACTIVE);
	}
}


/**
 * Returns HTMLElement related to specified hint ID
 * @returns	{HTMLElement}
 */
wFORMS.behaviors.hint.instance.prototype.getElementByHintId = function(hintId){
	var id = hintId.substr(0, hintId.length - wFORMS.behaviors.hint.HINT_SUFFIX.length);
	var e = document.getElementById(id);
	return e;
}

/**
 * Returns HTMLElement Hint element associated with element event catched from
 * @returns	{HTMLElement}
 */
wFORMS.behaviors.hint.instance.prototype.getHintElement = function(element){
	var e = document.getElementById(element.id + this.behavior.HINT_SUFFIX);
	return e && e != '' ? e : null;
}

/**
 * Setups hint position on the screen depend on the element
 * @param	{HTMLElement}	hint	Hint HTML element
 * @param   {HTMLElement}	source	HTML element with focus.
 */
wFORMS.behaviors.hint.instance.prototype.setup = function(hint, source){
	hint.style.left = ((source.tagName == 'SELECT' ? 
		 + source.offsetWidth : 0) + wFORMS.helpers.getLeft(source)) + "px";
	hint.style.top  = (wFORMS.helpers.getTop(source) + source.offsetHeight) + "px";	
}

/**
 * Returns if ID is of the HINT element. Used by repeat behavior to correctly 
 * update hint ID
 * @param	{DOMString}	id
 * @return	boolean
 */
wFORMS.behaviors.hint.isHintId = function(id){
	return id.match(new RegExp(wFORMS.behaviors.hint.HINT_SUFFIX + '$')) != null;
}


if (typeof(wFORMS) == "undefined") {
	throw new Error("wFORMS core not found. This behavior depends on the wFORMS core.");
}
/**
 * wForms paging behavior. 
 * See: http://www.formassembly.com/blog/the-pagination-behavior-explained/
 */
wFORMS.behaviors.paging = {

	/**
	 * Selector expression for catching elements
     * @final
     * @see	http://www.w3.org/TR/css3-selectors/
	 */
	SELECTOR : '*[class~="wfPage"]',

	/**
	 * CSS class indicates page
     * @final
	 */
	CSS_PAGE : 'wfPage',

	/**
	 * CSS class for current page
     * @final
	 */
	CSS_CURRENT_PAGE : 'wfCurrentPage',

	/**
	 * CSS class for next button
     * @final
	 */
	CSS_BUTTON_NEXT : 'wfPageNextButton',

	/**
	 * CSS class for next button
     * @final
	 */
	CSS_BUTTON_PREVIOUS : 'wfPagePreviousButton',
	
	/**
	 * CSS class for the div contains the previous/next buttons
     * @final
	 */
	CSS_BUTTON_PLACEHOLDER : 'wfPagingButtons',
	
	/**
	 * ID prefix for the next buttons
     * @final
	 */
	ID_BUTTON_NEXT_PREFIX : 'wfPageNextId',

	/**
	 * ID prefix for the previos buttons
     * @final
	 */
	ID_BUTTON_PREVIOUS_PREFIX : 'wfPagePreviousId',

	/**
	 * CSS class for hidden submit button
     * @final
	 */
	CSS_SUBMIT_HIDDEN : 'wfHideSubmit',

	/**
	 * ID attribute prefix for page area
     * @final
	 */
	ID_PAGE_PREFIX	: 'wfPgIndex-',

	/**
	 * ID attribute suffix for prev/next buttons placeholder
     * @final
	 */
	ID_PLACEHOLDER_SUFFIX : '-buttons',

	/**
	 * Attribute indicates index of the page button should activate
     * @final
	 */
	ATTR_INDEX : 'wfPageIndex_activate',

	/**
	 * Custom messages used for creating links
     * @final
	 */
	MESSAGES : {
		CAPTION_NEXT : 'Next Page',
		CAPTION_PREVIOUS : 'Previous Page'
	},

	/**
     * Indicates that form should be validated on Next clicked
     * TODO		Possible refactor functionality with validation
	 */
	runValidationOnPageNext : true,

	/**
	 * Creates new instance of the behavior
     * @param	{HTMLElement}	f	Form element
     * @constructor
	 */
	instance: function(f) {
		this.behavior = wFORMS.behaviors.paging; 
		this.target = f;

		this.currentPageIndex = 1;
	}
}

/**
 * Factory Method.
 * Applies the behavior to the given HTML element by setting the appropriate event handlers.
 * @param {domElement} f An HTML element, either nested inside a FORM element or (preferably) the FORM element itself.
 * @return {object} an instance of the behavior 
 */	
wFORMS.behaviors.paging.applyTo = function(f) {
	var b = new wFORMS.behaviors.paging.instance(f)
	var behavior = wFORMS.behaviors.paging;
	var isValidationAccepted = (wFORMS.behaviors.validation && wFORMS.behaviors.paging.runValidationOnPageNext);
	// Shows that form contains paging
	var isPagingApplied = false;
	// Iterates over the elements with specified class names
	f.matchAll(wFORMS.behaviors.paging.SELECTOR).forEach(
		function(elem){
			isPagingApplied = true;
			// Creates placeholder for buttons
			var ph = b.getOrCreatePlaceHolder(elem);
			var index = wFORMS.behaviors.paging.getPageIndex(elem);
			// If first page add just Next button
			if(index == 1){
				var ctrl = base2.DOM.bind(ph.appendChild(behavior._createNextPageButton(index)));
				
				if(isValidationAccepted){					
					ctrl.addEventListener('click', function(event) {							
							var v = wFORMS.getBehaviorInstance(b.target,'validation'); 
							if(v.run(event, elem)){b.run(event, ctrl);} 
						}, 
						false);					
				}else{
					ctrl.addEventListener('click', function(event) { b.run(event, ctrl); }, false);
				}

				wFORMS.behaviors.paging.showPage(elem);
			}else{
				// Adds previous button
				var ctrl = base2.DOM.bind(behavior._createPreviousPageButton(index));
				ph.insertBefore(ctrl, ph.firstChild);

				ctrl.addEventListener('click', function(event) { b.run(event, ctrl)}, false);

				// If NOT last page adds next button also
				if(!wFORMS.behaviors.paging.isLastPageIndex(index, true)){
					var _ctrl = base2.DOM.bind(ph.appendChild(behavior._createNextPageButton(index)));

					if(isValidationAccepted){						
						_ctrl.addEventListener('click', function(event) {
							var v = wFORMS.getBehaviorInstance(b.target,'validation'); 							 
							if(v.run(event, elem)){b.run(event, _ctrl);} 
						}, false);
					}else{
						_ctrl.addEventListener('click', function(event) { b.run(event, _ctrl); }, false);
					}
				}
			}
		}
	);
	// Looking for the first active page from 0. 0 is a "fake page"
	if(isPagingApplied){		
		p = b.findNextPage(0);
		b.currentPageIndex = 0;
		b.activatePage(wFORMS.behaviors.paging.getPageIndex(p));
	}

	return b;
}


/**
 * Returns page index by the page area element
 * @param	{HTMLElement}	elem
 * @return	{Integer}	or false
 */
wFORMS.behaviors.paging.getPageIndex = function(elem){
	if(elem && elem.id){
		var index = elem.id.replace(
			new RegExp(wFORMS.behaviors.paging.ID_PAGE_PREFIX + '(\\d+)'), "$1");

		index = parseInt(index);
		return !isNaN(index) ? index : false;

	}

	return false;
}

/**
 * Private method for creating button. Uses public method for design creating
 * @param	{Integer}	index 	Index of the page button belongs to
 * @return	{HTMLElement}
 * @private
 * @see wFORMS.behaviors.paging.createNextPageButton
 */
wFORMS.behaviors.paging._createNextPageButton = function(index){
	var elem = wFORMS.behaviors.paging.createNextPageButton();
	elem.setAttribute(wFORMS.behaviors.paging.ATTR_INDEX, index + 1);
	elem.id = wFORMS.behaviors.paging.ID_BUTTON_NEXT_PREFIX + index;

	return elem;
}

/**
 * Creates button for moving to the next page. This method could be overridden
 * And developed for easily customization for users. Behavior uses private method
 * @return	{HTMLElement}
 * @public
 */
wFORMS.behaviors.paging.createNextPageButton = function(){
	var elem = document.createElement('input'); 
	elem.setAttribute('value', wFORMS.behaviors.paging.MESSAGES.CAPTION_NEXT);
	elem.setAttribute('type', 'button');

	elem.className = wFORMS.behaviors.paging.CSS_BUTTON_NEXT;

	return elem;
}

/**
 * Private method for creating button. Uses public method for design creating
 * @param	{Integer}	index 	Index of the page button belongs to
 * @return	{HTMLElement}
 * @private
 * @see wFORMS.behaviors.paging.createPreviousPageButton
 */
wFORMS.behaviors.paging._createPreviousPageButton = function(index){
	var elem = wFORMS.behaviors.paging.createPreviousPageButton();
	elem.setAttribute(wFORMS.behaviors.paging.ATTR_INDEX, index - 1);
	elem.id = wFORMS.behaviors.paging.ID_BUTTON_PREVIOUS_PREFIX + index;;

	return elem;
}

/**
 * Creates button for moving to the next page. This method could be overridden
 * And developed for easily customization for users. Behavior uses private method
 * @return	{HTMLElement}
 * @public
 */
wFORMS.behaviors.paging.createPreviousPageButton = function(){
	var elem = document.createElement('input'); 
	elem.setAttribute('value', wFORMS.behaviors.paging.MESSAGES.CAPTION_PREVIOUS);
	elem.setAttribute('type', 'button');

	elem.className = wFORMS.behaviors.paging.CSS_BUTTON_PREVIOUS;

	return elem;
}

/**
 * Creates place holder for buttons
 * @param	{HTMLElement}	pageElem	Page where placeholder should be created
 * @return	{HTMLElement}
 */
wFORMS.behaviors.paging.instance.prototype.getOrCreatePlaceHolder = function(pageElem){
	var id = pageElem.id + wFORMS.behaviors.paging.ID_PLACEHOLDER_SUFFIX;
	var elem = document.getElementById(id);

	if(!elem){
		elem = pageElem.appendChild(document.createElement('div'));
		elem.id = id;
		elem.className = wFORMS.behaviors.paging.CSS_BUTTON_PLACEHOLDER;
	}	

	return elem;
}

/**
 * Hides page specified
 * @param	{HTMLElement}	e
 */
wFORMS.behaviors.paging.hidePage = function(e){
	if(e) {
		e.removeClass(wFORMS.behaviors.paging.CSS_CURRENT_PAGE);
		e.addClass(wFORMS.behaviors.paging.CSS_PAGE);
	}
}

/**
 * Shows page specified
 * @param	{HTMLElement}	e
 */
wFORMS.behaviors.paging.showPage = function(e){
	if(e) {
		e.removeClass(wFORMS.behaviors.paging.CSS_PAGE);
		e.addClass(wFORMS.behaviors.paging.CSS_CURRENT_PAGE);
	}
}

/**
 * Activates page by index
 * @param	{Integer}	index	
 */
wFORMS.behaviors.paging.instance.prototype.activatePage = function(index){

	if(index == this.currentPageIndex){
		return false;
	}
	
	// hack
	index = parseInt(index);
	//hack
	if(index > this.currentPageIndex){
		var p = this.findNextPage(this.currentPageIndex);
	} else {
		var p = this.findPreviousPage(this.currentPageIndex);
	}
	if(p) { 
		// rewrite index
		var index = wFORMS.behaviors.paging.getPageIndex(p);
	
		var b = wFORMS.behaviors.paging;
	
		// Workaround for Safari. Otherwise it crashes with Safari 1.2
		// Looks like hiding should be ran in "different thread"
		var clazz = this;
		setTimeout(
			function(){
				clazz.setupManagedControls(index);
				b.hidePage(b.getPageByIndex(clazz.currentPageIndex));				
				b.showPage(p);
				clazz.currentPageIndex = index;
			}, 1
		);
	}
}

/**
 * Setups managed controls: Next/Previous/Send buttons
 * @param	{int}	index	Index of the page to make controls setting up. If null setups current page
 */
wFORMS.behaviors.paging.instance.prototype.setupManagedControls = function(index){
	// new 
	if(!index){
		index = this.currentPageIndex;
	}
	
	// new
	var b = wFORMS.behaviors.paging;
	if(b.isFirstPageIndex(index)){
		if(ctrl = b.getPreviousButton(index)){
			ctrl.style.display = 'none';
		}
	}else{
		if(ctrl = b.getPreviousButton(index)){
			ctrl.style.display = 'inline';
		}
	}

	if(b.isLastPageIndex(index)){
		if(ctrl = b.getNextButton(index)){
			ctrl.style.display = 'none';
		}
		this.showSubmitButtons();
	} else {
		if(ctrl = b.getNextButton(index)){
			ctrl.style.display = 'inline';
		}
		this.hideSubmitButtons();
	}
}

/**
 * Shows all submit buttons
 */
wFORMS.behaviors.paging.instance.prototype.showSubmitButtons = function(){
	this.target.matchAll('input[type~="submit"]').forEach(function(elem){
		elem.removeClass(wFORMS.behaviors.paging.CSS_SUBMIT_HIDDEN);
	});
}

/**
 * Hides all submit button
 */
wFORMS.behaviors.paging.instance.prototype.hideSubmitButtons = function(){
	this.target.matchAll('input[type~="submit"]').forEach(function(elem){
		elem.addClass(wFORMS.behaviors.paging.CSS_SUBMIT_HIDDEN);
	});
}

/**
 * Returns page element specified by index
 * @param	{Integer}	index
 * @return	{HTMLElement}
 */
wFORMS.behaviors.paging.getPageByIndex = function(index){
	var page = document.getElementById(wFORMS.behaviors.paging.ID_PAGE_PREFIX + index);
	return page ? base2.DOM.bind(page) : false;
}

/**
 * Returns next button specified by index
 * @param	{int}	index	Index of the page button related to
 * @return	{HTMLElement}
 */
wFORMS.behaviors.paging.getNextButton = function(index){
	// base2 is not using here because of when control is absen it produces an error in IE
	// for example on last page there is not Next button, on first - Previous
	return document.getElementById(wFORMS.behaviors.paging.ID_BUTTON_NEXT_PREFIX + index);
}

/**
 * Returns previous button specified by index
 * @param	{int}	index	Index of the page button related to
 * @return	{HTMLElement}
 */
wFORMS.behaviors.paging.getPreviousButton = function(index){
	// base2 is not using here because of when control is absen it produces an error in IE
	// for example on last page there is not Next button, on first - Previous
	return document.getElementById(wFORMS.behaviors.paging.ID_BUTTON_PREVIOUS_PREFIX + index);
}

/**
 * Check if index passed is index of the last page
 * @param	{Integer}	index
 * @param	{bool}	ignoreSwitch	Ingoneres switch behavior when checking for last index
 * @return	{bool}
 */
wFORMS.behaviors.paging.isLastPageIndex = function(index, ignoreSwitch){
	index = parseInt(index) + 1;
	var b = wFORMS.behaviors.paging;
	var p = b.getPageByIndex(index);

	if((_b = wFORMS.behaviors['switch']) && !ignoreSwitch){
		while(p && _b.isSwitchedOff(p)){
			index++;
			p = b.getPageByIndex(index);
		}
	}

	return p ? false : true;
}

/**
 * Check if index passed is index of the first page
 * @param	{Integer}	index
 * @param	{bool}	ignoreSwitch	Ingoneres switch behavior when checking for first index
 * @return	{bool}
 */
wFORMS.behaviors.paging.isFirstPageIndex = function(index, ignoreSwitch){
	index = parseInt(index) - 1;
	var b = wFORMS.behaviors.paging;
	var p = b.getPageByIndex(index);
	if((_b = wFORMS.behaviors['switch']) && !ignoreSwitch){
		while(p && _b.isSwitchedOff(p)){
			index--;
			p = b.getPageByIndex(index);
		}
	}

	return p ? false : true;
}

/**
 * Returns Next page from the index. Takes in attention switch behavior
 * @param	{int}	index
 */
wFORMS.behaviors.paging.instance.prototype.findNextPage = function(index){
	index = parseInt(index) + 1;
	var b = wFORMS.behaviors.paging;
	var p = b.getPageByIndex(index);

	if(_b = wFORMS.behaviors['switch']){
		while(p && _b.isSwitchedOff(p)){
			index++;
			p = b.getPageByIndex(index);
		}
	}

	return p ? p : true;
}

/**
 * Returns Next page from the index. Takes in attention switch behavior
 * @param	{int}	index
 */
wFORMS.behaviors.paging.instance.prototype.findPreviousPage = function(index){
	index = parseInt(index) - 1;
	var b = wFORMS.behaviors.paging;
	var p = b.getPageByIndex(index);

	if(_b = wFORMS.behaviors['switch']){
		while(p && _b.isSwitchedOff(p)){
			index--;
			p = b.getPageByIndex(index);
		}
	}

	return p ? p : false;
}



/**
 * Executes the behavior
 * @param {event} e 
 * @param {domElement} element
 */
wFORMS.behaviors.paging.instance.prototype.run = function(e, element){
	this.activatePage(element.getAttribute(wFORMS.behaviors.paging.ATTR_INDEX));
}

	
if (typeof(wFORMS) == "undefined") {
	throw new Error("wFORMS core not found. This behavior depends on the wFORMS core.");
}
/**
 * wForms repeat behavior. 
 * See: http://www.formassembly.com/wForms/v2.0/documentation/examples/repeat.html
 */
wFORMS.behaviors.repeat = {

	/**
	 * Selector expression for catching repeat elements
     * @final
     * @see	http://www.w3.org/TR/css3-selectors/
	 */
	SELECTOR_REPEAT : '*[class~="repeat"]',

	/**
	 * Selector expression for catching removable section
     * @final
     * @see	http://www.w3.org/TR/css3-selectors/
	 */
	SELECTOR_REMOVEABLE : '*[class~="removeable"]',

	/**
	 * Suffix for the ID of the element that is a control field to make a duplicate 
     * of the section specififed
     * @final
	 */
	ID_SUFFIX_DUPLICATE_LINK : '-wfDL',

	/**
	 * Suffix for the ID of the repeat counter hidden element
     * @final
	 */
	ID_SUFFIX_COUNTER : '-RC',

	/**
	 * CSS class for duplicate link
     * @final
	 */
	CSS_DUPLICATE_LINK : 'duplicateLink',

	/**
	 * CSS class for delete link
     * @final
	 */
	CSS_DELETE_LINK : 'removeLink',

	/**
	 * CSS class for field group that could be removed
     * @final
	 */
	CSS_REMOVEABLE : 'removeable',

	/**
	 * CSS class for field group that could be repeat
     * @final
	 */
	CSS_REPEATABLE : 'repeat',

	/**
	 * Attribute specifies that current group is duplicate
     * @final
	 */
	ATTR_DUPLICATE : 'wfr__dup',

	/**
	 * Attribute specifies that current group is duplicate
     * @final
	 */
	ATTR_DUPLICATE_ELEM : 'wfr__dup_elem',


    /**
     * Means that element has been already handled by repeat behavior
     */
	ATTR_HANDLED : 'wfr_handled',

	/**
	 * Attribute specifies ID of the master section on its dublicate
     * @final
	 */
	ATTR_MASTER_SECTION : 'wfr__master_sec',

	/**
	 * Special attribute name that is set to Remove link with section ID
     * should be deleted when link is clicked
     * @final
	 */
	ATTR_LINK_SECTION_ID : 'wfr_sec_id',

	/**
	 * Messages collection used for creating links
     * @final
	 */
	MESSAGES : {
		ADD_CAPTION : "Add another response",
		ADD_TITLE : "Will duplicate this question or section.",

		REMOVE_CAPTION : "Remove",
		REMOVE_TITLE : "Will remove this question or section"
	},

	/**
	 * Array of the attribute names that shoud be updated in the duplicated tree
	 */
	UPDATEABLE_ATTR_ARRAY : [
		'id',
		'name',
		'for'
	],

	/**
	 * Allows to leave names of the radio buttons the same (behavior-wide setting)
	 */
	preserveRadioName : false,
	
	/**
	 * Allows to leave names of the radio buttons the same (field-level setting)
	 * This class attribute can be set on a repeated element to override the
	 * behavior's preserveRadioName setting.
	 */
	CSS_PRESERVE_RADIO_NAME: "preserveRadioName",
	
	/**
	 * Custom function that could be overridden. 
	 * Evaluates when section is duplicated
     * @param	{HTMLElement}	elem	Duplicated section
	 */
	onRepeat : function(elem){
	},

	/**
	 * Custom function that could be overridden. 
	 * Evaluates when section is removed
	 */
	onRemove : function(){
	},

	/**
	 * Custom function that could be overridden. 
	 * Returns if section could be repeated
     * @param	{HTMLElement}	elem	Section to be duplicated
     * @param	{wFORMS.behaviors.repeat}	b	Behavior mapped to repeatable section 
     * @return	boolean
	 */
	allowRepeat : function(elem, b){
		return true;
	},

	/**
	 * Creates new instance of the behavior
     * @param	{HTMLElement}	f	Form element
     * @constructor
	 */
	instance : function(f) {
		this.behavior = wFORMS.behaviors.repeat; 
		this.target = f;
	}
}

/**
 * Factory Method.
 * Applies the behavior to the given HTML element by setting the appropriate event handlers.
 * @param {domElement} f An HTML element, either nested inside a FORM element or (preferably) the FORM element itself.
 * @return {object} an instance of the behavior 
 */	
wFORMS.behaviors.repeat.applyTo = function(f) {
	// look up for the all elements that could be repeated.
	// Trying to add event listeners to elements for adding new container.
	// If need create Add new section element
	var _self = this;
	var b = new Array();

	f.matchAll(this.SELECTOR_REPEAT).forEach(
		function(elem){
			if(_self.isHandled(elem)){
				return ;
			}
			if(!elem.id) elem.id = wFORMS.helpers.randomId();
			
			var _b = new _self.instance(elem);
			var e = _b.getOrCreateRepeatLink(elem);
			e.addEventListener('click', function(event) { _b.run(event, e) }, false);
			b.push(_b);	
			
			// Sets element to handled state and iterates its child matching for
			// repeat behavior. The idea is to have different instances of repeat
			// behavior for every group that could be duplicated
			// for all indexes for each group begin from zero
			_self.handleElement(elem);			
			//elem.matchAll(_self.SELECTOR_REPEAT).forEach(
			//	function(__elem){
			//		_self.applyTo(__elem);
			//	}
			//);
		}
	);
	// When section is duplicated remove link should be added to it
	// It is done here because for each new section new behavior instance is created
	// And event from the correct instance should be assigned to Remove Link
	var addRemoveLinkToSection= function(elem){
		e = _self.createRemoveLink(elem.id);
		// looking for the place where to paste link
		if(elem.tagName == 'TR'){
			var tds = elem.getElementsByTagName('TD');
			var tdElem = tds[tds.length-1];
			tdElem.appendChild(e);
		} else {
			elem.appendChild(e)
		}
	}

	if(f.hasClass(wFORMS.behaviors.repeat.CSS_REMOVEABLE)){
		addRemoveLinkToSection(f);
	}
	
	f.matchAll(this.SELECTOR_REMOVEABLE).forEach(function(e){
		// 
		addRemoveLinkToSection(e);
	});
	
	return b;
}

/**
 * Returns repeat link for specified area if it exists, 
 * otherwise creates new one and returns it
 * @param	{HTMLElement}	elem	Element repeat link is related to
 * @return	{HTMLElement}
 */
wFORMS.behaviors.repeat.instance.prototype.getOrCreateRepeatLink = function(elem){
	var id = elem.id + wFORMS.behaviors.repeat.ID_SUFFIX_DUPLICATE_LINK
	// Using getElementById except matchSingle because of lib bug
	// when element is not exists exception is thrown
	var e = document.getElementById(id);
	if(!e || e == ''){
		e = this.createRepeatLink(id);
		if(elem.tagName.toUpperCase() == 'TR'){
			// TODO check again td:last-child
			var tdElem = elem.matchSingle("td:nth-last-child(0n+1)");
			if(tdElem == ''){
				tdElem = elem.appendChild(document.createElement('TD'));
			}
			tdElem.appendChild(e);
		}else{
			elem.appendChild(e)
		}
	}

	return base2.DOM.bind(e);
}

/**
 * Returns repeat link for specified area if it exists, 
 * otherwise creates new one and returns it
 * @param	{DOMString}	id	ID of the group
 * @return	{HTMLElement}
 */
wFORMS.behaviors.repeat.instance.prototype.createRepeatLink = function(id){
	// Creates repeat link element
	var linkElem = document.createElement("a");

	linkElem.id = id;
	linkElem.setAttribute('href', '#');	
	linkElem.className = wFORMS.behaviors.repeat.CSS_DUPLICATE_LINK;
	linkElem.setAttribute('title', wFORMS.behaviors.repeat.MESSAGES.ADD_TITLE);	

	// Appends text inside the <span element (CSS changing purposes) to <a element
	linkElem.appendChild(document.createElement('span').appendChild(
		document.createTextNode(wFORMS.behaviors.repeat.MESSAGES.ADD_CAPTION)));

	return linkElem;
}

/**
 * Returns remove link for specified area 
 * @param	{DOMString}	id	ID of the field group
 * @return	{HTMLElement}
 */
wFORMS.behaviors.repeat.createRemoveLink = function(id){
	// Creates repeat link element
	var linkElem = document.createElement("a");
	
	linkElem.id = id + wFORMS.behaviors.repeat.ID_SUFFIX_DUPLICATE_LINK;
	linkElem.setAttribute('href', '#');	
	linkElem.className = wFORMS.behaviors.repeat.CSS_DELETE_LINK;
	linkElem.setAttribute('title', wFORMS.behaviors.repeat.MESSAGES.REMOVE_TITLE);	
	linkElem.setAttribute(wFORMS.behaviors.repeat.ATTR_LINK_SECTION_ID, id);

	// Appends text inside the <span element (for CSS image replacement) to <a element
	var spanElem = document.createElement('span');
	spanElem.appendChild(document.createTextNode(wFORMS.behaviors.repeat.MESSAGES.REMOVE_CAPTION));
	linkElem.appendChild(spanElem);

	linkElem.onclick = function(event) { wFORMS.behaviors.repeat.onRemoveLinkClick(event, linkElem); };	

	
	return linkElem;
}


/**
 * Returns target area that should be copyied by repeat link element
 * @param	{HTMLElement}	elem	Repeat Link Element
 * @return	{HTMLElement}
 * @DEPRECATED
 */
wFORMS.behaviors.repeat.instance.prototype.getTargetByRepeatLink = function(elem){
	return this.target.matchSingle('#' + 
		elem.id.substring(0, elem.id.length - wFORMS.behaviors.repeat.ID_SUFFIX_DUPLICATE_LINK.length));

}

/**
 * Duplicates repeat section. Changes ID of the elements, adds event listeners
 * @param	{HTMLElement}	elem	Element to duplicate
 */
wFORMS.behaviors.repeat.instance.prototype.duplicateSection = function(elem){
	// Call custom function. By default return true
	if(!this.behavior.allowRepeat(elem, this)){
		return false;
	}
	this.updateMasterSection(elem);
	
	// Creates clone of the group
	// TODO: Cloning a radio group results in the loss of the selection (in Firefox at least)
	var newElem = base2.DOM.bind(elem.cloneNode(true));
	// Looking for the place where to add group
	var insertNode = elem;
	while(insertNode && 
		 (insertNode.nodeType==3 ||       // skip text-node that can be generated server-side when populating a previously repeated group 
		  insertNode.hasClass(wFORMS.behaviors.repeat.CSS_REMOVEABLE)  || 
		  insertNode.hasClass(wFORMS.behaviors.repeat.CSS_REPEATABLE))) {						
		insertNode = insertNode.nextSibling;
		if(insertNode && insertNode.nodeType==1 && !insertNode.hasClass) {
			insertNode = base2.DOM.bind(insertNode);
		}
	}
	
	elem.parentNode.insertBefore(newElem, insertNode);
	
	this.updateDuplicatedSection(newElem);	
	wFORMS.applyBehaviors(newElem);
	// Calls custom function
	wFORMS.behaviors.repeat.onRepeat(newElem);
}

/**
 * Removes section specified by id
 * @param	{DOMString}	id
 */
wFORMS.behaviors.repeat.removeSection = function(id){
	var elem = document.getElementById(id);

	// Removes section
	if(elem != ''){
		// SHOULD NOT Decrease count of the sections
		// or else, REPEAT TWICE + DELETE FIRST + REPEAT AGAIN => field name collision    
		//var cElem = wFORMS.behaviors.repeat.getOrCreateCounterField(
		//	wFORMS.behaviors.repeat.getMasterSection(elem));
		//cElem.value = parseInt(cElem.value) - 1;
		
		elem.parentNode.removeChild(elem);

		// Calls custom function
		wFORMS.behaviors.repeat.onRemove();
	}
}

/**
 * Evaluates when user clicks Remove link
 * @param	{DOMEvent}	Event	catched
 * @param	{HTMLElement}	elem	Element produced event
 */
wFORMS.behaviors.repeat.onRemoveLinkClick = function(event, elem){
	this.removeSection(elem.getAttribute(wFORMS.behaviors.repeat.ATTR_LINK_SECTION_ID));
	if(event) event.preventDefault();
}

/**
 * Updates attributes inside the master element
  * @param	{HTMLElement}	elem
 */
wFORMS.behaviors.repeat.instance.prototype.updateMasterSection = function(elem){
	// do it once 
	if(elem.doItOnce==true)
		return true;
	else
		elem.doItOnce=true;

	var suffix = this.createSuffix(elem);
	elem.id = this.clearSuffix(elem.id) + suffix;
	
	this.updateMasterElements(elem, suffix);	
}
wFORMS.behaviors.repeat.instance.prototype.updateMasterElements  = function(elem, suffix){
	
	if(!elem || elem.nodeType!=1) 
		return;
	
	var cn = elem.childNodes;
	for(var i=0;i<cn.length;i++) {
		var n = cn[i];
		if(n.nodeType!=1) continue;
		if(!n.hasClass) {
			base2.DOM.bind(n);
		}
		if(n.hasClass(wFORMS.behaviors.repeat.CSS_REPEATABLE))
			suffix += "[0]";
		if(!n.hasClass(wFORMS.behaviors.repeat.CSS_REMOVEABLE)){
			// Iterates over updateable attribute names
			for(var j = 0; j < wFORMS.behaviors.repeat.UPDATEABLE_ATTR_ARRAY.length; j++){
				var attrName = wFORMS.behaviors.repeat.UPDATEABLE_ATTR_ARRAY[j];
				var value = this.clearSuffix(n.getAttribute(attrName));
				if(!value){
					continue;
				}
				
				if(wFORMS.behaviors.hint && wFORMS.behaviors.hint.isHintId(n.id)){
					n.setAttribute(attrName, value.replace(new RegExp("(.*)(" + wFORMS.behaviors.hint.HINT_SUFFIX + ')$'),
						"$1" + suffix + "$2"));
				}else if(n.id.indexOf(wFORMS.behaviors.repeat.ID_SUFFIX_DUPLICATE_LINK) != -1){
					n.setAttribute(attrName, value.replace(new RegExp("(.*)(" + wFORMS.behaviors.repeat.ID_SUFFIX_DUPLICATE_LINK + ')$'),
						"$1" + suffix + "$2"));
				}else{
					n.setAttribute(attrName, value + suffix);					
				}
			}
			
			this.updateMasterElements(n, suffix);
		}
	}
}

/**
 * Updates attributes inside the duplicated tree
 * TODO rename
 * @param	{HTMLElement}	elem
 */
wFORMS.behaviors.repeat.instance.prototype.updateDuplicatedSection = function(elem){
	var clazz = this;
//	var idSuffix = this.getNextDuplicateIndex(this.target);

	// elem.setAttribute('dindex', this.getNextDuplicateIndex(this.target))
	var index  = this.getNextDuplicateIndex(this.target);
	var suffix = this.createSuffix(elem, index);

	// Caches master section ID in the dublicate
	elem.setAttribute(wFORMS.behaviors.repeat.ATTR_MASTER_SECTION, elem.id);
	// Updates element ID (possible problems when repeat element is Hint or switch etc)
	elem.id = this.clearSuffix(elem.id) + suffix;
	// Updates classname	
	elem.className = elem.className.replace(wFORMS.behaviors.repeat.CSS_REPEATABLE, wFORMS.behaviors.repeat.CSS_REMOVEABLE);

	// Check for preserverRadioName override
	if(elem.hasClass(wFORMS.behaviors.repeat.CSS_PRESERVE_RADIO_NAME)) 
		var _preserveRadioName = true;
	else
		var _preserveRadioName = wFORMS.behaviors.repeat.preserveRadioName;


	this.updateSectionChildNodes(elem.matchAll('> *'), suffix, _preserveRadioName);
}

// new {{{


/**
 * Updates NodeList. Changes ID and names attributes
 * For different node elements suffixes could be different - i.e. for the nested
 * repeat section IDs and names should store parent section number
 * @param	elems	Array of the elements should be updated
 * @param	suffix	Suffix value should be added to attributes
 */
wFORMS.behaviors.repeat.instance.prototype.updateSectionChildNodes = function(elems, suffix, preserveRadioName){
	
	var clazz = this;
    elems.forEach(function(e){
    	
		// Removes created descendant duplicated group if any
		if(wFORMS.behaviors.repeat.isDuplicate(e)){
			e.parentNode.removeChild(e);
			return;
		}
		// Removes duplicate link
		if(e.hasClass(wFORMS.behaviors.repeat.CSS_DUPLICATE_LINK)){
			e.parentNode.removeChild(e);
			return ;
		}
				
		// Clears value	
		var tagName = e.tagName.toUpperCase();
		if(tagName == 'INPUT' || tagName == 'TEXTAREA'){
				
			if(e.type != 'radio' && e.type != 'checkbox'){
				e.value = '';
			}else{
				e.checked = false;
			}
		}
		
		clazz.updateAttributes(e, suffix, preserveRadioName);
		
		if(_elems = e.matchAll('> *')){
			if(e.hasClass(wFORMS.behaviors.repeat.CSS_REPEATABLE)){
				clazz.updateSectionChildNodes(_elems, 
					wFORMS.behaviors.repeat.instance.prototype.createSuffix(e), 
					preserveRadioName);

			}else{
				clazz.updateSectionChildNodes(_elems, suffix, preserveRadioName);
			}
		}
    });
    
}

/**
 * Creates suffix that should be used inside duplicated repeat section
 * @param	e	Repeat section element
 */
wFORMS.behaviors.repeat.instance.prototype.createSuffix = function(e, index){

	// var idx = e.getAttribute('dindex');
	var suffix = '[' + (index ? index : '0' ) + ']';
    var reg = /\[(\d+)\]$/;
	e = e.parentNode;
	while(e){
		if(e.hasClass && (e.hasClass(wFORMS.behaviors.repeat.CSS_REPEATABLE) ||
			e.hasClass(wFORMS.behaviors.repeat.CSS_REMOVEABLE))){
			var idx = reg.exec(e.id);
			if(idx) idx = idx[1];
			//var idx = e.getAttribute('dindex');
			suffix = '[' + (idx ? idx : '0' ) + ']' + suffix;
		}
		e = e.parentNode;
	}
	return suffix;
}

/**
 * Removes suffix from ID id was previously set
 * @param	id	Current element id
 * @return	DOMString
 */
wFORMS.behaviors.repeat.instance.prototype.clearSuffix = function(value){
	if(!value){
		return;
	}
	if(value.indexOf('[') != -1){		
		return value.substring(0, value.indexOf('['));
	}

	return value;
}

/**
 * Updates attributes of the element in the section
 * TODO rename
 * @param	{HTMLElement}	elem
 */
wFORMS.behaviors.repeat.instance.prototype.updateAttributes = function(e, idSuffix, preserveRadioName){
	var isHint = wFORMS.behaviors.hint && wFORMS.behaviors.hint.isHintId(e.id);
	var isDuplicate = e.id.indexOf(wFORMS.behaviors.repeat.ID_SUFFIX_DUPLICATE_LINK) != -1;
	// Sets that element belongs to duplicate group
	wFORMS.behaviors.repeat.setInDuplicateGroup(e);

	// TODO Check if it is neccessary
	if(wFORMS.behaviors.repeat.isHandled(e)){
		wFORMS.behaviors.repeat.removeHandled(e)
	}

	if(wFORMS.behaviors['switch'] && wFORMS.behaviors['switch'].isHandled(e)){
		wFORMS.behaviors['switch'].removeHandle(e);
	}

	// Iterates over updateable attribute names
	for(var i = 0; i < wFORMS.behaviors.repeat.UPDATEABLE_ATTR_ARRAY.length; i++){
		var attrName = wFORMS.behaviors.repeat.UPDATEABLE_ATTR_ARRAY[i];
		
		var value = this.clearSuffix(e.getAttribute(attrName));	
		if(!value){
			continue;
		}

		if(attrName == 'name' && e.tagName.toUpperCase() == 'INPUT' && preserveRadioName){
			continue;
		}
		if(isHint && attrName=='id'){			
			e.setAttribute('id', value + idSuffix + wFORMS.behaviors.hint.HINT_SUFFIX);
		}else if(isDuplicate){
			e.setAttribute(attrName, value.replace(new RegExp("(.*)(" + wFORMS.behaviors.repeat.ID_SUFFIX_DUPLICATE_LINK + ')$'),
				"$1" + idSuffix + "$2"));
		}else{
			e.setAttribute(attrName, value + idSuffix);
		}
	}
}

/**
 * Returns index of the next created duplicate by section HTML element
 * @param	{HTMLElement}	elem
 * @return	{Integer}
 */
wFORMS.behaviors.repeat.instance.prototype.getNextDuplicateIndex = function(elem){
	var c = wFORMS.behaviors.repeat.getOrCreateCounterField(elem);
	var newValue = parseInt(c.value) + 1;
	c.value = newValue;
	return newValue;
}


/**
 * Returns counter field fo specified area if exists. Otherwise creates new one
 * @param	{HTMLElement}	elem
 * @return	{HTMLElement}
 */
wFORMS.behaviors.repeat.getOrCreateCounterField = function(elem){
		
	var cId = elem.id + wFORMS.behaviors.repeat.ID_SUFFIX_COUNTER;
	
	// Using getElementById except matchSingle because of lib bug
	// when element is not exists exception is thrown
	var cElem = document.getElementById(cId);
	if(!cElem || cElem == ''){
		cElem = wFORMS.behaviors.repeat.createCounterField(cId);
		// Trying to find form element
		var formElem = elem.parentNode;
		while(formElem && formElem.tagName.toUpperCase() != 'FORM'){
			formElem = formElem.parentNode;
		}

		formElem.appendChild(cElem);
	}
	return cElem;
}

/**
 * Creates counter field with specified ID
 * @param	{DOMString}	id
 * @return	{HTMLElement}
 */
wFORMS.behaviors.repeat.createCounterField = function(id){
	cElem = base2.DOM.bind(document.createElement('input'));
	cElem.id = id;
	cElem.setAttribute('type', 'hidden');
	cElem.setAttribute('name', id);
	cElem.value = '0';

	return cElem;
}

/**
 * Returns count of already duplicated sections. If was called from the behavior 
 * belonged to dupolicated section, returns false
 * @public
 * @DEPRECATED
 * @return	{Integer} or {boolean}
 */
wFORMS.behaviors.repeat.instance.prototype.getDuplicatedSectionsCount = function(){
	var b = wFORMS.behaviors.repeat;
	if(b.isDuplicate(this.target)){
		return false;
	}

	return parseInt(b.getOrCreateCounterField(this.target).value);
}

/**
 * Returns count of already duplicated sections. If was called from the behavior 
 * belonged to duplicated section, returns false
 * @public
 * @return	{Integer} or {boolean}
 */
wFORMS.behaviors.repeat.instance.prototype.getSectionsCount = function(){
	var b = wFORMS.behaviors.repeat;
	if(b.isDuplicate(this.target)){
		return false;
	}

	return parseInt(b.getOrCreateCounterField(this.target).value) + 1;
}


/**
 * Returns true if element is duplicate of initial group, false otherwise
 * @param	{HTMLElement}	elem
 * @return	boolean
 */
wFORMS.behaviors.repeat.isDuplicate = function(elem){
	return elem.hasClass(wFORMS.behaviors.repeat.CSS_REMOVEABLE);
}

/**
 * Sets duplicate flag to element
 * @param	{HTMLElement}	elem
 */
wFORMS.behaviors.repeat.setDuplicate = function(elem){
	// elem.setAttribute(wFORMS.behaviors.repeat.ATTR_DUPLICATE, 'true');
}

/**
 * Returns true if element belongs to duplicate group
 * @param	{HTMLElement}	elem
 * @return	boolean
 */
wFORMS.behaviors.repeat.isInDuplicateGroup = function(elem){
	return elem.getAttribute(wFORMS.behaviors.repeat.ATTR_DUPLICATE_ELEM) ? true : false;
}

/**
 * Specifies that element is inside the duplicate group
 * @param	{HTMLElement}	elem
 * @return	boolean
 */
wFORMS.behaviors.repeat.setInDuplicateGroup = function(elem){
	return elem.setAttribute(wFORMS.behaviors.repeat.ATTR_DUPLICATE_ELEM, true);
}

/**
 * Checks if element is already handled
 * @param	{HTMLElement}	elem
 * @return	boolean
 */
wFORMS.behaviors.repeat.isHandled = function(elem){
	return elem.getAttribute(wFORMS.behaviors.repeat.ATTR_HANDLED);
}


/**
 * Returns html element of the master section (repeatable) from its duplicate
 * @param	{HTMLElement}	elem
 * @return	{HTMLElement} or false
 */
wFORMS.behaviors.repeat.getMasterSection = function(elem){
	if(!wFORMS.behaviors.repeat.isDuplicate(elem)){
		return false;
	}
	var e = document.getElementById(elem.getAttribute(wFORMS.behaviors.repeat.ATTR_MASTER_SECTION));

	return e == '' ? null : e;
}

/**
 * Handles element
 * @param	{HTMLElement}	elem
 * @return	boolean
 */
wFORMS.behaviors.repeat.handleElement = function(elem){
	return elem.setAttribute(wFORMS.behaviors.repeat.ATTR_HANDLED, true);
}

/**
 * Remove handled attribute from element
 * @param	{HTMLElement}	elem
 * @return	boolean
 */
wFORMS.behaviors.repeat.removeHandled = function(elem){
	return elem.removeAttribute(wFORMS.behaviors.repeat.ATTR_HANDLED);
}

/**
 * Executes the behavior
 * @param {event} e 
 * @param {domElement} element
 */
wFORMS.behaviors.repeat.instance.prototype.run = function(e, element){ 
	this.duplicateSection(this.target);
	if(e) e.preventDefault();
}

if (typeof(wFORMS) == "undefined") {
	throw new Error("wFORMS core not found. This behavior depends on the wFORMS core.");
}
/**
 * wForms switch behavior.  
 * See: http://www.formassembly.com/wForms/v2.0/documentation/conditional-sections.php
 *  and http://www.formassembly.com/wForms/v2.0/documentation/examples/switch_validation.html 
 */
wFORMS.behaviors['switch']  = {

	/**
	 * Selector expression for the switch elements
     * @final
     * @see	http://www.w3.org/TR/css3-selectors/
     * @TODO	Possible change hint due to switch could be not the very first prefix
     * 			but element could contains it as a CSS class
     * @TODO	!!!!!! for some reasons it is not working with IE!!!
     * 			looks like IE does not support selecting by class with selectors
     *			but selectors API allows such thing
	 */
	SELECTOR : '*[class*="switch-"]',

	/**
	 * CSS class name prefix for switch elements
     * @final
	 */
	CSS_PREFIX : 'switch-',

	/**
	 * CSS class prefix for the off state of the target element
     * @final
	 */
	CSS_OFFSTATE_PREFIX : 'offstate-',

	/**
	 * CSS class prefix for the on state of the target element
     * @final
	 */
	CSS_ONSTATE_PREFIX : 'onstate-',
	
	/**
	 * CSS class for switch elements that don't have a native ON state (ie. links)
     * @final
	 */
	CSS_ONSTATE_FLAG : 'swtchIsOn',
	
	/**
	 * CSS class for switch elements that don't have a native OFF state (ie. links)
     * @final
	 */
	CSS_OFFSTATE_FLAG : 'swtchIsOff',
	
	/**
	 * Custom function that could be overridden. 
	 * Evaluates when an element is switched on
     * @param	{HTMLElement}	elem	Duplicated section
	 */
	onSwitchOn: function(elem){ 
	},
	
	/**
	 * Custom function that could be overridden. 
	 * Evaluates when an element is switched off
     * @param	{HTMLElement}	elem	Duplicated section
	 */
	onSwitchOff: function(elem){ 
	},
	
	/**
	 * Custom function that could be overridden. 
	 * Evaluates after a switch is triggered
	 * (after all onSwitchOn and onSwitchOff events)
     * @param	{HTMLElement}	elem	Duplicated section
	 */
	onSwitch: function(form){  
	},
	
	/**
	 * Creates new instance of the behavior
     * @constructor
	 */
	instance : function(f){
		this.behavior = wFORMS.behaviors['switch']; 
		this.target = f;
	}
}

/**
 * Factory Method.
 * Applies the behavior to the given HTML element by setting the appropriate event handlers.
 * @param {domElement} f An HTML element, either nested inside a FORM element or (preferably) the FORM element itself.
 * @return {object} an instance of the behavior 
 */	
wFORMS.behaviors['switch'].applyTo = function(f){
	var b = new wFORMS.behaviors['switch'].instance(f);	
	// Iterates all switch elements. Lookup for its triggers and add event listeners
	f.matchAll(wFORMS.behaviors['switch'].SELECTOR).forEach(
		function(elem){
			if(!elem.id){
				elem.id = wFORMS.helpers.randomId()
			}
			switch(elem.tagName.toUpperCase()){
				case 'OPTION' : 
					var sNode = elem.parentNode;
					// Tries to get <select node
					while (sNode && sNode.tagName.toUpperCase() != 'SELECT'){
						sNode = sNode.parentNode;
					} 

					base2.DOM.bind(sNode);

					if(sNode && !wFORMS.behaviors['switch'].isHandled(sNode)){
						sNode.addEventListener('change', function(event) { b.run(event, sNode) }, false);
						b.setupTargets(elem);
						wFORMS.behaviors['switch'].handleElement(sNode);
					}
					break;

				case 'INPUT' : 
					if(elem.type && elem.type.toUpperCase() == 'RADIO'){
						if(!wFORMS.behaviors['switch'].isHandled(elem)){
							b.setupTargets(elem);		
						}
						// Retreives all radio group
						var radioGroup = elem.form[elem.name];
						for(var i=radioGroup.length-1;i>=0;i--) {
							// [don] Added element binding
							var _elem = base2.DOM.bind(radioGroup[i]);
							if(!wFORMS.behaviors['switch'].isHandled(_elem)){
								_elem.addEventListener('click', function(event) { b.run(event, _elem) }, false);								
								wFORMS.behaviors['switch'].handleElement(_elem);
							}
						}
					}else{
						elem.addEventListener('click', function(event) { b.run(event, elem) }, false);
						b.setupTargets(elem);
					}
					break;
					
				default:
					// Other type of element with a switch (links for instance).
					// The behavior is not run on initialization (no b.setupTargets(elem))
					elem.addEventListener('click', function(event) { b.run(event, elem) }, false);					
					break;
			}
		}
	);

	return b;
}


/**
 * Checks if element is already handled
 * @param	{HTMLElement}	elem
 * @return	boolean
 */
wFORMS.behaviors['switch'].isHandled = function(elem){
	// TODO remove wHandled to final constant
	return elem.getAttribute('rel') && elem.getAttribute('rel').indexOf('wfHandled') > -1;
}

/**
 * Checks if element is already handled
 * @param	{HTMLElement}	elem
 * @return	boolean
 */
wFORMS.behaviors['switch'].handleElement = function(elem){
	// TODO remove wHandled to final constant
	return elem.setAttribute('rel', (elem.getAttribute('rel') || "") + ' wfHandled');
}

/**
 * Removes handle attribute from element
 * @param	{HTMLElement}	elem
 * @return	boolean
 */
wFORMS.behaviors['switch'].removeHandle = function(elem){
	// TODO remove wHandled to final constant
	if(attr = elem.getAttribute('rel')){
		if(attr == 'wfHandled'){
			elem.removeAttribute('rel');
		}else if(attr.indexOf('wfHandled') != -1){
			elem.setAttribute('rel', attr.replace(/(.*)( wfHandled)(.*)/, "$1$3"));
		}
	}
}

/**
 * Returns object with two triggers collection: ON, OFF
 * @param	{Array}	elems	HTML Elements array to create triggers from
 * @param	{Array}	includeSwitches	Only that switches should be included
 * @returns	{Object}	Object of type {ON: Array, OFF: Array}
 *
 * Notes: 
 * May 26th (CS) Replaced base2.forEach with a regular loop when possible
 *               Fixed ON/OFF array to remove duplicates
 *               Replaced base2.matchAll to get a radio group. Used form.fields collection instead.
 */
wFORMS.behaviors['switch'].instance.prototype.getTriggersByElements = function(elems, includeSwitches){
	var o = {
		ON : new Array(), 
		OFF : new Array(), 
		toString : function(){
			return "ON: " + this.ON + "\nOFF: " + this.OFF
		}
	};
	for(var i=0;i<elems.length;i++) {
		var elem = elems[i];
		
		// TODO on switch if checked.
		switch(elem.tagName.toUpperCase()){
			case 'OPTION' :
				if(elem.selected){
					o.ON = o.ON.concat(this.behavior.getSwitchNamesFromTrigger(elem, includeSwitches));
				}else{
					o.OFF = o.OFF.concat(this.behavior.getSwitchNamesFromTrigger(elem, includeSwitches));
				}
				break;
				
			case 'SELECT' : 
				// TODO Check behavior
				for(var j=0; j < elem.options.length; j++){
					var opt = elem.options.item(j);
					if(opt.selected){
						o.ON = o.ON.concat(this.behavior.getSwitchNamesFromTrigger(opt, includeSwitches));
					}else{
						o.OFF = o.OFF.concat(this.behavior.getSwitchNamesFromTrigger(opt, includeSwitches));
					}
				}
				break;

			case 'INPUT' : 
				if(elem.type && elem.type.toUpperCase() == 'RADIO'){					
					var radioGroup = elem.form[elem.name];
					
					for(var j=radioGroup.length-1;j>=0;j--) {						
						var _elem = radioGroup[j];
						// Do not call getSwitchNamesFromTrigger on this radio input 
						// if we have/will process it anyway because it's part of the 
						// collection being evaluated. 
						if(_elem==elem || !base2.Array2.contains(elems, _elem)) { 							
							if(_elem.checked){
								o.ON  = o.ON.concat(this.behavior.getSwitchNamesFromTrigger(_elem, includeSwitches));
							} else {
								o.OFF = o.OFF.concat(this.behavior.getSwitchNamesFromTrigger(_elem, includeSwitches));
							}						
						}
					}					
				}else{
					if(elem.checked){
						o.ON = o.ON.concat(this.behavior.getSwitchNamesFromTrigger(elem, includeSwitches));
					}else{
						o.OFF = o.OFF.concat(this.behavior.getSwitchNamesFromTrigger(elem, includeSwitches));
					}
				}
				break;
				
			default:
				if(elem.hasClass(this.behavior.CSS_ONSTATE_FLAG)){
					o.ON  = o.ON.concat(this.behavior.getSwitchNamesFromTrigger(elem, includeSwitches));
				}else{
					o.OFF = o.OFF.concat(this.behavior.getSwitchNamesFromTrigger(elem, includeSwitches));
				}
				break;
		}
	}
	
	// remove duplicates in arrays
	var _ON = new Array(); 
	for(var i=0;i<o.ON.length;i++) {
		if(!base2.Array2.contains(_ON, o.ON[i]))
			_ON.push(o.ON[i]);
	}
	var _OFF = new Array(); 
	for(var i=0;i<o.OFF.length;i++) {
		if(!base2.Array2.contains(_OFF, o.OFF[i]))
			_OFF.push(o.OFF[i]);
	}
	o.ON  = _ON;
	o.OFF = _OFF;
	
	return o;
}

/**
 * Returns all switch names for given trigger element
 * @param	{HTMLElement}	elem
 * @param	{Array}	includeSwitches	Only that switches should be included
 * @return	Array
 */
wFORMS.behaviors['switch'].getSwitchNamesFromTrigger = function(elem, includeSwitches){
	return wFORMS.behaviors['switch'].getSwitchNames(elem.className, "trigger", includeSwitches);
}

/**
 * Returns all switch names for given target element
 * @param	{HTMLElement}	elem
 * @param	{Array}	includeSwitches	Only that switches should be included
 * @return	Array
 */
wFORMS.behaviors['switch'].getSwitchNamesFromTarget = function(elem, includeSwitches){
	return wFORMS.behaviors['switch'].getSwitchNames(elem.className,"target", includeSwitches);
}


/**
 * Returns all switch names for given element
 * @param	{string}	value of class attribute
 * @param	{string}	switch part ('trigger' or 'target') 
 * @param	{Array}		Only these switches should be included
 * @return	Array
 */
wFORMS.behaviors['switch'].getSwitchNames = function(className, switchPart, includeSwitches){
	if(!className || className=='') return [];
	
	var names  = className.split(" ");
	var _names = new Array();
	
	if(switchPart=='trigger') 
		var doTriggers = true;
	else 
		var doTriggers = false; // do switch targets
	
	for(var i=names.length-1;i>=0;i--) {		
		var cn = names[i];
		if(doTriggers) {
			if(cn.indexOf(this.CSS_PREFIX)==0) 
				var sn = cn.substring(this.CSS_PREFIX.length);
		} else {
			if(cn.indexOf(this.CSS_ONSTATE_PREFIX)==0) 
				var sn = cn.substring(this.CSS_ONSTATE_PREFIX.length);
			else if(cn.indexOf(this.CSS_OFFSTATE_PREFIX)==0) 
				var sn = cn.substring(this.CSS_OFFSTATE_PREFIX.length);
		}
		if(sn && (!includeSwitches || base2.Array2.contains(includeSwitches, sn))){
			_names.push(sn);
		}
	}
	//console.log(_names);
	return _names;
}

/**
 * Returns all elements associated with switch name
 * @TODO	Remove to pre-cache
 * @param	{String}	sName
 * @param   'ON'|'OFF'	Specifies whether 'on-state' or 'off-state' elements should be returned 
 * @returns	StaticNodeList
 */
wFORMS.behaviors['switch'].instance.prototype.getTargetsBySwitchName = function(sName, state){
	var res = new Array();
	var clazz = this;
	var b = wFORMS.behaviors.repeat;
	
	if(arguments[1]=='ON')
		var className = [wFORMS.behaviors['switch'].CSS_ONSTATE_PREFIX + sName];
	else {
		var className = [wFORMS.behaviors['switch'].CSS_OFFSTATE_PREFIX + sName];
	}
	
	this.target.getElementsByClassName(className).forEach(
		function(elem){
			// In case target found, IS in the duplicate group and this 
			// behavior target is NOT in the duplicate section and NOT dupicate itself
			// SKIP this target
			// There is no need to make other checks, because that the only
			// situation could be. Behavior applied to its top element
			// and we are searching elements inside behavior target
			if(b && b.isInDuplicateGroup(elem) && 
				!(b.isDuplicate(clazz.target) || b.isInDuplicateGroup(clazz.target))){
				return;
			}
			res.push(base2.DOM.bind(elem));
		}	
	);
	
	return res;
}

/**
 * Returns all elements associated with switch name
 * @TODO	Remove to pre-cache
 * @param	{String}	sName
 * @returns	StaticNodeList
 */
wFORMS.behaviors['switch'].instance.prototype.getTriggersByTarget = function(target){
	var res = new Array();
	var clazz = this;
	var names = wFORMS.behaviors['switch'].getSwitchNamesFromTarget(target);
	var b = wFORMS.behaviors.repeat;

	base2.forEach(names, function(name){
		clazz.target.getElementsByClassName([
			wFORMS.behaviors['switch'].CSS_PREFIX + name]).forEach(
				function(elem){
					// In case trigger found IS in the duplicate group and the target
					// is NOT in the duplicate section and NOT dupicate itself
					// SKIP this trigger
					// There is no need to make other checking for because that the only
					// situation could be. Targets in the duplicate section CAN NOT 
					// reach triggers from other duplicates because bahaviors applied
					// to entire section element
					if(b && b.isInDuplicateGroup(elem) && 
						!(b.isDuplicate(target) || b.isInDuplicateGroup(target))){
						return;
					}
					res.push(base2.DOM.bind(elem));
				}	
		);
	
	});
	return this.getTriggersByElements(res, names);
}


/**
 * Setups targets depends on switches and control state. I.e. if control is ON
 * Targets should be ON. 
 * TODO Check for optimization, check for design
 * @param	{HTMLElement}	elem
 */
wFORMS.behaviors['switch'].instance.prototype.setupTargets = function(elem){
	this.run(null, elem)
}

/**
 * Checks if provided element is switched off
 * @param	{HTMLElement}	elem
 * @return	{bool}
 * @public
 */
wFORMS.behaviors['switch'].isSwitchedOff = function(elem){
	// TODO possible base2.DOM.bind
	return (elem.className.match(
		new RegExp(wFORMS.behaviors['switch'].CSS_OFFSTATE_PREFIX + "[^ ]*")) ?
		true : false) &&
		(elem.className.match(
		new RegExp(wFORMS.behaviors['switch'].CSS_ONSTATE_PREFIX + "[^ ]*")) ?
		false : true) ; 
}


/**
 * Executes the behavior
 * @param {event} e Event caught. (!In current implementation it could be null in case of the initialization)
 * @param {domElement} element
 */
wFORMS.behaviors['switch'].instance.prototype.run = function(e, element){ 
	
	if(!element.hasClass) base2.DOM.bind(element);
	
	// If this element does not have a native state attribute (ie. checked/selected)
	// the classes CSS_ONSTATE_FLAG|CSS_OFFSTATE_FLAG are used and must be switched.
	if(element.hasClass(this.behavior.CSS_ONSTATE_FLAG)) {	 	
		element.removeClass(this.behavior.CSS_ONSTATE_FLAG);
		element.addClass(this.behavior.CSS_OFFSTATE_FLAG);
		if(e) e.preventDefault();
		
	} else if(element.hasClass(this.behavior.CSS_OFFSTATE_FLAG)) {
		element.removeClass(this.behavior.CSS_OFFSTATE_FLAG);
		element.addClass(this.behavior.CSS_ONSTATE_FLAG);
		if(e) e.preventDefault();
	}
		
	var triggers = this.getTriggersByElements(new Array(element));
	var clazz = this;
	
	
	base2.forEach(triggers.OFF, function(switchName){
		var targets = clazz.getTargetsBySwitchName(switchName, 'ON');
		base2.forEach(targets, function(elem){
			var _triggers = clazz.getTriggersByTarget(elem);
			if(_triggers.ON.length == 0){
				elem.addClass(wFORMS.behaviors['switch'].CSS_OFFSTATE_PREFIX + switchName);
				elem.removeClass(wFORMS.behaviors['switch'].CSS_ONSTATE_PREFIX + switchName);
				clazz.behavior.onSwitchOff(elem);
			}
		})
	});

	base2.forEach(triggers.ON, function(switchName){
		var targets = clazz.getTargetsBySwitchName(switchName, 'OFF');
		base2.forEach(targets, function(elem){
			elem.removeClass(wFORMS.behaviors['switch'].CSS_OFFSTATE_PREFIX + switchName);
			elem.addClass(wFORMS.behaviors['switch'].CSS_ONSTATE_PREFIX + switchName);
			clazz.behavior.onSwitchOn(elem);
		})
	});

	if(b = wFORMS.getBehaviorInstance(this.target, 'paging')){
		b.setupManagedControls();
	}
	
	this.behavior.onSwitch(this.target);
	
}



if (typeof(wFORMS) == "undefined") {
	throw new Error("wFORMS core not found. This behavior depends on the wFORMS core.");
}
/**
 * wForms validation behavior
 * 
 */
wFORMS.behaviors.validation = {

	rules: {	
		isRequired	: { selector: ".required", 			  check: 'validateRequired'}, 
		isAlpha		: { selector: ".validate-alpha", 	  check: 'validateAlpha'},
		isAlphanum	: { selector: ".validate-alphanum",	  check: 'validateAlphanum'}, 
		isDate		: { selector: ".validate-date", 	  check: 'validateDate'}, 
		isTime		: { selector: ".validate-time", 	  check: 'validateTime'}, 
		isEmail		: { selector: ".validate-email", 	  check: 'validateEmail'}, 
		isInteger	: { selector: ".validate-integer", 	  check: 'validateInteger'}, 
		isFloat		: { selector: ".validate-float", 	  check: 'validateFloat'}, 
		isCustom	: { selector: ".validate-custom",	  check: 'validateCustom'}
	},	
	
	styling: {
		fieldError	: "errFld",
		errorMessage: "errMsg"
	},
	
	messages: {
		isRequired 		: "This field is required. ",
		isAlpha 		: "The text must use alphabetic characters only (a-z, A-Z). Numbers are not allowed.",
		isEmail 		: "This does not appear to be a valid email address.",
		isInteger 		: "Please enter an integer.",
		isFloat 		: "Please enter a number (ex. 1.9).",
		isAlphanum 		: "Please use alpha-numeric characters only [a-z 0-9].",
		isDate 			: "This does not appear to be a valid date.",
		isCustom		: "Please enter a valid value.",
		notification	: "%% error(s) detected. Your form has not been submitted yet.\nPlease check the information you provided."  // %% will be replaced by the actual number of errors.
	},
	
	
	instance: function(f) {
		this.behavior = wFORMS.behaviors.validation; 
		this.target = f;
	},
	
	onPass: function(f) {},
	onFail: function(f) {}
}

/**
 * Factory Method
 * Applies the behavior to the given HTML element by setting the appropriate event handlers.
 * @param {domElement} f An HTML element, either nested inside a FORM element or (preferably) the FORM element itself.
 * @return {object} an instance of the behavior 
 */	
wFORMS.behaviors.validation.applyTo = function(f) {

	if(!f || !f.tagName) {
		throw new Error("Can't apply behavior to " + f);
	}
	if(f.tagName!="FORM") {
		// look for form tag in the ancestor nodes.
		if(f.form) 
			f=f.form;
		else {
			var _f = f;
			for(f = f.parentNode; f && f.tagName!="FORM" ;f = f.parentNode) continue;
			if(!f || f.tagName!="FORM") {
				// form tag not found, look for nested forms.
				f = _f.getElementsByTagName('form');				
			}
		}
	}
	if(!f.tagName && f.length>0) {
		var v = new Array();
		for(var i=0;i<f.length;i++) {
			var _v = new wFORMS.behaviors.validation.instance(f[i]); 	
			if(!f[i].addEventListener) base2.DOM.bind(f[i]);		
			f[i].addEventListener('submit', function(e){ return _v.run(e, this)} ,false);
			v.push(_v);	
		}
	} else {
		var v = new wFORMS.behaviors.validation.instance(f);
		if(!f.addEventListener) base2.DOM.bind(f);
		f.addEventListener('submit', function(e){ return v.run(e, this)} ,false);	
	}
		
	return v;	   
}
 
 
/**
 * Executes the behavior
 * @param {event} e 
 * @param {domElement} element
 */
wFORMS.behaviors.validation.instance.prototype.run = function(e, element) { 
 	var errorCount = 0;
 	this.elementsInError = {};
 	
 	for (var ruleName in this.behavior.rules) {
 		var rule = this.behavior.rules[ruleName];
   		var _self = this;
		
		if(!element.matchAll)
			base2.DOM.bind(element);
		
 		element.matchAll(rule.selector).forEach(function(element) { 
									
			// TODO: Check if the element is in a multi-page form
			
			// Do not validate elements that are switched off by the switch behavior
			if(_self.isSwitchedOff(element))
				return;			
			
			var	value = wFORMS.helpers.getFieldValue(element);	
			if(rule.check.call) {
				var passed = rule.check.call(_self, element, value);
			} else {
				var passed = _self[rule.check].call(_self, element, value);
			}				
 			if(!passed) { 
 				if(!element.id) element.id = wFORMS.helpers.randomId();
 				_self.elementsInError[element.id] = { id:element.id, rule: ruleName };
 				_self.removeErrorMessage(element); 
 				if(rule.fail) {
 					// custom fail method
 					rule.fail.call(_self, element, ruleName);
 				} else {
 					// default fail method
 					_self.fail.call(_self, element, ruleName);
 				} 					
 				errorCount ++;
 			} else {
 				// If no previous rule has found an error on that field,
 				// remove any error message from a previous validation run.
 				if(!_self.elementsInError[element.id])
 					_self.removeErrorMessage(element);
 				
 				if(rule.pass) {
	 				// runs custom pass method. 
	 				rule.pass.call(_self, element);
	 			} else {
	 				// default pass method
	 				_self.pass.call(_self, element);
	 			}	 			
 			}
 		});
 	}
	
 	if(errorCount > 0) {
 		e.preventDefault?e.preventDefault():e.returnValue = false;
 		if(this.behavior.onFail) this.behavior.onFail();
 		return false;
 	}
 	if(this.behavior.onPass) this.behavior.onPass();
 	return true; 
}
/**
 * fail
 * @param {domElement} element 
 */
wFORMS.behaviors.validation.instance.prototype.fail = function(element, ruleName) { 

	// set class to show that the field has an error
	element.addClass(this.behavior.styling.fieldError);
	// show error message.
	this.addErrorMessage(element, this.behavior.messages[ruleName]);			
},
	
/**
 * pass
 * @param {domElement} element 
 */	
wFORMS.behaviors.validation.instance.prototype.pass = function(element) { /* no implementation needed */ }

/**
 * addErrorMessage
 * @param {domElement} element 
 * @param {string} error message 
 */
wFORMS.behaviors.validation.instance.prototype.addErrorMessage = function(element, message) {
	
	// we'll need an id here.
	if (!element.id) element.id = wFORMS.helpers.randomId(); 
	
	// Prepare error message
	var txtNode = document.createTextNode(" " + message);
	
	// Find error message placeholder.
	var p = document.getElementById(element.id + "-E");
	if(!p) { // create placeholder.
		p = document.createElement("div"); 
		p.setAttribute('id', element.id + "-E");			
		p = element.parentNode.insertBefore(p,element.nextSibling);
	}
	// Finish the error message.
	p.appendChild(txtNode);
	base2.DOM.bind(p);  
	p.addClass(this.behavior.styling.errorMessage);							
}

/**
 * removeErrorMessage
 * @param {domElement} element 
 */
wFORMS.behaviors.validation.instance.prototype.removeErrorMessage = function(element) { 
	if(!element.hasClass) base2.DOM.bind(element);
	if(element.hasClass(this.behavior.styling.fieldError)) {
		element.removeClass(this.behavior.styling.fieldError);
		var errorMessage  = document.getElementById(element.id + "-E");
		if(errorMessage)  {				
			errorMessage.parentNode.removeChild(errorMessage); 
		}
	}
}

/**
 * Checks the element's 'visibility' (switch behavior)
 * @param {domElement} element 
 * @return	{boolean}	true if the element is not 'visible' (switched off), false otherwise.
 */
wFORMS.behaviors.validation.instance.prototype.isSwitchedOff = function(element) {
	var sb = wFORMS.getBehaviorInstance(this.target,'switch');
	if(sb) { 
		var parentElement = element;
		while(parentElement && parentElement.tagName!='BODY') {
			// TODO: Check what happens with elements with multiple ON and OFF switches	
			if(parentElement.className && 
			   parentElement.className.indexOf(sb.behavior.CSS_OFFSTATE_PREFIX)!=-1 &&
			   parentElement.className.indexOf(sb.behavior.CSS_ONSTATE_PREFIX)==-1
			   ) {
				// switched off. skip element.
				return true;
			}
			parentElement = parentElement.parentNode;
		}
	}	
	return false;
}
  
/**
 * Checks if the given string is empty (null or whitespace only)
 * @param {string} s 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.isEmpty = function(s) {				
	var regexpWhitespace = /^\s+$/;
	return  ((s == null) || (s.length == 0) || regexpWhitespace.test(s));
}

/**
 * validateRequired
 * @param {domElement} element 
 * @param {string} element's value (if available) 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateRequired = function(element, value) {
	switch(element.tagName) {
		case "INPUT":
			var inputType = element.getAttribute("type");
			if(!inputType) inputType = 'text'; 
			switch(inputType.toLowerCase()) {
				case "checkbox":
				case "radio":
					return element.checked; 
					break;
				default:
					return !this.isEmpty(value);
			}
			break;
		case "SELECT":							
			return !this.isEmpty(value);
			break;
		case "TEXTAREA":
			return !this.isEmpty(value);
			break;
		default:
			return this.validateOneRequired(element);
			break;
	} 	 
	return false 
};

/**
 * validateOneRequired
 * @param {domElement} element 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateOneRequired = function(element) {
	if(element.nodeType != 1) return false;
	
	if(this.isSwitchedOff(element))
		return false;	
	
	switch(element.tagName) {
		case "INPUT":
			var inputType = element.getAttribute("type");
			if(!inputType) inputType = 'text'; 
			switch(inputType.toLowerCase()) {
				case "checkbox":
				case "radio":
					return element.checked; 
					break;
				default:
					return !this.isEmpty(wFORMS.helpers.getFieldValue(element));
			}
			break;
		case "SELECT":							
			return !this.isEmpty(wFORMS.helpers.getFieldValue(element));
			break;
		case "TEXTAREA":
			return !this.isEmpty(wFORMS.helpers.getFieldValue(element));
			break;
		default:
			for(var i=0; i<element.childNodes.length;i++) {
				if(this.validateOneRequired(element.childNodes[i])) return true;
			}
			break;
	} 	 
	return false 
}

/**
 * validateAlpha
 * @param {domElement} element 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateAlpha = function(element, value) {
	var regexp = /^[a-zA-Z\s]+$/; // Add ' and - ?
	return this.isEmpty(value) || regexp.test(value);
}

/**
 * validateAlphanum
 * @param {domElement} element 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateAlphanum = function(element, value) {
	var regexp = /^[\w\s]+$/;
	return this.isEmpty(value) || regexp.test(value);
}

/**
 * validateDate
 * @param {domElement} element 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateDate = function(element, value) {
	var testDate = new Date(value);
	return this.isEmpty(value) || !isNaN(testDate);
}

/**
 * validateTime
 * @param {domElement} element 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateTime = function(element, value) {
	/* not yet implemented */	
	return true;
}

/**
 * validateEmail
 * @param {domElement} element 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateEmail = function(element, value) {
	var regexpEmail = /\w{1,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/;
	return this.isEmpty(value) || regexpEmail.test(value);
}

/**
 * validateInteger
 * @param {domElement} element 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateInteger = function(element, value) {
	var regexp = /^[+]?\d+$/;
	return this.isEmpty(value) || regexp.test(value);
}

/**
 * validateFloat
 * @param {domElement} element 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateFloat = function(element, value) {
	return this.isEmpty(value) || !isNaN(parseFloat(value));
}

/**
 * validateCustom
 * @param {domElement} element 
 * @returns {boolean} 
 */
wFORMS.behaviors.validation.instance.prototype.validateCustom = function(element, value) {	
	var pattern = new RegExp("\/(.*)\/([gi]*)");
	var matches = element.className.match(pattern);
	//console.log(matches);
	if(matches && matches[0]) {										
		var validationPattern = new RegExp(matches[1],matches[2]);
		if(!value.match(validationPattern)) {
			return false									
		}
	}		
	return true;
}


if (typeof(wFORMS) == "undefined") {
	throw new Error("wFORMS core not found. This behavior depends on the wFORMS core.");
}
/**
 * wForms calculation behavior. 
 */
wFORMS.behaviors.calculation  = { 
	
	/**
	 * Selector expression for the variable used in a calculation
     * @final
     * @see	http://www.w3.org/TR/css3-selectors/
	 */
	VARIABLE_SELECTOR_PREFIX : "calc-",
	
	/**
	 * Behavior uses value defined in the class with this prefix if available (e.g. calcval-9.99)
	 * otherwise uses field value property. 
	 */
	CHOICE_VALUE_SELECTOR_PREFIX : "calcval-",

	/**
	 * Suffix of the ID for the hint element
     * @final
	 */
	CALCULATION_SELECTOR : '*[class*="formula="]',

	/**
	 * The error message displayed next to a field with a calculation error
	 */
	CALCULATION_ERROR_MESSAGE : "There was an error computing this field.",
	
	/**
	 * Creates new instance of the behavior
     * @constructor
	 */
	instance : function(f) {
		this.behavior = wFORMS.behaviors.calculation; 
		this.target = f;
		this.calculations = [];
		//this.variables = [];
	}
}

/**
 * Factory Method.
 * Applies the behavior to the given HTML element by setting the appropriate event handlers.
 * @param {domElement} f An HTML element, either nested inside a FORM element or (preferably) the FORM element itself.
 * @return {object} an instance of the behavior 
 */	
wFORMS.behaviors.calculation.applyTo = function(f) {
	var b = new wFORMS.behaviors.calculation.instance(f);

	// Selects all hints elements using predefined selector and attaches
	// event listeners to related HTML elements for each hint
	f.matchAll(wFORMS.behaviors.calculation.CALCULATION_SELECTOR).forEach(
		function(elem){
			// extract formula
			var formula = elem.className.substr(elem.className.indexOf('formula=')+8).split(' ')[0];

			var variables = formula.split(/[^a-zA-Z]+/g);
			b.varFields = [];
			
			// process variables, add onchange/onblur event to update total.
			for (var i = 0; i < variables.length; i++) {
				if(variables[i]!='') {
					f.matchAll("*[class*=\""+wFORMS.behaviors.calculation.VARIABLE_SELECTOR_PREFIX+variables[i]+"\"]").forEach(
						function(variable){
							
							// make sure the variable is an exact match.
							var exactMatch = ((' ' + variable.className + ' ').indexOf(' '+wFORMS.behaviors.calculation.VARIABLE_SELECTOR_PREFIX+variables[i]+' ')!=-1);
							if(!exactMatch) return;
							
							switch(variable.tagName + ":" + variable.getAttribute('type') ) {
								case 'INPUT:':			// (type attribute empty)
								case 'INPUT:null': 		// (type attribute missing)
								case 'INPUT:text':
								case 'INPUT:hidden':
								case 'INPUT:password':
								case 'TEXTAREA:null':
									if(!variable._wforms_calc_handled) {
										variable.addEventListener('blur', function(e){ return b.run(e, this)}, false);
										variable._wforms_calc_handled = true;
									}
									break;
								case 'INPUT:radio':		 						
								case 'INPUT:checkbox':
									if(!variable._wforms_calc_handled) {
										variable.addEventListener('click', function(e){ return b.run(e, this)}, false);
										variable._wforms_calc_handled = true;
									}
									break;			
								case 'SELECT:null':
									if(!variable._wforms_calc_handled) {
										variable.addEventListener('change',  function(e){ return b.run(e, this)}, false);
										variable._wforms_calc_handled = true;
									}
									break;		
								default:
									// error: variable refers to a non supported element.
									return;
									break;
							}
							b.varFields.push({name: variables[i], field: variable});						
						}
					);			
				}		
			}		
			var calc = { field: elem, formula: formula, variables: b.varFields };		
			b.calculations.push(calc);	
			b.compute(calc);
		}
	);
	
	b.onApply();
	
	return b;
}

/**
 * Executed once the behavior has been applied to the document.
 * Can be overwritten.
 */
wFORMS.behaviors.calculation.instance.prototype.onApply = function() {} 

/**
 * Runs when a field is changed, update dependent calculated fields. 
 * @param {event} event
 * @param {domElement} elem
 */
wFORMS.behaviors.calculation.instance.prototype.run = function(event, element) { 
	
	for(var i=0; i<this.calculations.length;i++) {		
		var calc = this.calculations[i];
		for(var j=0; j<calc.variables.length;j++) {		
					
			if(element==calc.variables[j].field) {
				// this element is part of the calculation for calc.field
				this.compute(calc);
			}
		}
	}
} 
 
wFORMS.behaviors.calculation.instance.prototype.compute = function(calculation) {
	var f = this.target;
	var formula = calculation.formula;
	
	for(var i=0; i<calculation.variables.length;i++) {
		var v = calculation.variables[i];
		var varval = 0;
		var _self  = this;
		// we don't rely on calculation.variables[i].field because 
		// the form may have changed since we've applied the behavior
		// (repeat behavior for instance).
		// TODO: Exclude switched-off variables?
		f.matchAll("*[class*=\""+wFORMS.behaviors.calculation.VARIABLE_SELECTOR_PREFIX+v.name+"\"]").forEach(
			function(f){
				
				// make sure the variable is an exact match.
				var exactMatch = ((' ' + f.className + ' ').indexOf(' '+wFORMS.behaviors.calculation.VARIABLE_SELECTOR_PREFIX+v.name+' ')!=-1);
				if(!exactMatch) return;
				
				// If field value has a different purpose, the value for the calculation can be set in the
				// class attribute, prefixed with CHOICE_VALUE_SELECTOR_PREFIX
				if(_self.hasValueInClassName(f)) {
					var value = _self.getValueFromClassName(f);
				} else {
					var value = wFORMS.helpers.getFieldValue(f);					
				}
				
				if(!value) value=0;
				
				if(value.constructor.toString().indexOf("Array") !== -1) { // array (multiple select)
					for(var j=0;j<value.length;j++) { 
						varval += parseFloat(value[j]);
					}
				} else {
					varval += parseFloat(value);
				}
			}
		);		
		
	    var rgx = new RegExp("[^a-z]+("+v.name+")[^a-z]+","gi");
	    
	    var m = rgx.exec(' '+formula+' ');
	    if (m) {
	    	if(m[1])
				formula = formula.replace(m[1], varval);
			else
				formula = formula.replace(m[0], varval);
	    }		
	} 
	try {
		var result = eval(formula);
//		if(result=='Infinity' || result=='NaN') {
		// [don] Added for Safari
		// TODO check if "result == 'NaN'" result == 'Infinity' is neccessary
		if(result == 'Infinity' || result == 'NaN' || isNaN(result)){
			result = 'error';
		}
	} catch(x) {		
		result = 'error';		
	} 
	// Check if validation behavior is available. Then flag field if error.
	var validationBehavior = wFORMS.getBehaviorInstance(this.target,'validation');	
	if(validationBehavior) {		
		// add validation error message 
		if(!wFORMS.behaviors.validation.messages['calculation']) {
			wFORMS.behaviors.validation.messages['calculation'] = this.behavior.CALCULATION_ERROR_MESSAGE;
		}
		validationBehavior.removeErrorMessage(calculation.field);
		if(result=='error') {			
			validationBehavior.fail(calculation.field, 'calculation');
		}
	}
	calculation.field.value = result;
	
	// If the calcualted field is also a variable, recursively update dependant calculations
	if(calculation.field.className && (calculation.field.className.indexOf(this.behavior.VARIABLE_SELECTOR_PREFIX)!=-1)) {
		// TODO: Check for infinite loops?
		this.run(null,calculation.field);
	} 
}
	
wFORMS.behaviors.calculation.instance.prototype.hasValueInClassName = function(element) {
	switch(element.tagName) {
		case "SELECT": 
			for(var i=0;i<element.options.length;i++) {
				if(element.options[i].className && element.options[i].className.indexOf(this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)!=-1) {
					return true; 
				}
			}
			return false; 
			break;
		default:
			if(!element.className || (' '+element.className).indexOf(' '+this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)==-1)
				return false;
			break;
	}
	return true;
}
/**
 * getValueFromClassName 
 * If field value has a different purpose, the value for the calculation can be set in the
 * class attribute, prefixed with CHOICE_VALUE_SELECTOR_PREFIX 
 * @param {domElement} element 
 * @returns {string} the value of the field, as set in the className
 */
wFORMS.behaviors.calculation.instance.prototype.getValueFromClassName = function(element) {
	switch(element.tagName) {
		case "INPUT":
			if(!element.className || element.className.indexOf(this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)==-1) 
				return null;
			
			var value = element.className.split(this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)[1].split(' ')[0];								
			if(element.type=='checkbox')
				return element.checked?value:null;
			if(element.type=='radio')
				return element.checked?value:null;
			return value;
			break;
		case "SELECT":		
			if(element.selectedIndex==-1) {					
				return null; 
			} 
			if(element.getAttribute('multiple')) {
				var v=[];
				for(var i=0;i<element.options.length;i++) {
					if(element.options[i].selected) {
						if(element.options[i].className && element.options[i].className.indexOf(this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)!=-1) { 
							var value = element.options[i].className.split(this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)[1].split(' ')[0];								
							v.push(value);
						}
					}
				}
				if(v.length==0) return null;
				return v;
			}	
			if (element.options[element.selectedIndex].className &&  element.options[element.selectedIndex].className.indexOf(this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)!=-1) { 
				var value =  element.options[element.selectedIndex].className.split(this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)[1].split(' ')[0];								
				return value;
			}													
			break;
		case "TEXTAREA":
			if(!element.className || element.className.indexOf(this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)==-1) 
				return null;
			var value = element.className.split(this.behavior.CHOICE_VALUE_SELECTOR_PREFIX)[1].split(' ')[0];								
			
			return value;
			break;
		default:
			return null; 
			break;
	} 	 
	return null; 
}