/*  Prototype JavaScript framework, version 1.6.1
 *  (c) 2005-2009 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://www.prototypejs.org/
 *
 *--------------------------------------------------------------------------*/
 
 /* Patched to address this bug: https://prototype.lighthouseapp.com/projects/8886-prototype/tickets/618 */

var Prototype = {
  Version: '1.6.1',

  Browser: (function(){
    var ua = navigator.userAgent;
    var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
    return {
      IE:             !!window.attachEvent && !isOpera,
      Opera:          isOpera,
      WebKit:         ua.indexOf('AppleWebKit/') > -1,
      Gecko:          ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1,
      MobileSafari:   /Apple.*Mobile.*Safari/.test(ua)
    }
  })(),

  BrowserFeatures: {
    XPath: !!document.evaluate,
    SelectorsAPI: !!document.querySelector,
    ElementExtensions: (function() {
      var constructor = window.Element || window.HTMLElement;
      return !!(constructor && constructor.prototype);
    })(),
    SpecificElementExtensions: (function() {
      if (typeof window.HTMLDivElement !== 'undefined')
        return true;

      var div = document.createElement('div');
      var form = document.createElement('form');
      var isSupported = false;

      if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) {
        isSupported = true;
      }

      div = form = null;

      return isSupported;
    })()
  },

  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,

  emptyFunction: function() { },
  K: function(x) { return x }
};

if (Prototype.Browser.MobileSafari)
  Prototype.BrowserFeatures.SpecificElementExtensions = false;


var Abstract = { };


var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) { }
    }

    return returnValue;
  }
};

/* Based on Alex Arnell's inheritance implementation. */

var Class = (function() {
  function subclass() {};
  function create() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

    function klass() {
      this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    klass.prototype.constructor = klass;
    return klass;
  }

  function addMethods(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = Object.keys(source);

    if (!Object.keys({ toString: true }).length) {
      if (source.toString != Object.prototype.toString)
        properties.push("toString");
      if (source.valueOf != Object.prototype.valueOf)
        properties.push("valueOf");
    }

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() == "$super") {
        var method = value;
        value = (function(m) {
          return function() { return ancestor[m].apply(this, arguments); };
        })(property).wrap(method);

        value.valueOf = method.valueOf.bind(method);
        value.toString = method.toString.bind(method);
      }
      this.prototype[property] = value;
    }

    return this;
  }

  return {
    create: create,
    Methods: {
      addMethods: addMethods
    }
  };
})();
(function() {

  var _toString = Object.prototype.toString;

  function extend(destination, source) {
    for (var property in source)
      destination[property] = source[property];
    return destination;
  }

  function inspect(object) {
    try {
      if (isUndefined(object)) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : String(object);
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  }

  function toJSON(object) {
    var type = typeof object;
    switch (type) {
      case 'undefined':
      case 'function':
      case 'unknown': return;
      case 'boolean': return object.toString();
    }

    if (object === null) return 'null';
    if (object.toJSON) return object.toJSON();
    if (isElement(object)) return;

    var results = [];
    for (var property in object) {
      var value = toJSON(object[property]);
      if (!isUndefined(value))
        results.push(property.toJSON() + ': ' + value);
    }

    return '{' + results.join(', ') + '}';
  }

  function toQueryString(object) {
    return $H(object).toQueryString();
  }

  function toHTML(object) {
    return object && object.toHTML ? object.toHTML() : String.interpret(object);
  }

  function keys(object) {
    var results = [];
    for (var property in object)
      results.push(property);
    return results;
  }

  function values(object) {
    var results = [];
    for (var property in object)
      results.push(object[property]);
    return results;
  }

  function clone(object) {
    return extend({ }, object);
  }

  function isElement(object) {
    return !!(object && object.nodeType == 1);
  }

  function isArray(object) {
    return _toString.call(object) == "[object Array]";
  }


  function isHash(object) {
    return object instanceof Hash;
  }

  function isFunction(object) {
    return typeof object === "function";
  }

  function isString(object) {
    return _toString.call(object) == "[object String]";
  }

  function isNumber(object) {
    return _toString.call(object) == "[object Number]";
  }

  function isUndefined(object) {
    return typeof object === "undefined";
  }

  extend(Object, {
    extend:        extend,
    inspect:       inspect,
    toJSON:        toJSON,
    toQueryString: toQueryString,
    toHTML:        toHTML,
    keys:          keys,
    values:        values,
    clone:         clone,
    isElement:     isElement,
    isArray:       isArray,
    isHash:        isHash,
    isFunction:    isFunction,
    isString:      isString,
    isNumber:      isNumber,
    isUndefined:   isUndefined
  });
})();
Object.extend(Function.prototype, (function() {
  var slice = Array.prototype.slice;

  function update(array, args) {
    var arrayLength = array.length, length = args.length;
    while (length--) array[arrayLength + length] = args[length];
    return array;
  }

  function merge(array, args) {
    array = slice.call(array, 0);
    return update(array, args);
  }

  function argumentNames() {
    var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
      .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
      .replace(/\s+/g, '').split(',');
    return names.length == 1 && !names[0] ? [] : names;
  }

  function bind(context) {
    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
    var __method = this, args = slice.call(arguments, 1);
    return function() {
      var a = merge(args, arguments);
      return __method.apply(context, a);
    }
  }

  function bindAsEventListener(context) {
    var __method = this, args = slice.call(arguments, 1);
    return function(event) {
      var a = update([event || window.event], args);
      return __method.apply(context, a);
    }
  }

  function curry() {
    if (!arguments.length) return this;
    var __method = this, args = slice.call(arguments, 0);
    return function() {
      var a = merge(args, arguments);
      return __method.apply(this, a);
    }
  }

  function delay(timeout) {
    var __method = this, args = slice.call(arguments, 1);
    timeout = timeout * 1000
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  }

  function defer() {
    var args = update([0.01], arguments);
    return this.delay.apply(this, args);
  }

  function wrap(wrapper) {
    var __method = this;
    return function() {
      var a = update([__method.bind(this)], arguments);
      return wrapper.apply(this, a);
    }
  }

  function methodize() {
    if (this._methodized) return this._methodized;
    var __method = this;
    return this._methodized = function() {
      var a = update([this], arguments);
      return __method.apply(null, a);
    };
  }

  return {
    argumentNames:       argumentNames,
    bind:                bind,
    bindAsEventListener: bindAsEventListener,
    curry:               curry,
    delay:               delay,
    defer:               defer,
    wrap:                wrap,
    methodize:           methodize
  }
})());


Date.prototype.toJSON = function() {
  return '"' + this.getUTCFullYear() + '-' +
    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
    this.getUTCDate().toPaddedString(2) + 'T' +
    this.getUTCHours().toPaddedString(2) + ':' +
    this.getUTCMinutes().toPaddedString(2) + ':' +
    this.getUTCSeconds().toPaddedString(2) + 'Z"';
};


RegExp.prototype.match = RegExp.prototype.test;

RegExp.escape = function(str) {
  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};
var PeriodicalExecuter = Class.create({
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

  registerCallback: function() {
    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  execute: function() {
    this.callback(this);
  },

  stop: function() {
    if (!this.timer) return;
    clearInterval(this.timer);
    this.timer = null;
  },

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.execute();
        this.currentlyExecuting = false;
      } catch(e) {
        this.currentlyExecuting = false;
        throw e;
      }
    }
  }
});
Object.extend(String, {
  interpret: function(value) {
    return value == null ? '' : String(value);
  },
  specialChar: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '\\': '\\\\'
  }
});

Object.extend(String.prototype, (function() {

  function prepareReplacement(replacement) {
    if (Object.isFunction(replacement)) return replacement;
    var template = new Template(replacement);
    return function(match) { return template.evaluate(match) };
  }

  function gsub(pattern, replacement) {
    var result = '', source = this, match;
    replacement = prepareReplacement(replacement);

    if (Object.isString(pattern))
      pattern = RegExp.escape(pattern);

    if (!(pattern.length || pattern.source)) {
      replacement = replacement('');
      return replacement + source.split('').join(replacement) + replacement;
    }

    while (source.length > 0) {
      if (match = source.match(pattern)) {
        result += source.slice(0, match.index);
        result += String.interpret(replacement(match));
        source  = source.slice(match.index + match[0].length);
      } else {
        result += source, source = '';
      }
    }
    return result;
  }

  function sub(pattern, replacement, count) {
    replacement = prepareReplacement(replacement);
    count = Object.isUndefined(count) ? 1 : count;

    return this.gsub(pattern, function(match) {
      if (--count < 0) return match[0];
      return replacement(match);
    });
  }

  function scan(pattern, iterator) {
    this.gsub(pattern, iterator);
    return String(this);
  }

  function truncate(length, truncation) {
    length = length || 30;
    truncation = Object.isUndefined(truncation) ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : String(this);
  }

  function strip() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  }

  function stripTags() {
    return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, '');
  }

  function stripScripts() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  }

  function extractScripts() {
    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
    return (this.match(matchAll) || []).map(function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  }

  function evalScripts() {
    return this.extractScripts().map(function(script) { return eval(script) });
  }

  function escapeHTML() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }

  function unescapeHTML() {
    return this.stripTags().replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&');
  }


  function toQueryParams(separator) {
    var match = this.strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return { };

    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var key = decodeURIComponent(pair.shift());
        var value = pair.length > 1 ? pair.join('=') : pair[0];
        if (value != undefined) value = decodeURIComponent(value);

        if (key in hash) {
          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
          hash[key].push(value);
        }
        else hash[key] = value;
      }
      return hash;
    });
  }

  function toArray() {
    return this.split('');
  }

  function succ() {
    return this.slice(0, this.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
  }

  function times(count) {
    return count < 1 ? '' : new Array(count + 1).join(this);
  }

  function camelize() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  }

  function capitalize() {
    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
  }

  function underscore() {
    return this.replace(/::/g, '/')
               .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
               .replace(/([a-z\d])([A-Z])/g, '$1_$2')
               .replace(/-/g, '_')
               .toLowerCase();
  }

  function dasherize() {
    return this.replace(/_/g, '-');
  }

  function inspect(useDoubleQuotes) {
    var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) {
      if (character in String.specialChar) {
        return String.specialChar[character];
      }
      return '\\u00' + character.charCodeAt().toPaddedString(2, 16);
    });
    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  }

  function toJSON() {
    return this.inspect(true);
  }

  function unfilterJSON(filter) {
    return this.replace(filter || Prototype.JSONFilter, '$1');
  }

  function isJSON() {
    var str = this;
    if (str.blank()) return false;
    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
  }

  function evalJSON(sanitize) {
    var json = this.unfilterJSON();
    try {
      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
    } catch (e) { }
    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
  }

  function include(pattern) {
    return this.indexOf(pattern) > -1;
  }

  function startsWith(pattern) {
    return this.indexOf(pattern) === 0;
  }

  function endsWith(pattern) {
    var d = this.length - pattern.length;
    return d >= 0 && this.lastIndexOf(pattern) === d;
  }

  function empty() {
    return this == '';
  }

  function blank() {
    return /^\s*$/.test(this);
  }

  function interpolate(object, pattern) {
    return new Template(this, pattern).evaluate(object);
  }

  return {
    gsub:           gsub,
    sub:            sub,
    scan:           scan,
    truncate:       truncate,
    strip:          String.prototype.trim ? String.prototype.trim : strip,
    stripTags:      stripTags,
    stripScripts:   stripScripts,
    extractScripts: extractScripts,
    evalScripts:    evalScripts,
    escapeHTML:     escapeHTML,
    unescapeHTML:   unescapeHTML,
    toQueryParams:  toQueryParams,
    parseQuery:     toQueryParams,
    toArray:        toArray,
    succ:           succ,
    times:          times,
    camelize:       camelize,
    capitalize:     capitalize,
    underscore:     underscore,
    dasherize:      dasherize,
    inspect:        inspect,
    toJSON:         toJSON,
    unfilterJSON:   unfilterJSON,
    isJSON:         isJSON,
    evalJSON:       evalJSON,
    include:        include,
    startsWith:     startsWith,
    endsWith:       endsWith,
    empty:          empty,
    blank:          blank,
    interpolate:    interpolate
  };
})());

var Template = Class.create({
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    if (object && Object.isFunction(object.toTemplateReplacements))
      object = object.toTemplateReplacements();

    return this.template.gsub(this.pattern, function(match) {
      if (object == null) return (match[1] + '');

      var before = match[1] || '';
      if (before == '\\') return match[2];

      var ctx = object, expr = match[3];
      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
      match = pattern.exec(expr);
      if (match == null) return before;

      while (match != null) {
        var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : match[1];
        ctx = ctx[comp];
        if (null == ctx || '' == match[3]) break;
        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
        match = pattern.exec(expr);
      }

      return before + String.interpret(ctx);
    });
  }
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

var $break = { };

var Enumerable = (function() {
  function each(iterator, context) {
    var index = 0;
    try {
      this._each(function(value) {
        iterator.call(context, value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  }

  function eachSlice(number, iterator, context) {
    var index = -number, slices = [], array = this.toArray();
    if (number < 1) return array;
    while ((index += number) < array.length)
      slices.push(array.slice(index, index+number));
    return slices.collect(iterator, context);
  }

  function all(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = true;
    this.each(function(value, index) {
      result = result && !!iterator.call(context, value, index);
      if (!result) throw $break;
    });
    return result;
  }

  function any(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = false;
    this.each(function(value, index) {
      if (result = !!iterator.call(context, value, index))
        throw $break;
    });
    return result;
  }

  function collect(iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];
    this.each(function(value, index) {
      results.push(iterator.call(context, value, index));
    });
    return results;
  }

  function detect(iterator, context) {
    var result;
    this.each(function(value, index) {
      if (iterator.call(context, value, index)) {
        result = value;
        throw $break;
      }
    });
    return result;
  }

  function findAll(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  }

  function grep(filter, iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];

    if (Object.isString(filter))
      filter = new RegExp(RegExp.escape(filter));

    this.each(function(value, index) {
      if (filter.match(value))
        results.push(iterator.call(context, value, index));
    });
    return results;
  }

  function include(object) {
    if (Object.isFunction(this.indexOf))
      if (this.indexOf(object) != -1) return true;

    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  }

  function inGroupsOf(number, fillWith) {
    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
    return this.eachSlice(number, function(slice) {
      while(slice.length < number) slice.push(fillWith);
      return slice;
    });
  }

  function inject(memo, iterator, context) {
    this.each(function(value, index) {
      memo = iterator.call(context, memo, value, index);
    });
    return memo;
  }

  function invoke(method) {
    var args = $A(arguments).slice(1);
    return this.map(function(value) {
      return value[method].apply(value, args);
    });
  }

  function max(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value >= result)
        result = value;
    });
    return result;
  }

  function min(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value < result)
        result = value;
    });
    return result;
  }

  function partition(iterator, context) {
    iterator = iterator || Prototype.K;
    var trues = [], falses = [];
    this.each(function(value, index) {
      (iterator.call(context, value, index) ?
        trues : falses).push(value);
    });
    return [trues, falses];
  }

  function pluck(property) {
    var results = [];
    this.each(function(value) {
      results.push(value[property]);
    });
    return results;
  }

  function reject(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (!iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  }

  function sortBy(iterator, context) {
    return this.map(function(value, index) {
      return {
        value: value,
        criteria: iterator.call(context, value, index)
      };
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }).pluck('value');
  }

  function toArray() {
    return this.map();
  }

  function zip() {
    var iterator = Prototype.K, args = $A(arguments);
    if (Object.isFunction(args.last()))
      iterator = args.pop();

    var collections = [this].concat(args).map($A);
    return this.map(function(value, index) {
      return iterator(collections.pluck(index));
    });
  }

  function size() {
    return this.toArray().length;
  }

  function inspect() {
    return '#<Enumerable:' + this.toArray().inspect() + '>';
  }









  return {
    each:       each,
    eachSlice:  eachSlice,
    all:        all,
    every:      all,
    any:        any,
    some:       any,
    collect:    collect,
    map:        collect,
    detect:     detect,
    findAll:    findAll,
    select:     findAll,
    filter:     findAll,
    grep:       grep,
    include:    include,
    member:     include,
    inGroupsOf: inGroupsOf,
    inject:     inject,
    invoke:     invoke,
    max:        max,
    min:        min,
    partition:  partition,
    pluck:      pluck,
    reject:     reject,
    sortBy:     sortBy,
    toArray:    toArray,
    entries:    toArray,
    zip:        zip,
    size:       size,
    inspect:    inspect,
    find:       detect
  };
})();
function $A(iterable) {
  if (!iterable) return [];
  if ('toArray' in Object(iterable)) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

function $w(string) {
  if (!Object.isString(string)) return [];
  string = string.strip();
  return string ? string.split(/\s+/) : [];
}

Array.from = $A;


(function() {
  var arrayProto = Array.prototype,
      slice = arrayProto.slice,
      _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available

  function each(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  }
  if (!_each) _each = each;

  function clear() {
    this.length = 0;
    return this;
  }

  function first() {
    return this[0];
  }

  function last() {
    return this[this.length - 1];
  }

  function compact() {
    return this.select(function(value) {
      return value != null;
    });
  }

  function flatten() {
    return this.inject([], function(array, value) {
      if (Object.isArray(value))
        return array.concat(value.flatten());
      array.push(value);
      return array;
    });
  }

  function without() {
    var values = slice.call(arguments, 0);
    return this.select(function(value) {
      return !values.include(value);
    });
  }

  function reverse(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  }

  function uniq(sorted) {
    return this.inject([], function(array, value, index) {
      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
        array.push(value);
      return array;
    });
  }

  function intersect(array) {
    return this.uniq().findAll(function(item) {
      return array.detect(function(value) { return item === value });
    });
  }


  function clone() {
    return slice.call(this, 0);
  }

  function size() {
    return this.length;
  }

  function inspect() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  }

  function toJSON() {
    var results = [];
    this.each(function(object) {
      var value = Object.toJSON(object);
      if (!Object.isUndefined(value)) results.push(value);
    });
    return '[' + results.join(', ') + ']';
  }

  function indexOf(item, i) {
    i || (i = 0);
    var length = this.length;
    if (i < 0) i = length + i;
    for (; i < length; i++)
      if (this[i] === item) return i;
    return -1;
  }

  function lastIndexOf(item, i) {
    i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
    var n = this.slice(0, i).reverse().indexOf(item);
    return (n < 0) ? n : i - n - 1;
  }

  function concat() {
    var array = slice.call(this, 0), item;
    for (var i = 0, length = arguments.length; i < length; i++) {
      item = arguments[i];
      if (Object.isArray(item) && !('callee' in item)) {
        for (var j = 0, arrayLength = item.length; j < arrayLength; j++)
          array.push(item[j]);
      } else {
        array.push(item);
      }
    }
    return array;
  }

  Object.extend(arrayProto, Enumerable);

  if (!arrayProto._reverse)
    arrayProto._reverse = arrayProto.reverse;

  Object.extend(arrayProto, {
    _each:     _each,
    clear:     clear,
    first:     first,
    last:      last,
    compact:   compact,
    flatten:   flatten,
    without:   without,
    reverse:   reverse,
    uniq:      uniq,
    intersect: intersect,
    clone:     clone,
    toArray:   clone,
    size:      size,
    inspect:   inspect,
    toJSON:    toJSON
  });

  var CONCAT_ARGUMENTS_BUGGY = (function() {
    return [].concat(arguments)[0][0] !== 1;
  })(1,2)

  if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat;

  if (!arrayProto.indexOf) arrayProto.indexOf = indexOf;
  if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf;
})();
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {
  function initialize(object) {
    this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
  }

  function _each(iterator) {
    for (var key in this._object) {
      var value = this._object[key], pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  }

  function set(key, value) {
    return this._object[key] = value;
  }

  function get(key) {
    if (this._object[key] !== Object.prototype[key])
      return this._object[key];
  }

  function unset(key) {
    var value = this._object[key];
    delete this._object[key];
    return value;
  }

  function toObject() {
    return Object.clone(this._object);
  }

  function keys() {
    return this.pluck('key');
  }

  function values() {
    return this.pluck('value');
  }

  function index(value) {
    var match = this.detect(function(pair) {
      return pair.value === value;
    });
    return match && match.key;
  }

  function merge(object) {
    return this.clone().update(object);
  }

  function update(object) {
    return new Hash(object).inject(this, function(result, pair) {
      result.set(pair.key, pair.value);
      return result;
    });
  }

  function toQueryPair(key, value) {
    if (Object.isUndefined(value)) return key;
    return key + '=' + encodeURIComponent(String.interpret(value));
  }

  function toQueryString() {
    return this.inject([], function(results, pair) {
      var key = encodeURIComponent(pair.key), values = pair.value;

      if (values && typeof values == 'object') {
        if (Object.isArray(values))
          return results.concat(values.map(toQueryPair.curry(key)));
      } else results.push(toQueryPair(key, values));
      return results;
    }).join('&');
  }

  function inspect() {
    return '#<Hash:{' + this.map(function(pair) {
      return pair.map(Object.inspect).join(': ');
    }).join(', ') + '}>';
  }

  function toJSON() {
    return Object.toJSON(this.toObject());
  }

  function clone() {
    return new Hash(this);
  }

  return {
    initialize:             initialize,
    _each:                  _each,
    set:                    set,
    get:                    get,
    unset:                  unset,
    toObject:               toObject,
    toTemplateReplacements: toObject,
    keys:                   keys,
    values:                 values,
    index:                  index,
    merge:                  merge,
    update:                 update,
    toQueryString:          toQueryString,
    inspect:                inspect,
    toJSON:                 toJSON,
    clone:                  clone
  };
})());

Hash.from = $H;
Object.extend(Number.prototype, (function() {
  function toColorPart() {
    return this.toPaddedString(2, 16);
  }

  function succ() {
    return this + 1;
  }

  function times(iterator, context) {
    $R(0, this, true).each(iterator, context);
    return this;
  }

  function toPaddedString(length, radix) {
    var string = this.toString(radix || 10);
    return '0'.times(length - string.length) + string;
  }

  function toJSON() {
    return isFinite(this) ? this.toString() : 'null';
  }

  function abs() {
    return Math.abs(this);
  }

  function round() {
    return Math.round(this);
  }

  function ceil() {
    return Math.ceil(this);
  }

  function floor() {
    return Math.floor(this);
  }

  return {
    toColorPart:    toColorPart,
    succ:           succ,
    times:          times,
    toPaddedString: toPaddedString,
    toJSON:         toJSON,
    abs:            abs,
    round:          round,
    ceil:           ceil,
    floor:          floor
  };
})());

function $R(start, end, exclusive) {
  return new ObjectRange(start, end, exclusive);
}

var ObjectRange = Class.create(Enumerable, (function() {
  function initialize(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  }

  function _each(iterator) {
    var value = this.start;
    while (this.include(value)) {
      iterator(value);
      value = value.succ();
    }
  }

  function include(value) {
    if (value < this.start)
      return false;
    if (this.exclusive)
      return value < this.end;
    return value <= this.end;
  }

  return {
    initialize: initialize,
    _each:      _each,
    include:    include
  };
})());



var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (Object.isFunction(responder[callback])) {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) { }
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate:   function() { Ajax.activeRequestCount++ },
  onComplete: function() { Ajax.activeRequestCount-- }
});
Ajax.Base = Class.create({
  initialize: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   '',
      evalJSON:     true,
      evalJS:       true
    };
    Object.extend(this.options, options || { });

    this.options.method = this.options.method.toLowerCase();

    if (Object.isString(this.options.parameters))
      this.options.parameters = this.options.parameters.toQueryParams();
    else if (Object.isHash(this.options.parameters))
      this.options.parameters = this.options.parameters.toObject();
  }
});
Ajax.Request = Class.create(Ajax.Base, {
  _complete: false,

  initialize: function($super, url, options) {
    $super(options);
    this.transport = Ajax.getTransport();
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = Object.clone(this.options.parameters);

    if (!['get', 'post'].include(this.method)) {
      params['_method'] = this.method;
      this.method = 'post';
    }

    this.parameters = params;

    if (params = Object.toQueryString(params)) {
      if (this.method == 'get')
        this.url += (this.url.include('?') ? '&' : '?') + params;
      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
        params += '&_=';
    }

    try {
      var response = new Ajax.Response(this);
      if (this.options.onCreate) this.options.onCreate(response);
      Ajax.Responders.dispatch('onCreate', this, response);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
      this.transport.send(this.body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'X-Prototype-Version': Prototype.Version,
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    };

    if (this.method == 'post') {
      headers['Content-type'] = this.options.contentType +
        (this.options.encoding ? '; charset=' + this.options.encoding : '');

      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (Object.isFunction(extras.push))
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    var status = this.getStatus();
    return !status || (status >= 200 && status < 300);
  },

  getStatus: function() {
    try {
      return this.transport.status || 0;
    } catch (e) { return 0 }
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && this.isSameOrigin() && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  isSameOrigin: function() {
    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
      protocol: location.protocol,
      domain: document.domain,
      port: location.port ? ':' + location.port : ''
    }));
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name) || null;
    } catch (e) { return null; }
  },

  evalResponse: function() {
    try {
      return eval((this.transport.responseText || '').unfilterJSON());
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];








Ajax.Response = Class.create({
  initialize: function(request){
    this.request = request;
    var transport  = this.transport  = request.transport,
        readyState = this.readyState = transport.readyState;

    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
      this.status       = this.getStatus();
      this.statusText   = this.getStatusText();
      this.responseText = String.interpret(transport.responseText);
      this.headerJSON   = this._getHeaderJSON();
    }

    if(readyState == 4) {
      var xml = transport.responseXML;
      this.responseXML  = Object.isUndefined(xml) ? null : xml;
      this.responseJSON = this._getResponseJSON();
    }
  },

  status:      0,

  statusText: '',

  getStatus: Ajax.Request.prototype.getStatus,

  getStatusText: function() {
    try {
      return this.transport.statusText || '';
    } catch (e) { return '' }
  },

  getHeader: Ajax.Request.prototype.getHeader,

  getAllHeaders: function() {
    try {
      return this.getAllResponseHeaders();
    } catch (e) { return null }
  },

  getResponseHeader: function(name) {
    return this.transport.getResponseHeader(name);
  },

  getAllResponseHeaders: function() {
    return this.transport.getAllResponseHeaders();
  },

  _getHeaderJSON: function() {
    var json = this.getHeader('X-JSON');
    if (!json) return null;
    json = decodeURIComponent(escape(json));
    try {
      return json.evalJSON(this.request.options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  },

  _getResponseJSON: function() {
    var options = this.request.options;
    if (!options.evalJSON || (options.evalJSON != 'force' &&
      !(this.getHeader('Content-type') || '').include('application/json')) ||
        this.responseText.blank())
          return null;
    try {
      return this.responseText.evalJSON(options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  }
});

Ajax.Updater = Class.create(Ajax.Request, {
  initialize: function($super, container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    };

    options = Object.clone(options);
    var onComplete = options.onComplete;
    options.onComplete = (function(response, json) {
      this.updateContent(response.responseText);
      if (Object.isFunction(onComplete)) onComplete(response, json);
    }).bind(this);

    $super(url, options);
  },

  updateContent: function(responseText) {
    var receiver = this.container[this.success() ? 'success' : 'failure'],
        options = this.options;

    if (!options.evalScripts) responseText = responseText.stripScripts();

    if (receiver = $(receiver)) {
      if (options.insertion) {
        if (Object.isString(options.insertion)) {
          var insertion = { }; insertion[options.insertion] = responseText;
          receiver.insert(insertion);
        }
        else options.insertion(receiver, responseText);
      }
      else receiver.update(responseText);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
  initialize: function($super, container, url, options) {
    $super(options);
    this.onComplete = this.options.onComplete;

    this.frequency = (this.options.frequency || 2);
    this.decay = (this.options.decay || 1);

    this.updater = { };
    this.container = container;
    this.url = url;

    this.start();
  },

  start: function() {
    this.options.onComplete = this.updateComplete.bind(this);
    this.onTimerEvent();
  },

  stop: function() {
    this.updater.options.onComplete = undefined;
    clearTimeout(this.timer);
    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
  },

  updateComplete: function(response) {
    if (this.options.decay) {
      this.decay = (response.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = response.responseText;
    }
    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});



function $(element) {
  if (arguments.length > 1) {
    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
      elements.push($(arguments[i]));
    return elements;
  }
  if (Object.isString(element))
    element = document.getElementById(element);
  return Element.extend(element);
}

if (Prototype.BrowserFeatures.XPath) {
  document._getElementsByXPath = function(expression, parentElement) {
    var results = [];
    var query = document.evaluate(expression, $(parentElement) || document,
      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, length = query.snapshotLength; i < length; i++)
      results.push(Element.extend(query.snapshotItem(i)));
    return results;
  };
}

/*--------------------------------------------------------------------------*/

if (!window.Node) var Node = { };

if (!Node.ELEMENT_NODE) {
  Object.extend(Node, {
    ELEMENT_NODE: 1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE: 3,
    CDATA_SECTION_NODE: 4,
    ENTITY_REFERENCE_NODE: 5,
    ENTITY_NODE: 6,
    PROCESSING_INSTRUCTION_NODE: 7,
    COMMENT_NODE: 8,
    DOCUMENT_NODE: 9,
    DOCUMENT_TYPE_NODE: 10,
    DOCUMENT_FRAGMENT_NODE: 11,
    NOTATION_NODE: 12
  });
}


(function(global) {

  var SETATTRIBUTE_IGNORES_NAME = (function(){
    var elForm = document.createElement("form");
    var elInput = document.createElement("input");
    var root = document.documentElement;
    elInput.setAttribute("name", "test");
    elForm.appendChild(elInput);
    root.appendChild(elForm);
    var isBuggy = elForm.elements
      ? (typeof elForm.elements.test == "undefined")
      : null;
    root.removeChild(elForm);
    elForm = elInput = null;
    return isBuggy;
  })();

  var element = global.Element;
  global.Element = function(tagName, attributes) {
    attributes = attributes || { };
    tagName = tagName.toLowerCase();
    var cache = Element.cache;
    if (SETATTRIBUTE_IGNORES_NAME && attributes.name) {
      tagName = '<' + tagName + ' name="' + attributes.name + '">';
      delete attributes.name;
      return Element.writeAttribute(document.createElement(tagName), attributes);
    }
    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
  };
  Object.extend(global.Element, element || { });
  if (element) global.Element.prototype = element.prototype;
})(this);

Element.cache = { };
Element.idCounter = 1;

Element.Methods = {
  visible: function(element) {
    return $(element).style.display != 'none';
  },

  toggle: function(element) {
    element = $(element);
    Element[Element.visible(element) ? 'hide' : 'show'](element);
    return element;
  },


  hide: function(element) {
    element = $(element);
    element.style.display = 'none';
    return element;
  },

  show: function(element) {
    element = $(element);
    element.style.display = '';
    return element;
  },

  remove: function(element) {
    element = $(element);
    element.parentNode.removeChild(element);
    return element;
  },

  update: (function(){

    var SELECT_ELEMENT_INNERHTML_BUGGY = (function(){
      var el = document.createElement("select"),
          isBuggy = true;
      el.innerHTML = "<option value=\"test\">test</option>";
      if (el.options && el.options[0]) {
        isBuggy = el.options[0].nodeName.toUpperCase() !== "OPTION";
      }
      el = null;
      return isBuggy;
    })();

    var TABLE_ELEMENT_INNERHTML_BUGGY = (function(){
      try {
        var el = document.createElement("table");
        if (el && el.tBodies) {
          el.innerHTML = "<tbody><tr><td>test</td></tr></tbody>";
          var isBuggy = typeof el.tBodies[0] == "undefined";
          el = null;
          return isBuggy;
        }
      } catch (e) {
        return true;
      }
    })();

    var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () {
      var s = document.createElement("script"),
          isBuggy = false;
      try {
        s.appendChild(document.createTextNode(""));
        isBuggy = !s.firstChild ||
          s.firstChild && s.firstChild.nodeType !== 3;
      } catch (e) {
        isBuggy = true;
      }
      s = null;
      return isBuggy;
    })();

    function update(element, content) {
      element = $(element);

      if (content && content.toElement)
        content = content.toElement();

      if (Object.isElement(content))
        return element.update().insert(content);

      content = Object.toHTML(content);

      var tagName = element.tagName.toUpperCase();

      if (tagName === 'SCRIPT' && SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING) {
        element.text = content;
        return element;
      }

      if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) {
        if (tagName in Element._insertionTranslations.tags) {
          while (element.firstChild) {
            element.removeChild(element.firstChild);
          }
          Element._getContentFromAnonymousElement(tagName, content.stripScripts())
            .each(function(node) {
              element.appendChild(node)
            });
        }
        else {
          element.innerHTML = content.stripScripts();
        }
      }
      else {
        element.innerHTML = content.stripScripts();
      }

      content.evalScripts.bind(content).defer();
      return element;
    }

    return update;
  })(),

  replace: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    else if (!Object.isElement(content)) {
      content = Object.toHTML(content);
      var range = element.ownerDocument.createRange();
      range.selectNode(element);
      content.evalScripts.bind(content).defer();
      content = range.createContextualFragment(content.stripScripts());
    }
    element.parentNode.replaceChild(content, element);
    return element;
  },

  insert: function(element, insertions) {
    element = $(element);

    if (Object.isString(insertions) || Object.isNumber(insertions) ||
        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
          insertions = {bottom:insertions};

    var content, insert, tagName, childNodes;

    for (var position in insertions) {
      content  = insertions[position];
      position = position.toLowerCase();
      insert = Element._insertionTranslations[position];

      if (content && content.toElement) content = content.toElement();
      if (Object.isElement(content)) {
        insert(element, content);
        continue;
      }

      content = Object.toHTML(content);

      tagName = ((position == 'before' || position == 'after')
        ? element.parentNode : element).tagName.toUpperCase();

      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

      if (position == 'top' || position == 'after') childNodes.reverse();
      childNodes.each(insert.curry(element));

      content.evalScripts.bind(content).defer();
    }

    return element;
  },

  wrap: function(element, wrapper, attributes) {
    element = $(element);
    if (Object.isElement(wrapper))
      $(wrapper).writeAttribute(attributes || { });
    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
    else wrapper = new Element('div', wrapper);
    if (element.parentNode)
      element.parentNode.replaceChild(wrapper, element);
    wrapper.appendChild(element);
    return wrapper;
  },

  inspect: function(element) {
    element = $(element);
    var result = '<' + element.tagName.toLowerCase();
    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
      var property = pair.first(), attribute = pair.last();
      var value = (element[property] || '').toString();
      if (value) result += ' ' + attribute + '=' + value.inspect(true);
    });
    return result + '>';
  },

  recursivelyCollect: function(element, property) {
    element = $(element);
    var elements = [];
    while (element = element[property])
      if (element.nodeType == 1)
        elements.push(Element.extend(element));
    return elements;
  },

  ancestors: function(element) {
    return Element.recursivelyCollect(element, 'parentNode');
  },

  descendants: function(element) {
    return Element.select(element, "*");
  },

  firstDescendant: function(element) {
    element = $(element).firstChild;
    while (element && element.nodeType != 1) element = element.nextSibling;
    return $(element);
  },

  immediateDescendants: function(element) {
    if (!(element = $(element).firstChild)) return [];
    while (element && element.nodeType != 1) element = element.nextSibling;
    if (element) return [element].concat($(element).nextSiblings());
    return [];
  },

  previousSiblings: function(element) {
    return Element.recursivelyCollect(element, 'previousSibling');
  },

  nextSiblings: function(element) {
    return Element.recursivelyCollect(element, 'nextSibling');
  },

  siblings: function(element) {
    element = $(element);
    return Element.previousSiblings(element).reverse()
      .concat(Element.nextSiblings(element));
  },

  match: function(element, selector) {
    if (Object.isString(selector))
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(element.parentNode);
    var ancestors = Element.ancestors(element);
    return Object.isNumber(expression) ? ancestors[expression] :
      Selector.findElement(ancestors, expression, index);
  },

  down: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return Element.firstDescendant(element);
    return Object.isNumber(expression) ? Element.descendants(element)[expression] :
      Element.select(element, expression)[index || 0];
  },

  previous: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
    var previousSiblings = Element.previousSiblings(element);
    return Object.isNumber(expression) ? previousSiblings[expression] :
      Selector.findElement(previousSiblings, expression, index);
  },

  next: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
    var nextSiblings = Element.nextSiblings(element);
    return Object.isNumber(expression) ? nextSiblings[expression] :
      Selector.findElement(nextSiblings, expression, index);
  },


  select: function(element) {
    var args = Array.prototype.slice.call(arguments, 1);
    return Selector.findChildElements(element, args);
  },

  adjacent: function(element) {
    var args = Array.prototype.slice.call(arguments, 1);
    return Selector.findChildElements(element.parentNode, args).without(element);
  },

  identify: function(element) {
    element = $(element);
    var id = Element.readAttribute(element, 'id');
    if (id) return id;
    do { id = 'anonymous_element_' + Element.idCounter++ } while ($(id));
    Element.writeAttribute(element, 'id', id);
    return id;
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (Prototype.Browser.IE) {
      var t = Element._attributeTranslations.read;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name]) name = t.names[name];
      if (name.include(':')) {
        return (!element.attributes || !element.attributes[name]) ? null :
         element.attributes[name].value;
      }
    }
    return element.getAttribute(name);
  },

  writeAttribute: function(element, name, value) {
    element = $(element);
    var attributes = { }, t = Element._attributeTranslations.write;

    if (typeof name == 'object') attributes = name;
    else attributes[name] = Object.isUndefined(value) ? true : value;

    for (var attr in attributes) {
      name = t.names[attr] || attr;
      value = attributes[attr];
      if (t.values[attr]) name = t.values[attr](element, value);
      if (value === false || value === null)
        element.removeAttribute(name);
      else if (value === true)
        element.setAttribute(name, name);
      else element.setAttribute(name, value);
    }
    return element;
  },

  getHeight: function(element) {
    return Element.getDimensions(element).height;
  },

  getWidth: function(element) {
    return Element.getDimensions(element).width;
  },

  classNames: function(element) {
    return new Element.ClassNames(element);
  },

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    var elementClassName = element.className;
    return (elementClassName.length > 0 && (elementClassName == className ||
      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    if (!Element.hasClassName(element, className))
      element.className += (element.className ? ' ' : '') + className;
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    element.className = element.className.replace(
      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
    return element;
  },

  toggleClassName: function(element, className) {
    if (!(element = $(element))) return;
    return Element[Element.hasClassName(element, className) ?
      'removeClassName' : 'addClassName'](element, className);
  },

  cleanWhitespace: function(element) {
    element = $(element);
    var node = element.firstChild;
    while (node) {
      var nextNode = node.nextSibling;
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        element.removeChild(node);
      node = nextNode;
    }
    return element;
  },

  empty: function(element) {
    return $(element).innerHTML.blank();
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);

    if (element.compareDocumentPosition)
      return (element.compareDocumentPosition(ancestor) & 8) === 8;

    if (ancestor.contains)
      return ancestor.contains(element) && ancestor !== element;

    while (element = element.parentNode)
      if (element == ancestor) return true;

    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = Element.cumulativeOffset(element);
    window.scrollTo(pos[0], pos[1]);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    style = style == 'float' ? 'cssFloat' : style.camelize();
    var value = element.style[style];
    if (!value || value == 'auto') {
      var css = document.defaultView.getComputedStyle(element, null);
      value = css ? css[style] : null;
    }
    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
    return value == 'auto' ? null : value;
  },

  getOpacity: function(element) {
    return $(element).getStyle('opacity');
  },

  setStyle: function(element, styles) {
    element = $(element);
    var elementStyle = element.style, match;
    if (Object.isString(styles)) {
      element.style.cssText += ';' + styles;
      return styles.include('opacity') ?
        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
    }
    for (var property in styles)
      if (property == 'opacity') element.setOpacity(styles[property]);
      else
        elementStyle[(property == 'float' || property == 'cssFloat') ?
          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
            property] = styles[property];

    return element;
  },

  setOpacity: function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    var display = Element.getStyle(element, 'display');
    if (display != 'none' && display != null) // Safari bug
      return {width: element.offsetWidth, height: element.offsetHeight};

    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    if (originalPosition != 'fixed') // Switching fixed to absolute causes issues in Safari
      els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};
  },

  makePositioned: function(element) {
    element = $(element);
    var pos = Element.getStyle(element, 'position');
    if (pos == 'static' || !pos) {
      element._madePositioned = true;
      element.style.position = 'relative';
      if (Prototype.Browser.Opera) {
        element.style.top = 0;
        element.style.left = 0;
      }
    }
    return element;
  },

  undoPositioned: function(element) {
    element = $(element);
    if (element._madePositioned) {
      element._madePositioned = undefined;
      element.style.position =
        element.style.top =
        element.style.left =
        element.style.bottom =
        element.style.right = '';
    }
    return element;
  },

  makeClipping: function(element) {
    element = $(element);
    if (element._overflow) return element;
    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
    if (element._overflow !== 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

  undoClipping: function(element) {
    element = $(element);
    if (!element._overflow) return element;
    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
    element._overflow = null;
    return element;
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        if (element.tagName.toUpperCase() == 'BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p !== 'static') break;
      }
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  absolutize: function(element) {
    element = $(element);
    if (Element.getStyle(element, 'position') == 'absolute') return element;

    var offsets = Element.positionedOffset(element);
    var top     = offsets[1];
    var left    = offsets[0];
    var width   = element.clientWidth;
    var height  = element.clientHeight;

    element._originalLeft   = left - parseFloat(element.style.left  || 0);
    element._originalTop    = top  - parseFloat(element.style.top || 0);
    element._originalWidth  = element.style.width;
    element._originalHeight = element.style.height;

    element.style.position = 'absolute';
    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.width  = width + 'px';
    element.style.height = height + 'px';
    return element;
  },

  relativize: function(element) {
    element = $(element);
    if (Element.getStyle(element, 'position') == 'relative') return element;

    element.style.position = 'relative';
    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.height = element._originalHeight;
    element.style.width  = element._originalWidth;
    return element;
  },

  cumulativeScrollOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  getOffsetParent: function(element) {
    // PATCH for Prototype Bug: https://prototype.lighthouseapp.com/projects/8886-prototype/tickets/618
    // Resolves issues with autocompleters in floating windows in IE8
    if (element.offsetParent && Element.visible(element)) return $(element.offsetParent);
    if (element == document.body) return $(element);

    while ((element = element.parentNode) && element != document.body)
      if (Element.getStyle(element, 'position') != 'static')
        return $(element);

    return $(document.body);
  },

  viewportOffset: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      if (element.offsetParent == document.body &&
        Element.getStyle(element, 'position') == 'absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return Element._returnOffset(valueL, valueT);
  },

  clonePosition: function(element, source) {
    var options = Object.extend({
      setLeft:    true,
      setTop:     true,
      setWidth:   true,
      setHeight:  true,
      offsetTop:  0,
      offsetLeft: 0
    }, arguments[2] || { });

    source = $(source);
    var p = Element.viewportOffset(source);

    element = $(element);
    var delta = [0, 0];
    var parent = null;
    if (Element.getStyle(element, 'position') == 'absolute') {
      parent = Element.getOffsetParent(element);
      delta = Element.viewportOffset(parent);
    }

    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
    return element;
  }
};

Object.extend(Element.Methods, {
  getElementsBySelector: Element.Methods.select,

  childElements: Element.Methods.immediateDescendants
});

Element._attributeTranslations = {
  write: {
    names: {
      className: 'class',
      htmlFor:   'for'
    },
    values: { }
  }
};

if (Prototype.Browser.Opera) {
  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
    function(proceed, element, style) {
      switch (style) {
        case 'left': case 'top': case 'right': case 'bottom':
          if (proceed(element, 'position') === 'static') return null;
        case 'height': case 'width':
          if (!Element.visible(element)) return null;

          var dim = parseInt(proceed(element, style), 10);

          if (dim !== element['offset' + style.capitalize()])
            return dim + 'px';

          var properties;
          if (style === 'height') {
            properties = ['border-top-width', 'padding-top',
             'padding-bottom', 'border-bottom-width'];
          }
          else {
            properties = ['border-left-width', 'padding-left',
             'padding-right', 'border-right-width'];
          }
          return properties.inject(dim, function(memo, property) {
            var val = proceed(element, property);
            return val === null ? memo : memo - parseInt(val, 10);
          }) + 'px';
        default: return proceed(element, style);
      }
    }
  );

  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
    function(proceed, element, attribute) {
      if (attribute === 'title') return element.title;
      return proceed(element, attribute);
    }
  );
}

else if (Prototype.Browser.IE) {
  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
    function(proceed, element) {
      element = $(element);
      try { element.offsetParent }
      catch(e) { return $(document.body) }
      var position = element.getStyle('position');
      if (position !== 'static') return proceed(element);
      element.setStyle({ position: 'relative' });
      var value = proceed(element);
      element.setStyle({ position: position });
      return value;
    }
  );

  $w('positionedOffset viewportOffset').each(function(method) {
    Element.Methods[method] = Element.Methods[method].wrap(
      function(proceed, element) {
        element = $(element);
        try { element.offsetParent }
        catch(e) { return Element._returnOffset(0,0) }
        var position = element.getStyle('position');
        if (position !== 'static') return proceed(element);
        var offsetParent = element.getOffsetParent();
        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
          offsetParent.setStyle({ zoom: 1 });
        element.setStyle({ position: 'relative' });
        var value = proceed(element);
        element.setStyle({ position: position });
        return value;
      }
    );
  });

  Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
    function(proceed, element) {
      try { element.offsetParent }
      catch(e) { return Element._returnOffset(0,0) }
      return proceed(element);
    }
  );

  Element.Methods.getStyle = function(element, style) {
    element = $(element);
    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
    var value = element.style[style];
    if (!value && element.currentStyle) value = element.currentStyle[style];

    if (style == 'opacity') {
      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if (value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }

    if (value == 'auto') {
      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
        return element['offset' + style.capitalize()] + 'px';
      return null;
    }
    return value;
  };

  Element.Methods.setOpacity = function(element, value) {
    function stripAlpha(filter){
      return filter.replace(/alpha\([^\)]*\)/gi,'');
    }
    element = $(element);
    var currentStyle = element.currentStyle;
    if ((currentStyle && !currentStyle.hasLayout) ||
      (!currentStyle && element.style.zoom == 'normal'))
        element.style.zoom = 1;

    var filter = element.getStyle('filter'), style = element.style;
    if (value == 1 || value === '') {
      (filter = stripAlpha(filter)) ?
        style.filter = filter : style.removeAttribute('filter');
      return element;
    } else if (value < 0.00001) value = 0;
    style.filter = stripAlpha(filter) +
      'alpha(opacity=' + (value * 100) + ')';
    return element;
  };

  Element._attributeTranslations = (function(){

    var classProp = 'className';
    var forProp = 'for';

    var el = document.createElement('div');

    el.setAttribute(classProp, 'x');

    if (el.className !== 'x') {
      el.setAttribute('class', 'x');
      if (el.className === 'x') {
        classProp = 'class';
      }
    }
    el = null;

    el = document.createElement('label');
    el.setAttribute(forProp, 'x');
    if (el.htmlFor !== 'x') {
      el.setAttribute('htmlFor', 'x');
      if (el.htmlFor === 'x') {
        forProp = 'htmlFor';
      }
    }
    el = null;

    return {
      read: {
        names: {
          'class':      classProp,
          'className':  classProp,
          'for':        forProp,
          'htmlFor':    forProp
        },
        values: {
          _getAttr: function(element, attribute) {
            return element.getAttribute(attribute);
          },
          _getAttr2: function(element, attribute) {
            return element.getAttribute(attribute, 2);
          },
          _getAttrNode: function(element, attribute) {
            var node = element.getAttributeNode(attribute);
            return node ? node.value : "";
          },
          _getEv: (function(){

            var el = document.createElement('div');
            el.onclick = Prototype.emptyFunction;
            var value = el.getAttribute('onclick');
            var f;

            if (String(value).indexOf('{') > -1) {
              f = function(element, attribute) {
                attribute = element.getAttribute(attribute);
                if (!attribute) return null;
                attribute = attribute.toString();
                attribute = attribute.split('{')[1];
                attribute = attribute.split('}')[0];
                return attribute.strip();
              };
            }
            else if (value === '') {
              f = function(element, attribute) {
                attribute = element.getAttribute(attribute);
                if (!attribute) return null;
                return attribute.strip();
              };
            }
            el = null;
            return f;
          })(),
          _flag: function(element, attribute) {
            return $(element).hasAttribute(attribute) ? attribute : null;
          },
          style: function(element) {
            return element.style.cssText.toLowerCase();
          },
          title: function(element) {
            return element.title;
          }
        }
      }
    }
  })();

  Element._attributeTranslations.write = {
    names: Object.extend({
      cellpadding: 'cellPadding',
      cellspacing: 'cellSpacing'
    }, Element._attributeTranslations.read.names),
    values: {
      checked: function(element, value) {
        element.checked = !!value;
      },

      style: function(element, value) {
        element.style.cssText = value ? value : '';
      }
    }
  };

  Element._attributeTranslations.has = {};

  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
      'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
  });

  (function(v) {
    Object.extend(v, {
      href:        v._getAttr2,
      src:         v._getAttr2,
      type:        v._getAttr,
      action:      v._getAttrNode,
      disabled:    v._flag,
      checked:     v._flag,
      readonly:    v._flag,
      multiple:    v._flag,
      onload:      v._getEv,
      onunload:    v._getEv,
      onclick:     v._getEv,
      ondblclick:  v._getEv,
      onmousedown: v._getEv,
      onmouseup:   v._getEv,
      onmouseover: v._getEv,
      onmousemove: v._getEv,
      onmouseout:  v._getEv,
      onfocus:     v._getEv,
      onblur:      v._getEv,
      onkeypress:  v._getEv,
      onkeydown:   v._getEv,
      onkeyup:     v._getEv,
      onsubmit:    v._getEv,
      onreset:     v._getEv,
      onselect:    v._getEv,
      onchange:    v._getEv
    });
  })(Element._attributeTranslations.read.values);

  if (Prototype.BrowserFeatures.ElementExtensions) {
    (function() {
      function _descendants(element) {
        var nodes = element.getElementsByTagName('*'), results = [];
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName !== "!") // Filter out comment nodes.
            results.push(node);
        return results;
      }

      Element.Methods.down = function(element, expression, index) {
        element = $(element);
        if (arguments.length == 1) return element.firstDescendant();
        return Object.isNumber(expression) ? _descendants(element)[expression] :
          Element.select(element, expression)[index || 0];
      }
    })();
  }

}

else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1) ? 0.999999 :
      (value === '') ? '' : (value < 0.00001) ? 0 : value;
    return element;
  };
}

else if (Prototype.Browser.WebKit) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;

    if (value == 1)
      if(element.tagName.toUpperCase() == 'IMG' && element.width) {
        element.width++; element.width--;
      } else try {
        var n = document.createTextNode(' ');
        element.appendChild(n);
        element.removeChild(n);
      } catch (e) { }

    return element;
  };

  Element.Methods.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return Element._returnOffset(valueL, valueT);
  };
}

if ('outerHTML' in document.documentElement) {
  Element.Methods.replace = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) {
      element.parentNode.replaceChild(content, element);
      return element;
    }

    content = Object.toHTML(content);
    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

    if (Element._insertionTranslations.tags[tagName]) {
      var nextSibling = element.next();
      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
      parent.removeChild(element);
      if (nextSibling)
        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
      else
        fragments.each(function(node) { parent.appendChild(node) });
    }
    else element.outerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

Element._returnOffset = function(l, t) {
  var result = [l, t];
  result.left = l;
  result.top = t;
  return result;
};

Element._getContentFromAnonymousElement = function(tagName, html) {
  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
  if (t) {
    div.innerHTML = t[0] + html + t[1];
    t[2].times(function() { div = div.firstChild });
  } else div.innerHTML = html;
  return $A(div.childNodes);
};

Element._insertionTranslations = {
  before: function(element, node) {
    element.parentNode.insertBefore(node, element);
  },
  top: function(element, node) {
    element.insertBefore(node, element.firstChild);
  },
  bottom: function(element, node) {
    element.appendChild(node);
  },
  after: function(element, node) {
    element.parentNode.insertBefore(node, element.nextSibling);
  },
  tags: {
    TABLE:  ['<table>',                '</table>',                   1],
    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
    SELECT: ['<select>',               '</select>',                  1]
  }
};

(function() {
  var tags = Element._insertionTranslations.tags;
  Object.extend(tags, {
    THEAD: tags.TBODY,
    TFOOT: tags.TBODY,
    TH:    tags.TD
  });
})();

Element.Methods.Simulated = {
  hasAttribute: function(element, attribute) {
    attribute = Element._attributeTranslations.has[attribute] || attribute;
    var node = $(element).getAttributeNode(attribute);
    return !!(node && node.specified);
  }
};

Element.Methods.ByTag = { };

Object.extend(Element, Element.Methods);

(function(div) {

  if (!Prototype.BrowserFeatures.ElementExtensions && div['__proto__']) {
    window.HTMLElement = { };
    window.HTMLElement.prototype = div['__proto__'];
    Prototype.BrowserFeatures.ElementExtensions = true;
  }

  div = null;

})(document.createElement('div'))

Element.extend = (function() {

  function checkDeficiency(tagName) {
    if (typeof window.Element != 'undefined') {
      var proto = window.Element.prototype;
      if (proto) {
        var id = '_' + (Math.random()+'').slice(2);
        var el = document.createElement(tagName);
        proto[id] = 'x';
        var isBuggy = (el[id] !== 'x');
        delete proto[id];
        el = null;
        return isBuggy;
      }
    }
    return false;
  }

  function extendElementWith(element, methods) {
    for (var property in methods) {
      var value = methods[property];
      if (Object.isFunction(value) && !(property in element))
        element[property] = value.methodize();
    }
  }

  var HTMLOBJECTELEMENT_PROTOTYPE_BUGGY = checkDeficiency('object');

  if (Prototype.BrowserFeatures.SpecificElementExtensions) {
    if (HTMLOBJECTELEMENT_PROTOTYPE_BUGGY) {
      return function(element) {
        if (element && typeof element._extendedByPrototype == 'undefined') {
          var t = element.tagName;
          if (t && (/^(?:object|applet|embed)$/i.test(t))) {
            extendElementWith(element, Element.Methods);
            extendElementWith(element, Element.Methods.Simulated);
            extendElementWith(element, Element.Methods.ByTag[t.toUpperCase()]);
          }
        }
        return element;
      }
    }
    return Prototype.K;
  }

  var Methods = { }, ByTag = Element.Methods.ByTag;

  var extend = Object.extend(function(element) {
    if (!element || typeof element._extendedByPrototype != 'undefined' ||
        element.nodeType != 1 || element == window) return element;

    var methods = Object.clone(Methods),
        tagName = element.tagName.toUpperCase();

    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

    extendElementWith(element, methods);

    element._extendedByPrototype = Prototype.emptyFunction;
    return element;

  }, {
    refresh: function() {
      if (!Prototype.BrowserFeatures.ElementExtensions) {
        Object.extend(Methods, Element.Methods);
        Object.extend(Methods, Element.Methods.Simulated);
      }
    }
  });

  extend.refresh();
  return extend;
})();

Element.hasAttribute = function(element, attribute) {
  if (element.hasAttribute) return element.hasAttribute(attribute);
  return Element.Methods.Simulated.hasAttribute(element, attribute);
};

Element.addMethods = function(methods) {
  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

  if (!methods) {
    Object.extend(Form, Form.Methods);
    Object.extend(Form.Element, Form.Element.Methods);
    Object.extend(Element.Methods.ByTag, {
      "FORM":     Object.clone(Form.Methods),
      "INPUT":    Object.clone(Form.Element.Methods),
      "SELECT":   Object.clone(Form.Element.Methods),
      "TEXTAREA": Object.clone(Form.Element.Methods)
    });
  }

  if (arguments.length == 2) {
    var tagName = methods;
    methods = arguments[1];
  }

  if (!tagName) Object.extend(Element.Methods, methods || { });
  else {
    if (Object.isArray(tagName)) tagName.each(extend);
    else extend(tagName);
  }

  function extend(tagName) {
    tagName = tagName.toUpperCase();
    if (!Element.Methods.ByTag[tagName])
      Element.Methods.ByTag[tagName] = { };
    Object.extend(Element.Methods.ByTag[tagName], methods);
  }

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    for (var property in methods) {
      var value = methods[property];
      if (!Object.isFunction(value)) continue;
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = value.methodize();
    }
  }

  function findDOMClass(tagName) {
    var klass;
    var trans = {
      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
      "FrameSet", "IFRAME": "IFrame"
    };
    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName.capitalize() + 'Element';
    if (window[klass]) return window[klass];

    var element = document.createElement(tagName);
    var proto = element['__proto__'] || element.constructor.prototype;
    element = null;
    return proto;
  }

  var elementPrototype = window.HTMLElement ? HTMLElement.prototype :
   Element.prototype;

  if (F.ElementExtensions) {
    copy(Element.Methods, elementPrototype);
    copy(Element.Methods.Simulated, elementPrototype, true);
  }

  if (F.SpecificElementExtensions) {
    for (var tag in Element.Methods.ByTag) {
      var klass = findDOMClass(tag);
      if (Object.isUndefined(klass)) continue;
      copy(T[tag], klass.prototype);
    }
  }

  Object.extend(Element, Element.Methods);
  delete Element.ByTag;

  if (Element.extend.refresh) Element.extend.refresh();
  Element.cache = { };
};


document.viewport = {

  getDimensions: function() {
    return { width: this.getWidth(), height: this.getHeight() };
  },

  getScrollOffsets: function() {
    return Element._returnOffset(
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop  || document.body.scrollTop);
  }
};

(function(viewport) {
  var B = Prototype.Browser, doc = document, element, property = {};

  function getRootElement() {
    if (B.WebKit && !doc.evaluate)
      return document;

    if (B.Opera && window.parseFloat(window.opera.version()) < 9.5)
      return document.body;

    return document.documentElement;
  }

  function define(D) {
    if (!element) element = getRootElement();

    property[D] = 'client' + D;

    viewport['get' + D] = function() { return element[property[D]] };
    return viewport['get' + D]();
  }

  viewport.getWidth  = define.curry('Width');

  viewport.getHeight = define.curry('Height');
})(document.viewport);


Element.Storage = {
  UID: 1
};

Element.addMethods({
  getStorage: function(element) {
    if (!(element = $(element))) return;

    var uid;
    if (element === window) {
      uid = 0;
    } else {
      if (typeof element._prototypeUID === "undefined")
        element._prototypeUID = [Element.Storage.UID++];
      uid = element._prototypeUID[0];
    }

    if (!Element.Storage[uid])
      Element.Storage[uid] = $H();

    return Element.Storage[uid];
  },

  store: function(element, key, value) {
    if (!(element = $(element))) return;

    if (arguments.length === 2) {
      Element.getStorage(element).update(key);
    } else {
      Element.getStorage(element).set(key, value);
    }

    return element;
  },

  retrieve: function(element, key, defaultValue) {
    if (!(element = $(element))) return;
    var hash = Element.getStorage(element), value = hash.get(key);

    if (Object.isUndefined(value)) {
      hash.set(key, defaultValue);
      value = defaultValue;
    }

    return value;
  },

  clone: function(element, deep) {
    if (!(element = $(element))) return;
    var clone = element.cloneNode(deep);
    clone._prototypeUID = void 0;
    if (deep) {
      var descendants = Element.select(clone, '*'),
          i = descendants.length;
      while (i--) {
        descendants[i]._prototypeUID = void 0;
      }
    }
    return Element.extend(clone);
  }
});
/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
 * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
 * license.  Please see http://www.yui-ext.com/ for more information. */

var Selector = Class.create({
  initialize: function(expression) {
    this.expression = expression.strip();

    if (this.shouldUseSelectorsAPI()) {
      this.mode = 'selectorsAPI';
    } else if (this.shouldUseXPath()) {
      this.mode = 'xpath';
      this.compileXPathMatcher();
    } else {
      this.mode = "normal";
      this.compileMatcher();
    }

  },

  shouldUseXPath: (function() {

    var IS_DESCENDANT_SELECTOR_BUGGY = (function(){
      var isBuggy = false;
      if (document.evaluate && window.XPathResult) {
        var el = document.createElement('div');
        el.innerHTML = '<ul><li></li></ul><div><ul><li></li></ul></div>';

        var xpath = ".//*[local-name()='ul' or local-name()='UL']" +
          "//*[local-name()='li' or local-name()='LI']";

        var result = document.evaluate(xpath, el, null,
          XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

        isBuggy = (result.snapshotLength !== 2);
        el = null;
      }
      return isBuggy;
    })();

    return function() {
      if (!Prototype.BrowserFeatures.XPath) return false;

      var e = this.expression;

      if (Prototype.Browser.WebKit &&
       (e.include("-of-type") || e.include(":empty")))
        return false;

      if ((/(\[[\w-]*?:|:checked)/).test(e))
        return false;

      if (IS_DESCENDANT_SELECTOR_BUGGY) return false;

      return true;
    }

  })(),

  shouldUseSelectorsAPI: function() {
    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;

    if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false;

    if (!Selector._div) Selector._div = new Element('div');

    try {
      Selector._div.querySelector(this.expression);
    } catch(e) {
      return false;
    }

    return true;
  },

  compileMatcher: function() {
    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
        c = Selector.criteria, le, p, m, len = ps.length, name;

    if (Selector._cache[e]) {
      this.matcher = Selector._cache[e];
      return;
    }

    this.matcher = ["this.matcher = function(root) {",
                    "var r = root, h = Selector.handlers, c = false, n;"];

    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i = 0; i<len; i++) {
        p = ps[i].re;
        name = ps[i].name;
        if (m = e.match(p)) {
          this.matcher.push(Object.isFunction(c[name]) ? c[name](m) :
            new Template(c[name]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.matcher.push("return h.unique(n);\n}");
    eval(this.matcher.join('\n'));
    Selector._cache[this.expression] = this.matcher;
  },

  compileXPathMatcher: function() {
    var e = this.expression, ps = Selector.patterns,
        x = Selector.xpath, le, m, len = ps.length, name;

    if (Selector._cache[e]) {
      this.xpath = Selector._cache[e]; return;
    }

    this.matcher = ['.//*'];
    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i = 0; i<len; i++) {
        name = ps[i].name;
        if (m = e.match(ps[i].re)) {
          this.matcher.push(Object.isFunction(x[name]) ? x[name](m) :
            new Template(x[name]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.xpath = this.matcher.join('');
    Selector._cache[this.expression] = this.xpath;
  },

  findElements: function(root) {
    root = root || document;
    var e = this.expression, results;

    switch (this.mode) {
      case 'selectorsAPI':
        if (root !== document) {
          var oldId = root.id, id = $(root).identify();
          id = id.replace(/([\.:])/g, "\\$1");
          e = "#" + id + " " + e;
        }

        results = $A(root.querySelectorAll(e)).map(Element.extend);
        root.id = oldId;

        return results;
      case 'xpath':
        return document._getElementsByXPath(this.xpath, root);
      default:
       return this.matcher(root);
    }
  },

  match: function(element) {
    this.tokens = [];

    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
    var le, p, m, len = ps.length, name;

    while (e && le !== e && (/\S/).test(e)) {
      le = e;
      for (var i = 0; i<len; i++) {
        p = ps[i].re;
        name = ps[i].name;
        if (m = e.match(p)) {
          if (as[name]) {
            this.tokens.push([name, Object.clone(m)]);
            e = e.replace(m[0], '');
          } else {
            return this.findElements(document).include(element);
          }
        }
      }
    }

    var match = true, name, matches;
    for (var i = 0, token; token = this.tokens[i]; i++) {
      name = token[0], matches = token[1];
      if (!Selector.assertions[name](element, matches)) {
        match = false; break;
      }
    }

    return match;
  },

  toString: function() {
    return this.expression;
  },

  inspect: function() {
    return "#<Selector:" + this.expression.inspect() + ">";
  }
});

if (Prototype.BrowserFeatures.SelectorsAPI &&
 document.compatMode === 'BackCompat') {
  Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){
    var div = document.createElement('div'),
     span = document.createElement('span');

    div.id = "prototype_test_id";
    span.className = 'Test';
    div.appendChild(span);
    var isIgnored = (div.querySelector('#prototype_test_id .test') !== null);
    div = span = null;
    return isIgnored;
  })();
}

Object.extend(Selector, {
  _cache: { },

  xpath: {
    descendant:   "//*",
    child:        "/*",
    adjacent:     "/following-sibling::*[1]",
    laterSibling: '/following-sibling::*',
    tagName:      function(m) {
      if (m[1] == '*') return '';
      return "[local-name()='" + m[1].toLowerCase() +
             "' or local-name()='" + m[1].toUpperCase() + "']";
    },
    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
    id:           "[@id='#{1}']",
    attrPresence: function(m) {
      m[1] = m[1].toLowerCase();
      return new Template("[@#{1}]").evaluate(m);
    },
    attr: function(m) {
      m[1] = m[1].toLowerCase();
      m[3] = m[5] || m[6];
      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
    },
    pseudo: function(m) {
      var h = Selector.xpath.pseudos[m[1]];
      if (!h) return '';
      if (Object.isFunction(h)) return h(m);
      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
    },
    operators: {
      '=':  "[@#{1}='#{3}']",
      '!=': "[@#{1}!='#{3}']",
      '^=': "[starts-with(@#{1}, '#{3}')]",
      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
      '*=': "[contains(@#{1}, '#{3}')]",
      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
    },
    pseudos: {
      'first-child': '[not(preceding-sibling::*)]',
      'last-child':  '[not(following-sibling::*)]',
      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
      'empty':       "[count(*) = 0 and (count(text()) = 0)]",
      'checked':     "[@checked]",
      'disabled':    "[(@disabled) and (@type!='hidden')]",
      'enabled':     "[not(@disabled) and (@type!='hidden')]",
      'not': function(m) {
        var e = m[6], p = Selector.patterns,
            x = Selector.xpath, le, v, len = p.length, name;

        var exclusion = [];
        while (e && le != e && (/\S/).test(e)) {
          le = e;
          for (var i = 0; i<len; i++) {
            name = p[i].name
            if (m = e.match(p[i].re)) {
              v = Object.isFunction(x[name]) ? x[name](m) : new Template(x[name]).evaluate(m);
              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
              e = e.replace(m[0], '');
              break;
            }
          }
        }
        return "[not(" + exclusion.join(" and ") + ")]";
      },
      'nth-child':      function(m) {
        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
      },
      'nth-last-child': function(m) {
        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
      },
      'nth-of-type':    function(m) {
        return Selector.xpath.pseudos.nth("position() ", m);
      },
      'nth-last-of-type': function(m) {
        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
      },
      'first-of-type':  function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
      },
      'last-of-type':   function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
      },
      'only-of-type':   function(m) {
        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
      },
      nth: function(fragment, m) {
        var mm, formula = m[6], predicate;
        if (formula == 'even') formula = '2n+0';
        if (formula == 'odd')  formula = '2n+1';
        if (mm = formula.match(/^(\d+)$/)) // digit only
          return '[' + fragment + "= " + mm[1] + ']';
        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
          if (mm[1] == "-") mm[1] = -1;
          var a = mm[1] ? Number(mm[1]) : 1;
          var b = mm[2] ? Number(mm[2]) : 0;
          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
          "((#{fragment} - #{b}) div #{a} >= 0)]";
          return new Template(predicate).evaluate({
            fragment: fragment, a: a, b: b });
        }
      }
    }
  },

  criteria: {
    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
    attr: function(m) {
      m[3] = (m[5] || m[6]);
      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
    },
    pseudo: function(m) {
      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
    },
    descendant:   'c = "descendant";',
    child:        'c = "child";',
    adjacent:     'c = "adjacent";',
    laterSibling: 'c = "laterSibling";'
  },

  patterns: [
    { name: 'laterSibling', re: /^\s*~\s*/ },
    { name: 'child',        re: /^\s*>\s*/ },
    { name: 'adjacent',     re: /^\s*\+\s*/ },
    { name: 'descendant',   re: /^\s/ },

    { name: 'tagName',      re: /^\s*(\*|[\w\-]+)(\b|$)?/ },
    { name: 'id',           re: /^#([\w\-\*]+)(\b|$)/ },
    { name: 'className',    re: /^\.([\w\-\*]+)(\b|$)/ },
    { name: 'pseudo',       re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ },
    { name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ },
    { name: 'attr',         re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ }
  ],

  assertions: {
    tagName: function(element, matches) {
      return matches[1].toUpperCase() == element.tagName.toUpperCase();
    },

    className: function(element, matches) {
      return Element.hasClassName(element, matches[1]);
    },

    id: function(element, matches) {
      return element.id === matches[1];
    },

    attrPresence: function(element, matches) {
      return Element.hasAttribute(element, matches[1]);
    },

    attr: function(element, matches) {
      var nodeValue = Element.readAttribute(element, matches[1]);
      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
    }
  },

  handlers: {
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        a.push(node);
      return a;
    },

    mark: function(nodes) {
      var _true = Prototype.emptyFunction;
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = _true;
      return nodes;
    },

    unmark: (function(){

      var PROPERTIES_ATTRIBUTES_MAP = (function(){
        var el = document.createElement('div'),
            isBuggy = false,
            propName = '_countedByPrototype',
            value = 'x'
        el[propName] = value;
        isBuggy = (el.getAttribute(propName) === value);
        el = null;
        return isBuggy;
      })();

      return PROPERTIES_ATTRIBUTES_MAP ?
        function(nodes) {
          for (var i = 0, node; node = nodes[i]; i++)
            node.removeAttribute('_countedByPrototype');
          return nodes;
        } :
        function(nodes) {
          for (var i = 0, node; node = nodes[i]; i++)
            node._countedByPrototype = void 0;
          return nodes;
        }
    })(),

    index: function(parentNode, reverse, ofType) {
      parentNode._countedByPrototype = Prototype.emptyFunction;
      if (reverse) {
        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
          var node = nodes[i];
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
        }
      } else {
        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
      }
    },

    unique: function(nodes) {
      if (nodes.length == 0) return nodes;
      var results = [], n;
      for (var i = 0, l = nodes.length; i < l; i++)
        if (typeof (n = nodes[i])._countedByPrototype == 'undefined') {
          n._countedByPrototype = Prototype.emptyFunction;
          results.push(Element.extend(n));
        }
      return Selector.handlers.unmark(results);
    },

    descendant: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, node.getElementsByTagName('*'));
      return results;
    },

    child: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        for (var j = 0, child; child = node.childNodes[j]; j++)
          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
      }
      return results;
    },

    adjacent: function(nodes) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        var next = this.nextElementSibling(node);
        if (next) results.push(next);
      }
      return results;
    },

    laterSibling: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, Element.nextSiblings(node));
      return results;
    },

    nextElementSibling: function(node) {
      while (node = node.nextSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    previousElementSibling: function(node) {
      while (node = node.previousSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    tagName: function(nodes, root, tagName, combinator) {
      var uTagName = tagName.toUpperCase();
      var results = [], h = Selector.handlers;
      if (nodes) {
        if (combinator) {
          if (combinator == "descendant") {
            for (var i = 0, node; node = nodes[i]; i++)
              h.concat(results, node.getElementsByTagName(tagName));
            return results;
          } else nodes = this[combinator](nodes);
          if (tagName == "*") return nodes;
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName.toUpperCase() === uTagName) results.push(node);
        return results;
      } else return root.getElementsByTagName(tagName);
    },

    id: function(nodes, root, id, combinator) {
      var targetNode = $(id), h = Selector.handlers;

      if (root == document) {
        if (!targetNode) return [];
        if (!nodes) return [targetNode];
      } else {
        if (!root.sourceIndex || root.sourceIndex < 1) {
          var nodes = root.getElementsByTagName('*');
          for (var j = 0, node; node = nodes[j]; j++) {
            if (node.id === id) return [node];
          }
        }
      }

      if (nodes) {
        if (combinator) {
          if (combinator == 'child') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (targetNode.parentNode == node) return [targetNode];
          } else if (combinator == 'descendant') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Element.descendantOf(targetNode, node)) return [targetNode];
          } else if (combinator == 'adjacent') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Selector.handlers.previousElementSibling(targetNode) == node)
                return [targetNode];
          } else nodes = h[combinator](nodes);
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node == targetNode) return [targetNode];
        return [];
      }
      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
    },

    className: function(nodes, root, className, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      return Selector.handlers.byClassName(nodes, root, className);
    },

    byClassName: function(nodes, root, className) {
      if (!nodes) nodes = Selector.handlers.descendant([root]);
      var needle = ' ' + className + ' ';
      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
        nodeClassName = node.className;
        if (nodeClassName.length == 0) continue;
        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
          results.push(node);
      }
      return results;
    },

    attrPresence: function(nodes, root, attr, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var results = [];
      for (var i = 0, node; node = nodes[i]; i++)
        if (Element.hasAttribute(node, attr)) results.push(node);
      return results;
    },

    attr: function(nodes, root, attr, value, operator, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var handler = Selector.operators[operator], results = [];
      for (var i = 0, node; node = nodes[i]; i++) {
        var nodeValue = Element.readAttribute(node, attr);
        if (nodeValue === null) continue;
        if (handler(nodeValue, value)) results.push(node);
      }
      return results;
    },

    pseudo: function(nodes, name, value, root, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      if (!nodes) nodes = root.getElementsByTagName("*");
      return Selector.pseudos[name](nodes, value, root);
    }
  },

  pseudos: {
    'first-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.previousElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'last-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.nextElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'only-child': function(nodes, value, root) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
          results.push(node);
      return results;
    },
    'nth-child':        function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root);
    },
    'nth-last-child':   function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true);
    },
    'nth-of-type':      function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, false, true);
    },
    'nth-last-of-type': function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true, true);
    },
    'first-of-type':    function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, false, true);
    },
    'last-of-type':     function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, true, true);
    },
    'only-of-type':     function(nodes, formula, root) {
      var p = Selector.pseudos;
      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
    },

    getIndices: function(a, b, total) {
      if (a == 0) return b > 0 ? [b] : [];
      return $R(1, total).inject([], function(memo, i) {
        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
        return memo;
      });
    },

    nth: function(nodes, formula, root, reverse, ofType) {
      if (nodes.length == 0) return [];
      if (formula == 'even') formula = '2n+0';
      if (formula == 'odd')  formula = '2n+1';
      var h = Selector.handlers, results = [], indexed = [], m;
      h.mark(nodes);
      for (var i = 0, node; node = nodes[i]; i++) {
        if (!node.parentNode._countedByPrototype) {
          h.index(node.parentNode, reverse, ofType);
          indexed.push(node.parentNode);
        }
      }
      if (formula.match(/^\d+$/)) { // just a number
        formula = Number(formula);
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.nodeIndex == formula) results.push(node);
      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
        if (m[1] == "-") m[1] = -1;
        var a = m[1] ? Number(m[1]) : 1;
        var b = m[2] ? Number(m[2]) : 0;
        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
          for (var j = 0; j < l; j++)
            if (node.nodeIndex == indices[j]) results.push(node);
        }
      }
      h.unmark(nodes);
      h.unmark(indexed);
      return results;
    },

    'empty': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (node.tagName == '!' || node.firstChild) continue;
        results.push(node);
      }
      return results;
    },

    'not': function(nodes, selector, root) {
      var h = Selector.handlers, selectorType, m;
      var exclusions = new Selector(selector).findElements(root);
      h.mark(exclusions);
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node._countedByPrototype) results.push(node);
      h.unmark(exclusions);
      return results;
    },

    'enabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node.disabled && (!node.type || node.type !== 'hidden'))
          results.push(node);
      return results;
    },

    'disabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.disabled) results.push(node);
      return results;
    },

    'checked': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.checked) results.push(node);
      return results;
    }
  },

  operators: {
    '=':  function(nv, v) { return nv == v; },
    '!=': function(nv, v) { return nv != v; },
    '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
    '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
     '-').include('-' + (v || "").toUpperCase() + '-'); }
  },

  split: function(expression) {
    var expressions = [];
    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
      expressions.push(m[1].strip());
    });
    return expressions;
  },

  matchElements: function(elements, expression) {
    var matches = $$(expression), h = Selector.handlers;
    h.mark(matches);
    for (var i = 0, results = [], element; element = elements[i]; i++)
      if (element._countedByPrototype) results.push(element);
    h.unmark(matches);
    return results;
  },

  findElement: function(elements, expression, index) {
    if (Object.isNumber(expression)) {
      index = expression; expression = false;
    }
    return Selector.matchElements(elements, expression || '*')[index || 0];
  },

  findChildElements: function(element, expressions) {
    expressions = Selector.split(expressions.join(','));
    var results = [], h = Selector.handlers;
    for (var i = 0, l = expressions.length, selector; i < l; i++) {
      selector = new Selector(expressions[i].strip());
      h.concat(results, selector.findElements(element));
    }
    return (l > 1) ? h.unique(results) : results;
  }
});

if (Prototype.Browser.IE) {
  Object.extend(Selector.handlers, {
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        if (node.tagName !== "!") a.push(node);
      return a;
    }
  });
}

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}

var Form = {
  reset: function(form) {
    form = $(form);
    form.reset();
    return form;
  },

  serializeElements: function(elements, options) {
    if (typeof options != 'object') options = { hash: !!options };
    else if (Object.isUndefined(options.hash)) options.hash = true;
    var key, value, submitted = false, submit = options.submit;

    var data = elements.inject({ }, function(result, element) {
      if (!element.disabled && element.name) {
        key = element.name; value = $(element).getValue();
        if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted = true)))) {
          if (key in result) {
            if (!Object.isArray(result[key])) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return options.hash ? data : Object.toQueryString(data);
  }
};

Form.Methods = {
  serialize: function(form, options) {
    return Form.serializeElements(Form.getElements(form), options);
  },

  getElements: function(form) {
    var elements = $(form).getElementsByTagName('*'),
        element,
        arr = [ ],
        serializers = Form.Element.Serializers;
    for (var i = 0; element = elements[i]; i++) {
      arr.push(element);
    }
    return arr.inject([], function(elements, child) {
      if (serializers[child.tagName.toLowerCase()])
        elements.push(Element.extend(child));
      return elements;
    })
  },

  getInputs: function(form, typeName, name) {
    form = $(form);
    var inputs = form.getElementsByTagName('input');

    if (!typeName && !name) return $A(inputs).map(Element.extend);

    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
      var input = inputs[i];
      if ((typeName && input.type != typeName) || (name && input.name != name))
        continue;
      matchingInputs.push(Element.extend(input));
    }

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('disable');
    return form;
  },

  enable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('enable');
    return form;
  },

  findFirstElement: function(form) {
    var elements = $(form).getElements().findAll(function(element) {
      return 'hidden' != element.type && !element.disabled;
    });
    var firstByIndex = elements.findAll(function(element) {
      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
    }).sortBy(function(element) { return element.tabIndex }).first();

    return firstByIndex ? firstByIndex : elements.find(function(element) {
      return /^(?:input|select|textarea)$/i.test(element.tagName);
    });
  },

  focusFirstElement: function(form) {
    form = $(form);
    form.findFirstElement().activate();
    return form;
  },

  request: function(form, options) {
    form = $(form), options = Object.clone(options || { });

    var params = options.parameters, action = form.readAttribute('action') || '';
    if (action.blank()) action = window.location.href;
    options.parameters = form.serialize(true);

    if (params) {
      if (Object.isString(params)) params = params.toQueryParams();
      Object.extend(options.parameters, params);
    }

    if (form.hasAttribute('method') && !options.method)
      options.method = form.method;

    return new Ajax.Request(action, options);
  }
};

/*--------------------------------------------------------------------------*/


Form.Element = {
  focus: function(element) {
    $(element).focus();
    return element;
  },

  select: function(element) {
    $(element).select();
    return element;
  }
};

Form.Element.Methods = {

  serialize: function(element) {
    element = $(element);
    if (!element.disabled && element.name) {
      var value = element.getValue();
      if (value != undefined) {
        var pair = { };
        pair[element.name] = value;
        return Object.toQueryString(pair);
      }
    }
    return '';
  },

  getValue: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    return Form.Element.Serializers[method](element);
  },

  setValue: function(element, value) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    Form.Element.Serializers[method](element, value);
    return element;
  },

  clear: function(element) {
    $(element).value = '';
    return element;
  },

  present: function(element) {
    return $(element).value != '';
  },

  activate: function(element) {
    element = $(element);
    try {
      element.focus();
      if (element.select && (element.tagName.toLowerCase() != 'input' ||
          !(/^(?:button|reset|submit)$/i.test(element.type))))
        element.select();
    } catch (e) { }
    return element;
  },

  disable: function(element) {
    element = $(element);
    element.disabled = true;
    return element;
  },

  enable: function(element) {
    element = $(element);
    element.disabled = false;
    return element;
  }
};

/*--------------------------------------------------------------------------*/

var Field = Form.Element;

var $F = Form.Element.Methods.getValue;

/*--------------------------------------------------------------------------*/

Form.Element.Serializers = {
  input: function(element, value) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        return Form.Element.Serializers.inputSelector(element, value);
      default:
        return Form.Element.Serializers.textarea(element, value);
    }
  },

  inputSelector: function(element, value) {
    if (Object.isUndefined(value)) return element.checked ? element.value : null;
    else element.checked = !!value;
  },

  textarea: function(element, value) {
    if (Object.isUndefined(value)) return element.value;
    else element.value = value;
  },

  select: function(element, value) {
    if (Object.isUndefined(value))
      return this[element.type == 'select-one' ?
        'selectOne' : 'selectMany'](element);
    else {
      var opt, currentValue, single = !Object.isArray(value);
      for (var i = 0, length = element.length; i < length; i++) {
        opt = element.options[i];
        currentValue = this.optionValue(opt);
        if (single) {
          if (currentValue == value) {
            opt.selected = true;
            return;
          }
        }
        else opt.selected = value.include(currentValue);
      }
    }
  },

  selectOne: function(element) {
    var index = element.selectedIndex;
    return index >= 0 ? this.optionValue(element.options[index]) : null;
  },

  selectMany: function(element) {
    var values, length = element.length;
    if (!length) return null;

    for (var i = 0, values = []; i < length; i++) {
      var opt = element.options[i];
      if (opt.selected) values.push(this.optionValue(opt));
    }
    return values;
  },

  optionValue: function(opt) {
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
};

/*--------------------------------------------------------------------------*/


Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
  initialize: function($super, element, frequency, callback) {
    $super(callback, frequency);
    this.element   = $(element);
    this.lastValue = this.getValue();
  },

  execute: function() {
    var value = this.getValue();
    if (Object.isString(this.lastValue) && Object.isString(value) ?
        this.lastValue != value : String(this.lastValue) != String(value)) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
});

Form.Element.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});

/*--------------------------------------------------------------------------*/

Abstract.EventObserver = Class.create({
  initialize: function(element, callback) {
    this.element  = $(element);
    this.callback = callback;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback, this);
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          Event.observe(element, 'click', this.onElementEvent.bind(this));
          break;
        default:
          Event.observe(element, 'change', this.onElementEvent.bind(this));
          break;
      }
    }
  }
});

Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
(function() {

  var Event = {
    KEY_BACKSPACE: 8,
    KEY_TAB:       9,
    KEY_RETURN:   13,
    KEY_ESC:      27,
    KEY_LEFT:     37,
    KEY_UP:       38,
    KEY_RIGHT:    39,
    KEY_DOWN:     40,
    KEY_DELETE:   46,
    KEY_HOME:     36,
    KEY_END:      35,
    KEY_PAGEUP:   33,
    KEY_PAGEDOWN: 34,
    KEY_INSERT:   45,

    cache: {}
  };

  var docEl = document.documentElement;
  var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl
    && 'onmouseleave' in docEl;

  var _isButton;
  if (Prototype.Browser.IE) {
    var buttonMap = { 0: 1, 1: 4, 2: 2 };
    _isButton = function(event, code) {
      return event.button === buttonMap[code];
    };
  } else if (Prototype.Browser.WebKit) {
    _isButton = function(event, code) {
      switch (code) {
        case 0: return event.which == 1 && !event.metaKey;
        case 1: return event.which == 1 && event.metaKey;
        default: return false;
      }
    };
  } else {
    _isButton = function(event, code) {
      return event.which ? (event.which === code + 1) : (event.button === code);
    };
  }

  function isLeftClick(event)   { return _isButton(event, 0) }

  function isMiddleClick(event) { return _isButton(event, 1) }

  function isRightClick(event)  { return _isButton(event, 2) }

  function element(event) {
    event = Event.extend(event);

    var node = event.target, type = event.type,
     currentTarget = event.currentTarget;

    if (currentTarget && currentTarget.tagName) {
      if (type === 'load' || type === 'error' ||
        (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
          && currentTarget.type === 'radio'))
            node = currentTarget;
    }

    if (node.nodeType == Node.TEXT_NODE)
      node = node.parentNode;

    return Element.extend(node);
  }

  function findElement(event, expression) {
    var element = Event.element(event);
    if (!expression) return element;
    var elements = [element].concat(element.ancestors());
    return Selector.findElement(elements, expression, 0);
  }

  function pointer(event) {
    return { x: pointerX(event), y: pointerY(event) };
  }

  function pointerX(event) {
    var docElement = document.documentElement,
     body = document.body || { scrollLeft: 0 };

    return event.pageX || (event.clientX +
      (docElement.scrollLeft || body.scrollLeft) -
      (docElement.clientLeft || 0));
  }

  function pointerY(event) {
    var docElement = document.documentElement,
     body = document.body || { scrollTop: 0 };

    return  event.pageY || (event.clientY +
       (docElement.scrollTop || body.scrollTop) -
       (docElement.clientTop || 0));
  }


  function stop(event) {
    Event.extend(event);
    event.preventDefault();
    event.stopPropagation();

    event.stopped = true;
  }

  Event.Methods = {
    isLeftClick: isLeftClick,
    isMiddleClick: isMiddleClick,
    isRightClick: isRightClick,

    element: element,
    findElement: findElement,

    pointer: pointer,
    pointerX: pointerX,
    pointerY: pointerY,

    stop: stop
  };


  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
    m[name] = Event.Methods[name].methodize();
    return m;
  });

  if (Prototype.Browser.IE) {
    function _relatedTarget(event) {
      var element;
      switch (event.type) {
        case 'mouseover': element = event.fromElement; break;
        case 'mouseout':  element = event.toElement;   break;
        default: return null;
      }
      return Element.extend(element);
    }

    Object.extend(methods, {
      stopPropagation: function() { this.cancelBubble = true },
      preventDefault:  function() { this.returnValue = false },
      inspect: function() { return '[object Event]' }
    });

    Event.extend = function(event, element) {
      if (!event) return false;
      if (event._extendedByPrototype) return event;

      event._extendedByPrototype = Prototype.emptyFunction;
      var pointer = Event.pointer(event);

      Object.extend(event, {
        target: event.srcElement || element,
        relatedTarget: _relatedTarget(event),
        pageX:  pointer.x,
        pageY:  pointer.y
      });

      return Object.extend(event, methods);
    };
  } else {
    Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__;
    Object.extend(Event.prototype, methods);
    Event.extend = Prototype.K;
  }

  function _createResponder(element, eventName, handler) {
    var registry = Element.retrieve(element, 'prototype_event_registry');

    if (Object.isUndefined(registry)) {
      CACHE.push(element);
      registry = Element.retrieve(element, 'prototype_event_registry', $H());
    }

    var respondersForEvent = registry.get(eventName);
    if (Object.isUndefined(respondersForEvent)) {
      respondersForEvent = [];
      registry.set(eventName, respondersForEvent);
    }

    if (respondersForEvent.pluck('handler').include(handler)) return false;

    var responder;
    if (eventName.include(":")) {
      responder = function(event) {
        if (Object.isUndefined(event.eventName))
          return false;

        if (event.eventName !== eventName)
          return false;

        Event.extend(event, element);
        handler.call(element, event);
      };
    } else {
      if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED &&
       (eventName === "mouseenter" || eventName === "mouseleave")) {
        if (eventName === "mouseenter" || eventName === "mouseleave") {
          responder = function(event) {
            Event.extend(event, element);

            var parent = event.relatedTarget;
            while (parent && parent !== element) {
              try { parent = parent.parentNode; }
              catch(e) { parent = element; }
            }

            if (parent === element) return;

            handler.call(element, event);
          };
        }
      } else {
        responder = function(event) {
          Event.extend(event, element);
          handler.call(element, event);
        };
      }
    }

    responder.handler = handler;
    respondersForEvent.push(responder);
    return responder;
  }

  function _destroyCache() {
    for (var i = 0, length = CACHE.length; i < length; i++) {
      Event.stopObserving(CACHE[i]);
      CACHE[i] = null;
    }
  }

  var CACHE = [];

  if (Prototype.Browser.IE)
    window.attachEvent('onunload', _destroyCache);

  if (Prototype.Browser.WebKit)
    window.addEventListener('unload', Prototype.emptyFunction, false);


  var _getDOMEventName = Prototype.K;

  if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) {
    _getDOMEventName = function(eventName) {
      var translations = { mouseenter: "mouseover", mouseleave: "mouseout" };
      return eventName in translations ? translations[eventName] : eventName;
    };
  }

  function observe(element, eventName, handler) {
    element = $(element);

    var responder = _createResponder(element, eventName, handler);

    if (!responder) return element;

    if (eventName.include(':')) {
      if (element.addEventListener)
        element.addEventListener("dataavailable", responder, false);
      else {
        element.attachEvent("ondataavailable", responder);
        element.attachEvent("onfilterchange", responder);
      }
    } else {
      var actualEventName = _getDOMEventName(eventName);

      if (element.addEventListener)
        element.addEventListener(actualEventName, responder, false);
      else
        element.attachEvent("on" + actualEventName, responder);
    }

    return element;
  }

  function stopObserving(element, eventName, handler) {
    element = $(element);

    var registry = Element.retrieve(element, 'prototype_event_registry');

    if (Object.isUndefined(registry)) return element;

    if (eventName && !handler) {
      var responders = registry.get(eventName);

      if (Object.isUndefined(responders)) return element;

      responders.each( function(r) {
        Element.stopObserving(element, eventName, r.handler);
      });
      return element;
    } else if (!eventName) {
      registry.each( function(pair) {
        var eventName = pair.key, responders = pair.value;

        responders.each( function(r) {
          Element.stopObserving(element, eventName, r.handler);
        });
      });
      return element;
    }

    var responders = registry.get(eventName);

    if (!responders) return;

    var responder = responders.find( function(r) { return r.handler === handler; });
    if (!responder) return element;

    var actualEventName = _getDOMEventName(eventName);

    if (eventName.include(':')) {
      if (element.removeEventListener)
        element.removeEventListener("dataavailable", responder, false);
      else {
        element.detachEvent("ondataavailable", responder);
        element.detachEvent("onfilterchange",  responder);
      }
    } else {
      if (element.removeEventListener)
        element.removeEventListener(actualEventName, responder, false);
      else
        element.detachEvent('on' + actualEventName, responder);
    }

    registry.set(eventName, responders.without(responder));

    return element;
  }

  function fire(element, eventName, memo, bubble) {
    element = $(element);

    if (Object.isUndefined(bubble))
      bubble = true;

    if (element == document && document.createEvent && !element.dispatchEvent)
      element = document.documentElement;

    var event;
    if (document.createEvent) {
      event = document.createEvent('HTMLEvents');
      event.initEvent('dataavailable', true, true);
    } else {
      event = document.createEventObject();
      event.eventType = bubble ? 'ondataavailable' : 'onfilterchange';
    }

    event.eventName = eventName;
    event.memo = memo || { };

    if (document.createEvent)
      element.dispatchEvent(event);
    else
      element.fireEvent(event.eventType, event);

    return Event.extend(event);
  }


  Object.extend(Event, Event.Methods);

  Object.extend(Event, {
    fire:          fire,
    observe:       observe,
    stopObserving: stopObserving
  });

  Element.addMethods({
    fire:          fire,

    observe:       observe,

    stopObserving: stopObserving
  });

  Object.extend(document, {
    fire:          fire.methodize(),

    observe:       observe.methodize(),

    stopObserving: stopObserving.methodize(),

    loaded:        false
  });

  if (window.Event) Object.extend(window.Event, Event);
  else window.Event = Event;
})();

(function() {
  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
     Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */

  var timer;

  function fireContentLoadedEvent() {
    if (document.loaded) return;
    if (timer) window.clearTimeout(timer);
    document.loaded = true;
    document.fire('dom:loaded');
  }

  function checkReadyState() {
    if (document.readyState === 'complete') {
      document.stopObserving('readystatechange', checkReadyState);
      fireContentLoadedEvent();
    }
  }

  function pollDoScroll() {
    try { document.documentElement.doScroll('left'); }
    catch(e) {
      timer = pollDoScroll.defer();
      return;
    }
    fireContentLoadedEvent();
  }

  if (document.addEventListener) {
    document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false);
  } else {
    document.observe('readystatechange', checkReadyState);
    if (window == top)
      timer = pollDoScroll.defer();
  }

  Event.observe(window, 'load', fireContentLoadedEvent);
})();

Element.addMethods();

/*------------------------------- DEPRECATED -------------------------------*/

Hash.toQueryString = Object.toQueryString;

var Toggle = { display: Element.toggle };

Element.Methods.childOf = Element.Methods.descendantOf;

var Insertion = {
  Before: function(element, content) {
    return Element.insert(element, {before:content});
  },

  Top: function(element, content) {
    return Element.insert(element, {top:content});
  },

  Bottom: function(element, content) {
    return Element.insert(element, {bottom:content});
  },

  After: function(element, content) {
    return Element.insert(element, {after:content});
  }
};

var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

var Position = {
  includeScrollOffsets: false,

  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = Element.cumulativeOffset(element);

    return (y >= this.offset[1] &&
            y <  this.offset[1] + element.offsetHeight &&
            x >= this.offset[0] &&
            x <  this.offset[0] + element.offsetWidth);
  },

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = Element.cumulativeScrollOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = Element.cumulativeOffset(element);

    return (this.ycomp >= this.offset[1] &&
            this.ycomp <  this.offset[1] + element.offsetHeight &&
            this.xcomp >= this.offset[0] &&
            this.xcomp <  this.offset[0] + element.offsetWidth);
  },

  overlap: function(mode, element) {
    if (!mode) return 0;
    if (mode == 'vertical')
      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
        element.offsetHeight;
    if (mode == 'horizontal')
      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
        element.offsetWidth;
  },


  cumulativeOffset: Element.Methods.cumulativeOffset,

  positionedOffset: Element.Methods.positionedOffset,

  absolutize: function(element) {
    Position.prepare();
    return Element.absolutize(element);
  },

  relativize: function(element) {
    Position.prepare();
    return Element.relativize(element);
  },

  realOffset: Element.Methods.cumulativeScrollOffset,

  offsetParent: Element.Methods.getOffsetParent,

  page: Element.Methods.viewportOffset,

  clone: function(source, target, options) {
    options = options || { };
    return Element.clonePosition(target, source, options);
  }
};

/*--------------------------------------------------------------------------*/

if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
  function iter(name) {
    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
  }

  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
  function(element, className) {
    className = className.toString().strip();
    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
  } : function(element, className) {
    className = className.toString().strip();
    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
    if (!classNames && !className) return elements;

    var nodes = $(element).getElementsByTagName('*');
    className = ' ' + className + ' ';

    for (var i = 0, child, cn; child = nodes[i]; i++) {
      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
          (classNames && classNames.all(function(name) {
            return !name.toString().blank() && cn.include(' ' + name + ' ');
          }))))
        elements.push(Element.extend(child));
    }
    return elements;
  };

  return function(className, parentElement) {
    return $(parentElement || document.body).getElementsByClassName(className);
  };
}(Element.Methods);

/*--------------------------------------------------------------------------*/

Element.ClassNames = Class.create();
Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = $(element);
  },

  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },

  set: function(className) {
    this.element.className = className;
  },

  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set($A(this).concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set($A(this).without(classNameToRemove).join(' '));
  },

  toString: function() {
    return $A(this).join(' ');
  }
};

Object.extend(Element.ClassNames.prototype, Enumerable);

/*--------------------------------------------------------------------------*/


// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
//  Justin Palmer (http://encytemedia.com/)
//  Mark Pilgrim (http://diveintomark.org/)
//  Martin Bialasinki
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
  var color = '#';
  if (this.slice(0,4) == 'rgb(') {
    var cols = this.slice(4,this.length-1).split(',');
    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
  } else {
    if (this.slice(0,1) == '#') {
      if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
      if (this.length==7) color = this.toLowerCase();
    }
  }
  return (color.length==7 ? color : (arguments[0] || this));
};

/*--------------------------------------------------------------------------*/

Element.collectTextNodes = function(element) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
  }).flatten().join('');
};

Element.collectTextNodesIgnoreClass = function(element, className) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
        Element.collectTextNodesIgnoreClass(node, className) : ''));
  }).flatten().join('');
};

Element.setContentZoom = function(element, percent) {
  element = $(element);
  element.setStyle({fontSize: (percent/100) + 'em'});
  if (Prototype.Browser.WebKit) window.scrollBy(0,0);
  return element;
};

Element.getInlineOpacity = function(element){
  return $(element).style.opacity || '';
};

Element.forceRerendering = function(element) {
  try {
    element = $(element);
    var n = document.createTextNode(' ');
    element.appendChild(n);
    element.removeChild(n);
  } catch(e) { }
};

/*--------------------------------------------------------------------------*/

var Effect = {
  _elementDoesNotExistError: {
    name: 'ElementDoesNotExistError',
    message: 'The specified DOM element does not exist, but is required for this effect to operate'
  },
  Transitions: {
    linear: Prototype.K,
    sinoidal: function(pos) {
      return (-Math.cos(pos*Math.PI)/2) + .5;
    },
    reverse: function(pos) {
      return 1-pos;
    },
    flicker: function(pos) {
      var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
      return pos > 1 ? 1 : pos;
    },
    wobble: function(pos) {
      return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
    },
    pulse: function(pos, pulses) {
      return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
    },
    spring: function(pos) {
      return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
    },
    none: function(pos) {
      return 0;
    },
    full: function(pos) {
      return 1;
    }
  },
  DefaultOptions: {
    duration:   1.0,   // seconds
    fps:        100,   // 100= assume 66fps max.
    sync:       false, // true for combining
    from:       0.0,
    to:         1.0,
    delay:      0.0,
    queue:      'parallel'
  },
  tagifyText: function(element) {
    var tagifyStyle = 'position:relative';
    if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';

    element = $(element);
    $A(element.childNodes).each( function(child) {
      if (child.nodeType==3) {
        child.nodeValue.toArray().each( function(character) {
          element.insertBefore(
            new Element('span', {style: tagifyStyle}).update(
              character == ' ' ? String.fromCharCode(160) : character),
              child);
        });
        Element.remove(child);
      }
    });
  },
  multiple: function(element, effect) {
    var elements;
    if (((typeof element == 'object') ||
        Object.isFunction(element)) &&
       (element.length))
      elements = element;
    else
      elements = $(element).childNodes;

    var options = Object.extend({
      speed: 0.1,
      delay: 0.0
    }, arguments[2] || { });
    var masterDelay = options.delay;

    $A(elements).each( function(element, index) {
      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
    });
  },
  PAIRS: {
    'slide':  ['SlideDown','SlideUp'],
    'blind':  ['BlindDown','BlindUp'],
    'appear': ['Appear','Fade']
  },
  toggle: function(element, effect) {
    element = $(element);
    effect = (effect || 'appear').toLowerCase();
    var options = Object.extend({
      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
    }, arguments[2] || { });
    Effect[element.visible() ?
      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
  }
};

Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;

/* ------------- core effects ------------- */

Effect.ScopedQueue = Class.create(Enumerable, {
  initialize: function() {
    this.effects  = [];
    this.interval = null;
  },
  _each: function(iterator) {
    this.effects._each(iterator);
  },
  add: function(effect) {
    var timestamp = new Date().getTime();

    var position = Object.isString(effect.options.queue) ?
      effect.options.queue : effect.options.queue.position;

    switch(position) {
      case 'front':
        // move unstarted effects after this effect
        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
            e.startOn  += effect.finishOn;
            e.finishOn += effect.finishOn;
          });
        break;
      case 'with-last':
        timestamp = this.effects.pluck('startOn').max() || timestamp;
        break;
      case 'end':
        // start effect after last queued effect has finished
        timestamp = this.effects.pluck('finishOn').max() || timestamp;
        break;
    }

    effect.startOn  += timestamp;
    effect.finishOn += timestamp;

    if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
      this.effects.push(effect);

    if (!this.interval)
      this.interval = setInterval(this.loop.bind(this), 15);
  },
  remove: function(effect) {
    this.effects = this.effects.reject(function(e) { return e==effect });
    if (this.effects.length == 0) {
      clearInterval(this.interval);
      this.interval = null;
    }
  },
  loop: function() {
    var timePos = new Date().getTime();
    for(var i=0, len=this.effects.length;i<len;i++)
      this.effects[i] && this.effects[i].loop(timePos);
  }
});

Effect.Queues = {
  instances: $H(),
  get: function(queueName) {
    if (!Object.isString(queueName)) return queueName;

    return this.instances.get(queueName) ||
      this.instances.set(queueName, new Effect.ScopedQueue());
  }
};
Effect.Queue = Effect.Queues.get('global');

Effect.Base = Class.create({
  position: null,
  start: function(options) {
    function codeForEvent(options,eventName){
      return (
        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
      );
    }
    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
    this.currentFrame = 0;
    this.state        = 'idle';
    this.startOn      = this.options.delay*1000;
    this.finishOn     = this.startOn+(this.options.duration*1000);
    this.fromToDelta  = this.options.to-this.options.from;
    this.totalTime    = this.finishOn-this.startOn;
    this.totalFrames  = this.options.fps*this.options.duration;

    this.render = (function() {
      function dispatch(effect, eventName) {
        if (effect.options[eventName + 'Internal'])
          effect.options[eventName + 'Internal'](effect);
        if (effect.options[eventName])
          effect.options[eventName](effect);
      }

      return function(pos) {
        if (this.state === "idle") {
          this.state = "running";
          dispatch(this, 'beforeSetup');
          if (this.setup) this.setup();
          dispatch(this, 'afterSetup');
        }
        if (this.state === "running") {
          pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
          this.position = pos;
          dispatch(this, 'beforeUpdate');
          if (this.update) this.update(pos);
          dispatch(this, 'afterUpdate');
        }
      };
    })();

    this.event('beforeStart');
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).add(this);
  },
  loop: function(timePos) {
    if (timePos >= this.startOn) {
      if (timePos >= this.finishOn) {
        this.render(1.0);
        this.cancel();
        this.event('beforeFinish');
        if (this.finish) this.finish();
        this.event('afterFinish');
        return;
      }
      var pos   = (timePos - this.startOn) / this.totalTime,
          frame = (pos * this.totalFrames).round();
      if (frame > this.currentFrame) {
        this.render(pos);
        this.currentFrame = frame;
      }
    }
  },
  cancel: function() {
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).remove(this);
    this.state = 'finished';
  },
  event: function(eventName) {
    if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
    if (this.options[eventName]) this.options[eventName](this);
  },
  inspect: function() {
    var data = $H();
    for(property in this)
      if (!Object.isFunction(this[property])) data.set(property, this[property]);
    return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
  }
});

Effect.Parallel = Class.create(Effect.Base, {
  initialize: function(effects) {
    this.effects = effects || [];
    this.start(arguments[1]);
  },
  update: function(position) {
    this.effects.invoke('render', position);
  },
  finish: function(position) {
    this.effects.each( function(effect) {
      effect.render(1.0);
      effect.cancel();
      effect.event('beforeFinish');
      if (effect.finish) effect.finish(position);
      effect.event('afterFinish');
    });
  }
});

Effect.Tween = Class.create(Effect.Base, {
  initialize: function(object, from, to) {
    object = Object.isString(object) ? $(object) : object;
    var args = $A(arguments), method = args.last(),
      options = args.length == 5 ? args[3] : null;
    this.method = Object.isFunction(method) ? method.bind(object) :
      Object.isFunction(object[method]) ? object[method].bind(object) :
      function(value) { object[method] = value };
    this.start(Object.extend({ from: from, to: to }, options || { }));
  },
  update: function(position) {
    this.method(position);
  }
});

Effect.Event = Class.create(Effect.Base, {
  initialize: function() {
    this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
  },
  update: Prototype.emptyFunction
});

Effect.Opacity = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    // make this work on IE on elements without 'layout'
    if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
      this.element.setStyle({zoom: 1});
    var options = Object.extend({
      from: this.element.getOpacity() || 0.0,
      to:   1.0
    }, arguments[1] || { });
    this.start(options);
  },
  update: function(position) {
    this.element.setOpacity(position);
  }
});

Effect.Move = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      x:    0,
      y:    0,
      mode: 'relative'
    }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    this.element.makePositioned();
    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
    if (this.options.mode == 'absolute') {
      this.options.x = this.options.x - this.originalLeft;
      this.options.y = this.options.y - this.originalTop;
    }
  },
  update: function(position) {
    this.element.setStyle({
      left: (this.options.x  * position + this.originalLeft).round() + 'px',
      top:  (this.options.y  * position + this.originalTop).round()  + 'px'
    });
  }
});

// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
  return new Effect.Move(element,
    Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
};

Effect.Scale = Class.create(Effect.Base, {
  initialize: function(element, percent) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      scaleX: true,
      scaleY: true,
      scaleContent: true,
      scaleFromCenter: false,
      scaleMode: 'box',        // 'box' or 'contents' or { } with provided values
      scaleFrom: 100.0,
      scaleTo:   percent
    }, arguments[2] || { });
    this.start(options);
  },
  setup: function() {
    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
    this.elementPositioning = this.element.getStyle('position');

    this.originalStyle = { };
    ['top','left','width','height','fontSize'].each( function(k) {
      this.originalStyle[k] = this.element.style[k];
    }.bind(this));

    this.originalTop  = this.element.offsetTop;
    this.originalLeft = this.element.offsetLeft;

    var fontSize = this.element.getStyle('font-size') || '100%';
    ['em','px','%','pt'].each( function(fontSizeType) {
      if (fontSize.indexOf(fontSizeType)>0) {
        this.fontSize     = parseFloat(fontSize);
        this.fontSizeType = fontSizeType;
      }
    }.bind(this));

    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;

    this.dims = null;
    if (this.options.scaleMode=='box')
      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
    if (/^content/.test(this.options.scaleMode))
      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
    if (!this.dims)
      this.dims = [this.options.scaleMode.originalHeight,
                   this.options.scaleMode.originalWidth];
  },
  update: function(position) {
    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
    if (this.options.scaleContent && this.fontSize)
      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
  },
  finish: function(position) {
    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
  },
  setDimensions: function(height, width) {
    var d = { };
    if (this.options.scaleX) d.width = width.round() + 'px';
    if (this.options.scaleY) d.height = height.round() + 'px';
    if (this.options.scaleFromCenter) {
      var topd  = (height - this.dims[0])/2;
      var leftd = (width  - this.dims[1])/2;
      if (this.elementPositioning == 'absolute') {
        if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
        if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
      } else {
        if (this.options.scaleY) d.top = -topd + 'px';
        if (this.options.scaleX) d.left = -leftd + 'px';
      }
    }
    this.element.setStyle(d);
  }
});

Effect.Highlight = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    // Prevent executing on elements not in the layout flow
    if (this.element.getStyle('display')=='none') { this.cancel(); return; }
    // Disable background image during the effect
    this.oldStyle = { };
    if (!this.options.keepBackgroundImage) {
      this.oldStyle.backgroundImage = this.element.getStyle('background-image');
      this.element.setStyle({backgroundImage: 'none'});
    }
    if (!this.options.endcolor)
      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
    if (!this.options.restorecolor)
      this.options.restorecolor = this.element.getStyle('background-color');
    // init color calculations
    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
  },
  update: function(position) {
    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
      return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
  },
  finish: function() {
    this.element.setStyle(Object.extend(this.oldStyle, {
      backgroundColor: this.options.restorecolor
    }));
  }
});

Effect.ScrollTo = function(element) {
  var options = arguments[1] || { },
  scrollOffsets = document.viewport.getScrollOffsets(),
  elementOffsets = $(element).cumulativeOffset();

  if (options.offset) elementOffsets[1] += options.offset;

  return new Effect.Tween(null,
    scrollOffsets.top,
    elementOffsets[1],
    options,
    function(p){ scrollTo(scrollOffsets.left, p.round()); }
  );
};

/* ------------- combination effects ------------- */

Effect.Fade = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  var options = Object.extend({
    from: element.getOpacity() || 1.0,
    to:   0.0,
    afterFinishInternal: function(effect) {
      if (effect.options.to!=0) return;
      effect.element.hide().setStyle({opacity: oldOpacity});
    }
  }, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Appear = function(element) {
  element = $(element);
  var options = Object.extend({
  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
  to:   1.0,
  // force Safari to render floated elements properly
  afterFinishInternal: function(effect) {
    effect.element.forceRerendering();
  },
  beforeSetup: function(effect) {
    effect.element.setOpacity(effect.options.from).show();
  }}, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Puff = function(element) {
  element = $(element);
  var oldStyle = {
    opacity: element.getInlineOpacity(),
    position: element.getStyle('position'),
    top:  element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height
  };
  return new Effect.Parallel(
   [ new Effect.Scale(element, 200,
      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
     Object.extend({ duration: 1.0,
      beforeSetupInternal: function(effect) {
        Position.absolutize(effect.effects[0].element);
      },
      afterFinishInternal: function(effect) {
         effect.effects[0].element.hide().setStyle(oldStyle); }
     }, arguments[1] || { })
   );
};

Effect.BlindUp = function(element) {
  element = $(element);
  element.makeClipping();
  return new Effect.Scale(element, 0,
    Object.extend({ scaleContent: false,
      scaleX: false,
      restoreAfterFinish: true,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping();
      }
    }, arguments[1] || { })
  );
};

Effect.BlindDown = function(element) {
  element = $(element);
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: 0,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping();
    }
  }, arguments[1] || { }));
};

Effect.SwitchOff = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  return new Effect.Appear(element, Object.extend({
    duration: 0.4,
    from: 0,
    transition: Effect.Transitions.flicker,
    afterFinishInternal: function(effect) {
      new Effect.Scale(effect.element, 1, {
        duration: 0.3, scaleFromCenter: true,
        scaleX: false, scaleContent: false, restoreAfterFinish: true,
        beforeSetup: function(effect) {
          effect.element.makePositioned().makeClipping();
        },
        afterFinishInternal: function(effect) {
          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
        }
      });
    }
  }, arguments[1] || { }));
};

Effect.DropOut = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left'),
    opacity: element.getInlineOpacity() };
  return new Effect.Parallel(
    [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
    Object.extend(
      { duration: 0.5,
        beforeSetup: function(effect) {
          effect.effects[0].element.makePositioned();
        },
        afterFinishInternal: function(effect) {
          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
        }
      }, arguments[1] || { }));
};

Effect.Shake = function(element) {
  element = $(element);
  var options = Object.extend({
    distance: 20,
    duration: 0.5
  }, arguments[1] || {});
  var distance = parseFloat(options.distance);
  var split = parseFloat(options.duration) / 10.0;
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left') };
    return new Effect.Move(element,
      { x:  distance, y: 0, duration: split, afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
        effect.element.undoPositioned().setStyle(oldStyle);
  }}); }}); }}); }}); }}); }});
};

Effect.SlideDown = function(element) {
  element = $(element).cleanWhitespace();
  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: window.opera ? 0 : 1,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
    }, arguments[1] || { })
  );
};

Effect.SlideUp = function(element) {
  element = $(element).cleanWhitespace();
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, window.opera ? 0 : 1,
   Object.extend({ scaleContent: false,
    scaleX: false,
    scaleMode: 'box',
    scaleFrom: 100,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
    }
   }, arguments[1] || { })
  );
};

// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
  return new Effect.Scale(element, window.opera ? 1 : 0, {
    restoreAfterFinish: true,
    beforeSetup: function(effect) {
      effect.element.makeClipping();
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping();
    }
  });
};

Effect.Grow = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.full
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var initialMoveX, initialMoveY;
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      initialMoveX = initialMoveY = moveX = moveY = 0;
      break;
    case 'top-right':
      initialMoveX = dims.width;
      initialMoveY = moveY = 0;
      moveX = -dims.width;
      break;
    case 'bottom-left':
      initialMoveX = moveX = 0;
      initialMoveY = dims.height;
      moveY = -dims.height;
      break;
    case 'bottom-right':
      initialMoveX = dims.width;
      initialMoveY = dims.height;
      moveX = -dims.width;
      moveY = -dims.height;
      break;
    case 'center':
      initialMoveX = dims.width / 2;
      initialMoveY = dims.height / 2;
      moveX = -dims.width / 2;
      moveY = -dims.height / 2;
      break;
  }

  return new Effect.Move(element, {
    x: initialMoveX,
    y: initialMoveY,
    duration: 0.01,
    beforeSetup: function(effect) {
      effect.element.hide().makeClipping().makePositioned();
    },
    afterFinishInternal: function(effect) {
      new Effect.Parallel(
        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
          new Effect.Scale(effect.element, 100, {
            scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
        ], Object.extend({
             beforeSetup: function(effect) {
               effect.effects[0].element.setStyle({height: '0px'}).show();
             },
             afterFinishInternal: function(effect) {
               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
             }
           }, options)
      );
    }
  });
};

Effect.Shrink = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.none
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      moveX = moveY = 0;
      break;
    case 'top-right':
      moveX = dims.width;
      moveY = 0;
      break;
    case 'bottom-left':
      moveX = 0;
      moveY = dims.height;
      break;
    case 'bottom-right':
      moveX = dims.width;
      moveY = dims.height;
      break;
    case 'center':
      moveX = dims.width / 2;
      moveY = dims.height / 2;
      break;
  }

  return new Effect.Parallel(
    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
    ], Object.extend({
         beforeStartInternal: function(effect) {
           effect.effects[0].element.makePositioned().makeClipping();
         },
         afterFinishInternal: function(effect) {
           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
       }, options)
  );
};

Effect.Pulsate = function(element) {
  element = $(element);
  var options    = arguments[1] || { },
    oldOpacity = element.getInlineOpacity(),
    transition = options.transition || Effect.Transitions.linear,
    reverser   = function(pos){
      return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
    };

  return new Effect.Opacity(element,
    Object.extend(Object.extend({  duration: 2.0, from: 0,
      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
    }, options), {transition: reverser}));
};

Effect.Fold = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height };
  element.makeClipping();
  return new Effect.Scale(element, 5, Object.extend({
    scaleContent: false,
    scaleX: false,
    afterFinishInternal: function(effect) {
    new Effect.Scale(element, 1, {
      scaleContent: false,
      scaleY: false,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping().setStyle(oldStyle);
      } });
  }}, arguments[1] || { }));
};

Effect.Morph = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      style: { }
    }, arguments[1] || { });

    if (!Object.isString(options.style)) this.style = $H(options.style);
    else {
      if (options.style.include(':'))
        this.style = options.style.parseStyle();
      else {
        this.element.addClassName(options.style);
        this.style = $H(this.element.getStyles());
        this.element.removeClassName(options.style);
        var css = this.element.getStyles();
        this.style = this.style.reject(function(style) {
          return style.value == css[style.key];
        });
        options.afterFinishInternal = function(effect) {
          effect.element.addClassName(effect.options.style);
          effect.transforms.each(function(transform) {
            effect.element.style[transform.style] = '';
          });
        };
      }
    }
    this.start(options);
  },

  setup: function(){
    function parseColor(color){
      if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
      color = color.parseColor();
      return $R(0,2).map(function(i){
        return parseInt( color.slice(i*2+1,i*2+3), 16 );
      });
    }
    this.transforms = this.style.map(function(pair){
      var property = pair[0], value = pair[1], unit = null;

      if (value.parseColor('#zzzzzz') != '#zzzzzz') {
        value = value.parseColor();
        unit  = 'color';
      } else if (property == 'opacity') {
        value = parseFloat(value);
        if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
          this.element.setStyle({zoom: 1});
      } else if (Element.CSS_LENGTH.test(value)) {
          var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
          value = parseFloat(components[1]);
          unit = (components.length == 3) ? components[2] : null;
      }

      var originalValue = this.element.getStyle(property);
      return {
        style: property.camelize(),
        originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
        targetValue: unit=='color' ? parseColor(value) : value,
        unit: unit
      };
    }.bind(this)).reject(function(transform){
      return (
        (transform.originalValue == transform.targetValue) ||
        (
          transform.unit != 'color' &&
          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
        )
      );
    });
  },
  update: function(position) {
    var style = { }, transform, i = this.transforms.length;
    while(i--)
      style[(transform = this.transforms[i]).style] =
        transform.unit=='color' ? '#'+
          (Math.round(transform.originalValue[0]+
            (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
          (Math.round(transform.originalValue[1]+
            (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
          (Math.round(transform.originalValue[2]+
            (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
        (transform.originalValue +
          (transform.targetValue - transform.originalValue) * position).toFixed(3) +
            (transform.unit === null ? '' : transform.unit);
    this.element.setStyle(style, true);
  }
});

Effect.Transform = Class.create({
  initialize: function(tracks){
    this.tracks  = [];
    this.options = arguments[1] || { };
    this.addTracks(tracks);
  },
  addTracks: function(tracks){
    tracks.each(function(track){
      track = $H(track);
      var data = track.values().first();
      this.tracks.push($H({
        ids:     track.keys().first(),
        effect:  Effect.Morph,
        options: { style: data }
      }));
    }.bind(this));
    return this;
  },
  play: function(){
    return new Effect.Parallel(
      this.tracks.map(function(track){
        var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
        var elements = [$(ids) || $$(ids)].flatten();
        return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
      }).flatten(),
      this.options
    );
  }
});

Element.CSS_PROPERTIES = $w(
  'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
  'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
  'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
  'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
  'fontSize fontWeight height left letterSpacing lineHeight ' +
  'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
  'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
  'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
  'right textIndent top width wordSpacing zIndex');

Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;

String.__parseStyleElement = document.createElement('div');
String.prototype.parseStyle = function(){
  var style, styleRules = $H();
  if (Prototype.Browser.WebKit)
    style = new Element('div',{style:this}).style;
  else {
    String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
    style = String.__parseStyleElement.childNodes[0].style;
  }

  Element.CSS_PROPERTIES.each(function(property){
    if (style[property]) styleRules.set(property, style[property]);
  });

  if (Prototype.Browser.IE && this.include('opacity'))
    styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);

  return styleRules;
};

if (document.defaultView && document.defaultView.getComputedStyle) {
  Element.getStyles = function(element) {
    var css = document.defaultView.getComputedStyle($(element), null);
    return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
      styles[property] = css[property];
      return styles;
    });
  };
} else {
  Element.getStyles = function(element) {
    element = $(element);
    var css = element.currentStyle, styles;
    styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
      results[property] = css[property];
      return results;
    });
    if (!styles.opacity) styles.opacity = element.getOpacity();
    return styles;
  };
}

Effect.Methods = {
  morph: function(element, style) {
    element = $(element);
    new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
    return element;
  },
  visualEffect: function(element, effect, options) {
    element = $(element);
    var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
    new Effect[klass](element, options);
    return element;
  },
  highlight: function(element, options) {
    element = $(element);
    new Effect.Highlight(element, options);
    return element;
  }
};

$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
  'pulsate shake puff squish switchOff dropOut').each(
  function(effect) {
    Effect.Methods[effect] = function(element, options){
      element = $(element);
      Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
      return element;
    };
  }
);

$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
  function(f) { Effect.Methods[f] = Element[f]; }
);

Element.addMethods(Effect.Methods);

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

if(Object.isUndefined(Effect))
  throw("dragdrop.js requires including script.aculo.us' effects.js library");

var Droppables = {
  drops: [],

  remove: function(element) {
    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
  },

  add: function(element) {
    element = $(element);
    var options = Object.extend({
      greedy:     true,
      hoverclass: null,
      tree:       false
    }, arguments[1] || { });

    // cache containers
    if(options.containment) {
      options._containers = [];
      var containment = options.containment;
      if(Object.isArray(containment)) {
        containment.each( function(c) { options._containers.push($(c)) });
      } else {
        options._containers.push($(containment));
      }
    }

    if(options.accept) options.accept = [options.accept].flatten();

    Element.makePositioned(element); // fix IE
    options.element = element;

    this.drops.push(options);
  },

  findDeepestChild: function(drops) {
    deepest = drops[0];

    for (i = 1; i < drops.length; ++i)
      if (Element.isParent(drops[i].element, deepest.element))
        deepest = drops[i];

    return deepest;
  },

  isContained: function(element, drop) {
    var containmentNode;
    if(drop.tree) {
      containmentNode = element.treeNode;
    } else {
      containmentNode = element.parentNode;
    }
    return drop._containers.detect(function(c) { return containmentNode == c });
  },

  isAffected: function(point, element, drop) {
    return (
      (drop.element!=element) &&
      ((!drop._containers) ||
        this.isContained(element, drop)) &&
      ((!drop.accept) ||
        (Element.classNames(element).detect(
          function(v) { return drop.accept.include(v) } ) )) &&
      Position.within(drop.element, point[0], point[1]) );
  },

  deactivate: function(drop) {
    if(drop.hoverclass)
      Element.removeClassName(drop.element, drop.hoverclass);
    this.last_active = null;
  },

  activate: function(drop) {
    if(drop.hoverclass)
      Element.addClassName(drop.element, drop.hoverclass);
    this.last_active = drop;
  },

  show: function(point, element) {
    if(!this.drops.length) return;
    var drop, affected = [];

    this.drops.each( function(drop) {
      if(Droppables.isAffected(point, element, drop))
        affected.push(drop);
    });

    if(affected.length>0)
      drop = Droppables.findDeepestChild(affected);

    if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
    if (drop) {
      Position.within(drop.element, point[0], point[1]);
      if(drop.onHover)
        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));

      if (drop != this.last_active) Droppables.activate(drop);
    }
  },

  fire: function(event, element) {
    if(!this.last_active) return;
    Position.prepare();

    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
      if (this.last_active.onDrop) {
        this.last_active.onDrop(element, this.last_active.element, event);
        return true;
      }
  },

  reset: function() {
    if(this.last_active)
      this.deactivate(this.last_active);
  }
};

var Draggables = {
  drags: [],
  observers: [],

  register: function(draggable) {
    if(this.drags.length == 0) {
      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
      this.eventKeypress  = this.keyPress.bindAsEventListener(this);

      Event.observe(document, "mouseup", this.eventMouseUp);
      Event.observe(document, "mousemove", this.eventMouseMove);
      Event.observe(document, "keypress", this.eventKeypress);
    }
    this.drags.push(draggable);
  },

  unregister: function(draggable) {
    this.drags = this.drags.reject(function(d) { return d==draggable });
    if(this.drags.length == 0) {
      Event.stopObserving(document, "mouseup", this.eventMouseUp);
      Event.stopObserving(document, "mousemove", this.eventMouseMove);
      Event.stopObserving(document, "keypress", this.eventKeypress);
    }
  },

  activate: function(draggable) {
    if(draggable.options.delay) {
      this._timeout = setTimeout(function() {
        Draggables._timeout = null;
        window.focus();
        Draggables.activeDraggable = draggable;
      }.bind(this), draggable.options.delay);
    } else {
      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
      this.activeDraggable = draggable;
    }
  },

  deactivate: function() {
    this.activeDraggable = null;
  },

  updateDrag: function(event) {
    if(!this.activeDraggable) return;
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    // Mozilla-based browsers fire successive mousemove events with
    // the same coordinates, prevent needless redrawing (moz bug?)
    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
    this._lastPointer = pointer;

    this.activeDraggable.updateDrag(event, pointer);
  },

  endDrag: function(event) {
    if(this._timeout) {
      clearTimeout(this._timeout);
      this._timeout = null;
    }
    if(!this.activeDraggable) return;
    this._lastPointer = null;
    this.activeDraggable.endDrag(event);
    this.activeDraggable = null;
  },

  keyPress: function(event) {
    if(this.activeDraggable)
      this.activeDraggable.keyPress(event);
  },

  addObserver: function(observer) {
    this.observers.push(observer);
    this._cacheObserverCallbacks();
  },

  removeObserver: function(element) {  // element instead of observer fixes mem leaks
    this.observers = this.observers.reject( function(o) { return o.element==element });
    this._cacheObserverCallbacks();
  },

  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
    if(this[eventName+'Count'] > 0)
      this.observers.each( function(o) {
        if(o[eventName]) o[eventName](eventName, draggable, event);
      });
    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
  },

  _cacheObserverCallbacks: function() {
    ['onStart','onEnd','onDrag'].each( function(eventName) {
      Draggables[eventName+'Count'] = Draggables.observers.select(
        function(o) { return o[eventName]; }
      ).length;
    });
  }
};

/*--------------------------------------------------------------------------*/

var Draggable = Class.create({
  initialize: function(element) {
    var defaults = {
      handle: false,
      reverteffect: function(element, top_offset, left_offset) {
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
          queue: {scope:'_draggable', position:'end'}
        });
      },
      endeffect: function(element) {
        var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
          queue: {scope:'_draggable', position:'end'},
          afterFinish: function(){
            Draggable._dragging[element] = false
          }
        });
      },
      zindex: 1000,
      revert: false,
      quiet: false,
      scroll: false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
      delay: 0
    };

    if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
      Object.extend(defaults, {
        starteffect: function(element) {
          element._opacity = Element.getOpacity(element);
          Draggable._dragging[element] = true;
          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
        }
      });

    var options = Object.extend(defaults, arguments[1] || { });

    this.element = $(element);

    if(options.handle && Object.isString(options.handle))
      this.handle = this.element.down('.'+options.handle, 0);

    if(!this.handle) this.handle = $(options.handle);
    if(!this.handle) this.handle = this.element;

    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
      options.scroll = $(options.scroll);
      this._isScrollChild = Element.childOf(this.element, options.scroll);
    }

    Element.makePositioned(this.element); // fix IE

    this.options  = options;
    this.dragging = false;

    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);

    Draggables.register(this);
  },

  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    Draggables.unregister(this);
  },

  currentDelta: function() {
    return([
      parseInt(Element.getStyle(this.element,'left') || '0'),
      parseInt(Element.getStyle(this.element,'top') || '0')]);
  },

  initDrag: function(event) {
    if(!Object.isUndefined(Draggable._dragging[this.element]) &&
      Draggable._dragging[this.element]) return;
    if(Event.isLeftClick(event)) {
      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if((tag_name = src.tagName.toUpperCase()) && (
        tag_name=='INPUT' ||
        tag_name=='SELECT' ||
        tag_name=='OPTION' ||
        tag_name=='BUTTON' ||
        tag_name=='TEXTAREA')) return;

      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      var pos     = Position.cumulativeOffset(this.element);
      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });

      Draggables.activate(this);
      Event.stop(event);
    }
  },

  startDrag: function(event) {
    this.dragging = true;
    if(!this.delta)
      this.delta = this.currentDelta();

    if(this.options.zindex) {
      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
      this.element.style.zIndex = this.options.zindex;
    }

    if(this.options.ghosting) {
      this._clone = this.element.cloneNode(true);
      this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
      if (!this._originallyAbsolute)
        Position.absolutize(this.element);
      this.element.parentNode.insertBefore(this._clone, this.element);
    }

    if(this.options.scroll) {
      if (this.options.scroll == window) {
        var where = this._getWindowScroll(this.options.scroll);
        this.originalScrollLeft = where.left;
        this.originalScrollTop = where.top;
      } else {
        this.originalScrollLeft = this.options.scroll.scrollLeft;
        this.originalScrollTop = this.options.scroll.scrollTop;
      }
    }

    Draggables.notify('onStart', this, event);

    if(this.options.starteffect) this.options.starteffect(this.element);
  },

  updateDrag: function(event, pointer) {
    if(!this.dragging) this.startDrag(event);

    if(!this.options.quiet){
      Position.prepare();
      Droppables.show(pointer, this.element);
    }

    Draggables.notify('onDrag', this, event);

    this.draw(pointer);
    if(this.options.change) this.options.change(this);

    if(this.options.scroll) {
      this.stopScrolling();

      var p;
      if (this.options.scroll == window) {
        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
      } else {
        p = Position.page(this.options.scroll);
        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
        p[1] += this.options.scroll.scrollTop + Position.deltaY;
        p.push(p[0]+this.options.scroll.offsetWidth);
        p.push(p[1]+this.options.scroll.offsetHeight);
      }
      var speed = [0,0];
      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
      this.startScrolling(speed);
    }

    // fix AppleWebKit rendering
    if(Prototype.Browser.WebKit) window.scrollBy(0,0);

    Event.stop(event);
  },

  finishDrag: function(event, success) {
    this.dragging = false;

    if(this.options.quiet){
      Position.prepare();
      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      Droppables.show(pointer, this.element);
    }

    if(this.options.ghosting) {
      if (!this._originallyAbsolute)
        Position.relativize(this.element);
      delete this._originallyAbsolute;
      Element.remove(this._clone);
      this._clone = null;
    }

    var dropped = false;
    if(success) {
      dropped = Droppables.fire(event, this.element);
      if (!dropped) dropped = false;
    }
    if(dropped && this.options.onDropped) this.options.onDropped(this.element);
    Draggables.notify('onEnd', this, event);

    var revert = this.options.revert;
    if(revert && Object.isFunction(revert)) revert = revert(this.element);

    var d = this.currentDelta();
    if(revert && this.options.reverteffect) {
      if (dropped == 0 || revert != 'failure')
        this.options.reverteffect(this.element,
          d[1]-this.delta[1], d[0]-this.delta[0]);
    } else {
      this.delta = d;
    }

    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if(this.options.endeffect)
      this.options.endeffect(this.element);

    Draggables.deactivate(this);
    Droppables.reset();
  },

  keyPress: function(event) {
    if(event.keyCode!=Event.KEY_ESC) return;
    this.finishDrag(event, false);
    Event.stop(event);
  },

  endDrag: function(event) {
    if(!this.dragging) return;
    this.stopScrolling();
    this.finishDrag(event, true);
    Event.stop(event);
  },

  draw: function(point) {
    var pos = Position.cumulativeOffset(this.element);
    if(this.options.ghosting) {
      var r   = Position.realOffset(this.element);
      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
    }

    var d = this.currentDelta();
    pos[0] -= d[0]; pos[1] -= d[1];

    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
    }

    var p = [0,1].map(function(i){
      return (point[i]-pos[i]-this.offset[i])
    }.bind(this));

    if(this.options.snap) {
      if(Object.isFunction(this.options.snap)) {
        p = this.options.snap(p[0],p[1],this);
      } else {
      if(Object.isArray(this.options.snap)) {
        p = p.map( function(v, i) {
          return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
      } else {
        p = p.map( function(v) {
          return (v/this.options.snap).round()*this.options.snap }.bind(this));
      }
    }}

    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
      style.left = p[0] + "px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
      style.top  = p[1] + "px";

    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },

  stopScrolling: function() {
    if(this.scrollInterval) {
      clearInterval(this.scrollInterval);
      this.scrollInterval = null;
      Draggables._lastScrollPointer = null;
    }
  },

  startScrolling: function(speed) {
    if(!(speed[0] || speed[1])) return;
    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
    this.lastScrolled = new Date();
    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
  },

  scroll: function() {
    var current = new Date();
    var delta = current - this.lastScrolled;
    this.lastScrolled = current;
    if(this.options.scroll == window) {
      with (this._getWindowScroll(this.options.scroll)) {
        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
          var d = delta / 1000;
          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
        }
      }
    } else {
      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
    }

    Position.prepare();
    Droppables.show(Draggables._lastPointer, this.element);
    Draggables.notify('onDrag', this);
    if (this._isScrollChild) {
      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
      if (Draggables._lastScrollPointer[0] < 0)
        Draggables._lastScrollPointer[0] = 0;
      if (Draggables._lastScrollPointer[1] < 0)
        Draggables._lastScrollPointer[1] = 0;
      this.draw(Draggables._lastScrollPointer);
    }

    if(this.options.change) this.options.change(this);
  },

  _getWindowScroll: function(w) {
    var T, L, W, H;
    with (w.document) {
      if (w.document.documentElement && documentElement.scrollTop) {
        T = documentElement.scrollTop;
        L = documentElement.scrollLeft;
      } else if (w.document.body) {
        T = body.scrollTop;
        L = body.scrollLeft;
      }
      if (w.innerWidth) {
        W = w.innerWidth;
        H = w.innerHeight;
      } else if (w.document.documentElement && documentElement.clientWidth) {
        W = documentElement.clientWidth;
        H = documentElement.clientHeight;
      } else {
        W = body.offsetWidth;
        H = body.offsetHeight;
      }
    }
    return { top: T, left: L, width: W, height: H };
  }
});

Draggable._dragging = { };

/*--------------------------------------------------------------------------*/

var SortableObserver = Class.create({
  initialize: function(element, observer) {
    this.element   = $(element);
    this.observer  = observer;
    this.lastValue = Sortable.serialize(this.element);
  },

  onStart: function() {
    this.lastValue = Sortable.serialize(this.element);
  },

  onEnd: function() {
    Sortable.unmark();
    if(this.lastValue != Sortable.serialize(this.element))
      this.observer(this.element)
  }
});

var Sortable = {
  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,

  sortables: { },

  _findRootElement: function(element) {
    while (element.tagName.toUpperCase() != "BODY") {
      if(element.id && Sortable.sortables[element.id]) return element;
      element = element.parentNode;
    }
  },

  options: function(element) {
    element = Sortable._findRootElement($(element));
    if(!element) return;
    return Sortable.sortables[element.id];
  },

  destroy: function(element){
    element = $(element);
    var s = Sortable.sortables[element.id];

    if(s) {
      Draggables.removeObserver(s.element);
      s.droppables.each(function(d){ Droppables.remove(d) });
      s.draggables.invoke('destroy');

      delete Sortable.sortables[s.element.id];
    }
  },

  create: function(element) {
    element = $(element);
    var options = Object.extend({
      element:     element,
      tag:         'li',       // assumes li children, override with tag: 'tagname'
      dropOnEmpty: false,
      tree:        false,
      treeTag:     'ul',
      overlap:     'vertical', // one of 'vertical', 'horizontal'
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
      containment: element,    // also takes array of elements (or id's); or false
      handle:      false,      // or a CSS class
      only:        false,
      delay:       0,
      hoverclass:  null,
      ghosting:    false,
      quiet:       false,
      scroll:      false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      format:      this.SERIALIZE_RULE,

      // these take arrays of elements or ids and can be
      // used for better initialization performance
      elements:    false,
      handles:     false,

      onChange:    Prototype.emptyFunction,
      onUpdate:    Prototype.emptyFunction
    }, arguments[1] || { });

    // clear any old sortable with same element
    this.destroy(element);

    // build options for the draggables
    var options_for_draggable = {
      revert:      true,
      quiet:       options.quiet,
      scroll:      options.scroll,
      scrollSpeed: options.scrollSpeed,
      scrollSensitivity: options.scrollSensitivity,
      delay:       options.delay,
      ghosting:    options.ghosting,
      constraint:  options.constraint,
      handle:      options.handle };

    if(options.starteffect)
      options_for_draggable.starteffect = options.starteffect;

    if(options.reverteffect)
      options_for_draggable.reverteffect = options.reverteffect;
    else
      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
        element.style.top  = 0;
        element.style.left = 0;
      };

    if(options.endeffect)
      options_for_draggable.endeffect = options.endeffect;

    if(options.zindex)
      options_for_draggable.zindex = options.zindex;

    // build options for the droppables
    var options_for_droppable = {
      overlap:     options.overlap,
      containment: options.containment,
      tree:        options.tree,
      hoverclass:  options.hoverclass,
      onHover:     Sortable.onHover
    };

    var options_for_tree = {
      onHover:      Sortable.onEmptyHover,
      overlap:      options.overlap,
      containment:  options.containment,
      hoverclass:   options.hoverclass
    };

    // fix for gecko engine
    Element.cleanWhitespace(element);

    options.draggables = [];
    options.droppables = [];

    // drop on empty handling
    if(options.dropOnEmpty || options.tree) {
      Droppables.add(element, options_for_tree);
      options.droppables.push(element);
    }

    (options.elements || this.findElements(element, options) || []).each( function(e,i) {
      var handle = options.handles ? $(options.handles[i]) :
        (options.handle ? $(e).select('.' + options.handle)[0] : e);
      options.draggables.push(
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
      Droppables.add(e, options_for_droppable);
      if(options.tree) e.treeNode = element;
      options.droppables.push(e);
    });

    if(options.tree) {
      (Sortable.findTreeElements(element, options) || []).each( function(e) {
        Droppables.add(e, options_for_tree);
        e.treeNode = element;
        options.droppables.push(e);
      });
    }

    // keep reference
    this.sortables[element.id] = options;

    // for onupdate
    Draggables.addObserver(new SortableObserver(element, options.onUpdate));

  },

  // return all suitable-for-sortable elements in a guaranteed order
  findElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.tag);
  },

  findTreeElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.treeTag);
  },

  onHover: function(element, dropon, overlap) {
    if(Element.isParent(dropon, element)) return;

    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
      return;
    } else if(overlap>0.5) {
      Sortable.mark(dropon, 'before');
      if(dropon.previousSibling != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, dropon);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    } else {
      Sortable.mark(dropon, 'after');
      var nextElement = dropon.nextSibling || null;
      if(nextElement != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, nextElement);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    }
  },

  onEmptyHover: function(element, dropon, overlap) {
    var oldParentNode = element.parentNode;
    var droponOptions = Sortable.options(dropon);

    if(!Element.isParent(dropon, element)) {
      var index;

      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
      var child = null;

      if(children) {
        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);

        for (index = 0; index < children.length; index += 1) {
          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
            offset -= Element.offsetSize (children[index], droponOptions.overlap);
          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
            child = index + 1 < children.length ? children[index + 1] : null;
            break;
          } else {
            child = children[index];
            break;
          }
        }
      }

      dropon.insertBefore(element, child);

      Sortable.options(oldParentNode).onChange(element);
      droponOptions.onChange(element);
    }
  },

  unmark: function() {
    if(Sortable._marker) Sortable._marker.hide();
  },

  mark: function(dropon, position) {
    // mark on ghosting only
    var sortable = Sortable.options(dropon.parentNode);
    if(sortable && !sortable.ghosting) return;

    if(!Sortable._marker) {
      Sortable._marker =
        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
          hide().addClassName('dropmarker').setStyle({position:'absolute'});
      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
    }
    var offsets = Position.cumulativeOffset(dropon);
    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});

    if(position=='after')
      if(sortable.overlap == 'horizontal')
        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
      else
        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});

    Sortable._marker.show();
  },

  _tree: function(element, options, parent) {
    var children = Sortable.findElements(element, options) || [];

    for (var i = 0; i < children.length; ++i) {
      var match = children[i].id.match(options.format);

      if (!match) continue;

      var child = {
        id: encodeURIComponent(match ? match[1] : null),
        element: element,
        parent: parent,
        children: [],
        position: parent.children.length,
        container: $(children[i]).down(options.treeTag)
      };

      /* Get the element containing the children and recurse over it */
      if (child.container)
        this._tree(child.container, options, child);

      parent.children.push (child);
    }

    return parent;
  },

  tree: function(element) {
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag: sortableOptions.tag,
      treeTag: sortableOptions.treeTag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format
    }, arguments[1] || { });

    var root = {
      id: null,
      parent: null,
      children: [],
      container: element,
      position: 0
    };

    return Sortable._tree(element, options, root);
  },

  /* Construct a [i] index for a particular node */
  _constructIndex: function(node) {
    var index = '';
    do {
      if (node.id) index = '[' + node.position + ']' + index;
    } while ((node = node.parent) != null);
    return index;
  },

  sequence: function(element) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[1] || { });

    return $(this.findElements(element, options) || []).map( function(item) {
      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
    });
  },

  setSequence: function(element, new_sequence) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[2] || { });

    var nodeMap = { };
    this.findElements(element, options).each( function(n) {
        if (n.id.match(options.format))
            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
        n.parentNode.removeChild(n);
    });

    new_sequence.each(function(ident) {
      var n = nodeMap[ident];
      if (n) {
        n[1].appendChild(n[0]);
        delete nodeMap[ident];
      }
    });
  },

  serialize: function(element) {
    element = $(element);
    var options = Object.extend(Sortable.options(element), arguments[1] || { });
    var name = encodeURIComponent(
      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);

    if (options.tree) {
      return Sortable.tree(element, arguments[1]).children.map( function (item) {
        return [name + Sortable._constructIndex(item) + "[id]=" +
                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
      }).flatten().join('&');
    } else {
      return Sortable.sequence(element, arguments[1]).map( function(item) {
        return name + "[]=" + encodeURIComponent(item);
      }).join('&');
    }
  }
};

// Returns true if child is contained within element
Element.isParent = function(child, element) {
  if (!child.parentNode || child == element) return false;
  if (child.parentNode == element) return true;
  return Element.isParent(child.parentNode, element);
};

Element.findChildren = function(element, only, recursive, tagName) {
  if(!element.hasChildNodes()) return null;
  tagName = tagName.toUpperCase();
  if(only) only = [only].flatten();
  var elements = [];
  $A(element.childNodes).each( function(e) {
    if(e.tagName && e.tagName.toUpperCase()==tagName &&
      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
        elements.push(e);
    if(recursive) {
      var grandchildren = Element.findChildren(e, only, recursive, tagName);
      if(grandchildren) elements.push(grandchildren);
    }
  });

  return (elements.length>0 ? elements.flatten() : []);
};

Element.offsetSize = function (element, type) {
  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
};

/*
 * FCKeditor - The text editor for Internet - http://www.fckeditor.net
 * Copyright (C) 2003-2009 Frederico Caldeira Knabben
 *
 * == BEGIN LICENSE ==
 *
 * Licensed under the terms of any of the following licenses at your
 * choice:
 *
 *  - GNU General Public License Version 2 or later (the "GPL")
 *    http://www.gnu.org/licenses/gpl.html
 *
 *  - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
 *    http://www.gnu.org/licenses/lgpl.html
 *
 *  - Mozilla Public License Version 1.1 or later (the "MPL")
 *    http://www.mozilla.org/MPL/MPL-1.1.html
 *
 * == END LICENSE ==
 *
 * This is the integration file for JavaScript.
 *
 * It defines the FCKeditor class that can be used to create editor
 * instances in a HTML page in the client side. For server side
 * operations, use the specific integration system.
 */

// FCKeditor Class
var FCKeditor = function( instanceName, width, height, toolbarSet, value )
{
	// Properties
	this.InstanceName	= instanceName ;
	this.Width			= width			|| '100%' ;
	this.Height			= height		|| '300' ;
	this.ToolbarSet		= toolbarSet	|| 'Default' ;
	this.Value			= value			|| '' ;
	this.BasePath		= FCKeditor.BasePath ;
	this.CheckBrowser	= true ;
	this.DisplayErrors	= true ;

	this.Config			= new Object() ;

	// Events
	this.OnError		= null ;	// function( source, errorNumber, errorDescription )
}

/**
 * This is the default BasePath used by all editor instances.
 */
FCKeditor.BasePath = '/fckeditor/' ;

/**
 * The minimum height used when replacing textareas.
 */
FCKeditor.MinHeight = 300 ;

/**
 * The minimum width used when replacing textareas.
 */
FCKeditor.MinWidth = 750 ;

FCKeditor.prototype.Version			= '2.6.4' ;
FCKeditor.prototype.VersionBuild	= '21629' ;

FCKeditor.prototype.Create = function()
{
	document.write( this.CreateHtml() ) ;
}

FCKeditor.prototype.CreateHtml = function()
{
	// Check for errors
	if ( !this.InstanceName || this.InstanceName.length == 0 )
	{
		this._ThrowError( 701, 'You must specify an instance name.' ) ;
		return '' ;
	}

	var sHtml = '' ;

	if ( !this.CheckBrowser || this._IsCompatibleBrowser() )
	{
		sHtml += '<input type="hidden" id="' + this.InstanceName + '" name="' + this.InstanceName + '" value="' + this._HTMLEncode( this.Value ) + '" style="display:none" />' ;
		sHtml += this._GetConfigHtml() ;
		sHtml += this._GetIFrameHtml() ;
	}
	else
	{
		var sWidth  = this.Width.toString().indexOf('%')  > 0 ? this.Width  : this.Width  + 'px' ;
		var sHeight = this.Height.toString().indexOf('%') > 0 ? this.Height : this.Height + 'px' ;

		sHtml += '<textarea name="' + this.InstanceName +
			'" rows="4" cols="40" style="width:' + sWidth +
			';height:' + sHeight ;

		if ( this.TabIndex )
			sHtml += '" tabindex="' + this.TabIndex ;

		sHtml += '">' +
			this._HTMLEncode( this.Value ) +
			'<\/textarea>' ;
	}

	return sHtml ;
}

FCKeditor.prototype.ReplaceTextarea = function()
{
	if ( document.getElementById( this.InstanceName + '___Frame' ) )
		return ;
	if ( !this.CheckBrowser || this._IsCompatibleBrowser() )
	{
		// We must check the elements firstly using the Id and then the name.
		var oTextarea = document.getElementById( this.InstanceName ) ;
		var colElementsByName = document.getElementsByName( this.InstanceName ) ;
		var i = 0;
		while ( oTextarea || i == 0 )
		{
			if ( oTextarea && oTextarea.tagName.toLowerCase() == 'textarea' )
				break ;
			oTextarea = colElementsByName[i++] ;
		}

		if ( !oTextarea )
		{
			alert( 'Error: The TEXTAREA with id or name set to "' + this.InstanceName + '" was not found' ) ;
			return ;
		}

		oTextarea.style.display = 'none' ;

		if ( oTextarea.tabIndex )
			this.TabIndex = oTextarea.tabIndex ;

		this._InsertHtmlBefore( this._GetConfigHtml(), oTextarea ) ;
		this._InsertHtmlBefore( this._GetIFrameHtml(), oTextarea ) ;
	}
}

FCKeditor.prototype._InsertHtmlBefore = function( html, element )
{
	if ( element.insertAdjacentHTML )	// IE
		element.insertAdjacentHTML( 'beforeBegin', html ) ;
	else								// Gecko
	{
		var oRange = document.createRange() ;
		oRange.setStartBefore( element ) ;
		var oFragment = oRange.createContextualFragment( html );
		element.parentNode.insertBefore( oFragment, element ) ;
	}
}

FCKeditor.prototype._GetConfigHtml = function()
{
	var sConfig = '' ;
	for ( var o in this.Config )
	{
		if ( sConfig.length > 0 ) sConfig += '&amp;' ;
		sConfig += encodeURIComponent( o ) + '=' + encodeURIComponent( this.Config[o] ) ;
	}

	return '<input type="hidden" id="' + this.InstanceName + '___Config" value="' + sConfig + '" style="display:none" />' ;
}

FCKeditor.prototype._GetIFrameHtml = function()
{
	var sFile = 'fckeditor.html' ;

	try
	{
		if ( (/fcksource=true/i).test( window.top.location.search ) )
			sFile = 'fckeditor.original.html' ;
	}
	catch (e) { /* Ignore it. Much probably we are inside a FRAME where the "top" is in another domain (security error). */ }

	var sLink = this.BasePath + 'editor/' + sFile + '?InstanceName=' + encodeURIComponent( this.InstanceName ) ;
	if (this.ToolbarSet)
		sLink += '&amp;Toolbar=' + this.ToolbarSet ;
	var html = '<iframe id="' + this.InstanceName +
		'___Frame" src="' + sLink +
		'" width="' + this.Width +
		'" height="' + this.Height ;

	if ( this.TabIndex )
		html += '" tabindex="' + this.TabIndex ;

	html += '" frameborder="0" scrolling="no"></iframe>' ;

	return html ;
}

FCKeditor.prototype._IsCompatibleBrowser = function()
{
	return FCKeditor_IsCompatibleBrowser() ;
}

FCKeditor.prototype._ThrowError = function( errorNumber, errorDescription )
{
	this.ErrorNumber		= errorNumber ;
	this.ErrorDescription	= errorDescription ;

	if ( this.DisplayErrors )
	{
		document.write( '<div style="COLOR: #ff0000">' ) ;
		document.write( '[ FCKeditor Error ' + this.ErrorNumber + ': ' + this.ErrorDescription + ' ]' ) ;
		document.write( '</div>' ) ;
	}

	if ( typeof( this.OnError ) == 'function' )
		this.OnError( this, errorNumber, errorDescription ) ;
}

FCKeditor.prototype._HTMLEncode = function( text )
{
	if ( typeof( text ) != "string" )
		text = text.toString() ;

	text = text.replace(
		/&/g, "&amp;").replace(
		/"/g, "&quot;").replace(
		/</g, "&lt;").replace(
		/>/g, "&gt;") ;

	return text ;
}

;(function()
{
	var textareaToEditor = function( textarea )
	{
		var editor = new FCKeditor( textarea.name ) ;

		editor.Width = Math.max( textarea.offsetWidth, FCKeditor.MinWidth ) ;
		editor.Height = Math.max( textarea.offsetHeight, FCKeditor.MinHeight ) ;

		return editor ;
	}

	/**
	 * Replace all <textarea> elements available in the document with FCKeditor
	 * instances.
	 *
	 *	// Replace all <textarea> elements in the page.
	 *	FCKeditor.ReplaceAllTextareas() ;
	 *
	 *	// Replace all <textarea class="myClassName"> elements in the page.
	 *	FCKeditor.ReplaceAllTextareas( 'myClassName' ) ;
	 *
	 *	// Selectively replace <textarea> elements, based on custom assertions.
	 *	FCKeditor.ReplaceAllTextareas( function( textarea, editor )
	 *		{
	 *			// Custom code to evaluate the replace, returning false if it
	 *			// must not be done.
	 *			// It also passes the "editor" parameter, so the developer can
	 *			// customize the instance.
	 *		} ) ;
	 */
	FCKeditor.ReplaceAllTextareas = function()
	{
		var textareas = document.getElementsByTagName( 'textarea' ) ;

		for ( var i = 0 ; i < textareas.length ; i++ )
		{
			var editor = null ;
			var textarea = textareas[i] ;
			var name = textarea.name ;

			// The "name" attribute must exist.
			if ( !name || name.length == 0 )
				continue ;

			if ( typeof arguments[0] == 'string' )
			{
				// The textarea class name could be passed as the function
				// parameter.

				var classRegex = new RegExp( '(?:^| )' + arguments[0] + '(?:$| )' ) ;

				if ( !classRegex.test( textarea.className ) )
					continue ;
			}
			else if ( typeof arguments[0] == 'function' )
			{
				// An assertion function could be passed as the function parameter.
				// It must explicitly return "false" to ignore a specific <textarea>.
				editor = textareaToEditor( textarea ) ;
				if ( arguments[0]( textarea, editor ) === false )
					continue ;
			}

			if ( !editor )
				editor = textareaToEditor( textarea ) ;

			editor.ReplaceTextarea() ;
		}
	}
})() ;

function FCKeditor_IsCompatibleBrowser()
{
	var sAgent = navigator.userAgent.toLowerCase() ;

	// Internet Explorer 5.5+
	if ( /*@cc_on!@*/false && sAgent.indexOf("mac") == -1 )
	{
		var sBrowserVersion = navigator.appVersion.match(/MSIE (.\..)/)[1] ;
		return ( sBrowserVersion >= 5.5 ) ;
	}

	// Gecko (Opera 9 tries to behave like Gecko at this point).
	if ( navigator.product == "Gecko" && navigator.productSub >= 20030210 && !( typeof(opera) == 'object' && opera.postError ) )
		return true ;

	// Opera 9.50+
	if ( window.opera && window.opera.version && parseFloat( window.opera.version() ) >= 9.5 )
		return true ;

	// Adobe AIR
	// Checked before Safari because AIR have the WebKit rich text editor
	// features from Safari 3.0.4, but the version reported is 420.
	if ( sAgent.indexOf( ' adobeair/' ) != -1 )
		return ( sAgent.match( / adobeair\/(\d+)/ )[1] >= 1 ) ;	// Build must be at least v1

	// Safari 3+
	if ( sAgent.indexOf( ' applewebkit/' ) != -1 )
		return ( sAgent.match( / applewebkit\/(\d+)/ )[1] >= 522 ) ;	// Build must be at least 522 (v3)

	return false ;
}


// Jester version 1.5
// Released October 25th, 2007

// Compatible, tested with Prototype 1.6.0.2

// Copyright 2007, thoughtbot, inc.
// Released under the MIT License.

// Minified wtih http://fmarcia.info/jsmin/test.html
Jester={};
Jester.Resource=function(){};Jester.Constructor=function(model){return(function CONSTRUCTOR(){this.klass=CONSTRUCTOR;this.initialize.apply(this,arguments);this.after_initialization.apply(this,arguments);}).toString().replace(/CONSTRUCTOR/g,model);}
var jesterCallback=null;Object.extend(Jester.Resource,{model:function(model,options)
{var new_model=null;new_model=eval(model+" = "+Jester.Constructor(model));new_model.prototype=new Jester.Resource();Object.extend(new_model,Jester.Resource);if(!Jester.Tree){Jester.Tree=new XML.ObjTree();Jester.Tree.attr_prefix="@";}
if(!options)options={};var default_options={format:"xml",singular:model.underscore(),name:model}
options=Object.extend(default_options,options);options.format=options.format.toLowerCase();options.plural=options.singular.pluralize(options.plural);options.singular_xml=options.singular.replace(/_/g,"-");options.plural_xml=options.plural.replace(/_/g,"-");options.remote=false;var default_prefix=window.location.protocol+"//"+window.location.hostname+(window.location.port?":"+window.location.port:"");if(options.prefix&&options.prefix.match(/^https?:/))
options.remote=true;if(!options.prefix)
options.prefix=default_prefix;if(!options.prefix.match(/^(https?|file):/))
options.prefix=default_prefix+(options.prefix.match(/^\//)?"":"/")+options.prefix;options.prefix=options.prefix.replace(/\b\/+$/,"");options.urls=Object.extend(this._default_urls(options),options.urls);new_model.name=model;new_model.options=options;for(var opt in options)
new_model["_"+opt]=options[opt];for(var url in options.urls)
eval('new_model._'+url+'_url = function(params) {return this._url_for("'+url+'", params);}');if(options.checkNew)
this.buildAttributes(new_model,options);if(window)
window[model]=new_model;return new_model;},buildAttributes:function(model,options){model=model||this;var async=options.asynchronous;if(async==null)
async=true;var buildWork=bind(model,function(doc){if(this._format=="json")
this._attributes=this._attributesFromJSON(doc);else
this._attributes=this._attributesFromTree(doc[this._singular_xml]);});model.requestAndParse(options.format,buildWork,model._new_url(),{asynchronous:async});},loadRemoteJSON:function(url,callback,user_callback){if(typeof(user_callback)=="function")
jesterCallback=function(doc){user_callback(callback(doc));}
else
jesterCallback=callback;var script=document.createElement("script");script.type="text/javascript";if(url.indexOf("?")==-1)
url+="?";else
url+="&";url+="callback=jesterCallback";script.src=url;document.firstChild.appendChild(script);},requestAndParse:function(format,callback,url,options,user_callback,remote){if(remote&&format=="json"&&user_callback)
return this.loadRemoteJSON(url,callback,user_callback)
parse_and_callback=null;if(format.toLowerCase()=="json"){parse_and_callback=function(transport){if(transport.status==500)return callback(null);eval("var attributes = "+transport.responseText);return callback(attributes);}}else{parse_and_callback=function(transport){if(transport.status==500)return callback(null);return callback(Jester.Tree.parseXML(transport.responseText));}}
if(!(options.postBody||options.parameters||options.postbody||options.method=="post")){options.method="get";}
return this.request(parse_and_callback,url,options,user_callback);},request:function(callback,url,options,user_callback){if(user_callback){options.asynchronous=true;if(typeof(user_callback)=="object"){for(var x in user_callback)
options[x]=user_callback[x];user_callback=options.onComplete;}}
else
user_callback=function(arg){return arg;}
if(options.asynchronous){options.onComplete=function(transport,json){user_callback(callback(transport),json);}
return new Ajax.Request(url,options).transport;}
else
{options.asynchronous=false;return callback(new Ajax.Request(url,options).transport);}},find:function(id,params,callback){if(!callback&&typeof(params)=="function"){callback=params;params=null;}
var findAllWork=bind(this,function(doc){if(!doc)return null;var collection=this._loadCollection(doc);if(!collection)return null;if(id=="first")
return collection[0];return collection;});var findOneWork=bind(this,function(doc){if(!doc)return null;var base=this._loadSingle(doc);if(!base||base._properties.length==0)return null;if(!base._properties.include("id"))base._setAttribute("id",parseInt(id))
return base;});if(id=="first"||id=="all"){var url=this._list_url(params);return this.requestAndParse(this._format,findAllWork,url,{},callback,this._remote);}
else{if(isNaN(parseInt(id)))return null;if(!params)params={};params.id=id;var url=this._show_url(params);return this.requestAndParse(this._format,findOneWork,url,{},callback,this._remote);}},build:function(attributes){return new this(attributes);},create:function(attributes,callback){var base=new this(attributes);createWork=bind(this,function(saved){return callback(base);});if(callback){return base.save(createWork);}
else{base.save();return base;}},destroy:function(params,callback){if(typeof(params)=="function"){callback=params;params=null;}
if(typeof(params)=="number"){params={id:params};}
params.id=params.id||this.id;if(!params.id)return false;var destroyWork=bind(this,function(transport){if(transport.status==200){if(!params.id||this.id==params.id)
this.id=null;return this;}
else
return false;});return this.request(destroyWork,this._destroy_url(params),{method:"delete"},callback);},_interpolate:function(string,params){if(!params)return string;var result=string;params.each(function(pair){var re=new RegExp(":"+pair.key,"g");if(result.match(re)){result=result.replace(re,pair.value);params.unset(pair.key);}});return result;},_url_for:function(action,params){if(!this._urls[action])return"";if(typeof(params)=="number")params={id:params}
if(params)params=$H(params);var url=this._interpolate(this._prefix+this._urls[action],params)
return url+(params&&params.any()?"?"+params.toQueryString():"");},_default_urls:function(options){urls={'show':"/"+options.plural+"/:id."+options.format,'list':"/"+options.plural+"."+options.format,'new':"/"+options.plural+"/new."+options.format}
urls.create=urls.list;urls.destroy=urls.update=urls.show;return urls;},_attributesFromJSON:function(json){if(!json||json.constructor!=Object)return false;if(json.attributes)json=json.attributes;var attributes={};var i=0;for(var attr in json){var value=json[attr];if(attr=="id")
value=parseInt(value);else if(attr.match(/(created_at|created_on|updated_at|updated_on)/)){var date=Date.parse(value);if(date&&!isNaN(date))value=date;}
attributes[attr]=value;i+=1;}
if(i==0)return false;return attributes;},_attributesFromTree:function(elements){var attributes={}
for(var attr in elements){var value=elements[attr];if(elements[attr]&&elements[attr]["@type"]){if(elements[attr]["#text"])
value=elements[attr]["#text"];else
value=undefined;}
if(!value){}
else if(typeof(value)=="string"){if(elements[attr]["@type"]=="integer"){var num=parseInt(value);if(!isNaN(num))value=num;}
else if(elements[attr]["@type"]=="boolean")
value=(value=="true");else if(elements[attr]["@type"]=="datetime"){var date=Date.parse(value);if(!isNaN(date))value=date;}}
else{var relation=value;var i=0;var singular=null;var has_many=false;for(var val in relation){if(i==0)
singular=val;i+=1;}
if(relation[singular]&&typeof(relation[singular])=="object"&&i==1){var value=[];var plural=attr;var name=singular.camelize().capitalize();if(!(elements[plural][singular].length>0))
elements[plural][singular]=[elements[plural][singular]];elements[plural][singular].each(bind(this,function(single){if(eval("typeof("+name+")")=="undefined"){Jester.Resource.model(name,{prefix:this._prefix,singular:singular,plural:plural,format:this._format});}
var base=eval(name+".build(this._attributesFromTree(single))");value.push(base);}));}
else{singular=attr;var name=singular.capitalize();if(eval("typeof("+name+")")=="undefined"){Jester.Resource.model(name,{prefix:this._prefix,singular:singular,format:this._format});}
value=eval(name+".build(this._attributesFromTree(value))");}}
attribute=attr.replace(/-/g,"_");attributes[attribute]=value;}
return attributes;},_loadSingle:function(doc){var attributes;if(this._format=="json")
attributes=this._attributesFromJSON(doc);else
attributes=this._attributesFromTree(doc[this._singular_xml]);return this.build(attributes);},_loadCollection:function(doc){var collection;if(this._format=="json"){collection=doc.map(bind(this,function(item){return this.build(this._attributesFromJSON(item));}));}
else{if(!Jester.Resource.elementHasMany(doc[this._plural_xml]))
doc[this._plural_xml][this._singular_xml]=[doc[this._plural_xml][this._singular_xml]];collection=doc[this._plural_xml][this._singular_xml].map(bind(this,function(elem){return this.build(this._attributesFromTree(elem));}));}
return collection;}});Object.extend(Jester.Resource.prototype,{initialize:function(attributes){this._properties=[];this._associations=[];this.setAttributes(this.klass._attributes||{});this.setAttributes(attributes);this.errors=[];for(var url in this.klass._urls)
eval('this._'+url+'_url = function(params) {return this._url_for("'+url+'", params);}');},after_initialization:function(){},new_record:function(){return!(this.id);},valid:function(){return!this.errors.any();},reload:function(callback){var reloadWork=bind(this,function(copy){this._resetAttributes(copy.attributes(true));if(callback)
return callback(this);else
return this;});if(this.id){if(callback)
return this.klass.find(this.id,{},reloadWork);else
return reloadWork(this.klass.find(this.id));}
else
return this;},destroy:function(params,callback){if(params===undefined){params={};}
if(typeof(params)=="function"){callback=params;params={};}
if(typeof(params)=="number"){params={id:params};}
if(!params.id){params.id=this.id;}
if(!params.id)return false;if(this._properties!==undefined){(this._properties).each(bind(this,function(value,i){if(params[value]===undefined){params[value]=this[value];}}));}
var destroyWork=bind(this,function(transport){if(transport.status==200){if(!params.id||this.id==params.id)
this.id=null;return this;}
else
return false;});return this.klass.request(destroyWork,this._destroy_url(params),{method:"delete"},callback);},save:function(callback){var saveWork=bind(this,function(transport){var saved=false;if(transport.responseText&&(transport.responseText.strip()!="")){var errors=this._errorsFrom(transport.responseText);if(errors)
this._setErrors(errors);else{var attributes;if(this.klass._format=="json"){attributes=this._attributesFromJSON(transport.responseText);}
else{var doc=Jester.Tree.parseXML(transport.responseText);if(doc[this.klass._singular_xml])
attributes=this._attributesFromTree(doc[this.klass._singular_xml]);}
if(attributes)
this._resetAttributes(attributes);}}
if(this.new_record()&&transport.status==201){loc=transport.getResponseHeader("location");if(loc){id=parseInt(loc.match(/\/([^\/]*?)(\.\w+)?$/)[1]);if(!isNaN(id))
this._setProperty("id",id)}}
return(transport.status>=200&&transport.status<300&&this.errors.length==0);});this._setErrors([]);var url=null;var method=null;var params={};var urlParams={};(this._properties).each(bind(this,function(value,i){params[this.klass._singular+"["+value+"]"]=this[value];urlParams[value]=this[value];}));if(this.new_record()){url=this._create_url(urlParams);method="post";}
else{url=this._update_url(urlParams);method="put";}
return this.klass.request(saveWork,url,{parameters:params,method:method},callback);},setAttributes:function(attributes)
{$H(attributes).each(bind(this,function(attr){this._setAttribute(attr.key,attr.value)}));return attributes;},updateAttributes:function(attributes,callback)
{this.setAttributes(attributes);return this.save(callback);},attributes:function(include_associations){var attributes={}
for(var i=0;i<this._properties.length;i++)
attributes[this._properties[i]]=this[this._properties[i]];if(include_associations){for(var i=0;i<this._associations.length;i++)
attributes[this._associations[i]]=this[this._associations[i]];}
return attributes;},_attributesFromJSON:function()
{return this.klass._attributesFromJSON.apply(this.klass,arguments);},_attributesFromTree:function()
{return this.klass._attributesFromTree.apply(this.klass,arguments);},_errorsFrom:function(raw){if(this.klass._format=="json")
return this._errorsFromJSON(raw);else
return this._errorsFromXML(raw);},_errorsFromJSON:function(json){try{json=eval(json);}catch(e){return false;}
if(!(json&&json.constructor==Array&&json[0]&&json[0].constructor==Array))return false;return json.map(function(pair){return pair[0].capitalize()+" "+pair[1];});},_errorsFromXML:function(xml){if(!xml)return false;var doc=Jester.Tree.parseXML(xml);if(doc&&doc.errors){var errors=[];if(typeof(doc.errors.error)=="string")
doc.errors.error=[doc.errors.error];doc.errors.error.each(function(value,index){errors.push(value);});return errors;}
else return false;},_setErrors:function(errors){this.errors=errors;},_resetAttributes:function(attributes){this._clear();for(var attr in attributes)
this._setAttribute(attr,attributes[attr]);},_setAttribute:function(attribute,value){if(value&&typeof(value)=="object"&&value.constructor!=Date)
this._setAssociation(attribute,value);else
this._setProperty(attribute,value);},_setProperties:function(properties){this._clearProperties();for(var prop in properties)
this._setProperty(prop,properties[prop])},_setAssociations:function(associations){this._clearAssociations();for(var assoc in associations)
this._setAssociation(assoc,associations[assoc])},_setProperty:function(property,value){this[property]=value;if(!(this._properties.include(property)))
this._properties.push(property);},_setAssociation:function(association,value){this[association]=value;if(!(this._associations.include(association)))
this._associations.push(association);},_clear:function(){this._clearProperties();this._clearAssociations();},_clearProperties:function(){for(var i=0;i<this._properties.length;i++)
this[this._properties[i]]=null;this._properties=[];},_clearAssociations:function(){for(var i=0;i<this._associations.length;i++)
this[this._associations[i]]=null;this._associations=[];},_url_for:function(action,params){if(!params)params=this.id;if(typeof(params)=="object"&&!params.id)
params.id=this.id;return this.klass._url_for(action,params);}});Jester.Resource.elementHasMany=function(element){var i=0;var singular=null;var has_many=false;for(var val in element){if(i==0)
singular=val;i+=1;}
return(element[singular]&&typeof(element[singular])=="object"&&element[singular].length!=null&&i==1);}
function bind(context,func){var __method=func,args=$A(func.arguments),object=context;return function(){return __method.apply(object,args.concat($A(arguments)));}}
if(typeof(Resource)=="undefined")
Resource=Jester.Resource;if(!String.prototype.pluralize)String.prototype.pluralize=function(plural){var str=this;if(plural)str=plural;else{var uncountable_words=['equipment','information','rice','money','species','series','fish','sheep','moose'];var uncountable=false;for(var x=0;!uncountable&&x<uncountable_words.length;x++)uncountable=(uncountable_words[x].toLowerCase()==str.toLowerCase());if(!uncountable){var rules=[[new RegExp('(m)an$','gi'),'$1en'],[new RegExp('(pe)rson$','gi'),'$1ople'],[new RegExp('(child)$','gi'),'$1ren'],[new RegExp('(ax|test)is$','gi'),'$1es'],[new RegExp('(octop|vir)us$','gi'),'$1i'],[new RegExp('(alias|status)$','gi'),'$1es'],[new RegExp('(bu)s$','gi'),'$1ses'],[new RegExp('(buffal|tomat)o$','gi'),'$1oes'],[new RegExp('([ti])um$','gi'),'$1a'],[new RegExp('sis$','gi'),'ses'],[new RegExp('(?:([^f])fe|([lr])f)$','gi'),'$1$2ves'],[new RegExp('(hive)$','gi'),'$1s'],[new RegExp('([^aeiouy]|qu)y$','gi'),'$1ies'],[new RegExp('(x|ch|ss|sh)$','gi'),'$1es'],[new RegExp('(matr|vert|ind)ix|ex$','gi'),'$1ices'],[new RegExp('([m|l])ouse$','gi'),'$1ice'],[new RegExp('^(ox)$','gi'),'$1en'],[new RegExp('(quiz)$','gi'),'$1zes'],[new RegExp('s$','gi'),'s'],[new RegExp('$','gi'),'s']];var matched=false;for(var x=0;!matched&&x<=rules.length;x++){matched=str.match(rules[x][0]);if(matched)str=str.replace(rules[x][0],rules[x][1]);}}}
return str;};

// XML.ObjTree -- XML source code from/to JavaScript object like E4X
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('5(p(o)==\'w\')o=v(){};o.r=v(){m 9};o.r.1i="0.1b";o.r.u.14=\'<?L 1s="1.0" 1o="1n-8" ?>\\n\';o.r.u.Y=\'-\';o.r.u.1c=\'1a/L\';o.r.u.N=v(a){6 b;5(W.U){6 c=K U();6 d=c.1r(a,"1p/L");5(!d)m;b=d.A}q 5(W.10){c=K 10(\'1k.1h\');c.1g=z;c.1e(a);b=c.A}5(!b)m;m 9.E(b)};o.r.u.1d=v(c,d,e){6 f={};y(6 g 19 d){f[g]=d[g]}5(!f.M){5(p(f.18)=="w"&&p(f.17)=="w"&&p(f.16)=="w"){f.M="15"}q{f.M="13"}}5(e){f.X=V;6 h=9;6 i=e;6 j=f.T;f.T=v(a){6 b;5(a&&a.x&&a.x.A){b=h.E(a.x.A)}q 5(a&&a.J){b=h.N(a.J)}i(b,a);5(j)j(a)}}q{f.X=z}6 k;5(p(S)!="w"&&S.I){f.1q=c;6 l=K S.I(f);5(l)k=l.12}q 5(p(Q)!="w"&&Q.I){6 l=K Q.I(c,f);5(l)k=l.12}5(e)m k;5(k&&k.x&&k.x.A){m 9.E(k.x.A)}q 5(k&&k.J){m 9.N(k.J)}};o.r.u.E=v(a){5(!a)m;9.H={};5(9.P){y(6 i=0;i<9.P.t;i++){9.H[9.P[i]]=1}}6 b=9.O(a);5(9.H[a.F]){b=[b]}5(a.B!=11){6 c={};c[a.F]=b;b=c}m b};o.r.u.O=v(a){5(a.B==7){m}5(a.B==3||a.B==4){6 b=a.G.1j(/[^\\1f-\\1l]/);5(b==1m)m z;m a.G}6 c;6 d={};5(a.D&&a.D.t){c={};y(6 i=0;i<a.D.t;i++){6 e=a.D[i].F;5(p(e)!="Z")C;6 f=a.D[i].G;5(!f)C;e=9.Y+e;5(p(d[e])=="w")d[e]=0;d[e]++;9.R(c,e,d[e],f)}}5(a.s&&a.s.t){6 g=V;5(c)g=z;y(6 i=0;i<a.s.t&&g;i++){6 h=a.s[i].B;5(h==3||h==4)C;g=z}5(g){5(!c)c="";y(6 i=0;i<a.s.t;i++){c+=a.s[i].G}}q{5(!c)c={};y(6 i=0;i<a.s.t;i++){6 e=a.s[i].F;5(p(e)!="Z")C;6 f=9.O(a.s[i]);5(f==z)C;5(p(d[e])=="w")d[e]=0;d[e]++;9.R(c,e,d[e],f)}}}m c};o.r.u.R=v(a,b,c,d){5(9.H[b]){5(c==1)a[b]=[];a[b][a[b].t]=d}q 5(c==1){a[b]=d}q 5(c==2){a[b]=[a[b],d]}q{a[b][a[b].t]=d}};',62,91,'|||||if|var|||this|||||||||||||return||XML|typeof|else|ObjTree|childNodes|length|prototype|function|undefined|responseXML|for|false|documentElement|nodeType|continue|attributes|parseDOM|nodeName|nodeValue|__force_array|Request|responseText|new|xml|method|parseXML|parseElement|force_array|Ajax|addNode|HTTP|onComplete|DOMParser|true|window|asynchronous|attr_prefix|string|ActiveXObject||transport|post|xmlDecl|get|parameters|postbody|postBody|in|text|24|overrideMimeType|parseHTTP|loadXML|x00|async|XMLDOM|VERSION|match|Microsoft|x20|null|UTF|encoding|application|uri|parseFromString|version'.split('|'),0,{}))
// This is a Date parsing library by Nicholas Barthelemy, packed to keep jester.js light.
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('N.q.F||(N.q.F=t(a){o u.1d().F(a)});O.q.F||(O.q.F=t(a){o\'0\'.1H(a-u.K)+u});O.q.1H||(O.q.1H=t(a){v s=\'\',i=0;2k(i++<a){s+=u}o s});N.q.1j||(N.q.1j=t(){o u.1d().1j()});O.q.1j||(O.q.1j=t(){v n=u,l=n.K,i=-1;2k(i++<l){u.20(i,i+1)==0?n=n.20(1,n.K):i=l}o n});k.1m="2H 2F 2z 2y 2x 2u 2r 3q 3n 3k 3i 3d".1x(" ");k.1o="38 35 2Y 2U 2Q 2O 2M".1x(" ");k.2K="31 28 31 30 31 30 31 31 30 31 30 31".1x(" ");k.1A={2G:"%Y-%m-%d %H:%M:%S",2w:"%Y-%m-%2v%H:%M:%S%T",2s:"%a, %d %b %Y %H:%M:%S %Z",3p:"%d %b %H:%M",3o:"%B %d, %Y %H:%M"};k.3l=-1;k.3j=-2;(t(){v d=k;d["3h"]=1;d["2i"]=1t;d["2h"]=d["2i"]*19;d["2e"]=d["2h"]*19;d["P"]=d["2e"]*24;d["37"]=d["P"]*7;d["34"]=d["P"]*31;d["1q"]=d["P"]*2X;d["2W"]=d["1q"]*10;d["2R"]=d["1q"]*23;d["2P"]=d["1q"]*1t})();k.q.1D||(k.q.1D=t(){o D k(u.1k())});k.q.26||(k.q.26=t(a,b){u.1F(u.1k()+((a||k.P)*(b||1)));o u});k.q.2a||(k.q.2a=t(a,b){u.1F(u.1k()-((a||k.P)*(b||1)));o u});k.q.1Z||(k.q.1Z=t(){u.1Y(0);u.1X(0);u.1U(0);u.1T(0);o u});k.q.1I||(k.q.1I=t(a,b){C(1i a==\'1p\')a=k.1J(a);o 18.2l((u.1k()-a.1k())/(b|k.P))});k.q.1N||(k.q.1N=k.q.1I);k.q.2n||(k.q.2n=t(){d=O(u);o d.1f(-(18.1y(d.K,2)))>3&&d.1f(-(18.1y(d.K,2)))<21?"V":["V","17","16","1a","V"][18.1y(N(d)%10,4)]});k.q.1w||(k.q.1w=t(){v f=(D k(u.1h(),0,1)).1e();o 18.2t((u.1n()+(f>3?f-4:f+3))/7)});k.q.1M=t(){o u.1d().1v(/^.*? ([A-Z]{3}) [0-9]{4}.*$/,"$1").1v(/^.*?\\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\\)$/,"$1$2$3")};k.q.2p=t(){o(u.1u()>0?"-":"+")+O(18.2l(u.1u()/19)).F(2)+O(u.1u()%19,2,"0").F(2)};k.q.1n||(k.q.1n=t(){o((k.2o(u.1h(),u.1c(),u.1b()+1,0,0,0)-k.2o(u.1h(),0,1,0,0,0))/k.P)});k.q.2m||(k.q.2m=t(){v a=u.1D();a.15(a.1c()+1);a.L(0);o a.1b()});k.2j||(k.2j=t(a,b){a=(a+12)%12;C(k.1K(b)&&a==1)o 29;o k.3g.3f[a]});k.1K||(k.1K=t(a){o(((a%4)==0)&&((a%23)!=0)||((a%3e)==0))});k.q.1B||(k.q.1B=t(c){C(!u.3c())o\'&3b;\';v d=u;C(k.1A[c.2g()])c=k.1A[c.2g()];o c.1v(/\\%([3a])/g,t(a,b){39(b){E\'a\':o k.1l(d.1e()).1f(0,3);E\'A\':o k.1l(d.1e());E\'b\':o k.13(d.1c()).1f(0,3);E\'B\':o k.13(d.1c());E\'c\':o d.1d();E\'d\':o d.1b().F(2);E\'H\':o d.1G().F(2);E\'I\':o((h=d.1G()%12)?h:12).F(2);E\'j\':o d.1n().F(3);E\'m\':o(d.1c()+1).F(2);E\'M\':o d.36().F(2);E\'p\':o d.1G()<12?\'33\':\'32\';E\'S\':o d.2Z().F(2);E\'U\':o d.1w().F(2);E\'W\':R Q("%W 2V 2T 2S 25");E\'w\':o d.1e();E\'x\':o d.1r("%m/%d/%Y");E\'X\':o d.1r("%I:%M%p");E\'y\':o d.1h().1d().1f(2);E\'Y\':o d.1h();E\'T\':o d.2p();E\'Z\':o d.1M()}})});k.q.1r||(k.q.1r=k.q.1B);k.22=k.1J;k.1J=t(a){C(1i a!=\'1p\')o a;C(a.K==0||(/^\\s+$/).1E(a))o;2N(v i=0;i<k.1g.K;i++){v r=k.1g[i].J.2L(a);C(r)o k.1g[i].G(r)}o D k(k.22(a))};k.13||(k.13=t(c){v d=-1;C(1i c==\'2J\'){o k.1m[c.1c()]}2I C(1i c==\'27\'){d=c-1;C(d<0||d>11)R D Q("1s 1C 2b 2q 1W 1V 2d 1 2c 12:"+d);o k.1m[d]}v m=k.1m.1S(t(a,b){C(D 1O("^"+c,"i").1E(a)){d=b;o 1R}o 2f});C(m.K==0)R D Q("1s 1C 1p");C(m.K>1)R D Q("1Q 1C");o k.1m[d]});k.1l||(k.1l=t(c){v d=-1;C(1i c==\'27\'){d=c-1;C(d<0||d>6)R D Q("1s 1z 2b 2q 1W 1V 2d 1 2c 7");o k.1o[d]}v m=k.1o.1S(t(a,b){C(D 1O("^"+c,"i").1E(a)){d=b;o 1R}o 2f});C(m.K==0)R D Q("1s 1z 1p");C(m.K>1)R D Q("1Q 1z");o k.1o[d]});k.1g||(k.1g=[{J:/(\\d{1,2})\\/(\\d{1,2})\\/(\\d{2,4})/,G:t(a){v d=D k();d.1L(a[3]);d.L(14(a[2],10));d.15(14(a[1],10)-1);o d}},{J:/(\\d{4})(?:-?(\\d{2})(?:-?(\\d{2})(?:[T ](\\d{2})(?::?(\\d{2})(?::?(\\d{2})(?:\\.(\\d+))?)?)?(?:Z|(?:([-+])(\\d{2})(?::?(\\d{2}))?)?)?)?)?)?/,G:t(a){v b=0;v d=D k(a[1],0,1);C(a[2])d.15(a[2]-1);C(a[3])d.L(a[3]);C(a[4])d.1Y(a[4]);C(a[5])d.1X(a[5]);C(a[6])d.1U(a[6]);C(a[7])d.1T(N("0."+a[7])*1t);C(a[9]){b=(N(a[9])*19)+N(a[10]);b*=((a[8]==\'-\')?1:-1)}b-=d.1u();1P=(N(d)+(b*19*1t));d.1F(N(1P));o d}},{J:/^2E/i,G:t(){o D k()}},{J:/^2D/i,G:t(){v d=D k();d.L(d.1b()+1);o d}},{J:/^2C/i,G:t(){v d=D k();d.L(d.1b()-1);o d}},{J:/^(\\d{1,2})(17|16|1a|V)?$/i,G:t(a){v d=D k();d.L(14(a[1],10));o d}},{J:/^(\\d{1,2})(?:17|16|1a|V)? (\\w+)$/i,G:t(a){v d=D k();d.L(14(a[1],10));d.15(k.13(a[2]));o d}},{J:/^(\\d{1,2})(?:17|16|1a|V)? (\\w+),? (\\d{4})$/i,G:t(a){v d=D k();d.L(14(a[1],10));d.15(k.13(a[2]));d.1L(a[3]);o d}},{J:/^(\\w+) (\\d{1,2})(?:17|16|1a|V)?$/i,G:t(a){v d=D k();d.L(14(a[2],10));d.15(k.13(a[1]));o d}},{J:/^(\\w+) (\\d{1,2})(?:17|16|1a|V)?,? (\\d{4})$/i,G:t(a){v d=D k();d.L(14(a[2],10));d.15(k.13(a[1]));d.1L(a[3]);o d}},{J:/^3m (\\w+)$/i,G:t(a){v d=D k();v b=d.1e();v c=k.1l(a[1]);v e=c-b;C(c<=b){e+=7}d.L(d.1b()+e);o d}},{J:/^2B (\\w+)$/i,G:t(a){R D Q("2A 25 3r");}}]);',62,214,'||||||||||||||||||||Date||||return||prototype|||function|this|var|||||||if|new|case|zf|handler|||re|length|setDate||Number|String|DAY|Error|throw||||th||||||||parseMonth|parseInt|setMonth|nd|st|Math|60|rd|getDate|getMonth|toString|getDay|substr|__PARSE_PATTERNS|getFullYear|typeof|rz|getTime|parseDay|MONTH_NAMES|getDayOfYear|DAY_NAMES|string|YEAR|format|Invalid|1000|getTimezoneOffset|replace|getWeek|split|min|day|FORMATS|strftime|month|clone|test|setTime|getHours|str|diff|parse|isLeapYear|setYear|getTimezone|compare|RegExp|time|Ambiguous|true|findAll|setMilliseconds|setSeconds|be|must|setMinutes|setHours|clearTime|substring||__native_parse|100||yet|increment|number|||decrement|index|and|between|HOUR|false|toLowerCase|MINUTE|SECOND|daysInMonth|while|floor|lastDayOfMonth|getOrdinal|UTC|getGMTOffset|value|July|rfc822|round|June|dT|iso8601|May|April|March|Not|last|yes|tom|tod|February|db|January|else|object|DAYS_PER_MONTH|exec|Saturday|for|Friday|MILLENNIUM|Thursday|CENTURY|supported|not|Wednesday|is|DECADE|365|Tuesday|getSeconds|||PM|AM|MONTH|Monday|getMinutes|WEEK|Sunday|switch|aAbBcdHIjmMpSUWwxXyYTZ|nbsp|valueOf|December|400|DAYS_IN_MONTH|Convensions|MILLISECOND|November|ERA|October|EPOCH|next|September|long|short|August|implemented'.split('|'),0,{}))
/******************************************************************************************************************************************************
end jester and dependent libraries, required for spiceworks plugins
******************************************************************************************************************************************************/


/* Some Jester Overrides */
// interpolate wasn't handling parameters like 'name[]' on IE.  Escaping the [] seems to work.
Jester.Resource._interpolate = (function (string, params) {
  if (!params) return string;

  var result = string;
  params.each(function(pair) {
    var re = new RegExp(":" + pair.key.gsub('[]','\\[\\]'), "g");
    if (result.match(re)) {
      result = result.replace(re, pair.value);
      params.unset(pair.key);
    }
  });
  return result;
});

// override the request method so that we can tack on an additional request parameter to let the server know that this request is coming from Jester
// we do this because Jester expects all resultsets to be in the form of an array, which is incompatible when passing a single object
Jester.Resource.request = function(callback, url, options, user_callback) {
  if (user_callback) {
    options.asynchronous = true;
    // if an options hash was given instead of a callback
    if (typeof(user_callback) == "object") {
      for (var x in user_callback)
      options[x] = user_callback[x];
      user_callback = options.onComplete;
    }
  }
  else
    user_callback = function(arg){return arg;}

  if (options.method == 'get') url += (url.indexOf('?') > -1 ? '&jester=t' : '?jester=t')
  else if (options.parameters) {
    if (typeof options.parameters == 'object') options.parameters.jester = 't';
    else options.parameters += '&jester=t';
  }

  if (options.asynchronous) {
    options.onComplete = function(transport, json) {user_callback(callback(transport), json);}
    return new Ajax.Request(url, options).transport;
  }
  else
  {
    options.asynchronous = false; // Make sure it's set, to avoid being overridden.
    return callback(new Ajax.Request(url, options).transport);
  }
};

//  Date#strftime: http://hacks.bluesmoon.info/strftime/index.html
//  Copyright (c) 2008, Philip S Tellis <philip@bluesmoon.info>
//  All rights reserved.
//  This code is distributed under the terms of the BSD licence
//  
//  Redistribution and use of this software in source and binary forms, with or without modification,
//  are permitted provided that the following conditions are met:
// 
//    * Redistributions of source code must retain the above copyright notice, this list of conditions
//      and the following disclaimer.
//    * Redistributions in binary form must reproduce the above copyright notice, this list of
//      conditions and the following disclaimer in the documentation and/or other materials provided
//      with the distribution.
//    * The names of the contributors to this file may not be used to endorse or promote products
//      derived from this software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(x,pad,r){if(typeof (r)=="undefined"){r=10}for(;parseInt(x,10)<r&&r>1;r/=10){x=pad.toString()+x}return x.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(d){return Date.ext.locales[d.locale].a[d.getDay()]},A:function(d){return Date.ext.locales[d.locale].A[d.getDay()]},b:function(d){return Date.ext.locales[d.locale].b[d.getMonth()]},B:function(d){return Date.ext.locales[d.locale].B[d.getMonth()]},c:"toLocaleString",C:function(d){return Date.ext.util.xPad(parseInt(d.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(d){return Date.ext.util.xPad(parseInt(Date.ext.util.G(d)/100,10),0)},G:function(d){var y=d.getFullYear();var V=parseInt(Date.ext.formats.V(d),10);var W=parseInt(Date.ext.formats.W(d),10);if(W>V){y++}else{if(W===0&&V>=52){y--}}return y},H:["getHours","0"],I:function(d){var I=d.getHours()%12;return Date.ext.util.xPad(I===0?12:I,0)},j:function(d){var ms=d-new Date(""+d.getFullYear()+"/1/1 GMT");ms+=d.getTimezoneOffset()*60000;var doy=parseInt(ms/60000/60/24,10)+1;return Date.ext.util.xPad(doy,0,100)},m:function(d){return Date.ext.util.xPad(d.getMonth()+1,0)},M:["getMinutes","0"],p:function(d){return Date.ext.locales[d.locale].p[d.getHours()>=12?1:0]},P:function(d){return Date.ext.locales[d.locale].P[d.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(d){var dow=d.getDay();return dow===0?7:dow},U:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=6-d.getDay();var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0)},V:function(d){var woy=parseInt(Date.ext.formats.W(d),10);var dow1_1=(new Date(""+d.getFullYear()+"/1/1")).getDay();var idow=woy+(dow1_1>4||dow1_1<=1?0:1);if(idow==53&&(new Date(""+d.getFullYear()+"/12/31")).getDay()<4){idow=1}else{if(idow===0){idow=Date.ext.formats.V(new Date(""+(d.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(idow,0)},w:"getDay",W:function(d){var doy=parseInt(Date.ext.formats.j(d),10);var rdow=7-Date.ext.formats.u(d);var woy=parseInt((doy+rdow)/7,10);return Date.ext.util.xPad(woy,0,10)},y:function(d){return Date.ext.util.xPad(d.getFullYear()%100,0)},Y:"getFullYear",z:function(d){var o=d.getTimezoneOffset();var H=Date.ext.util.xPad(parseInt(Math.abs(o/60),10),0);var M=Date.ext.util.xPad(o%60,0);return(o>0?"-":"+")+H+M},Z:function(d){return d.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(d){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(fmt){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var d=this;while(fmt.match(/%[cDhnrRtTxXzZ]/)){fmt=fmt.replace(/%([cDhnrRtTxXzZ])/g,function(m0,m1){var f=Date.ext.aggregates[m1];return(f=="locale"?Date.ext.locales[d.locale][m1]:f)})}var str=fmt.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(m0,m1){var f=Date.ext.formats[m1];if(typeof (f)=="string"){return d[f]()}else{if(typeof (f)=="function"){return f.call(d,d)}else{if(typeof (f)=="object"&&typeof (f[0])=="string"){return Date.ext.util.xPad(d[f[0]](),f[1])}else{return m1}}}});d=null;return str};

/**************
 * Global Namespace for all spiceworks stuff
 * THIS FILE SHOULD BE LOADED FIRST SINCE WE LOAD IN ALPHA ORDER (ignoring .js)
 */
var SPICEWORKS = {};

if (document.location.toString().match(/logEvents/)) {
  SPICEWORKS.logEvents = true;  
}

// JSLINT - The following statement will allow this file to pass the jslint tests by adding a couple of global variables
/*global Element, $, $$, Event, GMap2, Ajax */


// Create a closure, passing in the global namespace (SPICEWORKS) to a local variable (spiceworks)
(function (spiceworks) {
  
  // Spiceworks Events
  // Register for events that might be occuring
  // Passes through to the browser event code (prototype) for now, but by change in the future
  spiceworks.observe = function (event, func) {
    Event.observe(document, 'spiceworks:' + event, func);
  };
    
  spiceworks.stopObserving = function (event, func) {
    Event.stopObserving(document, 'spiceworks:' + event, func);
  };
  
  // catch the loaded event so we'll know that we're ready to go.
  spiceworks.isReady = false;
  
  // Fire an event within the spiceworks infrastructure.
  // the event name is currently attacted to the DOM.  
  // "spiceworks:" will be prepended to all events
  // immediate will force the event to fire even if the page is not "ready" (dom fully loaded).
  // By default, events will be queued up until the document is fully loaded
  // there is (currently) no guarentee on the order that events will be fired once added (FF and IE are different)
  spiceworks.fire = function (event, memo) {
    memo = memo || {};
        
    if (event == 'ready') {
      spiceworks.isReady = true;
    }
    
    // if the page is not ready, then try again once the page is ready.
    if (spiceworks.isReady || memo.immediate) {
      
      if (spiceworks.logEvents){
        /*TODO: add logging mechanism? */
      }
      document.fire('spiceworks:' + event, memo);
    } else {
      if (spiceworks.logEvents){
        /*TODO: add logging mechanism? */
      }
      spiceworks.ready(function () { spiceworks.fire(event, memo); });
    }
  };

  // This is called once all plugins are given the chance to load.  Use this if you depend on something in another plugin.
  // If this app is already loaded, then the function will be called immediately (unless onLoadOnly is passed as true).
  spiceworks.ready = function (func, loadOnly) {
    if (spiceworks.isReady && !loadOnly) {
      func(); // app already initialized, so let's just call the method.
    } else {
      spiceworks.observe('ready', func);      
    }
  };  
  
  // Helper function to define spiceworks observer functions
  spiceworks.ready_func = function(event) {
    return function( callback ) {
      spiceworks.observe(event, callback);
    };
  };

  // Create an object which derives from another object.
  // See: http://javascript.crockford.com/prototypal.html
  if (typeof Object.create !== 'function') {
      Object.create = function (o) {
          function F() {}
          F.prototype = o;
          return new F();
      };
  }
  
  // == SPICEWORKS.config ==
  // placeholder for configuration options  // see _script_includes.html.erb for more info on how this gets setup.
  spiceworks.config = {};
  
})(SPICEWORKS);


(function (spiceworks) {
  
  // == App Namespace ==
  spiceworks.app = function () {
    var that, userMethods;
    
    that = {
      linkTo: function(path, options){
          return new Element('a', Object.extend({ 'href': path }, options || {}));
      },
      ready: function (func) {
        spiceworks.observe('app:ready', func);
      },
      
      isShowing: function () {
        return $(document.body).hasClassName('spiceworks_application');        
      },
      
      setUser: function (user) {
        that.user = user;
      }      
    };
    return that;
  }();
  
})(SPICEWORKS);

(function (spiceworks) {

  // == SPICEWORKS.app.dashboard ==
  // Add Widget Types and tabs here.  A plugin should not add a widget to the user's page.
  spiceworks.app.dashboard = function () {
    var widgetTypes = [],
        tabs = [],
        that; // TODO

    that = {
      // Create a new type of widget
      addWidgetType: function (widgetType) {
        var imgsrc, widgetList, li;

        if (!widgetType.name) {
          throw 'WidgetType name required';
        }
        if (!widgetType.label) {
          throw 'WidgetType label required';
        }
        if (!widgetType.update) {
          throw 'WidgetType update method required';
        }

        /* Support either settingDefinitions or prefs for the preferences. */
        if (widgetType.prefs && !widgetType.settingDefinitions) {
          widgetType.settingDefinitions = widgetType.prefs;
        }

        /* Default the settingDefinitions to be an empty array if none were specified. */
        if( !widgetType.settingDefinitions ){
          widgetType.settingDefinitions = [];
        }

        /* add a method that either returns the evaluated preferences, or */
        /* evaluates, caches, and returns the preferences */
        widgetType.getSettings = function() {
          if ( typeof widgetType.settingDefinitions != 'function' ) {
            return widgetType.settingDefinitions;
          } else {
            widgetType.settingDefinitions = widgetType.settingDefinitions();
            return widgetType.settingDefinitions;
          }
        }

        widgetTypes.push(widgetType);

        /* Only do the following if we're on the dashboard */
        if (that.isShowing()) {
          imgsrc = widgetType.icon || '/images/icons/small/gear.png';

          widgetList = $$('#dashboard_quickform ul').first();
          if (widgetList) {
            li = new Element('li', {'id': widgetType.name + '_small', 'class': 'module-small classname-javascript'});
            li.update('<a href="#"><img src="' + imgsrc + '"></img><span class="title">' + widgetType.label + '</span></a>');
            widgetList.insert(li);
          }
        }
      },

      getWidgetTypes: function () {
        return widgetTypes;
      },

      getWidgetType: function (name) {
        var widgetType = widgetTypes.find(function (widgetType) {
          return (widgetType.name === name);
        });
        if (widgetType == null) {
          widgetType = {
            name: name,
            label: 'Missing Definition',
            settingDefinitions:[],
            getSettings: function(){},
            update: function (element, settings) {
              element.innerHTML = 'The definition of this widget is missing.  This is probably due to a plugin being disabled or removed.';
            }
          };
        }

        widgetType.getSettings();
        return widgetType;
      },

      isShowing: function () {
        return $(document.body).hasClassName('dashboard');
      },

      ready: function (callback) {
        spiceworks.observe('app:dashboard:ready', callback);
      }
    };

    return that;
  }();
})(SPICEWORKS);


(function (spiceworks) {
  
  // == SPICEWORKS.app.helpdesk ==
  // Currently, we just have stuff in here to help with events
  spiceworks.app.helpdesk = function () {
    var that = {
      /* Update the ticket table (optionally, check email first.) */
      updateTable: function (checkEmail) {
        // Make sure we're on the right page...
        if (typeof Ticket === 'undefined' || !Ticket.checkForNewTickets || !Ticket.updateTable){
          return;
        }

        if (checkEmail) {
          Ticket.checkForNewTickets();
        } else {
          Ticket.updateTable();
        }
      },
      
      ready: SPICEWORKS.ready_func('app:helpdesk:ready'),
      newTicket: {
        ready: SPICEWORKS.ready_func('app:helpdesk:new_ticket:ready')
      },
      ticket: {
        ready: SPICEWORKS.ready_func('app:helpdesk:ticket:ready'),
        closed: SPICEWORKS.ready_func('app:helpdesk:ticket:closed'),
        comments:{
          ready: SPICEWORKS.ready_func('app:helpdesk:ticket:comments:ready')
        },
        purchase_list:{
          ready: SPICEWORKS.ready_func('app:helpdesk:ticket:purchase_list:ready')
        },
        editor:{
          ready: SPICEWORKS.ready_func('app:helpdesk:ticket:editor:ready')
        },
        overview: {
          ready: SPICEWORKS.ready_func('app:helpdesk:ticket:overview:ready')
        }
      }
    };
    return that;
  }();
  
})(SPICEWORKS);

(function (spiceworks) {
  
  
  // == SPICEWORKS.app.inventory ==
  // Currently, we just have stuff in here to help with events
  spiceworks.app.inventory = function () {
    var that = {
      ready: SPICEWORKS.ready_func('app:inventory:ready'),
      
      dns:{
        ready: SPICEWORKS.ready_func('app:inventory:dns:ready')
      },
      
      environment: {
        ready: SPICEWORKS.ready_func('app:inventory:environment_summary:ready')
      },

      group: {
        ready: SPICEWORKS.ready_func('app:inventory:group:ready'),
        device: {
          ready: SPICEWORKS.ready_func('app:inventory:group:device:ready')
        },
        item: {
          ready: SPICEWORKS.ready_func('app:inventory:group:item:ready')
        },
        environment: {
          ready: SPICEWORKS.ready_func('app:inventory:group:environment_summary:ready')
        }
      },      
      
      softwareGroup: {
        ready: SPICEWORKS.ready_func('app:inventory:software:group:ready'),
        
        environment: {
          ready: SPICEWORKS.ready_func('app:inventory:software:group:environment_summary:ready')
        },

        software:{
          ready: SPICEWORKS.ready_func('app:inventory:software:group:software:ready')
        },
        service:{
          ready: SPICEWORKS.ready_func('app:inventory:software:group:services:ready')
        },
        hotfix:{
          ready: SPICEWORKS.ready_func('app:inventory:software:group:hotfixes:ready')
        }        
      }
    };
    return that;
  }();
  
})(SPICEWORKS);

(function (spiceworks) {
  
  // == SPICEWORKS.app.messaging ==
  // Allow users to push and pop messages into the header messaging area
  // This is a fairly thin wrapper around the current messaging infrastructure.
  spiceworks.app.messaging = {
    push: function (messageText, options) {
      var message = Object.extend({
        dismissable: false, // give the message a clickable element to remove it
        selfRemoving: false, // make the message remove itself after timeSeconds have lapsed
        timeoutSeconds: 5, // when selfRemoving is true, this is time duration the message is displayed
        id: 'p_' + (new Date()).getTime()
      }, options || {});
      
      Messaging.push( message.id, messageText, message );
            
      return message.id;
    },
    
    pop: function (messageId) {
      Messaging.pop(messageId);
    }
  };
  
})(SPICEWORKS);

(function (spiceworks) {
  

  // == SPICEWORKS.app.navigation ==
  // define new tools (i.e. pages) that will be shown when the user clicks on them.
  spiceworks.app.navigation = function () {
    var items = [],
        menuItems = [],
        that;

    that = {
      addItem: function (item) {
        var linkElem = null;

        items.push(item);

        if (!spiceworks.app.isShowing()) { 
          return; 
        } // do nothing unless the app is showing.
        
        item.element = new Element('dd', {});
        linkElem = new Element('a', {'href': '/tools/' + item.name, 'id': item.name});
        linkElem.update(item.label);
        item.element.insert(linkElem);

        $('navigation_my_stuff').insert(item.element);
      },

      // Show the specified tool
      showItem: function (name) {
        var content, item;        
        item = items.find(function (item) { 
          return (item.name === name); 
        });

        $$('dd.current').each(function (e) {
          e.removeClassName('current');
        });
        item.element.addClassName('current');
        content = $('content');
        content.innerHTML = '';

        item.update(content);
        document.title = 'Spiceworks - ' + item.label;
      },

      getItems: function () {
        return items;
      },
      
      addMenuItem: function (spec) {
        menuItems.push(spec);
      },
      
      addMenuItems: function (spec) {
        that.addMenuItem(spec);
      },
      
      // Add a menu item once the page is loaded.
      dynamicAddMenuItem: function (spec) {
        $$(spec.selector).each( function ( itemElemOrig ) {
          var menuItem, itemElem, items;
          
          // Find the DD
          if( itemElemOrig.nodeName != 'DD'){
            itemElem = itemElemOrig.up('DD');              
          }else{
            itemElem = itemElemOrig;
          }
          
          spec = Object.clone(spec);
          spec.itemElemOrig = itemElemOrig;
          
          SPICEWORKS.ui.sideMenu( itemElem, spec );

        });
      },
      
      updateNavMenus: function () {
        menuItems.each( function (spec) {
          that.dynamicAddMenuItem(spec);
        });        
      }
    };
    
    // Attach the menu items once the app is ready.
    // This is also done in _customizable_sections.html.erb
    // It's fine to call this twice, it figures out what to do.
    spiceworks.app.ready(that.updateNavMenus);

    return that;
  }();
  
})(SPICEWORKS);

(function (spiceworks) {
  
  
  // == SPICEWORKS.app.inventory ==
  // Currently, we just have stuff in here to help with events
  spiceworks.app.reports = function () {
    var that = {
      ready: SPICEWORKS.ready_func('app:reports:ready'),
      create:{
        ready: SPICEWORKS.ready_func('app:reports:create:ready')
      },
      edit:{
        ready: SPICEWORKS.ready_func('app:reports:edit:ready')
      },
      run: {
        ready: SPICEWORKS.ready_func('app:reports:run:ready')
      }
    };
    return that;
  }();
  
})(SPICEWORKS);

(function (spiceworks) {
  
  
  // == SPICEWORKS.app.settings ==
  spiceworks.app.settings = function () {
    var that = {
      ready: SPICEWORKS.ready_func('app:settings:ready')
    };
    
    // Setup ready functions for each othe settings panes.
    ['help_desk', 'monitors', 'network', 'email', 'accounts', 'spicemeter', 'events', 'categories', 'backup', 'advanced'].each(function (settingsPane) {
      that[settingsPane] = {
        ready: SPICEWORKS.ready_func('app:settings:' + settingsPane + ':ready')
      };
    }); 
    
    that.users = that.accounts;   
    
    return that;
  }();
  
  // == SPICEWORKS.app.settings.plugin ==
  // Some helpers for extending the plugins page.  Currently, there's only support here for
  // adding example code to the "Insert Code" dropdown.
  spiceworks.app.settings.plugin = function () {
    var codeExamples = [],
        that;
    
    that = {
      
      ready: SPICEWORKS.ready_func( 'app:settings:plugin:ready' ),
      
      viewer: {
        ready: SPICEWORKS.ready_func('app:settings:plugin:viewer:ready')
      },
      
      editor: {
        ready: SPICEWORKS.ready_func( 'app:settings:plugin:editor:ready' ),

        // spec needs {label: '', code: ''}
        addCodeExample: function (spec) {
          codeExamples.push(spec);
        },
        
        getCodeExamples: function () {
          return codeExamples;
        },
        
        insertCode: function (code) {
          // IE doesn't like it when the pre tag doesn't have focus and you try to insert code.
          if (Browser.ie6 || Browser.ie7) {
            plugin_content.editor.body.getElementsByTagName('pre')[0].focus();
            code = code.gsub(/\n/, '\n\n'); // IE doesn't deal with new lines properly for some reason.
          }

          plugin_content.editor.insertCode(code);
          plugin_content.editor.syntaxHighlight();
        },
        
        setCode: function (code) {
          // IE doesn't like it when the pre tag doesn't have focus and you try to insert code.
          if (Browser.ie6 || Browser.ie7) {
            plugin_content.editor.body.getElementsByTagName('pre')[0].focus();
            code = code.gsub(/\n/, '\n\n'); // IE doesn't deal with new lines properly for some reason.
          }

          plugin_content.editor.setCode(code);
          plugin_content.editor.syntaxHighlight();          
        }
        
      }
    };
    
    
    // Now watch for this event and actually plug in the new code.
    that.editor.ready( function () {
      var elem = $('insert_code_list'),
          link, li;

      codeExamples.each( function (example) {
        li = new Element('li');
        link = new Element('a', {href: '#'} );
        li.insert(link);
        link.update( example.label );
        
        Event.observe( link, 'click', function (e) {
          Event.stop(e);
          that.editor.insertCode( example.code );
        });
        
        // elem.insert(li);
      });
    });
    
    return that;
  }();
  
})(SPICEWORKS);

(function (spiceworks) {
  
  // == SPICEWORKS.community ==
  // Namespace for several APIs to pull down community content as JSON
  spiceworks.community = function () {
    var that;
    var communityHost;
    function addDefaultParams(params, callback){
      return Object.extend(params, {
        'uuid':Application.uuid,
        'ehash':Application.ehash,
        'callback':callback
      });
    }
    that = {
      loadJSON: function(path, params, callback, identifier){
        var url = 'http://' + that.host + path;
        params = params || {};
        if (typeof callback === 'function'){
          spiceworks.utils.jsonp(url, addDefaultParams(params), callback, identifier);
        } else {
          spiceworks.utils.include(url + '?' + Object.toQueryString(addDefaultParams(params, callback)));
        }
      },
      setHost: function(host){
        that.host = host;
      },
      url: function(path){
        return 'http://' + this.host + path;
      },
      trackingUrl: function(path, source, campaign) {
        return 'http://' + this.host + this.trackingPath(path, source, campaign);
      },
      linkTo: function(path, source, campaign){
        path = this.trackingPath(path, source, campaign);
        
        return new Element('a', {
          'href': '/community/login?redirect=#{path}'.interpolate({'path': encodeURIComponent(path)}),
          'onclick': 'Community.go(#{path})'.interpolate({'path': path.toJSON()})
        });
      },
      trackingPath: function(path, source, campaign) {
        if (!source) { source = "app_ui"; }
        if (!campaign) { campaign = "app_general"; }
                
        var google = "utm_medium=#{medium}&utm_source=#{source}&utm_campaign=#{campaign}".interpolate({
          source: source,
          campaign: campaign,
          medium: "app"
        });
        
        path = path + (!path.match("\\?") ? "?" : "&") + google;
        
        return path;
      }
    };
    return that;
  }();
  
  // == SPICEWORKS.adserver ==
  // Namespace for reading/writing the adserver preferences and generating techlinks
  spiceworks.adserver = function () {
    var that;
    that = {
      setWebclipPath: function(path){
        that.webclipPath = path;
      },
      setNewHost: function(url){
        that.newHost = url;
      },
      setOldHost: function(url){
        that.oldHost = url;
      },
      webclipUrl: function(){
        return 'http://' + that.newHost + that.webclipPath;
      },
      buildTechLink: function(element, zone, position, parms){
        var identifier = 'tech-link-' + (new Date()).getTime(), insertion = {};
        if (typeof position == 'undefined') position = 'bottom';
        insertion[position] = '<div id="' + identifier + '" class="tech-link"></div>';
        element.insert(insertion);

        // this code should be self-contained instead of calling into code in application.js
        techLink.builder(identifier, zone, that.webclipUrl(), parms);
      }
    };
    return that;
  }();
  
  
  // == SPICEWORKS.community.products ==
  // Get product information and community content by sending up model and manufacturer
  spiceworks.community.products = function () {
    var that;
    that = {
      getProductData: function(params, callback){
        spiceworks.community.loadJSON('/api/products/product_data.json', params, callback);
      },
      getRecentRatings: function(params, callback, identifier){
        if(identifier){
          identifier += '_getRecentRatings';
        }
        spiceworks.community.loadJSON('/api/products/recent_ratings.json', params, callback, identifier);
      }
    };
    return that;
  }();

  // == SPICEWORKS.community.forums ==
  // Get discussions from forums
  spiceworks.community.forums = function () {
    var that;
    that = {
      find: function(id, params, callback, identifier){
        if(identifier){
          identifier += '_community.forums.find/' + id;
        }
        spiceworks.community.loadJSON('/api/forums/' + id + '.json', params, callback, identifier);
      }
    };
    return that;
  }();

  // == SPICEWORKS.community.itNews ==
  // Get the latest links posted to IT News & Information
  spiceworks.community.itNews = function () {
    var that;
    that = {
      find: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.itNews.find';
        }
        spiceworks.community.loadJSON('/api/it_news.json', params, callback, identifier);
      }
    };
    return that;
  }();
  
  // == SPICEWORKS.community.groups ==
  // Get discussions, how-tos, spicelists, etc. from groups
  // id may be an integer or a case-insensitive name
  spiceworks.community.groups = function () {
    var that;
    that = {
      find: function(id, params, callback, identifier){
        if(identifier){
          identifier += '_community.groups.find/' + id;
        }
        spiceworks.community.loadJSON('/api/groups/' + id + '.json', params, callback, identifier);
      }
    };
    return that;
  }();
  
  // == SPICEWORKS.community.plugins ==
  // Get information about plugins that enhance your Spiceworks application
  spiceworks.community.plugins = function () {
    var that;
    that = {
      find: function(guid, params, callback, identifier){
        if(identifier){
          identifier += '_community.plugins.find/' + guid;
        }
        spiceworks.community.loadJSON('/api/plugins/show/' + guid + '.json', params, callback, identifier);
      },
      recent: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.plugins.recent';
        }
        spiceworks.community.loadJSON('/api/plugins/recent.json', params, callback, identifier);
      },
      topRated: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.plugins.topRated';
        }
        spiceworks.community.loadJSON('/api/plugins/top_rated.json', params, callback, identifier);
      },
      topDownloaded: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.plugins.topDownloaded';
        }
        spiceworks.community.loadJSON('/api/plugins/top_downloaded.json', params, callback, identifier);
      },
      featured: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.plugins.featured';
        }
        spiceworks.community.loadJSON('/api/plugins/featured.json', params, callback, identifier);
      },
      my: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.plugins.my';
        }
        spiceworks.community.loadJSON('/api/plugins/user.json', params, callback, identifier);
      }
    };
    return that;
  }();

  // == SPICEWORKS.community.languagePacks ==
  // Get information about language packs for translating the Spiceworks interface.
  spiceworks.community.languagePacks = function () {
    var that;
    that = {
      find: function(guid, params, callback, identifier){
        if(identifier){
          identifier += '_community.languagePacks.find/' + guid;
        }
        spiceworks.community.loadJSON('/api/language_packs/show/' + guid + '.json', params, callback, identifier);
      },
      recent: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.languagePacks.recent';
        }
        spiceworks.community.loadJSON('/api/language_packs/recent.json', params, callback, identifier);
      },
      topRated: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.languagePacks.topRated';
        }
        spiceworks.community.loadJSON('/api/language_packs/top_rated.json', params, callback, identifier);
      },
      topDownloaded: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.languagePacks.topDownloaded';
        }
        spiceworks.community.loadJSON('/api/language_packs/top_downloaded.json', params, callback, identifier);
      },
      featured: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.languagePacks.featured';
        }
        spiceworks.community.loadJSON('/api/language_packs/featured.json', params, callback, identifier);
      },
      my: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.languagePacks.my';
        }
        spiceworks.community.loadJSON('/api/language_packs/user.json', params, callback, identifier);
      }
    };
    return that;
  }();
  
  // == SPICEWORKS.community.messages ==
  // Retrieve information about a user's private messages from the community.
  spiceworks.community.messages = function () {
    var that;
    that = {
      inbox: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.messages.inbox';
        }
        spiceworks.community.loadJSON('/api/messages/inbox.json', params, callback, identifier);
      },
      unreadCount: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.messages.unreadCount';
        }
        spiceworks.community.loadJSON('/api/messages/unread_count.json', params, callback, identifier);
      }
    };
    return that;
  }();
  
  // == SPICEWORKS.community.reports ==
  // Get information about shared reports from the Spiceworks Community.
  spiceworks.community.reports = function () {
    var that;
    that = {
      find: function(id, params, callback, identifier){
        if(identifier){
          identifier += '_community.reports.find/' + id;
        }
        spiceworks.community.loadJSON('/api/reports/show/' + id + '.json', params, callback, identifier);
      },
      recent: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.reports.recent';
        }
        spiceworks.community.loadJSON('/api/reports/recent.json', params, callback, identifier);
      },
      topRated: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.reports.topRated';
        }
        spiceworks.community.loadJSON('/api/reports/top_rated.json', params, callback, identifier);
      },
      topDownloaded: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.reports.topDownloaded';
        }
        spiceworks.community.loadJSON('/api/reports/top_downloaded.json', params, callback, identifier);
      },
      featured: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.reports.featured';
        }
        spiceworks.community.loadJSON('/api/reports/featured.json', params, callback, identifier);
      },
      my: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.reports.my';
        }
        spiceworks.community.loadJSON('/api/reports/user.json', params, callback, identifier);
      },
      // type should be one of: device, ticket, software, it_service, sql
      type: function(type, params, callback, identifier){
        if(identifier){
          identifier += '_community.reports.type/' + type;
        }
        spiceworks.community.loadJSON('/api/reports/type/' + type + '.json', params, callback, identifier);
      }
    };
    return that;
  }();

  // == SPICEWORKS.community.search ==
  // Use the community's search engine to find community content
  spiceworks.community.search = function (query, params, callback, identifier) {
    params.query = query;
    if(identifier){
      identifier += '_community.search';
    }
    spiceworks.community.loadJSON('/api/search.json', params, callback, identifier);
  };
  
  // == SPICEWORKS.community.activity ==
  // Get a personalized live activity stream from the Spiceworks Community.
  spiceworks.community.activity = function () {
    var that;
    that = {
      user: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.activity.user';
        }
        spiceworks.community.loadJSON('/api/activity/user.json', params, callback, identifier);
      },
      userTimestamps: function(params, callback, identifier){
        if(identifier){
          identifier += '_community.activity.userTimestamps';
        }
        spiceworks.community.loadJSON('/api/activity/user_timestamps.json', params, callback, identifier);
      }
    };
    return that;
  }();
  
  
})(SPICEWORKS);


(function (spiceworks) {
  
  // == SPICEWORKS.configuration ==
  // Store documents (hashes) in a specified location for later retrieval.
  // Currently, just a key/value store.  Configuration can be a hash, an array, or just a string.
  spiceworks.configuration = function () {
    // helper method to process results from calls
    function wrap(callback) {
      if (callback) {
        return function (transport) {
          var result = transport.responseText;
          if (result.isJSON()) {
            result = result.evalJSON();
          }
          callback(result, transport);
        };        
      } else {
        return function () {};
      }
    }
    
    var that = {      
      // callback is optional
      store: function (key, doc, callback) {
        (new Ajax.Request('/configuration/' + key + '.json', {
          method: 'put',
          parameters: { 'value': Object.toJSON(doc) },
          onSuccess: wrap(callback)
        }));
      },
            
      load: function (key, callback, defaultValue) {
        (new Ajax.Request('/configuration/' + key + '.json', {
          method: 'get',
          onSuccess: wrap(callback),
          onFailure: function (transport) {
            callback(defaultValue,transport);
          }
        }));
      },
      
      loadAll: function (prefix, callback, defaultValue) {
        if (prefix && prefix !== '') {
          (new Ajax.Request('/configuration.json?prefix=' + prefix, {
            method: 'get',
            onSuccess: wrap(callback),
            onFailure: function (transport) {
              callback(defaultValue,transport);
            }
          }));          
        }
      },
      
      remove: function (key, callback) {
        (new Ajax.Request('/configuration/' + key + '.json', {
          method: 'delete',
          onSuccess: wrap(callback),
          onFailure: wrap(callback)
        }));
      }
    };
    
    return that;
  }();
  
})(SPICEWORKS);

(function(spiceworks){
  // == Stats Namespace ==
  // API for fetching the country code for the installation
  spiceworks.country = function(){
    var code;
    var that = {
      setCode: function(storedCode){
        code = storedCode;
      },
      code: function(){
        return code;
      }
    };
    
    return that;
  }();
  
})(SPICEWORKS);

(function (spiceworks) {
  

  // == SPICEWORKS.data ==
  spiceworks.data = function () {
    var tempStore = {},
        that;
    
    that = {
      
      // Models
      // This is very alpha right now.  Check here for status updates as we fix this.
      // See Jester Docs for more detail on how to use these: http://thoughtbot.com/projects/jester
      Ticket:       Jester.Resource.model('Ticket', {prefix: '/api', format: 'json'}),
      Device:       Jester.Resource.model('Device', {prefix: '/api', format: 'json'}),
      Alert:        Jester.Resource.model('Alert', {prefix: '/api', format:'json'}),
      DataMonitor:  Jester.Resource.model('DataMonitor', {prefix: '/api', format: 'json'}),
      Activity:     Jester.Resource.model('Activity', {format: 'json'}),
      Agreement:    Jester.Resource.model('Agreement', {format: 'json'}),

      // Following need more testing
      Group:        Jester.Resource.model('Group', {prefix: '/api', format:'json'}),
      Software:     Jester.Resource.model('Software', {prefix: '/api', format:'json', plural:'software'}),
      Service:      Jester.Resource.model('Service', {prefix: '/api', format:'json', plural:'services'}),
      Hotfix:       Jester.Resource.model('Hotfix', {prefix: '/api', format:'json', plural:'hotfixes'}),
      User:         Jester.Resource.model('User', {prefix: '/api', format:'json' }),
      Report:       Jester.Resource.model('Report', {prefix: '/api', format:'json' }),
      Disk:         Jester.Resource.model('Disk', {prefix: '/api', format:'json' }),      

      // Run a report and pass the resulting JSON to the callback...
      // callback => function(json, transport){}
      // TODO: get report name working as well as id
      runReport: function (report_id, callback) {
        (new Ajax.Request('/reports/show/' + report_id + '.json', {
          method: 'get',
          onSuccess: function (transport) {
            var json = transport.responseText.evalJSON();
            callback(json, transport);
          },
          onFailure: function (transport) {
            callback({ items:[], columns: [], error: transport.responseText });
          }
        }));
      }
    };

    return that;
  }();
  
})(SPICEWORKS);


// == SPICEWORKS.field ==
// Datatypes that will be used to render editors
// These objects are delegates that will handle rendering different types of values in the configuration sections of widgets
// Field builders are expected to return an object with a set() and get() method.  They can return more than this, but these two
// are required for them to work with widgets and the dashboard.
(function (spiceworks) {
  
  var fieldTypes = {};
  
  var ops = {
    beginsWith: 'begins with',
    notBeginsWith: 'does not begin with',
    endsWith: 'ends with',
    notEndsWith: 'does not end with',
    contains: 'contains',
    notContains: 'does not contain',
    between: 'is between',
    is: 'is',
    isNot: 'is not',
    isBefore: 'is before',
    isOnOrAfter: 'is on or after',
    isLessThan: 'is less than',
    isAtLeast: 'is at least',
    include: 'include',
    doesNotInclude: 'does not include',
    isAtMost: 'is at most',
    isGreaterThan: 'is greater than'
  };
  
  
  // Register a new type of field
  // spec:{
  //   typeName: 'string', // optionally, this can be an array!
  //   buildEditor: function (element, fieldDef) {},
  // }
  function registerFieldType(spec) {
    if (spec.typeName.each) {
      spec.typeName.each(function (name) {
        fieldTypes[name] = spec;
      });
    } else {        
      fieldTypes[spec.typeName] = spec;
    }
  }
  
  function getFieldType(typeName) {
    return fieldTypes[typeName] || fieldTypes.string;
  }

  // creation helper
  // fieldDesc => { name:'foo', type:'string', label:'Foo', defaultValue:'Life is good' }
  function createField(element, fieldDef, value) {
    var fieldType = getFieldType(fieldDef.type);
    var field = fieldType.buildEditor(element, fieldDef);
    if (value) {
      field.set(value);
    }
    return field;
  }

  // Create a set of fields within the element passed using the options passed.
  // options should be like widget options (including an id)
  function createFieldSet(element, fieldDefs) {
    var fields = {},
        nsField = spiceworks.field,  // temp namespace
        fieldset;
        
    fieldset = {
      // return all the values
      get: function () {
        var options = {};
        Object.keys(fields).each(function (fieldname) {
          options[fieldname] = fields[fieldname].get();
        });
        return options;
      },
      
      set: function (options) {
        Object.keys(fields).each(function (fieldname) {
          if (options[fieldname] !== undefined) {
            fields[fieldname].set(options[fieldname]);
          }
        });
      },
      fields: fields
    };
    
    // Initialize the fields
    fieldDefs.each(function (field) {
      if (field.name === undefined) {
        field.name = "n_" + (Math.floor(Math.random()*1000+1));
      }
      var elemType = (field['class'] && field['class'].match(/inline/)) ? 'span' : 'div';
      var fieldElement = new Element(elemType, {'class': field.name + ' setting ' + field['class'], 'id':field.name + "_" + (Math.floor(Math.random()*1000+1))});
      element.insert(fieldElement);
      fields[field.name] = nsField.createField(fieldElement, field);
      //if( options[field.name] ){  fields[field.name].set( options[field.name] ); }
    });
    
    return fieldset;
  }
  
  ///////////////////////////////////////////////////////
  // Define SPICEWORKS.field
  spiceworks.field = {
    registerFieldType: registerFieldType,
    getFieldType: getFieldType,
    createField: createField,
    createFieldSet:createFieldSet
  };
  


  /////////////////////////////////////////////////////
  // HIDDEN
  // Hidden value, accessible by Spiceworks on the backend, but not visible to the end user.
  registerFieldType({
    typeName: 'hidden',
    operators: [], // no operators allowed on hidden fields, doesn't make sense.
    buildEditor: function (element, fieldDef) {
      var hiddenValue = fieldDef.defaultValue;
      return {
        get: function () {
          return hiddenValue;
        },
        set: function (value) {
          hiddenValue = value;
        }
      };
    }
  });

  // STRING
  // Simple string type
  registerFieldType({
    typeName: ['string', 'textbox', 'text'],
    operators: [ops.is, ops.isNot, ops.beginsWith, ops.notBeginsWith, ops.endsWith, ops.notEndsWith, ops.contains, ops.notContains],
    buildEditor: function (element, fieldDef) {
      var id = element.id + "_value";
      if (fieldDef.label) {        
        element.insert("<label for='" + id + "'>" + fieldDef.label + "</label>");
      }
      element.insert("<input id='" + id + "' name='" + id + "' type='text' class='text'></input>");
      if (fieldDef.example) {
        element.insert(" <span class='example'>" + fieldDef.example + "</span>");
      }

      function set (value) {
        if (fieldDef.obKey) {
          element.down('input').value = value[fieldDef.obKey];
        } else {
          element.down('input').value = value || "";
        }
      }
      set( fieldDef.defaultValue );

      return {
        get: function () {
          var ob = {};
          if (fieldDef.obKey) {
            ob[fieldDef.obKey] = element.down('input').value;
            return ob;
          } else {
            return element.down('input').value;
          }
        },
        set: set
      };
    }
  });
  
  // INTEGER
  // Simple integer type... just like string, but with different options (and should have some numeric validation at some point.)
  registerFieldType({
    typeName: ['integer', 'int', 'float'],
    operators: [ops.is, ops.isNot, ops.isLessThan, ops.isGreaterThan, ops.isAtMost, ops.isAtLeast],
    buildEditor: function (element, fieldDef) {
      var id = element.id + "_value";
      if (fieldDef.label) {        
        element.insert("<label for='" + id + "'>" + fieldDef.label + "</label>");
      }
      element.insert("<input id='" + id + "' name='" + id + "' type='text' class='text'></input>");
      if (fieldDef.example) {
        element.insert(" <span class='example'>" + fieldDef.example + "</span>");
      }

      function set (value) {
        if (fieldDef.obKey) {
          element.down('input').value = value[fieldDef.obKey];
        } else {
          element.down('input').value = value || "";
        }
      }
      set( fieldDef.defaultValue );

      return {
        get: function () {
          var ob = {};
          if (fieldDef.obKey) {
            ob[fieldDef.obKey] = (parseInt(element.down('input').value, 10) || 0);
            return ob;
          } else {
            return (parseInt(element.down('input').value, 10) || 0);
          }
        },
        set: set
      };
    }
  });
  
  // PASSWORD
  registerFieldType({
    typeName: 'password',
    operators: [ops.is, ops.isNot],
    buildEditor: function(element, fieldDef){
      var id = element.id + "_value";
      element.insert("<label for='" + id + "'>" + fieldDef.label + "</label>");
      element.insert("<input id='" + id + "' name='" + id + "' type='password' class='text'></input>");
      if (fieldDef.example) {
        element.insert(" <span class='example'>" + fieldDef.example + "</span>");
      }
      return {
        get: function () {
          return element.down('input').value;
        },
        set: function (value) {
          // a password field is never to be assigned a value, always set to blank
          element.down('input').value = value;
        }
      };
    }
  });
  
  // ENUMERATION/SELECT
  // onChange = event for users changing this select field
  // onUpdate = event for API changing this select field
  registerFieldType({
    typeName:['enumeration', 'select', 'enum'],
    operators:[ops.is, ops.isNot],
    buildEditor: function (element, fieldDef) {
      var select, 
          id = element.id + "_value";

      if (fieldDef.label) {        
        element.insert("<label for='" + id + "'>" + fieldDef.label + "</label>");
      }

      select = new Element('select', {'id': id, 'name': id, 'class':fieldDef['class']});
      element.insert(select);

      if (fieldDef.options) {
        fieldDef.options.each( function (option) {
          if(typeof(option)==='string'){
            select.insert(new Element('option', {selected:(fieldDef['defaultValue'] == option)}).update(option));
          }else{
            select.insert(new Element('option', {'value':option[1], selected:(fieldDef['defaultValue'] == option[1])}).update(option[0]));
          }
        });
      }
      
      if (fieldDef.onChange) {
        select.observe('change', fieldDef.onChange);
      }
      if (fieldDef.onUpdate){
        select.observe('field:update', fieldDef.onUpdate);
      }

      if (fieldDef.example) {
        element.insert(" <span class='example'>" + fieldDef.example + "</span>");
      }

      return {
        get: function () {
          return $F(select);
        },
        set: function (value) {
          for(index = 0; index < select.options.length; index++) {
            if ((select.options[index].value || select.options[index].text) === value) {
              select.selectedIndex = index;
            }
          }
          Event.fire(select, 'field:update', value);
        }
      };
    }
  });
  
  // TEXTAREA
  registerFieldType({
    typeName:'textarea',
    operators: [ops.is, ops.isNot, ops.beginsWith, ops.notBeginsWith, ops.endsWith, ops.notEndsWith, ops.contains, ops.notContains],
    buildEditor: function (element, fieldDef) {
      var textarea,
          id = element.id + "_value";

      if (fieldDef.label) {        
        element.insert("<label for='" + id + "'>" + fieldDef.label + "</label>");
      }

      if (fieldDef.example) {
        element.insert("<div style='float:right;width: 210px'><span  class='example' style='vertical-align:top'>" + fieldDef.example + "</span></div>");
      }
      textarea = new Element('textarea', {'id': id, 'name': id, 'style':''});
      element.insert(textarea);

      return {
        get: function () {
          return textarea.value;
        },
        set: function (value) {
          textarea.value = value;
        }
      };
    }
  });


  // CHECKBOX
  registerFieldType({
    typeName:'checkbox',
    operators: [ops.is, ops.isNot],
    buildEditor: function (element, fieldDef) {
      var checkbox,
          id = element.id + "_value";
      if (fieldDef.label) {        
        element.insert("<label for='" + id + "'>" + fieldDef.label + "</label>");
      }

      checkbox = new Element('input', {'id':id, 'type':'checkbox'});
      if (fieldDef['defaultValue']) checkbox.checked = true;
      element.insert(checkbox);

      if (fieldDef.example) {
        element.insert(" <span class='example'>" + fieldDef.example + "</span>");
      }

      return {
        get: function () {
          return checkbox.checked;
        },
        set: function(value) {
          checkbox.checked = value;
        }
      };
    }
  });

  // SET (checkbox group)
  registerFieldType({
    typeName: 'set',
    operators: [ops.is, ops.isNot],
    buildEditor: function(element, fieldDef) {
      var id = element.id + "_value";
      if (fieldDef.label) {        
        element.insert("<label for='" + id + "'>" + fieldDef.label + "</label>");
      }
      var div = new Element('div');
      var boxes = $A([]);
      if (fieldDef.options) {
        fieldDef.options.each( function (option) {
          if(typeof(option)==='string'){
            var cb = new Element('input',
                {type: 'checkbox', name:fieldDef['name'], value:option, defaultChecked: fieldDef['defaultValue'].member(option), checked: fieldDef['defaultValue'].member(option)});
            boxes.push(cb);
            div.insert(cb);
            div.insert(option);
            div.insert('&nbsp;');
          }else{
            var cb = new Element('input',
                {type: 'checkbox', name:fieldDef['name'], value:option[1], defaultChecked: fieldDef['defaultValue'].member(option[1]), checked: fieldDef['defaultValue'].member(option[1])});
            boxes.push(cb);
            div.insert(cb);
            div.insert(option[0]);
            div.insert('&nbsp;');
          }
        });
      }
      element.insert(div);
      return {
        get: function () {
          var checked = $A([]);
          boxes.each(function (box) {
            if (box.checked) {
              checked.push(box.value);
            }
          });
          return checked;
        },
        set: function (value) {
          boxes.each(function (box) {
            box.checked = false;
            value.each(function (v) {
              if (box.value == v) {
                box.checked = 'checked';
              }
            });
          });
        }
      };
    }
  });

  // DATE
  registerFieldType({
    typeName:['date','datetime'],
    operators:[ops.is, ops.isNot, ops.isBefore, ops.isOnOrAfter], // figure out ops.isBetween
    buildEditor: function (element, fieldDef) {
      var trigger, input,  id = element.id + "_value";
      if (fieldDef.label) {        
        element.insert("<label for='" + id + "'>" + fieldDef.label + "</label>");
      }

      input = new Element('input', {'id':id, 'name':id, 'type':'string', 'class':'text date_field', 'title':fieldDef.title});
      element.insert(input);

      trigger = new Element('a', {'id': id + '_trigger', 'href':'javascript:void(0)', 'class':'calendar_trigger'} ).update('<img title="Choose a date from the calendar" src="/images/icons/calendar.png" alt="Calendar"/>');
      element.insert(trigger);

      CalendarPopup.setup(input, trigger);

      if (fieldDef.example) {
        element.insert(" <span class='example'>" + fieldDef.example + "</span>");
      }

      return {
        get: function () {
          return input.value;
        },
        set: function (value) {
          input.value = value.gsub('/','-');
        }
      };
    }
  });
  
  ////////////////////////////////////////////
  // ARRAY OF
  // fieldDef: {name:"match", label:"Match Any",  type:"arrayOf", defaultValue:[{"field":"summary", "valuere":"text"}], fieldDefs:[
  //   {name:"field", type: "select", options: SPICEWORKS.pluginEditor.ticketFields, defaultValue:'summary', 'class':'inline-field'},
  //   {name:"valuere", type:"text", defaultValue:"example", 'class':'inline-field'}
  // ]}
  registerFieldType({
    typeName:'arrayOf',
    operators:[], // no operators allowed on arrays for now
    buildEditor: function (element, fieldDef) {
      var id = element.id + "_value";
      var fields;
      var single = (fieldDef.fieldDefs.length == 1);

      if (fieldDef.label) {
        element.insert("<label for='" + id + "'>" + fieldDef.label + "</label>");    
      }

      var ul = new Element("ul", {"class":"array-of-fields " + fieldDef.ulClass});
      element.insert(ul);
      element.insert("<div style='clear:both'></div>");

      function set(arr) {
        ul.update("");
        fields = [];
        if (arr) {
          arr.each(addRow);
        }
        showHideRemove();
      }

      function showHideRemove() {
        if (fields === undefined || fields.length === 1) {
          ul.select(".remove").each( function (remove) {
            remove.addClassName('disabled');
          });
        }else{
          ul.select(".remove").each( function (remove) {
            remove.removeClassName('disabled');
          });
        }
      }

      function addRow(value, i, afterLi) {
        var li = new Element("li", {"class": fieldDef.liClass, "style":(!value ? "display:none" : "")});
        if (afterLi === undefined) {
          ul.insert(li);
        }else{
          afterLi.insert({after:li});
        }
        var fieldsInstance = SPICEWORKS.field.createFieldSet(li, fieldDef.fieldDefs);
        if (value){
          if (single) {
            var wrapper = {};
            wrapper[fieldDef.fieldDefs[0].name] = value;
            fieldsInstance.set(wrapper);
          } else {
            fieldsInstance.set(value);
          }
        } else {
          new Effect.BlindDown(li, {duration:0.25});
        }
        fields.push(fieldsInstance);

        //add remove links
        var adder = spiceworks.utils.quickLink({
          label: '+',
          title: 'Add a row',
          'class': 'sui-button small array-of-control add',
          onClick: function () {
            addRow(null, null, li);
            showHideRemove();
          }
        });

        var remover = spiceworks.utils.quickLink({
          label: '&ndash;',
          title: 'Remove this row',
          'class': 'sui-button small array-of-control remove',
          onClick: function (event) {
            new Effect.BlindUp(li, {
              duration:0.25, 
              afterFinish: function () {
                li.remove();
                fields = fields.without(fieldsInstance);
                showHideRemove();
              }
            });
          }
        });

        li.insert(remover).insert(adder);
      }

      if (fieldDef.defaultValue) {
        set(fieldDef.defaultValue);
      }
      
      return {
        get: function () {
          return fields.collect(function (field) {
            if (single) {
              return field.get()[fieldDef.fieldDefs[0].name];
            } else {
              return field.get();
            }
          });
        },
        set: set
      };
    }
  });
  
  ////////////////////////////////////////////////
  // FIELD FILTER
  // Allow for the selection of a field, an operator, and a value!
  // A single field is selected from the available fieldDefs.  Baded on it's type, the operators and options are laid out.
  // 
  // fieldDef: {name:'foo', type:'fieldFilter', defaultValue:{fieldName:'summary', operator:'is', value:'bar'}, fieldDefs:[
  //   {name:'summary', type:'string'},
  //   ...
  // ]}
  registerFieldType({
    typeName: 'fieldFilter',
    buildEditor:function (element, fieldDef) {
      
      var fieldSelect,
          operatorSelect,
          valueField,
          fieldNames,
          defs,
          def,
          opsAndValue;
          
      defs = fieldDef.fieldDefs;
      opsAndValue = new Element('span');
      
      fieldNames = defs.collect(function (fieldDef) {
        return [fieldDef.label||fieldDef.name, fieldDef.name];
      });
      
      if (fieldDef.label) {        
        element.insert("<label>" + fieldDef.label + "</label>");
      }
      
      function get() {
        return {key: fieldSelect.get(), operator:operatorSelect.get(), value: valueField.get()};
      }
      
      function set(value) {
        fieldSelect.set(value.key);
        fieldChanged();
        operatorSelect.set(value.operator);
        valueField.set(value.value);
      }
      
      function fieldChanged(event) {
        if (event) {
          Event.stop(event);
        }
        
        var fieldName = fieldSelect.get();
        
        def = defs.find(function (d) {
          return d.name == fieldName;
        });
        var fieldType = getFieldType(def.type);
        
        opsAndValue.update(''); // clean these out since we're going to rebuild
        operatorSelect = null;
        value = null;
        operatorSelect = getFieldType('select').buildEditor(opsAndValue, {
          name:'operator', type:'select', 'class':'operator', options: (def.operators || fieldType.operators)
        });
        
        valueField = fieldType.buildEditor(opsAndValue, {name:def.name, type:def.type, options:def.options, title: (def.type.match(/date/) ? 'EX: "2007-12-31", "this friday", "3 days from now"' : null )});
      }
      
      
      fieldSelect = getFieldType('select').buildEditor(element, {
        name:'key', type:'select', options:fieldNames, onChange: fieldChanged
      });
      element.insert(opsAndValue);
      
      if (fieldDef.defaultValue) {
        set(fieldDef.defaultValue);
      }
      
      return {
        get:get,
        set:set
      };
    }
  });
  
  /////////////////////////////////
  // Puts an additional option in the label area... scopes as an object
  // fieldDef:{ name:'match', label:'Match', labelFieldDefs:[{name:'foo', 'class':'inline'}], fieldDefs:[{name:'bar'}, {...}] }
  // get/set: {match: {foo:'value', bar:'value, ...}}
  registerFieldType({
    typeName:'group',
    buildEditor:function (element, fieldDef) {
      
      var label, labelFields, fields, fieldsDiv;
      
      
      if (fieldDef.label || fieldDef.labelFieldDefs) {
        label = new Element('label');
        label.update(fieldDef.label);
        element.insert(label);
        labelFields = SPICEWORKS.field.createFieldSet(label, fieldDef.labelFieldDefs);
      } 

      fieldsDiv = new Element('div', {'class':fieldDef['class']});
      element.insert(fieldsDiv);
      
      fields = SPICEWORKS.field.createFieldSet(fieldsDiv, fieldDef.fieldDefs);
      
      function get() {
        return Object.extend(labelFields.get(), fields.get());
      }
      
      function set(value) {
        labelFields.set(value);
        fields.set(value);
      }
      
      if (fieldDef.defaultValue) {
        set(fieldDef.defaultValue);
      }
      
      return {
        get:get,
        set:set
      };
    }
  });
  

})(SPICEWORKS);


(function (spiceworks) {
  
  // == SPICEWORKS.persistence ==
  // Store documents (hashes) in a specified location for later retrieval.
  // Currently, just a key/value store.  Document can be a hash, an array, or just a string.
  spiceworks.persistence = function () {
    // helper method to process results from calls
    function wrap(callback) {
      if (callback) {
        return function (transport) {
          var result = transport.responseText;
          if (result.isJSON()) {
            result = result.evalJSON();
          }
          callback(result, transport);
        };        
      } else {
        return function () {};
      }
    }
    
    var that = {      
      // callback is optional
      store: function (key, doc, callback) {
        (new Ajax.Request('/documents/' + key + '.json', {
          method: 'put',
          parameters: { 'doc': Object.toJSON(doc) },
          onSuccess: wrap(callback)
        }));
      },
            
      load: function (key, callback, defaultValue) {
        (new Ajax.Request('/documents/' + key + '.json', {
          method: 'get',
          onSuccess: wrap(callback),
          onFailure: function (transport) {
            callback(defaultValue,transport);
          }
        }));
      },
      
      loadAll: function (prefix, callback, defaultValue) {
        if (prefix && prefix !== '') {
          (new Ajax.Request('/documents.json?prefix=' + prefix, {
            method: 'get',
            onSuccess: wrap(callback),
            onFailure: function (transport) {
              callback(defaultValue,transport);
            }
          }));          
        }
      },
      
      remove: function (key, callback) {
        (new Ajax.Request('/documents/' + key + '.json', {
          method: 'delete',
          onSuccess: wrap(callback)
        }));
      }
    };
    
    return that;
  }();
  
  
})(SPICEWORKS);

(function (spiceworks) {
  
  // == Plugins Namespace ==
  // Manages the spiceworks plugins.
  spiceworks.plugin = function () {
    var plugins = [],
        that;
    that = {
      add: function (plugin) {
        /* Verify Attributes */
        if (!plugin.name) { 
          throw "Plugin name is required"; 
        }
        if (!plugin.version) { 
          throw "Plugin version is required"; 
        }
        if (!plugin.initialize) { 
          throw "Plugin initialize function is required"; 
        }

        /* A method for the plugin writer to define user settings*/
        plugin.configure = function (conf) {
          if ( conf.onSave ) {
            plugin.onSave = conf.onSave;
          }
          plugin.settingDefinitions = conf.settingDefinitions;
          if (!plugin.settings) {
            plugin.settings = {};
          }
          plugin.settingDefinitions.each( function (settingDefinition) {
            if (!plugin.settings[settingDefinition.name]){
              plugin.settings[settingDefinition.name] = settingDefinition.defaultValue;
            }
          });
        };
        
        /* A helper method for storing data for this plugin */
        plugin.store = function (key, data, callback) {
          spiceworks.persistence.store(plugin.guid + '_' + key, data, callback);
        };
        /* A helper method for loading data for this plugin*/
        plugin.load = function (key, callback, defaultValue) {
          spiceworks.persistence.load(plugin.guid + '_' + key, callback, defaultValue);
        };
        /* A helper method for removing data from the system*/
        plugin.remove = function (key, callback) {
          spiceworks.persistence.remove(plugin.guid + '_' + key, callback);
        };
        /* A method to store the settings for this plugin.  Settings are loaded by default. */
        plugin.storeSettings = function (settings, callback) {
          plugin.store( 'settings', settings, callback );
          plugin.settings = settings;
        };

        /* return an object with URL info for the file passed */
        plugin.contentUrl = function(name) {
          return '/settings/plugins/' + encodeURIComponent(plugin.guid) + "/content/" + encodeURIComponent(name);
        };
        
        /* return the content from a plugin content area */
        plugin.getContent = function(name,callback) {
            spiceworks.utils.getURLContent(plugin.contentUrl(name), callback);
        };
       
        /* do some HTML templating with content */
        plugin.renderHtmlTemplate = function(name,map,callback) {
           plugin.getContent(name, function(content) {
                var plate = new Template(content);
                callback( plate.evaluate(map) );
           }); 
        };

        /* Include all of the stylesheets */
        plugin.includeStyles = function () {
          plugin.contentAreas.each( function (content) {
            if (content.content_type == 'text/css') {
              spiceworks.utils.includeStyle(plugin.contentUrl(content.content_name));    
            }
          });
        };

        // bridge to the SPICEWORKS.stats API for recording plugin usages
        plugin.stats = function(){
          // use the plugin instance info to generate a unique stat string
          function statString(suffix){ return plugin.guid + '_' + plugin.version + '_' + suffix; };
          return {
            // we're using SPICEWORKS.stats#delayedRecord here to defer stats that might be recorded on page load
            // impression is assumed to be a "plugin viewed" sort of usage, which is different than plugin loaded, since a plugin can be loaded but not neccessarily modify the page
            impression: function(){ spiceworks.stats.delayedRecord(statString('impression')); },
            // record a generic stat for a plugin
            record: function(actionName){ spiceworks.stats.delayedRecord(statString(actionName)); }
          };
        }();

        /* Capture the plugin and return a reference to it. */
        plugins.push(plugin);
        return plugin;
      },

      initializePlugins: function () {
        plugins.each(function (plugin) {
          try {
            if(!plugin.hasBeenInitialized) {
              plugin.initialize(plugin);
              plugin.hasBeenInitialized = true;
              // Fire off an event to tell the world that we've initialized.
              spiceworks.fire('plugin:initialized', {immediate: true, pluginName:plugin.name});
            }
            
            // give any plugins the chance to record their impressions, this delay is a bit arbitrary but it's nice to wait rather than kick off an ajax load immediately
            spiceworks.stats.flushQueue.delay(1);
          } catch (e) {
            // there was an error initializing a plugin...
            // Ignoring this for now.
          }
        });
        spiceworks.fire('plugins:initialized', {immediate: true});
      },

      getPlugins: function () {
        return plugins;
      },
      
      getPlugin: function (guid) {
        var plugin = plugins.find(function (plugin) { 
          return (plugin.guid === guid); 
        });
        return plugin;
      }
    };

    return that;
  }();

  Event.observe(document, 'dom:loaded', function (e) {    
    spiceworks.plugin.initializePlugins();
    spiceworks.fire('ready');
  });
  
})(SPICEWORKS);


(function (spiceworks) {
  // == SPICEWORKS.portal ==
  spiceworks.portal = function () {
    var that = {
      ready: function (callback) {
        spiceworks.observe('portal:ready', callback);
      },
      
      isShowing: function () {
        return $(document.body).hasClassName('spiceworks_portal');
      }
    };
    return that;  
  }();
  
  // == SPICEWORKS.portalv2 ==
  spiceworks.portalv2 = function () {
    var that = {
      ready: function (callback) {
        spiceworks.observe('portalv2:ready', callback);
      },
      
      isShowing: function () {
        return $(document.body).hasClassName('spiceworks-portalv2');
      }
    };
    return that;  
  }();
})(SPICEWORKS);

(function(spiceworks){
  // == Stats Namespace ==
  // API for recording plugin stats
  spiceworks.stats = function(){
    // statsQueue is used to record page loads stats, so that we can build up a collection of stats to send to the server all at once
    // this is to reduce stress on the server
    // flushed is used to track if the statPayload queue has been sent or not
    var statsQueue = $A(), flushed = false;
    function prefixStat(stat){ return stat; };
    function save(stats){
      // send one or more stats to the server to be saved
      stats = $A([stats]).flatten();
      if (stats.size() > 0) new Ajax.Request('/utility/record_stats', {method:'post', parameters:'stats[]=' + stats.join('&stats[]=')});
    };
    var that = {
      // delayedRecord is called from a plugin instance at SPICEWORKS.plugin.add.stats#impression
      // it is assumed that impressions are tracked on page load and recording them should be deferred if possible
      delayedRecord: function(stat, options){
        // if the stat queue has already been sent, just immediately send this impression, otherwise push the stat onto the queue
        if (flushed) save(prefixStat(stat));
        else statsQueue.push(prefixStat(stat));
      },
      // flushQueue is called from SPICEWORKS.plugin#initializePlugins after all the init methods for the plugins has been invoked
      flushQueue: function(){
        // save the stat queue, mark it as flushed and clear it out
        save(statsQueue);
        flushed = true;
        statsQueue.clear();
      },
      // record is called from any plugin instance and is for recording arbitrary stats such as a user clicking something
      record: function(stat, options){
        save(prefixStat(stat));
      }
    };
    
    return that;
  }();
  
})(SPICEWORKS);

(function (spiceworks) {
  // prepare SUI in case it's not initialized
  if (typeof SUI === 'undefined') {
    SPICEWORKS.ui = {};
    window.SUI = SPICEWORKS.ui;
  }
  
  
  /*
   * Ex Usage: 
   *  SUI.sideMenu(destElement, {
   *    items:[
   *      '<a href="http://foo.com">Foo</a>',
   *      {label: 'Bar', itemName:'bar', href:'http://bar.com'},
   *      {separator: true, itemName: 'sep'},
   *      {label: 'Baz', itemName:'baz', onclick: function () { alert('baz'); } }
   *    ]
   *  })
   */
  spiceworks.ui.sideMenu = function (itemElem, spec) {
    if (!itemElem) return;
    var items,
        keepOpen = false,
        menuTimeout = null,
        openerTimeout = null,
        showMenuTimeout = null,
        target = $(spec.target || itemElem);
        sideMenu = null;
        
    if (Browser.ie6 && !spec.shown) {
      spec.openerUsesCss = false;
    }
    
    // == START HELPER FUNCTIONS
    function addItem(itemSpec) {
      if (!itemSpec) {
        return; // itemSpec is null, so do nothing!
      }
      
      var menuItem;
      var icon = (spec.icons === false ? '' : 'icon '); 
      
      
      //if it's just a string, then render the link without checking anything else.
      if (typeof itemSpec == 'string') {
        itemElem.menu.insert("<li>" + itemSpec + "</li>");
        return;
      }
      
      // Renamed className to itemName... keep support for plugin writers if possible.
      if(itemSpec.className && !itemSpec.itemName){
        itemSpec.itemName = itemSpec.className;
      }

      if (!itemSpec.itemName) {
        throw 'Menu item specification must include itemName.';
      }

      if (!itemElem.menu.down('.' + itemSpec.itemName)) {

        if (itemSpec.separator) {
          itemElem.menu.insert(new Element('li').insert(new Element('div', {'class':'separator ' + icon + itemSpec.itemName}).update('&nbsp;')));
        } 
        else if (itemSpec.rawLink) {
          itemElem.menu.insert("<li>" + itemSpec.rawLink + "</li>");
        } else {
          menuItem = new Element('a', {href:itemSpec.href || '#', 'class': icon + itemSpec.itemName, 'target': (itemSpec.target || '_top')}).update(itemSpec.label);
          if (itemSpec.onclick) {
            Event.observe( menuItem, 'click', function (e) {
              Event.stop(e);
              hideMenuAndOpener();
              itemSpec.onclick( spec.itemElemOrig || itemElem );
            });                  
          }
          // conditional separator
          if ( itemSpec.separator_up && itemElem.menu.childElements().length ) {
            itemElem.menu.insert(new Element('li').insert(new Element('div', {'class':'separator ' + icon + itemSpec.itemName + " separator_up"}).update('&nbsp;')));
          }
          itemElem.menu.insert(new Element('li').update(menuItem));                            
        }
      }
    }
        
    function showOpener() {
      if (!spec.openerUsesCss) {
        if (!itemElem.hasClassName('editing') && !itemElem.down('input')){
          itemElem.opener.style.display='block';
        }else{
          hideOpener();
        }              
      }
    }
    function hideOpener() {
      itemElem.opener.removeClassName('on');
      if (!spec.openerUsesCss) {
        itemElem.opener.style.display='none';              
      }
    }
    function hideMenu() {
      itemElem.opener.removeClassName('on');
      itemElem.pivotable.hide();
      itemElem.opener.style.zIndex='';
      itemElem.style.zIndex='';
      itemElem.pivotable.style.zIndex='';
      var dl = itemElem.up('dl,li,div');
      if(dl){
        dl.style.zIndex='';
      }
    }
    
    function showMenu() {
      if(!itemElem.pivotable.visible()) {
        itemElem.pivotable.show();
        itemElem.pivotable.clonePosition(itemElem.opener, {
          setHeight:false,
          setWidth:false,
          offsetTop: (itemElem.opener.offsetHeight - 2),
          offsetLeft: -(itemElem.pivotable.offsetWidth - itemElem.opener.offsetWidth - 6)
        });
        
        //itemElem.pivotable.style.right= spec.openerText ? '-6px' : '-1px';
      
        itemElem.style.zIndex=500;
        itemElem.opener.style.zIndex=301;
        itemElem.pivotable.style.zIndex=300;
        itemElem.opener.addClassName('on');
        var dl = itemElem.up('dl,li,div');
        if (dl) {
          dl.style.zIndex=500;
        }
      }
    }
    
    function hideMenuAndOpener() {
      hideMenu();
      hideOpener();
    }
    
    function itemOver(event) {
      Event.stop(event);
      clearTimeout(openerTimeout);
      clearTimeout(showMenuTimeout);
      openerTimeout = setTimeout( showOpener, 100);
    }
    function itemOut(event) {
      Event.stop(event);
      clearTimeout(openerTimeout);
      openerTimeout = setTimeout( hideMenuAndOpener, 100);
    }
    
    function openerOver(event) {
      Event.stop(event);
      clearTimeout(menuTimeout);
      clearTimeout(openerTimeout);
      showMenuTimeout = setTimeout(showMenu, 200);
    }
    function openerOut(event) {
      Event.stop(event);
      clearTimeout(menuTimeout);
      clearTimeout(openerTimeout);
      clearTimeout(showMenuTimeout);
      menuTimeout = setTimeout( hideMenu, 100);
    }
    
    function menuOver(event) {
      Event.stop(event);
      clearTimeout(openerTimeout);
      clearTimeout(menuTimeout);
    }
    
    function menuOut(event) {
      Event.stop(event);
      clearTimeout(menuTimeout);
      menuTimeout = setTimeout(hideMenu, 100);
      openerTimeout = setTimeout(hideOpener, 100);
    }
    // == END HELPER FUNCTIONS
    
    
    // == BUILD OUT MENU AND ADD MENU ITEMS
    // Build the menu ONLY if needed.
    if (!itemElem.menu) {
      itemElem.wrap = new Element('div', {'class':'sw-menu-wrap'});
      itemElem.insert({top:itemElem.wrap});
      itemElem.pivotable = new Element('div', {style: 'display:none; zoom:1; width:' + (spec.width || '130px'), 'class':'pivotable ' + spec.pivotableClass});
      itemElem.menu = new Element('ul', {'class':'nav_menu'});
      itemElem.pivotable.insert(itemElem.menu);
      itemElem.opener = new Element('a',{'class': 'edit sw-menu-opener', 'style':'cursor:pointer'});
      // optionally, an opener can be text based instead of an arrow.
      if(spec.openerText) {
        itemElem.opener.addClassName('sw-menu-opener-text');
        itemElem.opener.update(spec.openerText);
      }
      
      // itemElem.keepOpen = false;
      
      // insert the menu helper
      itemElem.wrap.insert({top: itemElem.opener});
      hideOpener();
      
      itemElem.wrap.insert({bottom: itemElem.pivotable});
      
      if (!spec.openerUsesCss) {
        Event.observe(target, 'mouseover', itemOver);
        Event.observe(target, 'mouseout', itemOut);
      }
      
      Event.observe(itemElem.opener, 'mouseover', openerOver);
      Event.observe(itemElem.opener, 'mouseout', openerOut);
      
      Event.observe(itemElem.pivotable, 'mouseover', menuOver);
      Event.observe(itemElem.pivotable, 'mouseout', menuOut);
    }
    
    // Add the new item(s) to the menu.          
    // determine if multiple items have been passed in or not.
    if (spec.items) {
      items = spec.items;
    } else {
      items = [spec];
    }
    
    items.each(addItem);
    
    // This is the actual side menu object.  We can add things to it using .addItem({})
    sideMenu = {
      hide: hideMenu,
      show: showMenu,
      addItem: addItem
    };
    
    return sideMenu;
  };
  
  spiceworks.ui.attachOpener = function (opener, menu, options) {
    var hideTimeout = null;
    opener = $(opener);
    menu = $(menu);
    options = Object.extend( {}, options );
    
    function show() {
      menu.show();
      opener.addClassName('active');
      if (options.onShow) options.onShow();
    }
    function hide() {
      menu.hide();
      opener.removeClassName('active');
      if (options.onHide) options.onHide();
    }
    
    function openerOver() {
      clearTimeout(hideTimeout);
      show();
    }
    function openerOut() {
      hideTimeout = setTimeout(function () {
        hide();
      },100);
    }
    function menuOver() {
      clearTimeout(hideTimeout);
      if (!menu.visible()) {
        show();
      }
    }
    function menuOut() {
      hideTimeout = setTimeout(function () {
        hide();
      },100);      
    }
    
    opener.observe('mouseover', openerOver);
    opener.observe('mouseout', openerOut);
    menu.observe('mouseover', menuOver);
    menu.observe('mouseout', menuOut);
  };
  
  // name: 'ok', 'medium/delete', 'medium/cancel', etc...
  // callback: function callback for this button
  spiceworks.ui.button = function(name, callback) {
    var button = new Element('img', {'src':'/images/forms/buttons/' + name + '.gif'});
    button.observe('mouseover', function (event) {
      Event.stop(event);
      button.src = '/images/forms/buttons/' + name + '_hover.gif';
    });
    button.observe('mouseout', function (event) {
      Event.stop(event);
      button.src = '/images/forms/buttons/' + name + '.gif';
    });
    button.observe('click', callback);
    
    return button;
  };
  
  // spec = {title: '', body:''}
  spiceworks.ui.popup = function (spec) {
    var popup = {},
        height, 
        docBody;
    
    docBody = $(document.body);
    
    height = [docBody.getHeight(), 'px'].join('');
    
    popup.darkbox = new Element('div', {'class':'darkbox'});
    popup.darkbox.setStyle({'height': height});
    docBody.insert(popup.darkbox);

    popup.lightbox = new Element('div', {'class':'lightbox'});
    popup.lightbox.setStyle({'height':height});
    docBody.insert(popup.lightbox);
    
    popup.content = new Element('div', {'class':'content', 'id':spec.id});
    popup.lightbox.insert(popup.content);
    
    popup.title = new Element('h1');
    popup.content.insert(popup.title);
    
    popup.body = new Element('div');
    popup.content.insert(popup.body);
    
    // Helpers
    popup.setTitle = function (title) {
      popup.title.update(title);
    };
    popup.setBody = function (body) {
      popup.body.update(body);
    };
    popup.close = function (blind) {
      if(popup.darkbox) {
        if (!blind) {
          popup.darkbox.remove();
          popup.lightbox.remove();          
        }else{
          new Effect.BlindUp(popup.darkbox);
          new Effect.BlindUp(popup.lightbox);
          setTimeout( function () {
            popup.darkbox.remove();
            popup.lightbox.remove();
          }, 2000);
          
        }
      }
    };

    // Initialize if needed
    if (spec.title) {
      popup.setTitle(spec.title);
    }
    if (spec.body) {
      popup.setBody(spec.body);
    }
    
    return popup;
  };
  
  spiceworks.ui.clearfix = "<div style='clear:both'></div>";
  spiceworks.ui.voidLink = "javascript:void(0)";
  
  
  // Create a toolbar in the specified element
  spiceworks.ui.toolbar = function(element,spec) {
    var actions = {},
        toolbar,
        toolbarRight;
    
    toolbar = new Element('div', {'class':'sui-toolbar', 'style':'display:none'}); // hidden by default
    element.insert(toolbar);
    
    // toolbarRight
    spec.actions = spec.actions || [];
    spec.actions.each(addToolbarAction);
    
    // Add an action to the toolbar.
    function addToolbarAction(actionSpec) {
      toolbar.show();
      actionSpec.action = new Element('a', {'href':actionSpec.href || SUI.voidLink, 'class':'sui-toolbar-action ' + (actionSpec.className || '')}).update(actionSpec.label);
      toolbar.insert(actionSpec.action);
      
      if (actionSpec.onclick) {
        Event.observe( actionSpec.action, 'click', function () {
          performAction(actionSpec.id);
        });
      }
      
      actions[actionSpec.id] = actionSpec;
    }
    
    function performAction(actionId) {
      actions[actionId].onclick(actionId);
    }
    
    return {
      actions:actions,
      element:toolbar,
      performAction:performAction
    };
  };
  
  // Encapsulation of the concept of a filter bar.  Requires a "content" area to scan
  // element is the parent to add the bar to
  // spec requires one object: {content: 'some_element_or_id'}
  spiceworks.ui.filterbar = function(element, spec) {
    var filters = {},
        content,
        filterbar,
        filterObject;
        
    element = $(element);
    content = $(spec.content);
    
    filterbar = new Element('div', {'class':'sui-filterbar'});
    element.insert(filterbar);
    
    function setObject(object) {
      // figure out filters!
      filterbar.update('');
      filters = {};
      tempFilters = {};
          
      if (!content) { 
        filterbar.hide();
        return; 
      }    
          
      addFilter('all', true);
            
      content.select('.filterable').each(function (filterable) {
        filterable.className.split(' ').each(function (className) {
          if (className.indexOf('filter-') == 0 && !tempFilters[className.substring(7)]) {
            tempFilters[className.substring(7)] = true;
          }
        });
      });
      
      Object.keys(tempFilters).sort().each(function (filtername) {
        addFilter(filtername);
      });
      
      if (Object.keys(filters).size() == 1) {
        filterbar.hide();
      } else {
        filterbar.show();
      }
    }
    
    function addFilter(filtername, active) {
      if (!filters[filtername]) {
        var filter = new Element('a', {'href': SUI.voidLink, 'class':'sui-filter ' + (active ? 'active' : '')}).update(filtername);
        filterbar.insert(filter);
        filters[filtername] = filter;
        Event.observe(filter, 'click', function (event) {
          Event.stop(event);
          setFilter(filtername);
        });        
      }
    }
    
    function setFilter(filtername) {
      var filter = filters[filtername];
      filterbar.select('.sui-filter').each( function (filter) {
        filter.removeClassName('active');
      });
      filter.addClassName('active');
      
      if (filtername === 'all') {
        content.select('.filterable').each(Element.show);
      } else {
        content.select('.filterable').each(Element.hide);
        content.select('.filterable.filter-' + filtername).each(Element.show);
      }
    }
    
    filterObject = {
      setObject: setObject,
      element: filterbar,
      addFilter: addFilter,
      setFilter: setFilter
    };
    
    spiceworks.ui.filterbars.push(filterObject);
    
    return filterObject;
  };
  
  spiceworks.ui.filterbars = [];
  
  spiceworks.ui.refreshAllFilterbars = function () {
    spiceworks.ui.filterbars.each( function (filterbar) {
      filterbar.setFilter("all");
      filterbar.setObject();
    });
  };
  
  // Button bar
  spiceworks.ui.togglebar = function(element, spec) {
    var buttons = {}, content, loadingMsg;
        
    element = $(element);
    content = $(spec.content);
    
    loadingMsg = Object.extend({ message: 'Loading&hellip;', matchHeight: true, 'class': 'loading' }, spec.loadingMessage || {});   
    
    function setObject(object) {
      element.select('.sui-toggle').each(function (button) {
        Event.observe(button, 'click', function (event) {
          setButton(button);
        });
      });
    }
    
    function clearButtons(section) {
      if (!section) { section = element; }
      
      section.select('.sui-toggle').each(function (button) {
        button.removeClassName("active");
      });
    }
    
    function setButton(button) {
      clearButtons(button.up());
      if (content) {
        LoadingMessage.set(content, loadingMsg.message, loadingMsg);
      }
      button.addClassName('active');
    }
    
    return {
      setObject: setObject,
      element: element,
      setButton: setButton,
      clearButtons: clearButtons
    };
  };
  
  spiceworks.ui.choicePill = function(element, spec) {
    var wrapper,
        hiddenInput,
        choiceElems,
        first = true;
        
    element = $(element);
    spec.options = spec.options || {};
    
    
    // remove the element with this id if it already exists...
    // if (spec.options.id || $(spec.options.id)) {
    //   $(spec.options.id).up('.sui-choice-pill').remove();
    // }
    
    choiceElems = [];
    
    wrapper = new Element('span', {'class':'sui-choice-pill'});
    hiddenInput = new Element('input', {'type':'hidden', 'name':spec.name, 'value':spec.value, 'id':spec.options.id || ''});
    wrapper.insert(hiddenInput);
    element.insert(wrapper);
    
    spec.choices.each( function (choice) {
      var name, value, choiceElem;
      name = choice[1];
      value = choice[0];
      options = choice[2] || {};
      choiceElem = new Element('a', {'class':'sui-pill ' + options['class'], 'href':SUI.voidLink, 'id': options['id']});
      if (first) {
        choiceElem.addClassName('sui-pill-first');
        first=false;
      }
      choiceElem.setAttribute('pillvalue', value);
      choiceElem.update(name);
      choiceElem.observe('click', function (event) {
        selectChoice(choiceElem);
      });
      choiceElems.push(choiceElem);
      wrapper.insert(choiceElem);
      
      if(value == spec.value){
        selectChoice(choiceElem);
      }
    });

    function selectChoice(choiceElem) {
      choiceElems.each( function(otherChoiceElem) {
        otherChoiceElem.removeClassName('sui-pill-selected');
      });
      choiceElem.addClassName('sui-pill-selected');
      hiddenInput.value = choiceElem.getAttribute('pillvalue');
      hiddenInput.fire('choice-pill:clicked');
    }
  };
  
  
  spiceworks.ui.simplemenu = function(activator, menu, spec) {
    // A menu system that should work for many purposes
    // Pass ids to the already-created activator and menu along with any of the following options
    
    // When menu is activated, activator gets the class "active" added to it
    
    // activateOn: by default 'mouseover'
    // update: optional callback that will get called with the menu object before the menu shows 
    // alignment: 'left', 'right' 
    // align: a callback that will get called with the menu and the activator
    // closeButton: one or more selectors that will close the window if clicked
    
    // FREE ADVICE:  Make the activator and the menu siblings, and your life will be easier in IE7
    
    var menuTimeout = null, showMenuTimeout = null, simpleMenu;
    activator = $(activator);
    menu = $(menu);
    
    if ((!menu) || (!activator)) { return; }
    
    spec = Object.extend({
      alignment: 'right',
      offsetLeft: 0, 
      offsetTop: 0,
      activateOn: 'mouseover',
      activatorZIndex: 500,
      menuZIndex:450,
      closeButton: [],
      afterHide: function(menu) {},
      afterShow: function(menu) {},
      update: function(menu) {}      
    }, spec || {});
    
    spec = Object.extend({
      align: function(menu, activator) {
        var offsetLeft, offsetTop;
        
        if (spec.alignment == "left") { offsetLeft = spec.offsetLeft; }          
        else { offsetLeft =  -($(menu).getWidth() - activator.getWidth() - 6 + spec.offsetLeft); }

        offsetTop = spec.offsetTop;
        
        menu.clonePosition(activator, {
          setHeight:false,
          setWidth:false,
          offsetTop: (activator.getHeight() - 3) + offsetTop,
          offsetLeft: offsetLeft
        });
      }
    }, spec);
    
    function unload() {
      Event.stopObserving(activator);
      Event.stopObserving(menu);
      SPICEWORKS.stopObserving("simplemenu:show", menuShownEvent);
      
      menu.select(spec.closeButton).each(function(button) {
        button.observe('click', hideMenu);
      });
    }
    
    function hideMenu() {
      $(menu).hide();
      activator.style.zIndex=0;
      activator.removeClassName("active");
      if (spec.activateOn == "click") { activator.blur(); } // or IE7 will freak
      SPICEWORKS.fire("simplemenu:hide", {id: menu.id});
      spec.afterHide(menu);
    }
    
    function showMenu() {
      spec.update(menu);
      SPICEWORKS.fire("simplemenu:show", {id: menu.id});
      $(menu).show();
      // we need to show the menu before aligning it so that offsetWidth is valid
      spec.align(menu, activator);
      activator.style.zIndex=spec.activatorZIndex;
      menu.style.zIndex=spec.menuZIndex;
      
      if ((activator.getStyle('position') != "absolute")) {
        activator.style.position = "relative";
      }
      
      activator.addClassName("active");
      activator.blur();
      spec.afterShow(menu);
    }

    function activatorOver(event) {
      clearTimeout(showMenuTimeout);
      showMenuTimeout = setTimeout(showMenu, 200);
    }
    function activatorOut(event) {
      clearTimeout(showMenuTimeout);
      showMenuTimeout = setTimeout( hideMenu, 100);
    }
    
    function menuOver(event) {
      clearTimeout(showMenuTimeout);
      clearTimeout(menuTimeout);
    }
    
    function menuOut(event) {
      clearTimeout(menuTimeout);
      menuTimeout = setTimeout(hideMenu, 100);
    }

    function menuClicked(event) { 
      var el = event.element();      
      if (menu.select(spec.closeButton).include(el)) {
        hideMenu();
      }
    }
    
    function toggleMenuVisibility(event) {
      event.stop();
      if ($(menu).visible()) { hideMenu(); }
      else { showMenu(); }
    }
    
    function menuShownEvent(event) {
      // This could be from any menu
      if (menu.id != event.memo.id) {
        hideMenu();
      }
    }
      
    if (spec.activateOn == "mouseover") {
      activator.observe('mouseover', activatorOver);
      activator.observe('mouseout', activatorOut);
      menu.observe('mouseover', menuOver);
      menu.observe('mouseout', menuOut);
      menu.observe('click', menuClicked);
    }
    else if (spec.activateOn == "click") {
      activator.observe('click', toggleMenuVisibility);
      menu.observe('click', menuClicked);
    }
      
    SPICEWORKS.observe("simplemenu:show", menuShownEvent);

    simpleMenu = {
      activator: activator,
      menu: menu,
      show: showMenu,
      hide: hideMenu,
      unload: unload
    };
    
    return simpleMenu;
  };
  
  
  
  // Build out the statusBarPopup from the spec passed...
  spiceworks.ui.statusBarPopup = function(statusBarItem, spec) {
    // adds a generic popup on a given icon in the status bar
    // Elements
    var alertBox, actionBox, panel, refreshLink, panelContent, header, footer, filterbar, filters, link, hideTimer, notifier, notifyTimer, close;

    var self = {};
    var data = {};
    // Flags and Data
    var initialized, totalCount, lastSeen, lastUpdated;    
    /*
      item: 
      title: 
      
      callbacks: 
        onShow: function(self)
        onCountUpdate: function(self, oldCount, newCount)
    */
    
    function togglePanel(event) {
      if (!initialized) { initializePopup(); }
      
      event.element().blur();
      event.element().up().blur();
            
      if (isOpen()) {
        hidePanel();
      }
      else {
        showPanel();
      }
    }
    
    function initializePopup() {
      // Create Elements
      panel = new Element('div', {'class':'popup ' + (spec.name ? spec.name : ""), 'style':'display:none'});
      close = new Element('a', {'class': 'close', 'href': SUI.voidLink }).update("close");
      header = new Element('h5', {'class':'header'}).update(spec.title).insert(close);
      filterbar = new Element('div', {'class': 'content-filterbar'});
      panelContent = new Element('div', {'class':'content'});
      footer = new Element('div', {'class': 'footer'});
      refreshLink = (new Element('a', {'href': SUI.voidLink, 'class': 'refreshLink'}).update("Refresh"));
      refreshLink.observe('click', refresh);
      alertBox = new Element('div', {'class': 'alertBox', 'style': 'display:none'});
      actionBox = new Element('div', {'class': 'actionBox', 'style': 'display:none'});
      
      // Inserts elements in the correct order, clearing out anything else
      resetLayout("Loading…");
            
      statusBarItem.insert(panel);
      
      // Register Events
      Event.observe((document.onresize ? document : window), "resize", adjustHeight);
      SPICEWORKS.observe("statusbar:popup:opened", hidePanel);
      close.observe('click', hidePanel);
      initialized = true;  
    }
    
    function safeResetLayout(message) {
      if (initialized) {
        resetLayout(message);
      }
    }
    
    function resetLayout(message) {
      panel.insert(header);
      panel.insert(filterbar);
      panel.insert(panelContent);
      panel.insert(actionBox);
      panel.insert(alertBox);
      panel.insert(footer);
      
      if (message) { setPanelMessage(message); }
      alertBox.hide();
      actionBox.hide();  
      footer.hide();
      adjustHeight();
    }

    // Indicator Notification

    function notify(text) {
      if (!statusBarItem.down('div.notifier')) {
        notifier = new Element('div', {'class':'notifier', style: 'display:none;'});
        notifier.insert(new Element('div'));
        notifier.down('div').insert(new Element('p', {'class':'content'}));
        notifier.down('div').insert(new Element('p', {'class':'bottom'}));

        statusBarItem.insert(notifier);
      }
      
      if (!panel || !panel.visible()) {
        statusBarItem.down('div.notifier p.content').update(text);
        showNotifier();
        
        if (notifyTimer) { clearTimeout(notifyTimer); }
        notifyTimer = window.setTimeout(function() { hideNotifier(); }, 3000);
      }
    }
    
    function hideNotifier() {
      if (notifier) {
        notifier.fade({duration:0.5});
      }
    }
    
    function showNotifier() {
      if (notifier) {
        notifier.appear({duration:0.5});
      }
    }
    
    function adjustHeight(lock) {
      var borderHeight = panel.getHeight() + 50 - panelContent.getHeight();
      if (((panelContent.scrollHeight + borderHeight) > document.viewport.getHeight()))  {
        panelContent.setStyle("overflow-y:scroll; overflow-x:hidden; height:" + (document.viewport.getHeight() - borderHeight) + "px");
      }
      else if (lock) {
        panelContent.setStyle("height:" + panelContent.getHeight() + "px");        
      }
      else {
        panelContent.setAttribute("style", " "); 
      }
    }
    
    function setPanelMessage(message) {
      panelContent.update(new Element('div', {'class': 'message'}).update(message));
    }
    
    function setAlertMessage(message) {    
      alertBox.update(message);
      alertBox.show();
      adjustHeight();
    }
    
    function setActionBox(content) {
      actionBox.update(content);
      actionBox.show();
      adjustHeight();
    }
    
    function hideActionBox() {
      actionBox.hide();
      adjustHeight();
    }
    
    function setPanel(message) {
      panelContent.update(new Element('div', {'class': 'message'}).update(message));
    }
    
    function setFooter(content) {
      footer.update(content);
      footer.show();
    }
    
    function hidePanel() {
      self.count = totalCount;
      
      if (spec.beforeHide) { spec.beforeHide(self); }
      
      statusBarItem.removeClassName('selected');
      statusBarItem.removeClassName('open');
      panel.hide();
      
      if (spec.afterHide) { spec.afterHide(self); }
    }
    
    function showPanel() {
      self.count = totalCount;
      if (spec.beforeShow) { spec.beforeShow(self); }
      
      SPICEWORKS.fire("statusbar:popup:opened", self);
      statusBarItem.addClassName('open');
      hideNotifier();
      panel.show();
      statusBarItem.addClassName('selected');
      self.lastSeen = lastSeen;
      
      try {
        if (spec.onShow) { spec.onShow(self); }
      }
      finally {
        adjustHeight();
        if (spec.afterShow) { spec.afterShow(self); }
      }
    }
    
    function refresh() {
      resetLayout("Refreshing Data…");
      showPanel();
    }
    
    function updateCanvas(content) {
      if (Object.isArray(content)) {
        panelContent.update("");
        content.each(function(element) {
          panelContent.insert(element);
        });
      }
      else {
        panelContent.update(content);
      }
      
      adjustHeight();
      buildFilters();
    }
    
    function buildFilters() {
      if (panelContent.down('ul li.filterable')) {
        filterbar.show();
        filterbar.update("");
        filters = SUI.filterbar(panel.down('div.content-filterbar'),{content: panelContent.down('ul') });
        filters.setObject();
      }
      adjustHeight(true);
    }
    
    function incrementCount(increment) {
      setCount((totalCount || 0) + (increment || 0));
    }
    
    function setCountWithoutCallback(newCount) {
      setCount(newCount, true);
    }
    
    function setCount(newCount, skipCallback) {
      statusBarItem.down('a span').update(newCount);
      if ((spec.onCountUpdate) && (newCount != totalCount) && (!skipCallback)) { spec.onCountUpdate(self, totalCount, newCount); }            
      totalCount = newCount;
      
      if (spec.hideCountOnZero && spec.hideCountOnZero == true && newCount == 0) { statusBarItem.down('a span').hide(); }
      else { statusBarItem.down('a span').show(); }
      
      if (spec.hideIndicatorOnZero && spec.hideIndicatorOnZero == true && newCount == 0) { statusBarItem.hide(); }
      else { statusBarItem.show(); }
    }
    
    function isOpen() {
      return (panel && panel.visible());
    }
    
    function setFlag(flag) {
      statusBarItem.addClassName(flag);
    }
    
    function clearFlag(flag) {
      statusBarItem.removeClassName(flag);
    }
    
    function elements(name) {
      var obj = {
        indicatorLink: statusBarItem,
        indicator: statusBarLink,
        panel: panel,
        canvas: panelContent,
        header: header,
        footer: footer,
        refreshLink: refreshLink,
        alertBox: alertBox,
        actionBox: actionBox
      };
      
      if (name) { return obj[name]; }
      else { return obj; }
    }
    
    function setData(name, value) {
      data[name] = value;
      
      return value;
    }
    function getData(name) {
      return data[name];
    }
    
    function clearData(name) {
      delete data[name];
    }
    
    function getCount() {
      return totalCount;
    }
    
    self.setData = setData;
    self.getData = getData;
    self.clearData = clearData;
    self.getCount = getCount;
    self.setCount=setCount;
    self.setCountWithoutCallback = setCountWithoutCallback;
    self.isOpen = isOpen;
    self.hidePanel=hidePanel;
    self.showPanel= showPanel;
    self.resetLayout = safeResetLayout; // IE has a bug that if this is called before initialize in a callback, the header gets lost
    self.setPanelMessage=setPanelMessage;
    self.setAlertMessage=setAlertMessage;
    self.setFooter=setFooter;
    self.setActionBox = setActionBox;
    self.hideActionBox = hideActionBox;
    self.update=updateCanvas;
    self.builders=SUI.builders.statusBarPopup;
    self.formatters=SUI.formatters.statusBarPopup;
    self.notify=notify;
    self.incrementCount=incrementCount;
    self.setFlag = setFlag;
    self.clearFlag = clearFlag;
    self.elements=elements;
    
    
    /* Basic Initialization */
    /* We don't create the panel content until we get click.  This stuff is just for the indicators */
    
    statusBarItem = $(statusBarItem);
    statusBarLink = statusBarItem.down('a');
    statusBarLink.observe('click', togglePanel);
    statusBarLink.setAttribute('title', spec.title);
    
    totalCount = spec.count || 0;    
    setCount(totalCount);
        
    // Add methods to the object
    if (spec.afterInitialize) { spec.afterInitialize(self); }
    
    return self;
  };
  
  spiceworks.ui.elements =  {
    primary: {
      area: function() { return $("primary"); },
      header: function() {  return $("content").down("div.sui-header"); },
      breadcrumbs: function() { return $$("#content > div.sui-header > h1 > span.crumb"); },
      toolbar: function() { return $("primary").down("div.sui-toolbar"); }
    },
    secondary: {
      area: function() { return $("secondary"); },
      header: function() {  return $("secondary").down("div.sui-header"); },
      breadcrumbs: function() {  return $$("#secondary div.sui-header span.crumb"); },
      toolbar: function() { return $("secondary").down("div.sui-toolbar"); },
      tabbedBox: function() { return $("secondary").down("div.sui-tabbed-box"); }
    }
  };
  
  
  // This is used by the tabbed_player helper in layout helper
  // spec requires defaultSize: {width: int, height: int} to calculate aspect ratio for resizing
  // pass in autoResize to resize on page resize and on tab change
  spiceworks.ui.tabbedplayer = function(playerContainer, videoContainer, spec) {
    var tabs, video, videoHeight, containerHeight, videoWidth, playerContent, aspectRatio;
    
    spec.defaultSize = spec.defaultSize || { width: 400, height:267 };
    spec.unloadOn = spec.unloadOn || ['app:ui:secondary:unload', 'app:ui:tab:unload'];
    
    autoResize = spec.autoResize || false;
    aspectRatio = spec.defaultSize.width / spec.defaultSize.height;

    playerContainer = $(playerContainer);
    playerContent = playerContainer.down('div');

    tabs = playerContainer.down("ul");

    tabs.select("li > a").each(function(a) {
      a.observe("click", function(e) { setVideo(a); });
    });

    if (autoResize) { 
      videoWidth = "100%";
      Event.observe((document.onresize ? document : window), "resize", setHeight);
    }
    else {
      videoWidth = spec.width || spec.defaultSize.width;
      videoHeight = spec.height || spec.defaultSize.height;
    }

    function setObject(object) {
      setVideo(tabs.down("li.active a") || tabs.down("li a"));
    }

    function setHeight() {
      if (!videoContainer) { return; }
      
      videoHeight = ($(videoContainer).up(".video_column").getWidth() / aspectRatio);
      if (spec.defaultSize.height > videoHeight) { videoHeight = spec.defaultSize.height; }
      $(videoContainer).setAttribute('height', videoHeight);
      playerContent.setStyle('height: ' +  videoHeight + 'px');
    }

    function setVideo(videoLink) {
      if (autoResize) { setHeight(); }

      var link = videoLink.getAttribute('href');
      videoLink.up("li").siblings().invoke("removeClassName", "active");
      videoLink.up("li").addClassName("active");
      
      $(videoContainer).setStyle({width: videoWidth, height:videoHeight});
      
      swfobject.embedSWF(link, videoContainer, videoWidth, videoHeight, "9.0.0", "/flash/expressinstall.swf", {}, {wmode: 'opaque', allowfullscreen: 'true'});
    }
    
    function unload() {
      tabs.select("li > a").invoke("stopObserving");
      Event.stopObserving((document.onresize ? document : window), "resize", setHeight);
    }
    
    SPICEWORKS.utils.unloader(SPICEWORKS, spec.unloadOn, unload);

    playerObject = {
      setObject: setObject,
      setVideo: setVideo,
      playerContainer: playerContainer,
      videoContainer: videoContainer
    };
    
    return playerObject;
  };

  // Builds a secondary content box and returns the element ready to be inserted in the DOM
  // SPICEWORKS.ui.secondary.build({
  //    parent: 'some-element-id-to-append-to',
  //    heading:'My Secondary Box', 
  //    tabs:[{visible:'Tab 1'}, {visible:'Tab 2', active:true}, {visible:'Tab 4'}], 
  //    sheet:'<h3>this is my content</h3><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>', 
  //    sideSections:[{title:'Side Section Title', items:[{classes:'item-class-one item class-two', visible:'some text and/or html'}]}]
  //  })
  spiceworks.ui.secondary = (function(){
    function build(options){
      options = Object.extend({
        parent:null,
        twoColumn:true,
        heading:'',
        tabs:[],
        sheet:'',
        sideSections:[]
      }, options || {});
      
      var secondary = new Element('div', {'class':'sui-secondary'});
      var header = new Element('div', {'class':'sui-header'}).update('<h2>' + options.heading + '</h2>');
      var innerClasses = $A(['inner']);
      if (options.twoColumn) innerClasses.push('two-column');
      var inner = new Element('div', {'class':innerClasses.compact().join(' ')});
      
      if (options.twoColumn && options.sideSections){
        var side = new Element('div', {'class':'sui-overview'});
        options.sideSections.each(function(section){
          var sectionElement = new Element('div', {'class':'section'});
          sectionElement.insert("<h3>" + section.title + "</h3>");
          var list = new Element('ul', {'class':'properties content'});
          list.update(section.items.collect(function(item){
            return '<li class="' + item.classes + '">' + item.visible + '</li>';
          }).join("\n"));
          sectionElement.insert(list);
          side.insert(sectionElement);
        });
        inner.insert(side);
      }
      
      var tabs = new Element('div', {'class':'sui-tabs'}), list = new Element('ul');
      list.update(options.tabs.collect(function(item){
        item.classes = (item.classes || '') + (item.active ? ' active' : '');
        return '<li class="' + item.classes + '" id="' + item.id + '">' + item.visible + '</li>';
      }).join("\n"));
      tabs.update(list);
      tabs.insert('<div class="sui-tab-more" style="display:none;">&raquo;</div>');
      
      var sheet = new Element('div', {'class':'sui-sheet'}).update(options.sheet);
      var tabbedBox = new Element('div', {'class':'sui-tabbed-box-inner'}).insert(tabs).insert(sheet);
      var contentInner = new Element('div', {'class':'left drop-shadow sui-tabbed-box'}).update(tabbedBox);
      inner.insert(contentInner);
      secondary.insert(header).insert(inner);
      
      if (options.parent){
        var parent = $(options.parent);
        if (parent.down('div.sui-secondary')) parent.down('div.sui-secondary').remove();
        parent.insert(secondary);
      }
      
      return secondary;
    };
    return {
      build:build
    };
  })();
  
  spiceworks.ui.tabbedbox = function(tabbedBox, spec) {
    tabbedBox = $(tabbedBox);
    
    var ul = tabbedBox.down("div.sui-tabs ul"), 
    firstTab = tabbedBox.down("div.sui-tabs ul li"),
    moreActivator = tabbedBox.down("div.sui-tabs div.sui-tab-more"),
    sheet = tabbedBox.down('div.sui-sheet'),
    state = {}, o = {}, li, simpleMenu, showMore, events = $A();
    
    spec = Object.extend({
      requestClass: 'secondary/tab',
      uriKey: 'tab',
      updateURI: true, 
      unloadOn: 'app:ui:secondary:unload'
    }, spec || {});
    
    spec = Object.extend({
      afterSelect: function(s) {
        if (s.activeTabName !== null) { // if this is null, the page will jump to the top in IE
          o[spec.uriKey] = s.activeTabName;
          Application.updateState(o, {updateUri: spec.updateURI});
        }          
      }
    }, spec);

    function tabSelected(event) {
      li = event.findElement("li");
      if (!li) { return; }
      
      selectTab(li);
      event.element().blur();
    }
    
    function selectTab(li, callback) {
      li = $(li);
      if (!li) { return; }

      SPICEWORKS.fire('app:ui:tab:unload');
      ul.select('li.active').invoke("removeClassName", "active");
      li.addClassName("active");
      var link = li.down('a');
      if (link && link.getAttribute('click_url')) loadContent(li.down('a').getAttribute('click_url'), callback);

      updateActiveState(li);
      spec.afterSelect(state);
      
      SPICEWORKS.fire('app:ui:tab:selected', object);
    }
    
    function loadContent(url, callback) {      
      LoadingMessage.set(sheet, "Loading &hellip;");      
      
      new Ajax.Request(url, {
        asynchronous:true, 
        evalScripts: true, 
        parameters:{requestClass: spec.requestClass},
        method: 'get',
        onFailure:function(request){
          StatusMessage.set(sheet, "Unable to load tab, please try again");
        },
        onComplete: function(request){
          if (callback) callback(request);
        }
      });
    }
    
    function updateActiveState(li) {
      if (!$(li)) { return; }
      
      state.activeTab = li;      
      state.activeTabName = (li.getAttribute("id") || "").split("_tab")[0];  
    }
      
    function unload() {
      Event.stopObserving(ul);
      $(moreMenuContent).select("li").invoke("stopObserving");
      simpleMenu.unload();
            
      SPICEWORKS.stopObserving("app:ui:secondary:unload", unload);
      Event.stopObserving((document.onresize ? document : window), "resize", updateMoreVisibility);
    }
    
    function updateMoreVisibility(){
      showMore = false;
      tabbedBox.select("div.sui-tabs ul li").reverse().each(function(tab) {
        if (Math.abs(firstTab.cumulativeOffset()['top'] - tab.cumulativeOffset()['top']) > 5) {
          showMore = true;
          throw $break;
        }
      });
      moreActivator[showMore ? 'show' : 'hide']();
    }
    
    SPICEWORKS.utils.unloader(SPICEWORKS, spec.unloadOn, unload);    
    Event.observe((document.onresize ? document : window), "resize", updateMoreVisibility);
    Event.observe(ul, "click", tabSelected);
        
    // Initialize internal state registry, and passed the currently selected tab to Application.
    // We need this on load since ocassionally we request the tab when submitting forms, etc
    updateActiveState(tabbedBox.down("div.sui-tabs > ul > li.active"));
    if (state.activeTabName !== null) {
      o[spec.uriKey] = state.activeTabName;
      // Don't update the URL on load, as IE freaks out and refreshes on anchor param changes, sometimes.
      Application.updateState(o, {updateUri: false});
    }
    
    // If the anchor param in the url is different the currently loaded tab, jump to it      
    if (Application.getUriAnchorParams().get(spec.uriKey) != state.activeTabName) {
      var tabParam = Application.getUriAnchorParams().get(spec.uriKey);
      if (tabParam && tabParam.indexOf('/') > -1) tabParam = tabParam.split('/')[0]; // ignore anything after the slash, if present
      if ($$$("div.sui-tabs ul li#" + tabParam + "_tab")) {
        selectTab(tabParam + "_tab");
      }
      else if ($$$("div.sui-tabs ul li#" + tabParam)) {
        selectTab(tabParam);
      }
    }
    
    // Set up more menu 
    updateMoreVisibility();
    
    var moreMenuContent = new Element('ul', {'class': 'menu'});
    tabbedBox.select("div.sui-tabs ul li a").each( function(a) {
      var li = new Element('li').update(a.clone().update(a.innerHTML));
      li.observe('click', function() { selectTab(a.up()); } );            
      moreMenuContent.insert(li);      
    });
    
    var contentID = "tab_more_content_" + (Math.floor(Math.random()*1000+1)).toString();
    $(tabbedBox).insert(new Element('div', {id: contentID, 'class': 'tab-more-menu simple-menu', style: 'display:none;'} ).update(moreMenuContent));
    
    var simpleMenu = SUI.simplemenu(moreActivator.down('a'), contentID, {alignment: 'right', update: function(content) {
      var items = $(content).select("ul.menu li");
      if (moreActivator) {
        items.invoke("show");
        items.invoke("removeClassName", "active");
        tabbedBox.select("div.sui-tabs ul li").each(function(tab, index) {
          if (tab.hasClassName('active')) { items[index].addClassName('active'); }
          items[index][(Math.abs(firstTab.cumulativeOffset()['top'] - tab.cumulativeOffset()['top']) > 5) ?  'show' : 'hide']();
        });
      }
    }});
    
    Object.extend(tabbedBox, {
      selectTab: selectTab,
      state: state
    });
    
    var object = {
      tabbedBox: tabbedBox,
      selectTab: selectTab,
      moreMenu: simpleMenu,
      state: state,
      unload: unload
    };
    
    return object;
  };
  
  spiceworks.ui.menuSet = function(container, spec){
    container = $(container);
    spec = Object.extend({
      items:$A(),
      activator:'<a href="#" class="menu-set-activator"><em>menu</em></a>',
      menuTag:'li',
      onclick: Prototype.emptyFunction,
      onshow: Prototype.emptyFunction,
      placementSelector: null,
      itemSelector:'tr',
      beforeInsertion:'',
      position:'bottom',
      template:null
    }, spec || {});
    
    if (!spec.template) spec.template = new Template('<div id="#{id}" class="menu-set" style="display:none"><ul>#{items}</ul></div>');

    var menu = {
      id:container.id + '-menu-set',
      items:spec.items.collect(function(item){
        return '<' + spec.menuTag + '>' + item + '</' + spec.menuTag + '>';
      }).join("\n")
    };

    container.insert({before:spec.template.evaluate(menu)}); // insert the menu just before the container node so that z-indexing and stuff works nicely
    var relatedMenu = $(menu.id);
    
    var insertion = {};
    insertion[spec.position] = spec.beforeInsertion + spec.activator;
    container.select(spec.placementSelector).each(function(insertionPoint){
      insertionPoint.insert(insertion);
    });
    
    SPICEWORKS.observe('table-row:added', function(event){
      var row = $(event.memo);
      if (row.up('table').id == container.down('table').id){
        // new row added to the table we are concerned with
        row.down(spec.placementSelector).insert(insertion);
      }
    });

    function showMenu(activator){
      clearHideTimeout();
      container.select('.menu-set-shown').invoke('removeClassName', 'menu-set-shown');
      activator.up(spec.itemSelector).addClassName('menu-set-shown');
      spec.onshow(activator, relatedMenu);
      relatedMenu.show();
      
      var offsetTop = activator.offsetHeight - 1;
      if (Prototype.Browser.IE) offsetTop = offsetTop - 2;
      if (Browser.ie7) offsetTop = (offsetTop - 3) + container.scrollTop;
      
      relatedMenu.clonePosition(activator, {
        setHeight:false,
        setWidth:false,
        offsetTop: offsetTop,
        offsetLeft: -(relatedMenu.offsetWidth - activator.offsetWidth)
      });
    };
    function beginHide(){
      hideTimeout = setTimeout(function(){ hideMenu(); }, 250);
    };
    function hideMenu(){
      container.select('.menu-set-shown').invoke('removeClassName', 'menu-set-shown');
      relatedMenu.hide();
    };
    var hideTimeout;
    function clearHideTimeout(){
      if (!hideTimeout) return;
      clearTimeout(hideTimeout);
      hideTimeout = null;
    };
    
    container.observe('mouseover', function(event){
      var element = event.element();
      if (element.hasClassName('menu-set-activator') || element.up('a.menu-set-activator')) showMenu(element);
    });
    container.observe('mouseout', function(event){
      var element = event.element();
      if (element.hasClassName('menu-set-activator') || element.up('a.menu-set-activator')) beginHide();
    });
    relatedMenu.observe('mouseover', function(event){ clearHideTimeout(); });
    relatedMenu.observe('mouseout', function(event){ beginHide(); });
    relatedMenu.observe('click', function(event){
      event.memo = container.down('tr.menu-set-shown');
      spec.onclick(event);
      beginHide();
    });
  };


  spiceworks.ui.templates = {};
  spiceworks.ui.templates.statusBarPopup = {};
  spiceworks.ui.templates.statusBarPopup.list = new Template("<li class='#{type}'><img src='#{imagePath}'/><span class='message'>#{message}</span></li>");

  spiceworks.ui.truncate = function(text, length, minDifference) {
    var container, full, brief, link, id;

    if (!length) { length = 300; }
    if (!minDifference) { minDifference = 100; }
    if (!text) { return text; }
    
    if ((text.length - length) > minDifference) {
      id = new Date().getTime();
      container = new Element('span');

      full = new Element('span', {'class': 'full', 'id': "full_text_for_#{id}".interpolate({id: id})});
      full.update(text);
      brief = new Element('span', {'class': 'brief', 'id': "brief_text_for_#{id}".interpolate({id: id})});
      brief.update(text.truncate(length));
      link = new Element('a', {
        'class': 'more',
        'href': SUI.voidLink, 
        'onclick': "TextHelper.showFullText('#{id}')".interpolate({id: id})
      }).update("More");
      
      full.hide();
      container.insert(full);
      brief.insert(link);
      container.insert(brief);
      
      return container.innerHTML;
    }
    else {
      return text;
    }
  };
  
  spiceworks.ui.builders = {};
  spiceworks.ui.builders.statusBarPopup = {};
  
  spiceworks.ui.formatters = {};
  spiceworks.ui.formatters.statusBarPopup = {};

  spiceworks.ui.formatters.statusBarPopup.date = function(date) {
    var formattedDate = PrettyDate.format(date, {isDate: true});
    if (formattedDate) {
      when = formattedDate;
    }
    else {
      when = new Date(date).strftime(Application.dateFormat) + " at " + new Date(date).strftime("%I:%M %p");
    }
    return when;
  };
  

  spiceworks.ui.builders.statusBarPopup.list = function(data, options) {
    //[{message: "foo", icon: "/path/to/icon", type: "type" }]
    var ul, li, span, img, when, actions, classes, bottom, filter, formattedDate, dismiss;
    
    if (!options) { options = {}; }
    ul = new Element('ul', {'class': 'sui-status-list'});
    
    filter = data.any(function(d) { return !!d['filter']; });
    data.each(function(row) {
      classes = [row['type']];
      
      if (filter) { classes.push('filterable'); }
      if (row['unseen']) { classes.push('unseen'); }
      if (row['filter']) { classes.push("filter-#{filter}".interpolate({filter:row['filter']})); }
      
      li = new Element('li', {'class': classes.join(" "), 'id':(row['id'] ? row['id'] : "")});      
      if(row['icon']) { 
        li.insert(new Element('img', {src: row['icon']}));
      }
      li.insert(new Element('h4', {'class':'title'}).update(row['title']));
      
      if (row['subtitle']) { li.insert(new Element('h5', {'class':'subtitle'}).update(row['subtitle']));  }
      if (row['message']) { li.insert(new Element('div', {'class':'message'}).update(row['message'])); }

      bottom = new Element('p', {'class':'bottom'});
      li.insert(bottom);
      if (row['when']) {
        when = row['when'];
        span = new Element('span', {'class':'date', 'title': row['date'] }).update(when);  
        bottom.insert(span);
      }
      
      if (row['action']) {
        actions = new Element('span', {'class':'actions'});
        bottom.insert(actions);
        if (Object.isArray(row['action'])) { 
          row['action'].each(function(action) { actions.insert(action); }); 
        }
        else { 
          actions.insert(row['action']);
        } 
      }
      
      if (row['url']) {
        li.setAttribute('data-url', row['url']); 
        li.addClassName('clickable');
      }
      
      if (row['communityUrl']) {
        li.setAttribute('data-community-url', row['communityUrl']); 
        li.addClassName('clickable');
      }
      
      if (row['dismissable']) { 
        li.addClassName('dismissable');
        li.insert(new Element('a', {'class':'sui-list-dismiss', 'href':SUI.voidLink, title: row['dismissable']['title']}).update("Dismiss")); 
      }
      
      if (row['selectable']) {
        li.insert({top:new Element('input', {type:'checkbox', name:row['selectable']['name'], value:row['selectable']['value']})});
        li.addClassName('selectable');
      }
      
      ul.insert(li);
    });
    
    ul.observe('click', function(event) {
      var url, e = event.element();
      if (e.tagName == "A") { 
        if (e.hasClassName('sui-list-dismiss') && options.onDismiss) {
          options.onDismiss(e.up("li"));
        }
        else {
          return; 
        }
      }
      else if (e.tagName.toLowerCase() == "input" && e.type.toLowerCase() == "checkbox") {
        if (e.checked) {
          e.up('li').addClassName('selected');
          if (options.onCheck) { options.onCheck(e.up('li')); }
        }
        else {
          e.up('li').removeClassName('selected');
          if (options.onUncheck) { options.onUncheck(e.up('li')); }
        } 
      }
      else {
        if (e.tagName != "LI") {
          e = e.up('li');
        }
        
        var url = e.getAttribute('data-url');
        if (url) {
          event.stop();
          window.location = url;
        }
        url = e.getAttribute('data-community-url');
        if (url) {
          event.stop();
          Community.go(url);
        }
      }
    });
  
    return ul;
  };
  
  spiceworks.ui.community = (function(){
    function buildPanel(spec){
      spec = Object.extend({
        help:true,
        mainHeading:'',
        classes:'',
        helpHeading:'',
        helpItems:$A()
      }, spec || {});
      var panel = new Element('div', {'class':spec.classes + ' community-panel'});
      var main = new Element('div', {'class':'main'});
      var help = new Element('div', {'class':'help'});
      main.insert('<h3 class="section-header"><span>' + spec.mainHeading + '</span></h3>');
      
      help.insert('<h3 class="section-header"><span>' + spec.helpHeading + '</span></h3>');
      var helpList = new Element('ul');
      helpList.insert(spec.helpItems.collect(function(item, index){
        return '<li class="item-' + (index % 2 == 0 ? 'even' : 'odd') + ' ' + item.classes +'">' + item.visible + '</li>';
      }).join("\n"));
      help.insert(helpList);

      panel.insert(main);
      panel.insert(help);

      return panel;
    };
    return {
      buildPanel: buildPanel
    };
  })();

})(SPICEWORKS);


(function (spiceworks) {
  

  // == SPICEWORKS.utils ==
  // A namespace for things that don't fit anywhere else currently.  These are mostly related to integration
  // with external services and the like.
  spiceworks.utils = {};
  
  
  // Include an external script
  // EX:
  //   SPICEWORKS.utils.include('http://myserver/my.js');
  spiceworks.utils.include = function (script_filename, callback) {
    var head = document.getElementsByTagName('head')[0], 
        script = document.createElement('script');
        
    script.type = 'text/javascript';
    script.src = script_filename;
    
    if (callback){
      var done = false;
      script.onload = script.onreadystatechange = function(){
        if (!done && (!this.readyState || this.readyState == "loaded" || this.readyState == "complete")) {
          done = true;
          callback();

          // Handle memory leak in IE
          script.onload = script.onreadystatechange = null;
          script.parentNode.removeChild(script);
        }
      };
    }

    head.appendChild(script);
  };
  
  // Load external JSONP and pass it into a supplied callback function.
  // Any query parameters should be passed in as a Javascript object.
  // EX:
  //   SPICEWORKS.utils.jsonp('http://example.com/foo.js', {'foo':'bar'}, function(response){
  //     console.log(response); 
  //   });
  spiceworks.utils.jsonpCallbacks = {}; // Container for callback functions
  spiceworks.utils.jsonp = function(url, params, callback, identifier) {
    if(!identifier){
      identifier = Math.floor(Math.random()*10000000);
    }
    spiceworks.utils.jsonpCallbacks[identifier] = callback;
    url = url + '?' + Object.toQueryString(Object.extend(params, {
      'callback':'SPICEWORKS.utils.jsonpCallbacks[' + identifier.toJSON() + ']'
    }));
    spiceworks.utils.include(url, function(){
      delete spiceworks.utils.jsonpCallbacks[identifier];
    });
  };
  
  
  // Quickly add some style to the document.  Any valid Stylesheet style string works here.
  // EX:  SPICEWORKS.utils.addStyle("body { background-color: red }");
  spiceworks.utils.addStyle = function (cssText) {
    var styleNode = document.createElement('style');
    styleNode.setAttribute("type", "text/css");
    if (styleNode.styleSheet) { // workaround for IE
      styleNode.styleSheet.cssText = cssText;
    } else if (Prototype.Browser.WebKit) { 
      styleNode.innerText = cssText;
    } else { // DOM
      styleNode.update(cssText);
    }
    $$('head').first().appendChild(styleNode);
  };
  
  spiceworks.utils.includeStyle = function (cssUrl) {
    // <link href="/stylesheets/print.css?1251735325" media="print" rel="stylesheet" type="text/css">
    var styleNode = document.createElement('link');
    styleNode.setAttribute("type", "text/css");
    styleNode.setAttribute("href", cssUrl);
    styleNode.setAttribute("rel", "stylesheet");
    $$('head').first().appendChild(styleNode);
  };
  
  // Switch to a new Locale
  spiceworks.utils.switchToLocale = function(locale, pluginId) {
    new Ajax.Request('/settings/advanced/save_international_settings.json', {
      parameters: 'international[locale]=' + encodeURIComponent(locale),
      onSuccess: function () {
        var loc = SPICEWORKS.utils.urlReplace({changed_locale_to:locale, selected_id:pluginId});
        document.location = loc;
      }
    });
  };

  // SPICEWORKS.utils.focusTimer(10, false, callback) #=> calls function every 10 seconds when window is focused
  // SPICEWORKS.utils.focusTimer(10, 20, callback) #=> calls function every 10 seconds when window is focused, every 20 when window is not focused
  
  // Will also fire callback immediately when window is focused if focusinterval has passed since the last time callback was called
  spiceworks.utils.focusTimer = function(focusInterval, blurInterval, callback) {
    focusInterval = focusInterval * 1000;
    if (blurInterval) { blurInterval = blurInterval * 1000; }
    
    var blurredAt, lastCalledAt, timer, delay, focused, timeSince;    
    
    function doCallback() {
      lastCalledAt = new Date();
      callback();
      clearTimeout(timer);
      delay = focused ? focusInterval : blurInterval;
      if (delay) { // blurInterval might be false
        timer = window.setTimeout(doCallback, delay);
      }
    }
    
    function delayCallback(delay) {
      timer = window.setTimeout(doCallback, delay);
    }  
    
    function registerBlur() {
      focused = false;
      blurredAt = new Date();
      clearTimeout(timer);
      if (blurInterval) {
        delayCallback(blurInterval);
      }
    }
    
    function registerFocus() {
      focused = true;
      clearTimeout(timer);
      
      timeSince = 0;
      if (lastCalledAt) { timeSince = (new Date().getTime()) - lastCalledAt.getTime(); }
      
      if (timeSince >= focusInterval) {
        doCallback();
      }
      else {
        delay = focusInterval - timeSince;
        delayCallback(delay);
      }
    }
    
    function unload() {
      clearTimeout(timer);
      Event.stopObserving(window, 'blur', registerBlur);
      Event.stopObserving(window, 'focus', registerFocus); 
      Event.stopObserving(document, 'focusout', registerBlur);
      Event.stopObserving(document, 'focusin', registerFocus);           
    }

    if (Prototype.Browser.IE) {
      /* IE's window blur and window focus events are flaky to say the least */
      Event.observe(document, 'focusout', registerBlur);
      Event.observe(document, 'focusin', registerFocus);
    }
    else {
      Event.observe(window, 'blur', registerBlur);
      Event.observe(window, 'focus', registerFocus);
    }    
    
    focused = true;
    delay = focused ? focusInterval : blurInterval;
    timer = window.setTimeout(doCallback, delay);
    
    return { 
      callback: callback,
      unload: unload
    };
  };

  // get the content of a plugin content area
  spiceworks.utils.getURLContent = function(location,callback) {
    var content = 'test';
    new Ajax.Request(location, {
            method: 'get',
            onSuccess: function(transport) {
                callback( transport.responseText );
            }
        });

    return content;
  };
  
  // == SPICEWORKS.utils.google ==
  // Namespace for all sorts of nice google goodies
  // Users will need to set their own "key" by setting 
  //    SPICEWORKS.utils.google.key = 'mykey';
  spiceworks.utils.google = function () {
    var googleLoaded = false,
        google = null,
        that;
    
    that = {
      key: 'ABQIAAAAXU7eNwZ9M4Sc9cn16StyDBRFChKT55K5FMfS9iKz1mkixBESPBSb4vJEgLxDzIfGJkiGB3x3myIrcQ',
      
      withGoogle: function (callback) {
        if (!googleLoaded) {
          spiceworks.utils.include("http://www.google.com/jsapi?key=" + that.key, function () {
            google = window.google; // google is global, just grab for now.
            googleLoaded = true;
            
            callback(google);
          });
        } else {
          callback(google);
        }
      },
      
      withMaps: function (callback) {
        that.withGoogle(function (google) {
          google.load('maps', '2.x', {callback: function () {
            callback(GMap2);
          }});
        });
      },
      
      withVisualizations: function (packages, callback) {
        that.withGoogle(function (google) {
          google.load("visualization", "1", {packages: packages, callback: function () {
            callback(google.visualization);
          }});
        });        
      }
    };
    
    return that;
  }();
  
  spiceworks.utils.unloader = function(object, events, callback) {
    if (!events) { return; }
    if (!object) { object = document; }
    
    events = $A([events]).flatten();    
    events.each(function(eventName) {
      var unload = function(){
        callback();
        object.stopObserving(eventName, unload);
      };
      
      object.observe(eventName, unload);
    });
  };
  
  spiceworks.utils.urlReplace = function (params, hash) {
    var newLoc = document.location.href.toString();
    hash = hash || document.location.hash.toString().sub(/\#/,'');
    newLoc = newLoc.sub(/\#.*/, ''); // strip off the hash for now.

    if (params) {
      Object.keys(params).each(function (param) {
        var exp = new RegExp('(\\?|\\&)' + param + '\\=[^\\&]*');

        if(newLoc.match(exp)) {
          if (!params[param]) {
            newLoc = newLoc.sub( exp, "#{1}");
          } else {
            newLoc = newLoc.sub( exp, "#{1}" + encodeURIComponent(param) + "=" + encodeURIComponent(params[param]));
          }
        } else if (newLoc.match(/\?/)) {
          if (params[param]) {
            newLoc = newLoc + "&" + encodeURIComponent(param) + "=" + encodeURIComponent(params[param]);
          }
        } else {
          if (params[param]) {
            newLoc = newLoc + "?" + encodeURIComponent(param) + "=" + encodeURIComponent(params[param]);
          }
        }
      });
    }
    newLoc = newLoc.sub( /\?\&/, "?");
    newLoc = newLoc.sub( /\&+/, "&");

    if (hash && hash !== '') {
      newLoc = newLoc + "#" + hash;
    }

    newLoc = newLoc.sub( /\&\#/, "#");

    return newLoc;
  };
  
  // QUICK LINK
  spiceworks.utils.quickLink = function ( options ) {
    var link = new Element('a', {'href':(options.href || SUI.voidLink), 'class':options['class'], 'title':options.title}).update(options.label);
    if (options.onClick) {
      link.observe('click', function (event) {
        if (!Event.element(event).hasClassName('disabled')) {
          options.onClick(event);
        }
      });
    }
    return link;
  };
  
  
  spiceworks.utils.contentType = {
    lang: 'application/x-vnd.spiceworks.lang',
    multipart: 'multipart/mixed',
    js: 'text/javascript',
    ticketqueue:'multipart/mixed+ticketqueue',
    helpdesk: 'application/x-vnd.spiceworks.helpdesk'
  };
  
  spiceworks.utils.sortFunctions = {
      string: function(element) {
        return element.innerHTML.stripTags().toLowerCase();
      },
      version: function(element) {
        var value = element.title || ""; // compare against the literal value
        if (value == "")  return -1;     // empty strings get sorted at the end

        // catch values like "v3.6.3" or "V 3.6.3"
        if ((/^\s*v\s*\d/i).test(value)) value = value.substring(1, value.length);
        else if (!(/^\d/).test(value)) return 0;

        // split it into tokens (["3", "6", "3"])
        var tokens = value.split('.').slice(0, 4);
        // pad it (["3", "6", "3", "00000"])
        while (tokens.length < 4) tokens.push('00000');
        tokens = tokens.map(function(token) {
          if (token.length > 5) token = token.substring(0, 5);

          // pad each token (["00003", "00006", "00003", "00000"])
          if (token.length < 5) token = ('0').times(5 - token.length) + token;
          return token;
        });

        // join it and convert it to a number (3000060000300000)
        return parseInt(tokens.join(''), 10);
      },
      fullName: function(element) {
        var sort_value = element.innerHTML.stripTags().toLowerCase();
        return sort_value.replace(/^(.*) (.*)$/, "$2 $1");
      },
      bytes: function(element) {
        var sort_value = element.innerHTML.stripTags().toLowerCase();
        // NOTE: this regex will choke on whitespace
        var result = /^(.*) (k|m|g)B/i.exec(sort_value);
        if (result) {
          return parseFloat(result[1]) * (result[2] == "m" ? (1024 * 1024) : (result[2] == "g" ? (1024 * 1024 * 1024) : 1024));
        } else {
          return 0;
        }
      },
      numeric: function(element) {
        var sort_value = element.innerHTML.stripTags().toLowerCase(), result;
        var result = parseFloat(sort_value.gsub(/\$/, '')); /* Strip out dollar sign for currency */
        // NaN values should return -1 so that they can be distinguished from 0
        return isNaN(result) ? -1 : result;
      },
      date: function(element) {
        if (element.getAttribute("millis")) {
          return new Date(parseInt(element.getAttribute("millis"), 10));
        }
        var sort_value = element.innerHTML, date;
        if(date = sort_value.match(/(\d+)\/(\d+)\/(\d+) @ (\d+):(\d+)([ap]m)/)){
          /* finder_date_time format */
          var hour = parseInt(date[4], 10);
          if(date[6] == 'pm'){hour += 12;}
          if(hour == 12 || hour == 24){hour -= 12;}
          return Date.UTC(date[3], date[1], date[2], hour, date[5], 0);
        }else{
          return Date.parse(sort_value.stripTags());
        }
      },
      ticketPriority: function(element){
        if(element == null)return 2; // Assume 2 if there is not column.
        var priority_hash = {'high':3, 'med':2, 'medium':2, 'low':1}, priority;
        priority = priority_hash[element.innerHTML.toLowerCase()];
        return priority;
      },
      // This is the default sort order.  status/priority/id
      ticketExternallyUpdated: function(element){
        var retval = "2";
        try{
            if(Element.hasClassName(element, 'past_due')){
              retval = "0";
            }else if(Element.hasClassName(element, 'externally_updated')){
              retval = "1";
            }else if(Element.hasClassName(element, 'closed')){
              retval = "3";
            }

          var id = null;
          id = element.id.split("_")[4];
          var priority_elem = null;
          var priority = null;

          // Sort first by past_due/externally_updated then by priority, then id
          priority_elem = $('ticket_table_priority_' + id);
          priority = SortableTableManager.sort_functions.ticket_priority(priority_elem);
          retval = retval + (3 - priority);

          retval = retval + id;
        }catch(ex){}
        return retval;
      },
      ipAddress: function(element) {
        var sort_value = element.innerHTML.stripTags();
        var result = sort_value.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/);
        if (result != null) {
          result = result.slice(1).collect(function(elem, idx) {
            switch(elem.length) {
              case 1:
                return "00" + elem;
              case 2:
                return "0" + elem;
              default:
                return elem;
            }
          });
          return result.join(".");
        } else {
          return sort_value;
        }
      },
      clickToEdit: function(element){
        var sort_value = element.innerHTML.stripTags();
        // so that the "click to edit" fields get sorted to the end of the list, after Z, instead of in the front of the list if we returned a blank string
        if (sort_value.match(/click to edit/)) sort_value = 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'; 
        else sort_value = sort_value.toLowerCase();

        return sort_value;
      }
  };
  // Aliases
  spiceworks.utils.sortFunctions.alphabetic = spiceworks.utils.sortFunctions.string;
  spiceworks.utils.sortFunctions.memory = spiceworks.utils.sortFunctions.bytes;
  
  
})(SPICEWORKS);

(function (spiceworks) {
  
  // == SPICEWORKS.version ==
  // see _script_includes.html.erb for more info on how this gets setup.
  // if( SPICEWORKS.version && SPICEWORKS.version.full_version)
  spiceworks.version = function () {
    function pad(str, length){
      str = new String(str);
      while (str.length < length) { 
        str = '0' + str; 
      }
      return str;
    }
    function normalize(major, minor, revision) {
      var normalized = [pad(major, 2), pad(minor,2), pad(revision, 10)].join('');
      return parseInt(normalized, 10);
    }
    
    var version = {
      pad: pad,
      normalize: normalize,
      setVersionInfo: function (versionInfo) {
        Object.keys(versionInfo).each(function (key) {
          version[key.sub(/_vers?ion|svn_/, '')] = versionInfo[key];
        });
      },
      
      toString: function () {
        return version.full;
      },
      
      // A helper function to figure out what version is running.
      // EX: version.is('>= 4')
      // Parameter Ex: >= 4, >= 4.1, = 4.5.12345
      is: function (check) {
        var match, operator,
            major, minor, revision,
            curMajor, curMinor, curRevision;
        
        match = check.match(/(\>|\<|\={1,2}|\>\=|\<\=)\s?(\d+)\.?(\d*)\.?(\d*)/);
        
        if (!match) {
          throw "Expects a string in the form '[operator] [version]'.  Ex: SPICEWORKS.version.is('>= 4.0')";
        }
        
        operator = match[1];
        major = match[2];
        minor = match[3];
        revision = match[4];
        
        curMajor = version.major;
        curMinor = version.minor;
        curRevision = version.revision;

        if (operator == '=') {
          operator = '==';
        }
        if (minor == '') {
          curMinor = 0;
        }
        if (revision == '') {
          curRevision = 0;
        }
        
        check = normalize(curMajor, curMinor, curRevision) + 
                operator + 
                normalize(major, minor, revision);
        
        return eval(check);
      }
    };
    
    return version;
  }();
  
})(SPICEWORKS);

// Copyright © 2006-10 Spiceworks, Inc.  All Rights Reserved.  http://www.spiceworks.com

var Activation = {
  activatingNewPortal: function(){
    var container = $('activate_new_portal').down('div.inner');
    LoadingMessage.set(container, 'Activating &hellip;');
  },
  activatedNewPortal: function(){
    var container = $('activate_new_portal').down('div.inner');
    StatusMessage.set(container, '<img src="/images/icons/medium/check-transparent.gif" /> Activated!');
    this.hideActivator();
    Flyover.destroy.delay(1, container);
  },
  reactivatingOldPortal: function(){
    var container = $('reactivate_old_portal').down('div.inner');
    LoadingMessage.set(container, 'Reactivating &hellip;');
  },
  reactivatedOldPortal: function(){
    var container = $('reactivate_old_portal').down('div.inner');
    StatusMessage.set(container, '<img src="/images/icons/medium/check-transparent.gif" /> Reactivated!');
    this.hideActivator();
    Flyover.destroy.delay(1, container);
  },
  hideActivator: function(){
    var activator = $$$('a.portal-activator');
    if (activator) activator.hide();
  }
};

var RichTextEditor = {
  replaceTextArea:function(id, options){
    options = (options || '{}').evalJSON();

    var element = this._textarea(id);
    if (!element || !element.id) return;
    
    var oFCKeditor = new FCKeditor(element.id, '100%', element.getStyle('height'));
    oFCKeditor.BasePath = "/javascripts/fckeditor/";

    if (element.up('#sidebar')) oFCKeditor.ToolbarSet = 'PortalSidebar';
    else oFCKeditor.ToolbarSet = 'PortalMain';
    
    oFCKeditor.Config['CustomConfigurationsPath'] = options.config_path || null;
    oFCKeditor.Config['EditorAreaCSS'] = options.css_path || null;
    
    oFCKeditor.ReplaceTextarea();
  },
  insertAtCursor:function(id, text){
    var editor = this.get(id);
    if (editor) editor.InsertHtml(text);
  },
  save: function(element){
    var editor = this.get(element);
    if (editor) editor.UpdateLinkedField();
  },
  get: function(id){
    var element = this._textarea(id);
    if (element) return FCKeditorAPI.GetInstance(element.id);
  },
  _textarea: function(id){
    var element = $(id);
    if (!element.hasClassName('wysiwyg')) element = element.down('textarea.wysiwyg');
    return element;
  }
};

/* Copyright © 2006-10 Spiceworks, Inc.  All Rights Reserved.  http://www.spiceworks.com */
/*

js for the portal only, not the admin side of the portal

*/

var Portal = {
  initialize: function(options){
    this.options = Object.extend({
      loggedIn:false,
      admin:false
    }, options || {});

    if (this.options.admin) this.setupAdmin();
  },
  setupAdmin: function(){
    AdminBar.initialize();
    this.makeSortable();

    // konami
    (function(){var keyed = [], code = "38,38,40,40,37,39,37,39,66,65";document.observe('keydown', function(e){keyed.push(e.keyCode);if (keyed.toString().indexOf(code) > -1){keyed = [];var el = new Element('div', {id:'konami', style:'position:absolute;top:0;left:0;'}).update('<img src="/images/other/gradient-dark-to-light.gif" />');document.body.insert(el);var endingDims = document.viewport.getDimensions();new Effect.Morph(el, {style:'top:' + endingDims.height + 'px;', duration:5.0});new Effect.Morph(el, {style:'left:' + endingDims.width + 'px;', duration:5.0});Element.remove.delay(5, el);}});})();
  },
  articleMenuSelect: function(menu){
    var selected = menu.value;
    if (selected != '') document.location.href = '/portal/page/' + selected;
    else document.location.href = '/portal/new_page?article=true';
  },
  
  disableSubmit: function(form){
    form = $(form);
    // disable the button so the user can't keep clicking it again and again like a silly goose
    form.down('p.button input').disabled = true;
  },
  showRemainingTickets: function(activator){
    activator = $(activator);
    var block = activator.up('.content-block');
    activator.up('p').hide();
    block.select('.ticket-cutoff').invoke('show');
    block.down('ul.ticket-list').addClassName('ticket-list-show-all');
    return false;
  },

  /* edit page methods
  **********************************************************************************************************/
  pageFormSubmit: function(){
    this.newPage = true;
    this.options.pageName = $F('user_portal_page_name');
    this.options.isProtected = $('user_portal_page_protected').checked;
    var oldPageName = $('navigation').down('li:last-child a').innerHTML;
    $('navigation').select('li:not(li.new-tab) a').last().update(this.options.pageName);
    document.title = document.title.replace(oldPageName, this.options.pageName);
    
    new Ajax.Request('/portal/check_page_name', {parameters:'name=' + encodeURIComponent(this.options.pageName)});
    return false;
  },
  newPageCreated: function(pageID){
    delete this.newPage;
    delete this.options.pageName;
    delete this.options.isProtected;
    this.options.page = pageID;
    SPICEWORKS.fire('portalv2:newPageCreated');
  },
  saveEdit: function(){
  },
  cancelEdit: function(){
  },
  removeBlock: function(activator){
    if (this.ensureRemovable(activator)){
      var block = $(activator).up('.content-block'), parent = block.up('div');
      var postBody = 'page=' + this.options.page + '&index=' + Portal.blockIndex(block.id) + '&location=' + parent.id + '&block=' + block.getAttribute('block') + '&element=' + encodeURIComponent(block.id);
      new Ajax.Request('/portal/remove_content', {parameters:postBody});
    }
  },
  ensureRemovable: function(activator){
    var block = $(activator).up('.content-block'), parent = block.up('div');
    
    // user is trying to remove a block that has never been saved, just remove it from the DOM and be done with it
    // return false so that the remove action isn't triggered on the server
    if (block.hasClassName('unsaved-content-block')) {
      block.remove();
      return false;
    } else if (parent.id == 'main' && parent.select('div.content-block').size() == 1) {
      // trying to remove the only main block on the page, this is not allowed
      alert('A page must have at least one main content block, this cannot be removed');
      return false;
    } else return confirm('Are you sure?');
  },
  
  // theme methods
  contentAdded: function(section){
    this.makeSortable();

    $(section).select('div.placeholder-block').invoke('remove');
    $(section).down('div.content-block').highlight({duration:0.5});
    SPICEWORKS.fire('portalv2:contentAdded');
  },
  themeChange: function(choice){
    choice = $(choice);
    // remember the originally active theme, but don't overwrite it a second time
    if (!this.originalTheme) this.originalTheme = this.activeTheme();
    this.switchTheme(choice.value);
    this.dirtyTheme = true;
  },
  undoThemeChange: function(){
    this.switchTheme(this.originalTheme);
  },
  activeTheme: function(){
    var theme = document.body.className.match(/theme-\w*/);
    return (theme ? theme[0] : null);
  },
  switchTheme: function(newTheme){
    if (this.activeTheme()) document.body.className = document.body.className.replace(/theme-\w*/, newTheme);
    else document.body.addClassName(newTheme);
    SPICEWORKS.fire('portalv2:themeChanged', newTheme);
  },
  cancelPreferenceChange: function(){
    var preferences = $('preferences');
    if (preferences) {
      preferences.hide();
      preferences.down('form').reset();
    }
    if (this.dirtyTheme) this.restoreState();
    return false;
  },
  cancelPageEdit: function(){
    $$('.darkbox, .lightbox').invoke('remove');
    return false;
  },
  themeBoxPreference: function(boxControl){
    if (!this.originalBoxPreference) this.originalBoxPreference = this.activeBoxPreference();
    this.switchBoxPreference(boxControl.value);
    this.dirtyTheme = true;
  },
  activeBoxPreference: function(){
    var box = document.body.className.match(/box-\w*/);
    return (box ? box[0] : null);
  },
  switchBoxPreference: function(newBoxPref){
    if (this.activeBoxPreference()) document.body.className = document.body.className.replace(/box-\w*/, newBoxPref);
    else document.body.addClassName(newBoxPref);
    SPICEWORKS.fire('portalv2:boxPreferenceChanged', newBoxPref);
  },
  restoreState: function(){
    if (this.originalTheme) this.switchTheme(this.originalTheme);
    if (this.originalBoxPreference) this.switchBoxPreference(this.originalBoxPreference);
  },

  /* rearranging methods
  **********************************************************************************************************/
  makeSortable: function(){
    // setup the sortable to allow rearranging of blocks within a column or from one column to another
    // relies on several overridden methods from dragdrop.js that are overridden in this file
    // Sortable#findElements, Sortable#mark, Draggables#activate
    if ($$('#content.is-article').size() > 0) return;
    Sortable.create('content', {tagSelector:'div.content-block', handle:'move', constraint:'', containment:['main', 'sidebar'], ghosting:true, onChange: this.renderPlaceholder.bind(this), onUpdate: this.reordered.bind(this)});
  },
  renderPlaceholder: function(element){
    // called whenever the content blocks are rearranged while in the middle of a drag
    // relies on the overridden Sortable#mark (runs before this callback is invoked) method setting two properties on the Sortable object

    // fetch the sortable object, the element passed in is the block that is being dragged
    var sortable = Sortable.options(element);
    
    // before we draw a placeholder, remove the existing placeholder (if applicable)
    $('content').select('.placeholder-block').invoke('remove');

    // the element that will be the new placeholder
    var placeholder = new Element('div', {'class':'content-block placeholder-block'}).update('<h3>Block will appear here</h3>');

    // the markedPosition & markedElement properties are set in our overridden Sortable#mark method
    if (sortable.markedPosition == 'before'){
      // most common case, draw the placeholder before the marked element
      element.parentNode.insertBefore(placeholder, sortable.markedElement);
    } else {
      // draw the placeholder after the marked element, if there is no next element then insert the placeholder at the end of the element
      if (sortable.markedElement.nextSibling) element.parentNode.insertBefore(placeholder, sortable.markedElement.nextSibling);
      else element.parentNode.appendChild(placeholder);
    }
  },
  reordered: function(sortableElement){
    // called after a dragon is complete

    // remove the placeholder element
    sortableElement.select('.placeholder-block').invoke('remove');
    
    // fetch the blocks in the main and sidebar areas, so we can do different things based on the state of those columns
    var sidebarContent = sortableElement.select('div#sidebar div.content-block'), mainContent = sortableElement.select('div#main div.content-block');
    if (sidebarContent.size() > 1 && sortableElement.hasClassName('no-sidebar-content')){
      // an element has been dragged into an empty sidebar, need to remove a few meta attributes on the sidebar now that it has real content
      sortableElement.removeClassName('no-sidebar-content');
      sortableElement.down('div.sidebar-placeholder').remove();
    } else if (sidebarContent.size() == 0){
      // the last element in the sidebar was dragged out, now we need to add a class to the container and render a placeholder
      // this placeholder element is a hack to allow dragging into the seemingly empty sidebar
      sortableElement.addClassName('no-sidebar-content');
      sortableElement.down('#sidebar').insert('<div class="content-block sidebar-placeholder">Drag content here to create a sidebar on this page</div>');
      // since we added a new element to the page, we need to renew our sortable
      this.makeSortable();
    }

    // if something was dragged out of the main area, we will need to do a few extra things if there is one element remaining (which cannot be moved)
    if (mainContent.size() == 1){
      // there is only one element in the main area, it cannot be moved until more than one element exists in the main area
      var mover = mainContent.first().down('a.move');
      // save off the title of the move control so we can add it back later
      mover.setAttribute('old_title', mover.title);
      mover.title = 'Cannot move the only main block';
      mover.up('li').addClassName('disabled');
    } else {
      // some other dragon event occurred, see if we need to enable a disabled move control
      var disabledMover = mainContent.first().up().select('div.edit-block li.disabled a');
      if (disabledMover.size() > 0){
        // there will only be one of these controls that is disabled
        disabledMover = disabledMover.first();
        // restore the saved title
        disabledMover.setAttribute('title', disabledMover.getAttribute('old_title'));
        disabledMover.up('li').removeClassName('disabled');
      }
    }
    
    // save the page state to the server
    this.saveReordering();
  },
  saveReordering: function(){
    SPICEWORKS.fire('portalv2:pageReordered');
    
    // do not try to save state if this is a new page (no page attribute present) or the user has removed all the main content
    if (!$('main').down('div.content-block')) return;

    new Ajax.Request('/portal/save_state', {parameters:this.serializePage()});
  },
  

  /* utility methods
  **********************************************************************************************************/
  dismissNotice: function(){
    $$('#notice').invoke('remove');
  },
  blockIndex: function(blockID){
    // for a given ID, which is assumed to be an immediate descendent of a primary wrapper (main or sidebar), then this will return the index of that item
    return $(blockID).up().select('div.content-block').pluck('id').indexOf(blockID);
  },
  
  
  // given a container, will reset its contents to the innerHTML of the container, useful for resetting parts of a form such as an file input control which cannot be accessed via JavaScript
  resetHTML: function(container){
    // need to use a delay here because the link that called into this method is going to overwritten .. IE doesn't like that
    (function(container){ container.update(container.innerHTML); }).delay(0.1, container);
    return false;
  },
  serializePage: function(location){
    // have to use a custom serializer here because the built-in one that comes with Sortable does not meet our needs
    var body = 'page=' + this.options.page + '&' + this._serializeSection('main') + '&' + this._serializeSection('sidebar') + '&authenticity_token=' + encodeURIComponent(this.options.authenticityToken);
    if (typeof this.newPage != 'undefined'){
      body += '&page_name=' + encodeURIComponent(this.options.pageName);
      if (this.options.isProtected) body += '&protected=true';
    }
    return body;
  },
  
  /* internal use methods
  **********************************************************************************************************/
  _serializeSection: function(section){
    // custom serializer for sortable
    // yields the value of the block attribute for each valid block
    // placeholder blocks are not valid, we will just reject them from the collection
    return section + '[]=' + $(section).select('.content-block').reject(function(el){
      return el.className.match(/placeholder/);
    }).collect(function(block){
      return encodeURIComponent(block.getAttribute('block'));
    }).join('&' + section + '[]=');
  }
};

var Block = {
  cancelEdit: function(activator){
    activator = $(activator);
    var editBlock = activator.up('.content-block');
    var block = $(editBlock.id.replace('edit-', ''));
    editBlock.remove();
    block.show();
    return false;
  },
  cancelNew: function(activator){
    activator = $(activator);
    var block = activator.up('.content-block'), parent = block.up('div');
    if (block) block.remove();
    return false;
  },
  moveNew: function(block){
    block = $(block);
    if (block) block.remove();
  },
  submit: function(form){
    form = $(form);
    if (typeof Portal.newPage != 'undefined'){
      form.down('input.new-page-name').setValue(Portal.options.pageName);
      form.down('input.new-page-protected').setValue(Portal.options.isProtected);
    }
    var editor = form.down('textarea.wysiwyg');
    if (editor) RichTextEditor.save(editor);
  }
};

// override a few methods that are defined in dragdrop.js for our special case usage
// this object only exists when dragdrop.js is included on the page and that only occurs when an admin is logged in
if (typeof Sortable != 'undefined'){

  // the Sortable#findElements method is a lot handier when it works with a selector
  Sortable.findElements = function(element, options){
    if (options.tagSelector) return element.select(options.tagSelector);
    else return Element.findChildren(element, options.only, options.tree ? true : false, options.tag);
  };
  
  // need to wrap Sortable#mark method so that we can setup a few properties on the sortable object since these values are not passed to the onChange callback like we want them to be
  Sortable.mark = Sortable.mark.wrap(function(){
    var args = $A(arguments), proceed = args.shift();
    proceed.apply(this, args);

    // fetch the sortable object
    var sortable = Sortable.options(args[0]);
    // in scriptaculous 1.8.2, this method is passed an element and a position where the dragged element will be inserted, we want to save those properties
    sortable.markedElement = args[0];
    sortable.markedPosition = args[1];
  });
  
  // need to wrap the Draggable#activate method so that if our placeholder element (for an empty sidebar) is dragged
  // we are using that placeholder element in the sidebar to allow dragging objects into the sidebar without having to do anything terribly tricky
  Draggables.activate = Draggables.activate.wrap(function(){
    var args = $A(arguments), proceed = args.shift(), draggable = args[0], parent = draggable.element.parentNode;
    
    // before allowing the dragon activation to occur, make sure the dragged element isn't a placeholder or isn't the last object in the main area
    if (draggable.element.hasClassName('sidebar-placeholder')) return;
    else if (parent.id == 'main' && parent.select('div.content-block').size() == 1) alert('Must leave at least one block in the main area');
    else proceed.apply(this, args);
  });
}

var AdminBar = {
  initialize: function(){
    this.bar = $('admin-bar');
    $('container').setStyle({paddingTop:this.bar.getHeight() + 'px'});

    if (document.location.search.indexOf('designer=true') > -1) this.designMode();
    else if (document.location.search.indexOf('preview=true') > -1) this.endUserMode();
    else if (Cookie.get('portal-admin-view') == 'end-user') this.endUserMode();
    
    SUI.simplemenu($("add-page-content-button"), $("add-content"), { activateOn: 'click', closeButton: ['input.close', 'a.button-add-new-content'], offsetTop:-17});
    SUI.simplemenu($("jump-to-button"), $("jump-to"), { activateOn: 'click', closeButton: ['input.close', 'a'], offsetTop:-19});
    SUI.simplemenu($("portal-preferences-button"), $("preferences"), { activateOn: 'click', closeButton: 'input.close', offsetTop:-19});
    SUI.simplemenu($("page-settings-button"), $("page-settings"), { activateOn: 'click', closeButton: 'input.close', offsetTop:-17});
    SUI.simplemenu($("new-tab-button"), $("new-tab"), { alignment:'left', activateOn: 'click', closeButton: 'input.close', offsetTop:-19});

    SPICEWORKS.fire('portalv2:adminBarInit');
  },
  addContent: function(blockToAdd){
    if (blockToAdd != ''){
      var postBody = '';
      if (Portal.options.page){
        postBody += '&page=' + Portal.options.page;
      } else if (typeof Portal.newPage != 'undefined') {
        postBody += '&new_page[name]=' + encodeURIComponent(Portal.options.pageName);
        if (Portal.options.isProtected) postBody += '&new_page[protected]=true';
      }
      if (blockToAdd != '__createnew__') postBody += '&block=' + blockToAdd;
      
      // don't let a new content block be added if there already is one unsaved block on the page
      if ($('new_user_portal_block')) return;
      
      new Ajax.Request('/portal/new_content', {parameters:postBody});
    }
  },
  designMode: function(){
    // note the $() call on body is for IE7 only, IE8 in IE7 compatibility mode even gets it right ARRGGHH
    $(document.body).removeClassName('end-user-mode');
    Cookie.set('portal-admin-view', 'admin-user');
    SPICEWORKS.fire('portalv2:designMode');
  },
  endUserMode: function(){
    // note the $() call on body is for IE7 only, IE8 in IE7 compatibility mode even gets it right ARRGGHH
    $(document.body).addClassName('end-user-mode');
    Cookie.set('portal-admin-view', 'end-user');
    SPICEWORKS.fire('portalv2:endUserMode');
  },
  toggleNewTab: function(clicked){
    document.location.href = '/portal/new_page';
  },
  toggleJumpTo: function(clicked){
    clicked = $(clicked);
    var jumpTo = $('jump-to');
    if (jumpTo && jumpTo.visible()) jumpTo.hide();
    else if (jumpTo && jumpTo.hasClassName('positioned')) this.showSimpleMenu(jumpTo);
    else if (jumpTo) this.showSimpleMenuFirstTime(jumpTo, clicked);
  },
  togglePreferences: function(clicked){
    clicked = $(clicked);
    var preferences = $('preferences');
    if (preferences && preferences.visible()) preferences.hide();
    else if (preferences) this.showSimpleMenu(preferences);
    else new Ajax.Request('/portal/preferences', {onComplete:this.showSimpleMenuFirstTime.bind(this, 'preferences', clicked)});
  },
  togglePageSettings: function(clicked){
    clicked = $(clicked);
    var settings = $('page-settings');
    if (settings && settings.visible()) settings.hide();
    else if (settings) this.showSimpleMenu(settings);    
    else if (settings && settings.hasClassName('positioned')) this.showSimpleMenu(settings);
    else if (settings) this.showSimpleMenuFirstTime(settings, clicked);
  },
  toggleAddContent: function(clicked){
    clicked = $(clicked);
    var addContent = $('add-content');
    if (addContent && addContent.visible()) addContent.hide();
    else if (addContent && addContent.hasClassName('positioned')) this.showSimpleMenu(addContent);
    else if (addContent) this.showSimpleMenuFirstTime(addContent, clicked);
  },
  showSimpleMenu: function(menu){
    $$('div.simple-menu').select(function(e){ return e.visible(); }).invoke('hide');
    menu.show();
  },
  showSimpleMenuFirstTime: function(menu, clickedFrom){
    menu = $(menu);
    $$('div.simple-menu').select(function(e){ return e.visible(); }).invoke('hide');
    var clickedDims = clickedFrom.getDimensions(), clickedOffsets = clickedFrom.cumulativeOffset();
    var right = (document.body.getWidth() - (clickedDims.width + clickedOffsets.left)) - 4;
    menu.setStyle({top: (clickedOffsets.top - 3) + 'px', right: right + 'px', marginRight: clickedFrom.getStyle('padding-right')}).show().addClassName('positioned');
  }
};

var Lightbox = {
  setup: function(){
    var viewportHeight = document.viewport.getHeight(), bodyHeight = $(document.body).getHeight();
    $$('.darkbox').invoke('setStyle', {height:(viewportHeight > bodyHeight ? viewportHeight : bodyHeight) + 'px'});
  },
  remove: function(){
    $$('.darkbox, .lightbox').invoke('remove');
  }
};

var Flyover = {destroy:Lightbox.remove};

var LoadingMessage = {
  set: function(el, message) {  
    if (!message) { message = "Loading&hellip;"; }
    StatusMessage.set(el, message, {'className': 'loading'});
  }
};

var StatusMessage = {
  set: function(el, message, options) {
    this.options = Object.extend({ className: '' }, options || {});    
     
    el = $$$(el);
    
    if (!el) { return; };    
    var loadingMessage = new Element('div', {'class': "sui-status-message" + " " + this.options['className'] }).update(new Element('h3').update(new Element("span").update(message)));
    
    el.update(loadingMessage);
  }
};

var CalendarPopup = { 
  setup:function(textFieldID, triggerID, additionalOptions) {
    var calendar;
    additionalOptions = additionalOptions || {};
    // silently fail if the nodes to attach the calendar to cannot be found
    if( $(textFieldID) && (!triggerID || $(triggerID)) ){
      calendar = Calendar.setup({ 
        inputField : textFieldID, // ID of the input field 
        ifFormat : additionalOptions.dateFormat, // the date format 
        button : triggerID, // ID of the button
        align : 'Bl',
        single_click : true,
        step : 1, // show every year in menu
        cache : true, // reuse the div is calendar is reopened
        showOthers : true,
        weekNumbers : false,
        onUpdate: additionalOptions.onUpdate,
        getDateStatus: additionalOptions.getDateStatus
      }); 
    }
    return calendar;
  }
};

var Cookie = {
  get: function( name ){
    var nameEQ = encodeURIComponent(name) + "=", ca = document.cookie.split(';');
    for (var i = 0, c; i < ca.length; i++) {
      c = ca[i];
      while (c.charAt(0) == ' ') c = c.substring(1, c.length);
      if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
  },
  set: function( name, value, options ){
    options = (options || {});
    if ( options.expiresInOneYear ){
      var today = new Date();
      today.setFullYear(today.getFullYear()+1, today.getMonth, today.getDay());
      options.expires = today;
    }
    var curCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + 
      ((options.expires) ? "; expires=" + options.expires.toGMTString() : "") + 
      ((options.path)    ? "; path="    + options.path : "") + 
      ((options.domain)  ? "; domain="  + options.domain : "") + 
      ((options.secure)  ? "; secure" : "");
    document.cookie = curCookie;
  },
  removeCookie: function (key) {
    var date = new Date();
    date.setTime(date.getTime()-(1*24*60*60*1000));
    this.set(key, '', {expires: date});
  },
  hasCookie: function( name ){
    return document.cookie.indexOf( encodeURIComponent(name) ) > -1;
  }
};

Ajax.Responders.register({
  onCreate: function(request){
    document.fire('ajax:started', request); // fire a custom event when an ajax request is started

    // This will ensure that all AJAX posts have an authenticity token so we won't
    // cause rails to throw an ActionController::InvalidAuthenticityToken exception.
    if (request.method == 'post' && Portal.options.authenticityToken) {
      // If we don't have a postBody, force one.  This is our only chance
      // to change what gets posted, because Ajax.Request will always use
      // the postBody if present and it's too late to add to request.options.parameters.
      if (!request.options.postBody)
        request.options.postBody = Object.toQueryString(request.parameters);
      
      var encodedToken = encodeURIComponent(Portal.options.authenticityToken);
      var regex = new RegExp(encodedToken);
      if (!regex.match(request.options.postBody)) request.options.postBody += "&authenticity_token=" + encodedToken;
    }
  },
  onComplete: function(request){
    document.fire('ajax:completed', request); // fire a custom event when an ajax request is completed for observers
  }
});

function $$$(selector) {
    return ($(selector) || $$(selector).first() || null);
}


/*  Copyright Mihai Bazon, 2002-2005  |  www.bazon.net/mishoo
 * -----------------------------------------------------------
 *
 * The DHTML Calendar, version 1.0 "It is happening again"
 *
 * Details and latest version at:
 * www.dynarch.com/projects/calendar
 *
 * This script is developed by Dynarch.com.  Visit us at www.dynarch.com.
 *
 * This script is distributed under the GNU Lesser General Public License.
 * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
 */
 Calendar=function(firstDayOfWeek,dateStr,onSelected,onClose){this.activeDiv=null;this.currentDateEl=null;this.getDateStatus=null;this.getDateToolTip=null;this.getDateText=null;this.timeout=null;this.onSelected=onSelected||null;this.onClose=onClose||null;this.dragging=false;this.hidden=false;this.minYear=1970;this.maxYear=2050;this.dateFormat=Calendar._TT["DEF_DATE_FORMAT"];this.ttDateFormat=Calendar._TT["TT_DATE_FORMAT"];this.isPopup=true;this.weekNumbers=true;this.firstDayOfWeek=typeof firstDayOfWeek=="number"?firstDayOfWeek:Calendar._FD;this.showsOtherMonths=false;this.dateStr=dateStr;this.ar_days=null;this.showsTime=false;this.time24=true;this.yearStep=2;this.hiliteToday=true;this.multiple=null;this.table=null;this.element=null;this.tbody=null;this.firstdayname=null;this.monthsCombo=null;this.yearsCombo=null;this.hilitedMonth=null;this.activeMonth=null;this.hilitedYear=null;this.activeYear=null;this.dateClicked=false;if(typeof Calendar._SDN=="undefined"){if(typeof Calendar._SDN_len=="undefined")Calendar._SDN_len=3;var ar=new Array();for(var i=8;i>0;){ar[--i]=Calendar._DN[i].substr(0,Calendar._SDN_len);}Calendar._SDN=ar;if(typeof Calendar._SMN_len=="undefined")Calendar._SMN_len=3;ar=new Array();for(var i=12;i>0;){ar[--i]=Calendar._MN[i].substr(0,Calendar._SMN_len);}Calendar._SMN=ar;}};Calendar._C=null;Calendar.is_ie=(/msie/i.test(navigator.userAgent)&&!/opera/i.test(navigator.userAgent));Calendar.is_ie5=(Calendar.is_ie&&/msie 5\.0/i.test(navigator.userAgent));Calendar.is_opera=/opera/i.test(navigator.userAgent);Calendar.is_khtml=/Konqueror|Safari|KHTML/i.test(navigator.userAgent);Calendar.getAbsolutePos=function(el){var SL=0,ST=0;var is_div=/^div$/i.test(el.tagName);if(is_div&&el.scrollLeft)SL=el.scrollLeft;if(is_div&&el.scrollTop)ST=el.scrollTop;var r={x:el.offsetLeft-SL,y:el.offsetTop-ST};if(el.offsetParent){var tmp=this.getAbsolutePos(el.offsetParent);r.x+=tmp.x;r.y+=tmp.y;}return r;};Calendar.isRelated=function(el,evt){var related=evt.relatedTarget;if(!related){var type=evt.type;if(type=="mouseover"){related=evt.fromElement;}else if(type=="mouseout"){related=evt.toElement;}}while(related){if(related==el){return true;}try{related=related.parentNode;}catch(e){related = null;}}return false;};Calendar.removeClass=function(el,className){if(!(el&&el.className)){return;}var cls=el.className.split(" ");var ar=new Array();for(var i=cls.length;i>0;){if(cls[--i]!=className){ar[ar.length]=cls[i];}}el.className=ar.join(" ");};Calendar.addClass=function(el,className){Calendar.removeClass(el,className);el.className+=" "+className;};Calendar.getElement=function(ev){var f=Calendar.is_ie?window.event.srcElement:ev.currentTarget;while(f.nodeType!=1||/^div$/i.test(f.tagName))f=f.parentNode;return f;};Calendar.getTargetElement=function(ev){var f=Calendar.is_ie?window.event.srcElement:ev.target;while(f.nodeType!=1)f=f.parentNode;return f;};Calendar.stopEvent=function(ev){ev||(ev=window.event);if(Calendar.is_ie){ev.cancelBubble=true;ev.returnValue=false;}else{ev.preventDefault();ev.stopPropagation();}return false;};Calendar.addEvent=function(el,evname,func){if(el.attachEvent){el.attachEvent("on"+evname,func);}else if(el.addEventListener){el.addEventListener(evname,func,true);}else{el["on"+evname]=func;}};Calendar.removeEvent=function(el,evname,func){if(el.detachEvent){el.detachEvent("on"+evname,func);}else if(el.removeEventListener){el.removeEventListener(evname,func,true);}else{el["on"+evname]=null;}};Calendar.createElement=function(type,parent){var el=null;if(document.createElementNS){el=document.createElementNS("http://www.w3.org/1999/xhtml",type);}else{el=document.createElement(type);}if(typeof parent!="undefined"){parent.appendChild(el);}return el;};Calendar._add_evs=function(el){with(Calendar){addEvent(el,"mouseover",dayMouseOver);addEvent(el,"mousedown",dayMouseDown);addEvent(el,"mouseout",dayMouseOut);if(is_ie){addEvent(el,"dblclick",dayMouseDblClick);el.setAttribute("unselectable",true);}}};Calendar.findMonth=function(el){if(typeof el.month!="undefined"){return el;}else if(typeof el.parentNode.month!="undefined"){return el.parentNode;}return null;};Calendar.findYear=function(el){if(typeof el.year!="undefined"){return el;}else if(typeof el.parentNode.year!="undefined"){return el.parentNode;}return null;};Calendar.showMonthsCombo=function(){var cal=Calendar._C;if(!cal){return false;}var cal=cal;var cd=cal.activeDiv;var mc=cal.monthsCombo;if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}if(cal.activeMonth){Calendar.removeClass(cal.activeMonth,"active");}var mon=cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()];Calendar.addClass(mon,"active");cal.activeMonth=mon;var s=mc.style;s.display="block";if(cd.navtype<0)s.left=cd.offsetLeft+"px";else{var mcw=mc.offsetWidth;if(typeof mcw=="undefined")mcw=50;s.left=(cd.offsetLeft+cd.offsetWidth-mcw)+"px";}s.top=(cd.offsetTop+cd.offsetHeight)+"px";};Calendar.showYearsCombo=function(fwd){var cal=Calendar._C;if(!cal){return false;}var cal=cal;var cd=cal.activeDiv;var yc=cal.yearsCombo;if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}if(cal.activeYear){Calendar.removeClass(cal.activeYear,"active");}cal.activeYear=null;var Y=cal.date.getFullYear()+(fwd?1:-1);var yr=yc.firstChild;var show=false;for(var i=12;i>0;--i){if(Y>=cal.minYear&&Y<=cal.maxYear){yr.innerHTML=Y;yr.year=Y;yr.style.display="block";show=true;}else{yr.style.display="none";}yr=yr.nextSibling;Y+=fwd?cal.yearStep:-cal.yearStep;}if(show){var s=yc.style;s.display="block";if(cd.navtype<0)s.left=cd.offsetLeft+"px";else{var ycw=yc.offsetWidth;if(typeof ycw=="undefined")ycw=50;s.left=(cd.offsetLeft+cd.offsetWidth-ycw)+"px";}s.top=(cd.offsetTop+cd.offsetHeight)+"px";}};Calendar.tableMouseUp=function(ev){var cal=Calendar._C;if(!cal){return false;}if(cal.timeout){clearTimeout(cal.timeout);}var el=cal.activeDiv;if(!el){return false;}var target=Calendar.getTargetElement(ev);ev||(ev=window.event);Calendar.removeClass(el,"active");if(target==el||target.parentNode==el){Calendar.cellClick(el,ev);}var mon=Calendar.findMonth(target);var date=null;if(mon){date=new Date(cal.date);if(mon.month!=date.getMonth()){date.setMonth(mon.month);cal.setDate(date);cal.dateClicked=false;cal.callHandler();}}else{var year=Calendar.findYear(target);if(year){date=new Date(cal.date);if(year.year!=date.getFullYear()){date.setFullYear(year.year);cal.setDate(date);cal.dateClicked=false;cal.callHandler();}}}with(Calendar){removeEvent(document,"mouseup",tableMouseUp);removeEvent(document,"mouseover",tableMouseOver);removeEvent(document,"mousemove",tableMouseOver);cal._hideCombos();_C=null;return stopEvent(ev);}};Calendar.tableMouseOver=function(ev){var cal=Calendar._C;if(!cal){return;}var el=cal.activeDiv;var target=Calendar.getTargetElement(ev);if(target==el||target.parentNode==el){Calendar.addClass(el,"hilite active");Calendar.addClass(el.parentNode,"rowhilite");}else{if(typeof el.navtype=="undefined"||(el.navtype!=50&&(el.navtype==0||Math.abs(el.navtype)>2)))Calendar.removeClass(el,"active");Calendar.removeClass(el,"hilite");Calendar.removeClass(el.parentNode,"rowhilite");}ev||(ev=window.event);if(el.navtype==50&&target!=el){var pos=Calendar.getAbsolutePos(el);var w=el.offsetWidth;var x=ev.clientX;var dx;var decrease=true;if(x>pos.x+w){dx=x-pos.x-w;decrease=false;}else dx=pos.x-x;if(dx<0)dx=0;var range=el._range;var current=el._current;var count=Math.floor(dx/10)%range.length;for(var i=range.length;--i>=0;)if(range[i]==current)break;while(count-->0)if(decrease){if(--i<0)i=range.length-1;}else if(++i>=range.length)i=0;var newval=range[i];el.innerHTML=newval;cal.onUpdateTime();}var mon=Calendar.findMonth(target);if(mon){if(mon.month!=cal.date.getMonth()){if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}Calendar.addClass(mon,"hilite");cal.hilitedMonth=mon;}else if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}}else{if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}var year=Calendar.findYear(target);if(year){if(year.year!=cal.date.getFullYear()){if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}Calendar.addClass(year,"hilite");cal.hilitedYear=year;}else if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}}else if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}}return Calendar.stopEvent(ev);};Calendar.tableMouseDown=function(ev){if(Calendar.getTargetElement(ev)==Calendar.getElement(ev)){return Calendar.stopEvent(ev);}};Calendar.calDragIt=function(ev){var cal=Calendar._C;if(!(cal&&cal.dragging)){return false;}var posX;var posY;if(Calendar.is_ie){posY=window.event.clientY+document.body.scrollTop;posX=window.event.clientX+document.body.scrollLeft;}else{posX=ev.pageX;posY=ev.pageY;}cal.hideShowCovered();var st=cal.element.style;st.left=(posX-cal.xOffs)+"px";st.top=(posY-cal.yOffs)+"px";return Calendar.stopEvent(ev);};Calendar.calDragEnd=function(ev){var cal=Calendar._C;if(!cal){return false;}cal.dragging=false;with(Calendar){removeEvent(document,"mousemove",calDragIt);removeEvent(document,"mouseup",calDragEnd);tableMouseUp(ev);}cal.hideShowCovered();};Calendar.dayMouseDown=function(ev){var el=Calendar.getElement(ev);if(el.disabled){return false;}var cal=el.calendar;cal.activeDiv=el;Calendar._C=cal;if(el.navtype!=300)with(Calendar){if(el.navtype==50){el._current=el.innerHTML;addEvent(document,"mousemove",tableMouseOver);}else addEvent(document,Calendar.is_ie5?"mousemove":"mouseover",tableMouseOver);addClass(el,"hilite active");addEvent(document,"mouseup",tableMouseUp);}else if(cal.isPopup){cal._dragStart(ev);}if(el.navtype==-1||el.navtype==1){if(cal.timeout)clearTimeout(cal.timeout);cal.timeout=setTimeout("Calendar.showMonthsCombo()",250);}else if(el.navtype==-2||el.navtype==2){if(cal.timeout)clearTimeout(cal.timeout);cal.timeout=setTimeout((el.navtype>0)?"Calendar.showYearsCombo(true)":"Calendar.showYearsCombo(false)",250);}else{cal.timeout=null;}return Calendar.stopEvent(ev);};Calendar.dayMouseDblClick=function(ev){Calendar.cellClick(Calendar.getElement(ev),ev||window.event);if(Calendar.is_ie){document.selection.empty();}};Calendar.dayMouseOver=function(ev){var el=Calendar.getElement(ev);if(Calendar.isRelated(el,ev)||Calendar._C||el.disabled){return false;}if(el.ttip){if(el.ttip.substr(0,1)=="_"){el.ttip=el.caldate.print(el.calendar.ttDateFormat)+el.ttip.substr(1);}el.calendar.tooltips.innerHTML=el.ttip;}if(el.navtype!=300){Calendar.addClass(el,"hilite");if(el.caldate){Calendar.addClass(el.parentNode,"rowhilite");}}return Calendar.stopEvent(ev);};Calendar.dayMouseOut=function(ev){with(Calendar){var el=getElement(ev);if(isRelated(el,ev)||_C||el.disabled)return false;removeClass(el,"hilite");if(el.caldate)removeClass(el.parentNode,"rowhilite");if(el.calendar)el.calendar.tooltips.innerHTML=_TT["SEL_DATE"];return stopEvent(ev);}};Calendar.cellClick=function(el,ev){var cal=el.calendar;var closing=false;var newdate=false;var date=null;if(typeof el.navtype=="undefined"){if(cal.currentDateEl){Calendar.removeClass(cal.currentDateEl,"selected");Calendar.addClass(el,"selected");closing=(cal.currentDateEl==el);if(!closing){cal.currentDateEl=el;}}cal.date.setDateOnly(el.caldate);date=cal.date;var other_month=!(cal.dateClicked=!el.otherMonth);if(!other_month&&!cal.currentDateEl)cal._toggleMultipleDate(new Date(date));else newdate=!el.disabled;if(other_month)cal._init(cal.firstDayOfWeek,date);}else{if(el.navtype==200){Calendar.removeClass(el,"hilite");cal.callCloseHandler();return;}date=new Date(cal.date);if(el.navtype==0)date.setDateOnly(new Date());cal.dateClicked=false;var year=date.getFullYear();var mon=date.getMonth();function setMonth(m){var day=date.getDate();var max=date.getMonthDays(m);if(day>max){date.setDate(max);}date.setMonth(m);};switch(el.navtype){case 400:Calendar.removeClass(el,"hilite");var text=Calendar._TT["ABOUT"];if(typeof text!="undefined"){text+=cal.showsTime?Calendar._TT["ABOUT_TIME"]:"";}else{text="Help and about box text is not translated into this language.\n"+"If you know this language and you feel generous please update\n"+"the corresponding file in \"lang\" subdir to match calendar-en.js\n"+"and send it back to <mihai_bazon@yahoo.com> to get it into the distribution  ;-)\n\n"+"Thank you!\n"+"http://dynarch.com/mishoo/calendar.epl\n";}alert(text);return;case-2:if(year>cal.minYear){date.setFullYear(year-1);}break;case-1:if(mon>0){setMonth(mon-1);}else if(year-->cal.minYear){date.setFullYear(year);setMonth(11);}break;case 1:if(mon<11){setMonth(mon+1);}else if(year<cal.maxYear){date.setFullYear(year+1);setMonth(0);}break;case 2:if(year<cal.maxYear){date.setFullYear(year+1);}break;case 100:cal.setFirstDayOfWeek(el.fdow);return;case 50:var range=el._range;var current=el.innerHTML;for(var i=range.length;--i>=0;)if(range[i]==current)break;if(ev&&ev.shiftKey){if(--i<0)i=range.length-1;}else if(++i>=range.length)i=0;var newval=range[i];el.innerHTML=newval;cal.onUpdateTime();return;case 0:if((typeof cal.getDateStatus=="function")&&cal.getDateStatus(date,date.getFullYear(),date.getMonth(),date.getDate())){return false;}break;}if(!date.equalsTo(cal.date)){cal.setDate(date);newdate=true;}else if(el.navtype==0)newdate=closing=true;}if(newdate){ev&&cal.callHandler();}if(closing){Calendar.removeClass(el,"hilite");ev&&cal.callCloseHandler();}};Calendar.prototype.create=function(_par){var parent=null;if(!_par){parent=document.getElementsByTagName("body")[0];this.isPopup=true;}else{parent=_par;this.isPopup=false;}this.date=this.dateStr?new Date(this.dateStr):new Date();var table=Calendar.createElement("table");this.table=table;table.cellSpacing=0;table.cellPadding=0;table.calendar=this;Calendar.addEvent(table,"mousedown",Calendar.tableMouseDown);var div=Calendar.createElement("div");this.element=div;div.className="calendar";if(this.isPopup){div.style.position="absolute";div.style.display="none";}div.appendChild(table);var thead=Calendar.createElement("thead",table);var cell=null;var row=null;var cal=this;var hh=function(text,cs,navtype){cell=Calendar.createElement("td",row);cell.colSpan=cs;cell.className="button";if(navtype!=0&&Math.abs(navtype)<=2)cell.className+=" nav";Calendar._add_evs(cell);cell.calendar=cal;cell.navtype=navtype;cell.innerHTML="<div unselectable='on'>"+text+"</div>";return cell;};row=Calendar.createElement("tr",thead);var title_length=6;(this.isPopup)&&--title_length;(this.weekNumbers)&&++title_length;hh("?",1,400).ttip=Calendar._TT["INFO"];this.title=hh("",title_length,300);this.title.className="title";if(this.isPopup){this.title.ttip=Calendar._TT["DRAG_TO_MOVE"];this.title.style.cursor="move";hh("&#x00d7;",1,200).ttip=Calendar._TT["CLOSE"];}row=Calendar.createElement("tr",thead);row.className="headrow";this._nav_py=hh("&#x00ab;",1,-2);this._nav_py.ttip=Calendar._TT["PREV_YEAR"];this._nav_pm=hh("&#x2039;",1,-1);this._nav_pm.ttip=Calendar._TT["PREV_MONTH"];this._nav_now=hh(Calendar._TT["TODAY"],this.weekNumbers?4:3,0);this._nav_now.ttip=Calendar._TT["GO_TODAY"];this._nav_nm=hh("&#x203a;",1,1);this._nav_nm.ttip=Calendar._TT["NEXT_MONTH"];this._nav_ny=hh("&#x00bb;",1,2);this._nav_ny.ttip=Calendar._TT["NEXT_YEAR"];row=Calendar.createElement("tr",thead);row.className="daynames";if(this.weekNumbers){cell=Calendar.createElement("td",row);cell.className="name wn";cell.innerHTML=Calendar._TT["WK"];}for(var i=7;i>0;--i){cell=Calendar.createElement("td",row);if(!i){cell.navtype=100;cell.calendar=this;Calendar._add_evs(cell);}}this.firstdayname=(this.weekNumbers)?row.firstChild.nextSibling:row.firstChild;this._displayWeekdays();var tbody=Calendar.createElement("tbody",table);this.tbody=tbody;for(i=6;i>0;--i){row=Calendar.createElement("tr",tbody);if(this.weekNumbers){cell=Calendar.createElement("td",row);}for(var j=7;j>0;--j){cell=Calendar.createElement("td",row);cell.calendar=this;Calendar._add_evs(cell);}}if(this.showsTime){row=Calendar.createElement("tr",tbody);row.className="time";cell=Calendar.createElement("td",row);cell.className="time";cell.colSpan=2;cell.innerHTML=Calendar._TT["TIME"]||"&nbsp;";cell=Calendar.createElement("td",row);cell.className="time";cell.colSpan=this.weekNumbers?4:3;(function(){function makeTimePart(className,init,range_start,range_end){var part=Calendar.createElement("span",cell);part.className=className;part.innerHTML=init;part.calendar=cal;part.ttip=Calendar._TT["TIME_PART"];part.navtype=50;part._range=[];if(typeof range_start!="number")part._range=range_start;else{for(var i=range_start;i<=range_end;++i){var txt;if(i<10&&range_end>=10)txt='0'+i;else txt=''+i;part._range[part._range.length]=txt;}}Calendar._add_evs(part);return part;};var hrs=cal.date.getHours();var mins=cal.date.getMinutes();var t12=!cal.time24;var pm=(hrs>12);if(t12&&pm)hrs-=12;var H=makeTimePart("hour",hrs,t12?1:0,t12?12:23);var span=Calendar.createElement("span",cell);span.innerHTML=":";span.className="colon";var M=makeTimePart("minute",mins,0,59);var AP=null;cell=Calendar.createElement("td",row);cell.className="time";cell.colSpan=2;if(t12)AP=makeTimePart("ampm",pm?"pm":"am",["am","pm"]);else cell.innerHTML="&nbsp;";cal.onSetTime=function(){var pm,hrs=this.date.getHours(),mins=this.date.getMinutes();if(t12){pm=(hrs>=12);if(pm)hrs-=12;if(hrs==0)hrs=12;AP.innerHTML=pm?"pm":"am";}H.innerHTML=(hrs<10)?("0"+hrs):hrs;M.innerHTML=(mins<10)?("0"+mins):mins;};cal.onUpdateTime=function(){var date=this.date;var h=parseInt(H.innerHTML,10);if(t12){if(/pm/i.test(AP.innerHTML)&&h<12)h+=12;else if(/am/i.test(AP.innerHTML)&&h==12)h=0;}var d=date.getDate();var m=date.getMonth();var y=date.getFullYear();date.setHours(h);date.setMinutes(parseInt(M.innerHTML,10));date.setFullYear(y);date.setMonth(m);date.setDate(d);this.dateClicked=false;this.callHandler();};})();}else{this.onSetTime=this.onUpdateTime=function(){};}var tfoot=Calendar.createElement("tfoot",table);row=Calendar.createElement("tr",tfoot);row.className="footrow";cell=hh(Calendar._TT["SEL_DATE"],this.weekNumbers?8:7,300);cell.className="ttip";if(this.isPopup){cell.ttip=Calendar._TT["DRAG_TO_MOVE"];cell.style.cursor="move";}this.tooltips=cell;div=Calendar.createElement("div",this.element);this.monthsCombo=div;div.className="combo";for(i=0;i<Calendar._MN.length;++i){var mn=Calendar.createElement("div");mn.className=Calendar.is_ie?"label-IEfix":"label";mn.month=i;mn.innerHTML=Calendar._SMN[i];div.appendChild(mn);}div=Calendar.createElement("div",this.element);this.yearsCombo=div;div.className="combo";for(i=12;i>0;--i){var yr=Calendar.createElement("div");yr.className=Calendar.is_ie?"label-IEfix":"label";div.appendChild(yr);}this._init(this.firstDayOfWeek,this.date);parent.appendChild(this.element);};Calendar._keyEvent=function(ev){var cal=window._dynarch_popupCalendar;if(!cal||cal.multiple)return false;(Calendar.is_ie)&&(ev=window.event);var act=(Calendar.is_ie||ev.type=="keypress"),K=ev.keyCode;if(ev.ctrlKey){switch(K){case 37:act&&Calendar.cellClick(cal._nav_pm);break;case 38:act&&Calendar.cellClick(cal._nav_py);break;case 39:act&&Calendar.cellClick(cal._nav_nm);break;case 40:act&&Calendar.cellClick(cal._nav_ny);break;default:return false;}}else switch(K){case 32:Calendar.cellClick(cal._nav_now);break;case 27:act&&cal.callCloseHandler();break;case 37:case 38:case 39:case 40:if(act){var prev,x,y,ne,el,step;prev=K==37||K==38;step=(K==37||K==39)?1:7;function setVars(){el=cal.currentDateEl;var p=el.pos;x=p&15;y=p>>4;ne=cal.ar_days[y][x];};setVars();function prevMonth(){var date=new Date(cal.date);date.setDate(date.getDate()-step);cal.setDate(date);};function nextMonth(){var date=new Date(cal.date);date.setDate(date.getDate()+step);cal.setDate(date);};while(1){switch(K){case 37:if(--x>=0)ne=cal.ar_days[y][x];else{x=6;K=38;continue;}break;case 38:if(--y>=0)ne=cal.ar_days[y][x];else{prevMonth();setVars();}break;case 39:if(++x<7)ne=cal.ar_days[y][x];else{x=0;K=40;continue;}break;case 40:if(++y<cal.ar_days.length)ne=cal.ar_days[y][x];else{nextMonth();setVars();}break;}break;}if(ne){if(!ne.disabled)Calendar.cellClick(ne);else if(prev)prevMonth();else nextMonth();}}break;case 13:if(act)Calendar.cellClick(cal.currentDateEl,ev);break;default:return false;}return Calendar.stopEvent(ev);};Calendar.prototype._init=function(firstDayOfWeek,date){var today=new Date(),TY=today.getFullYear(),TM=today.getMonth(),TD=today.getDate();this.table.style.visibility="hidden";var year=date.getFullYear();if(year<this.minYear){year=this.minYear;date.setFullYear(year);}else if(year>this.maxYear){year=this.maxYear;date.setFullYear(year);}this.firstDayOfWeek=firstDayOfWeek;this.date=new Date(date);var month=date.getMonth();var mday=date.getDate();var no_days=date.getMonthDays();date.setDate(1);var day1=(date.getDay()-this.firstDayOfWeek)%7;if(day1<0)day1+=7;date.setDate(-day1);date.setDate(date.getDate()+1);var row=this.tbody.firstChild;var MN=Calendar._SMN[month];var ar_days=this.ar_days=new Array();var weekend=Calendar._TT["WEEKEND"];var dates=this.multiple?(this.datesCells={}):null;for(var i=0;i<6;++i,row=row.nextSibling){var cell=row.firstChild;if(this.weekNumbers){cell.className="day wn";cell.innerHTML=date.getWeekNumber();cell=cell.nextSibling;}row.className="daysrow";var hasdays=false,iday,dpos=ar_days[i]=[];for(var j=0;j<7;++j,cell=cell.nextSibling,date.setDate(iday+1)){iday=date.getDate();var wday=date.getDay();cell.className="day";cell.pos=i<<4|j;dpos[j]=cell;var current_month=(date.getMonth()==month);if(!current_month){if(this.showsOtherMonths){cell.className+=" othermonth";cell.otherMonth=true;}else{cell.className="emptycell";cell.innerHTML="&nbsp;";cell.disabled=true;continue;}}else{cell.otherMonth=false;hasdays=true;}cell.disabled=false;cell.innerHTML=this.getDateText?this.getDateText(date,iday):iday;if(dates)dates[date.print("%Y%m%d")]=cell;if(this.getDateStatus){var status=this.getDateStatus(date,year,month,iday);if(this.getDateToolTip){var toolTip=this.getDateToolTip(date,year,month,iday);if(toolTip)cell.title=toolTip;}if(status===true){cell.className+=" disabled";cell.disabled=true;}else{if(/disabled/i.test(status))cell.disabled=true;cell.className+=" "+status;}}if(!cell.disabled){cell.caldate=new Date(date);cell.ttip="_";if(!this.multiple&&current_month&&iday==mday&&this.hiliteToday){cell.className+=" selected";this.currentDateEl=cell;}if(date.getFullYear()==TY&&date.getMonth()==TM&&iday==TD){cell.className+=" today";cell.ttip+=Calendar._TT["PART_TODAY"];}if(weekend.indexOf(wday.toString())!=-1)cell.className+=cell.otherMonth?" oweekend":" weekend";}}if(!(hasdays||this.showsOtherMonths))row.className="emptyrow";}this.title.innerHTML=Calendar._MN[month]+", "+year;this.onSetTime();this.table.style.visibility="visible";this._initMultipleDates();};Calendar.prototype._initMultipleDates=function(){if(this.multiple){for(var i in this.multiple){var cell=this.datesCells[i];var d=this.multiple[i];if(!d)continue;if(cell)cell.className+=" selected";}}};Calendar.prototype._toggleMultipleDate=function(date){if(this.multiple){var ds=date.print("%Y%m%d");var cell=this.datesCells[ds];if(cell){var d=this.multiple[ds];if(!d){Calendar.addClass(cell,"selected");this.multiple[ds]=date;}else{Calendar.removeClass(cell,"selected");delete this.multiple[ds];}}}};Calendar.prototype.setDateToolTipHandler=function(unaryFunction){this.getDateToolTip=unaryFunction;};Calendar.prototype.setDate=function(date){if(!date.equalsTo(this.date)){this._init(this.firstDayOfWeek,date);}};Calendar.prototype.refresh=function(){this._init(this.firstDayOfWeek,this.date);};Calendar.prototype.setFirstDayOfWeek=function(firstDayOfWeek){this._init(firstDayOfWeek,this.date);this._displayWeekdays();};Calendar.prototype.setDateStatusHandler=Calendar.prototype.setDisabledHandler=function(unaryFunction){this.getDateStatus=unaryFunction;};Calendar.prototype.setRange=function(a,z){this.minYear=a;this.maxYear=z;};Calendar.prototype.callHandler=function(){if(this.onSelected){this.onSelected(this,this.date.print(this.dateFormat));}};Calendar.prototype.callCloseHandler=function(){if(this.onClose){this.onClose(this);}this.hideShowCovered();};Calendar.prototype.destroy=function(){var el=this.element.parentNode;el.removeChild(this.element);Calendar._C=null;window._dynarch_popupCalendar=null;};Calendar.prototype.reparent=function(new_parent){var el=this.element;el.parentNode.removeChild(el);new_parent.appendChild(el);};Calendar._checkCalendar=function(ev){var calendar=window._dynarch_popupCalendar;if(!calendar){return false;}var el=Calendar.is_ie?Calendar.getElement(ev):Calendar.getTargetElement(ev);for(;el!=null&&el!=calendar.element;el=el.parentNode);if(el==null){window._dynarch_popupCalendar.callCloseHandler();return Calendar.stopEvent(ev);}};Calendar.prototype.show=function(){var rows=this.table.getElementsByTagName("tr");for(var i=rows.length;i>0;){var row=rows[--i];Calendar.removeClass(row,"rowhilite");var cells=row.getElementsByTagName("td");for(var j=cells.length;j>0;){var cell=cells[--j];Calendar.removeClass(cell,"hilite");Calendar.removeClass(cell,"active");}}this.element.style.display="block";this.hidden=false;if(this.isPopup){window._dynarch_popupCalendar=this;Calendar.addEvent(document,"keydown",Calendar._keyEvent);Calendar.addEvent(document,"keypress",Calendar._keyEvent);Calendar.addEvent(document,"mousedown",Calendar._checkCalendar);}this.hideShowCovered();};Calendar.prototype.hide=function(){if(this.isPopup){Calendar.removeEvent(document,"keydown",Calendar._keyEvent);Calendar.removeEvent(document,"keypress",Calendar._keyEvent);Calendar.removeEvent(document,"mousedown",Calendar._checkCalendar);}this.element.style.display="none";this.hidden=true;this.hideShowCovered();};Calendar.prototype.showAt=function(x,y){var s=this.element.style;s.left=x+"px";s.top=y+"px";this.show();};Calendar.prototype.showAtElement=function(el,opts){var self=this;var p=Calendar.getAbsolutePos(el);if(!opts||typeof opts!="string"){this.showAt(p.x,p.y+el.offsetHeight);return true;}function fixPosition(box){if(box.x<0)box.x=0;if(box.y<0)box.y=0;var cp=document.createElement("div");var s=cp.style;s.position="absolute";s.right=s.bottom=s.width=s.height="0px";document.body.appendChild(cp);var br=Calendar.getAbsolutePos(cp);document.body.removeChild(cp);if(Calendar.is_ie){br.y+=document.body.scrollTop;br.x+=document.body.scrollLeft;}else{br.y+=window.scrollY;br.x+=window.scrollX;}var tmp=box.x+box.width-br.x;if(tmp>0)box.x-=tmp;tmp=box.y+box.height-br.y;if(tmp>0)box.y-=tmp;};this.element.style.display="block";Calendar.continuation_for_the_fucking_khtml_browser=function(){var w=self.element.offsetWidth;var h=self.element.offsetHeight;self.element.style.display="none";var valign=opts.substr(0,1);var halign="l";if(opts.length>1){halign=opts.substr(1,1);}switch(valign){case "T":p.y-=h;break;case "B":p.y+=el.offsetHeight;break;case "C":p.y+=(el.offsetHeight-h)/2;break;case "t":p.y+=el.offsetHeight-h;break;case "b":break;}switch(halign){case "L":p.x-=w;break;case "R":p.x+=el.offsetWidth;break;case "C":p.x+=(el.offsetWidth-w)/2;break;case "l":p.x+=el.offsetWidth-w;break;case "r":break;}p.width=w;p.height=h+40;self.monthsCombo.style.display="none";fixPosition(p);self.showAt(p.x,p.y);};if(Calendar.is_khtml)setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()",10);else Calendar.continuation_for_the_fucking_khtml_browser();};Calendar.prototype.setDateFormat=function(str){this.dateFormat=str;};Calendar.prototype.setTtDateFormat=function(str){this.ttDateFormat=str;};Calendar.prototype.parseDate=function(str,fmt){if(!fmt)fmt=this.dateFormat;this.setDate(Date.parseDate(str,fmt));};Calendar.prototype.hideShowCovered=function(){if(!Calendar.is_ie&&!Calendar.is_opera)return;function getVisib(obj){var value=obj.style.visibility;if(!value){if(document.defaultView&&typeof(document.defaultView.getComputedStyle)=="function"){if(!Calendar.is_khtml)value=document.defaultView. getComputedStyle(obj,"").getPropertyValue("visibility");else value='';}else if(obj.currentStyle){value=obj.currentStyle.visibility;}else value='';}return value;};var tags=new Array("applet","iframe","select");var el=this.element;var p=Calendar.getAbsolutePos(el);var EX1=p.x;var EX2=el.offsetWidth+EX1;var EY1=p.y;var EY2=el.offsetHeight+EY1;for(var k=tags.length;k>0;){var ar=document.getElementsByTagName(tags[--k]);var cc=null;for(var i=ar.length;i>0;){cc=ar[--i];p=Calendar.getAbsolutePos(cc);var CX1=p.x;var CX2=cc.offsetWidth+CX1;var CY1=p.y;var CY2=cc.offsetHeight+CY1;if(this.hidden||(CX1>EX2)||(CX2<EX1)||(CY1>EY2)||(CY2<EY1)){if(!cc.__msh_save_visibility){cc.__msh_save_visibility=getVisib(cc);}cc.style.visibility=cc.__msh_save_visibility;}else{if(!cc.__msh_save_visibility){cc.__msh_save_visibility=getVisib(cc);}cc.style.visibility="hidden";}}}};Calendar.prototype._displayWeekdays=function(){var fdow=this.firstDayOfWeek;var cell=this.firstdayname;var weekend=Calendar._TT["WEEKEND"];for(var i=0;i<7;++i){cell.className="day name";var realday=(i+fdow)%7;if(i){cell.ttip=Calendar._TT["DAY_FIRST"].replace("%s",Calendar._DN[realday]);cell.navtype=100;cell.calendar=this;cell.fdow=realday;Calendar._add_evs(cell);}if(weekend.indexOf(realday.toString())!=-1){Calendar.addClass(cell,"weekend");}cell.innerHTML=Calendar._SDN[(i+fdow)%7];cell=cell.nextSibling;}};Calendar.prototype._hideCombos=function(){this.monthsCombo.style.display="none";this.yearsCombo.style.display="none";};Calendar.prototype._dragStart=function(ev){if(this.dragging){return;}this.dragging=true;var posX;var posY;if(Calendar.is_ie){posY=window.event.clientY+document.body.scrollTop;posX=window.event.clientX+document.body.scrollLeft;}else{posY=ev.clientY+window.scrollY;posX=ev.clientX+window.scrollX;}var st=this.element.style;this.xOffs=posX-parseInt(st.left);this.yOffs=posY-parseInt(st.top);with(Calendar){addEvent(document,"mousemove",calDragIt);addEvent(document,"mouseup",calDragEnd);}};Date._MD=new Array(31,28,31,30,31,30,31,31,30,31,30,31);Date.SECOND=1000;Date.MINUTE=60*Date.SECOND;Date.HOUR=60*Date.MINUTE;Date.DAY=24*Date.HOUR;Date.WEEK=7*Date.DAY;Date.parseDate=function(str,fmt){var today=new Date();var y=0;var m=-1;var d=0;var a=str.split(/\W+/);var b=fmt.match(/%./g);var i=0,j=0;var hr=0;var min=0;for(i=0;i<a.length;++i){if(!a[i])continue;switch(b[i]){case "%d":case "%e":d=parseInt(a[i],10);break;case "%m":m=parseInt(a[i],10)-1;break;case "%Y":case "%y":y=parseInt(a[i],10);(y<100)&&(y+=(y>29)?1900:2000);break;case "%b":case "%B":for(j=0;j<12;++j){if(Calendar._MN[j].substr(0,a[i].length).toLowerCase()==a[i].toLowerCase()){m=j;break;}}break;case "%H":case "%I":case "%k":case "%l":hr=parseInt(a[i],10);break;case "%P":case "%p":if(/pm/i.test(a[i])&&hr<12)hr+=12;else if(/am/i.test(a[i])&&hr>=12)hr-=12;break;case "%M":min=parseInt(a[i],10);break;}}if(isNaN(y))y=today.getFullYear();if(isNaN(m))m=today.getMonth();if(isNaN(d))d=today.getDate();if(isNaN(hr))hr=today.getHours();if(isNaN(min))min=today.getMinutes();if(y!=0&&m!=-1&&d!=0)return new Date(y,m,d,hr,min,0);y=0;m=-1;d=0;for(i=0;i<a.length;++i){if(a[i].search(/[a-zA-Z]+/)!=-1){var t=-1;for(j=0;j<12;++j){if(Calendar._MN[j].substr(0,a[i].length).toLowerCase()==a[i].toLowerCase()){t=j;break;}}if(t!=-1){if(m!=-1){d=m+1;}m=t;}}else if(parseInt(a[i],10)<=12&&m==-1){m=a[i]-1;}else if(parseInt(a[i],10)>31&&y==0){y=parseInt(a[i],10);(y<100)&&(y+=(y>29)?1900:2000);}else if(d==0){d=a[i];}}if(y==0)y=today.getFullYear();if(m!=-1&&d!=0)return new Date(y,m,d,hr,min,0);return today;};Date.prototype.getMonthDays=function(month){var year=this.getFullYear();if(typeof month=="undefined"){month=this.getMonth();}if(((0==(year%4))&&((0!=(year%100))||(0==(year%400))))&&month==1){return 29;}else{return Date._MD[month];}};Date.prototype.getDayOfYear=function(){var now=new Date(this.getFullYear(),this.getMonth(),this.getDate(),0,0,0);var then=new Date(this.getFullYear(),0,0,0,0,0);var time=now-then;return Math.floor(time/Date.DAY);};Date.prototype.getWeekNumber=function(){var d=new Date(this.getFullYear(),this.getMonth(),this.getDate(),0,0,0);var DoW=d.getDay();d.setDate(d.getDate()-(DoW+6)%7+3);var ms=d.valueOf();d.setMonth(0);d.setDate(4);return Math.round((ms-d.valueOf())/(7*864e5))+1;};Date.prototype.equalsTo=function(date){return((this.getFullYear()==date.getFullYear())&&(this.getMonth()==date.getMonth())&&(this.getDate()==date.getDate())&&(this.getHours()==date.getHours())&&(this.getMinutes()==date.getMinutes()));};Date.prototype.setDateOnly=function(date){var tmp=new Date(date);this.setDate(1);this.setFullYear(tmp.getFullYear());this.setMonth(tmp.getMonth());this.setDate(tmp.getDate());};Date.prototype.print=function(str){var m=this.getMonth();var d=this.getDate();var y=this.getFullYear();var wn=this.getWeekNumber();var w=this.getDay();var s={};var hr=this.getHours();var pm=(hr>=12);var ir=(pm)?(hr-12):hr;var dy=this.getDayOfYear();if(ir==0)ir=12;var min=this.getMinutes();var sec=this.getSeconds();s["%a"]=Calendar._SDN[w];s["%A"]=Calendar._DN[w];s["%b"]=Calendar._SMN[m];s["%B"]=Calendar._MN[m];s["%C"]=1+Math.floor(y/100);s["%d"]=(d<10)?("0"+d):d;s["%e"]=d;s["%H"]=(hr<10)?("0"+hr):hr;s["%I"]=(ir<10)?("0"+ir):ir;s["%j"]=(dy<100)?((dy<10)?("00"+dy):("0"+dy)):dy;s["%k"]=hr;s["%l"]=ir;s["%m"]=(m<9)?("0"+(1+m)):(1+m);s["%M"]=(min<10)?("0"+min):min;s["%n"]="\n";s["%p"]=pm?"PM":"AM";s["%P"]=pm?"pm":"am";s["%s"]=Math.floor(this.getTime()/1000);s["%S"]=(sec<10)?("0"+sec):sec;s["%t"]="\t";s["%U"]=s["%W"]=s["%V"]=(wn<10)?("0"+wn):wn;s["%u"]=w+1;s["%w"]=w;s["%y"]=(''+y).substr(2,2);s["%Y"]=y;s["%%"]="%";var re=/%./g;if(!Calendar.is_ie5&&!Calendar.is_khtml)return str.replace(re,function(par){return s[par]||par;});var a=str.match(re);for(var i=0;i<a.length;i++){var tmp=s[a[i]];if(tmp){re=new RegExp(a[i],'g');str=str.replace(re,tmp);}}return str;};Date.prototype.__msh_oldSetFullYear=Date.prototype.setFullYear;Date.prototype.setFullYear=function(y){var d=new Date(this);d.__msh_oldSetFullYear(y);if(d.getMonth()!=this.getMonth())this.setDate(28);this.__msh_oldSetFullYear(y);};window._dynarch_popupCalendar=null;

 //*********************************************************************************************************************************************************
 // ** I18N

 // Calendar EN language
 // Author: Mihai Bazon, <mihai_bazon@yahoo.com>
 // Encoding: any
 // Distributed under the same terms as the calendar itself.

 // For translators: please use UTF-8 if possible.  We strongly believe that
 // Unicode is the answer to a real internationalized world.  Also please
 // include your contact information in the header, as can be seen above.

 // full day names
 Calendar._DN = new Array
 ("Sunday",
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
  "Sunday");

 // Please note that the following array of short day names (and the same goes
 // for short month names, _SMN) isn't absolutely necessary.  We give it here
 // for exemplification on how one can customize the short day names, but if
 // they are simply the first N letters of the full name you can simply say:
 //
 //   Calendar._SDN_len = N; // short day name length
 //   Calendar._SMN_len = N; // short month name length
 //
 // If N = 3 then this is not needed either since we assume a value of 3 if not
 // present, to be compatible with translation files that were written before
 // this feature.

 // short day names
 Calendar._SDN = new Array
 ("Sun",
  "Mon",
  "Tue",
  "Wed",
  "Thu",
  "Fri",
  "Sat",
  "Sun");

 // First day of the week. "0" means display Sunday first, "1" means display
 // Monday first, etc.
 Calendar._FD = 0;

 // full month names
 Calendar._MN = new Array
 ("January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December");

 // short month names
 Calendar._SMN = new Array
 ("Jan",
  "Feb",
  "Mar",
  "Apr",
  "May",
  "Jun",
  "Jul",
  "Aug",
  "Sep",
  "Oct",
  "Nov",
  "Dec");

 // tooltips
 Calendar._TT = {};
 Calendar._TT["INFO"] = "About the calendar";

 Calendar._TT["ABOUT"] =
 "DHTML Date/Time Selector\n" +
 "(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
 "For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
 "Distributed under GNU LGPL.  See http://gnu.org/licenses/lgpl.html for details." +
 "\n\n" +
 "Date selection:\n" +
 "- Use the \xab, \xbb buttons to select year\n" +
 "- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
 "- Hold mouse button on any of the above buttons for faster selection.";
 Calendar._TT["ABOUT_TIME"] = "\n\n" +
 "Time selection:\n" +
 "- Click on any of the time parts to increase it\n" +
 "- or Shift-click to decrease it\n" +
 "- or click and drag for faster selection.";

 Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)";
 Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)";
 Calendar._TT["GO_TODAY"] = "Go Today";
 Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)";
 Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)";
 Calendar._TT["SEL_DATE"] = "Select date";
 Calendar._TT["DRAG_TO_MOVE"] = "Drag to move";
 Calendar._TT["PART_TODAY"] = " (today)";

 // the following is to inform that "%s" is to be the first day of week
 // %s will be replaced with the day name.
 Calendar._TT["DAY_FIRST"] = "Display %s first";

 // This may be locale-dependent.  It specifies the week-end days, as an array
 // of comma-separated numbers.  The numbers are from 0 to 6: 0 means Sunday, 1
 // means Monday, etc.
 Calendar._TT["WEEKEND"] = "0,6";

 Calendar._TT["CLOSE"] = "Close";
 Calendar._TT["TODAY"] = "Today";
 Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value";

 // date formats
 Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
 Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";

 Calendar._TT["WK"] = "wk";
 Calendar._TT["TIME"] = "Time:";
//*********************************************************************************************************************************************************

Calendar.setup=function(params){function param_default(pname,def){if(typeof params[pname]=="undefined"){params[pname]=def;}};param_default("inputField",null);param_default("displayArea",null);param_default("button",null);param_default("eventName","click");param_default("ifFormat","%Y/%m/%d");param_default("daFormat","%Y/%m/%d");param_default("singleClick",true);param_default("disableFunc",null);param_default("dateStatusFunc",params["disableFunc"]);param_default("dateText",null);param_default("firstDay",null);param_default("align","Br");param_default("range",[1900,2999]);param_default("weekNumbers",true);param_default("flat",null);param_default("flatCallback",null);param_default("onSelect",null);param_default("onClose",null);param_default("onUpdate",null);param_default("date",null);param_default("showsTime",false);param_default("timeFormat","24");param_default("electric",true);param_default("step",2);param_default("position",null);param_default("cache",false);param_default("showOthers",false);param_default("multiple",null);var tmp=["inputField","displayArea","button"];for(var i in tmp){if(typeof params[tmp[i]]=="string"){params[tmp[i]]=document.getElementById(params[tmp[i]]);}}if(!(params.flat||params.multiple||params.inputField||params.displayArea||params.button)){alert("Calendar.setup:\n  Nothing to setup (no fields found).  Please check your code");return false;}function onSelect(cal){var p=cal.params;var update=(cal.dateClicked||p.electric);if(update&&p.inputField){p.inputField.value=cal.date.print(p.ifFormat);if(typeof p.inputField.onchange=="function")p.inputField.onchange();}if(update&&p.displayArea)p.displayArea.innerHTML=cal.date.print(p.daFormat);if(update&&typeof p.onUpdate=="function")p.onUpdate(cal);if(update&&p.flat){if(typeof p.flatCallback=="function")p.flatCallback(cal);}if(update&&p.singleClick&&cal.dateClicked)cal.callCloseHandler();};if(params.flat!=null){if(typeof params.flat=="string")params.flat=document.getElementById(params.flat);if(!params.flat){alert("Calendar.setup:\n  Flat specified but can't find parent.");return false;}var cal=new Calendar(params.firstDay,params.date,params.onSelect||onSelect);cal.showsOtherMonths=params.showOthers;cal.showsTime=params.showsTime;cal.time24=(params.timeFormat=="24");cal.params=params;cal.weekNumbers=params.weekNumbers;cal.setRange(params.range[0],params.range[1]);cal.setDateStatusHandler(params.dateStatusFunc);cal.getDateText=params.dateText;if(params.ifFormat){cal.setDateFormat(params.ifFormat);}if(params.inputField&&typeof params.inputField.value=="string"){cal.parseDate(params.inputField.value);}cal.create(params.flat);cal.show();return false;}var triggerEl=params.button||params.displayArea||params.inputField;triggerEl["on"+params.eventName]=function(){var dateEl=params.inputField||params.displayArea;var dateFmt=params.inputField?params.ifFormat:params.daFormat;var mustCreate=false;var cal=window.calendar;if(dateEl)if(dateEl.value&&!(dateEl.value.match(/\d{1,2}\/\d{1,2}\/\d{2,4}/)||dateEl.value.match(/\d{2,4}\-\d{1,2}\-\d{1,2}/))){dateEl.value='';}params.date=Date.parseDate(dateEl.value||dateEl.innerHTML,dateFmt);if(!(cal&&params.cache)){window.calendar=cal=new Calendar(params.firstDay,params.date,params.onSelect||onSelect,params.onClose||function(cal){cal.hide();});cal.showsTime=params.showsTime;cal.time24=(params.timeFormat=="24");cal.weekNumbers=params.weekNumbers;mustCreate=true;}else{if(params.date)cal.setDate(params.date);cal.hide();}if(params.multiple){cal.multiple={};for(var i=params.multiple.length;--i>=0;){var d=params.multiple[i];var ds=d.print("%Y%m%d");cal.multiple[ds]=d;}}cal.showsOtherMonths=params.showOthers;cal.yearStep=params.step;cal.setRange(params.range[0],params.range[1]);cal.params=params;cal.setDateStatusHandler(params.dateStatusFunc);cal.getDateText=params.dateText;cal.setDateFormat(dateFmt);if(mustCreate)cal.create();cal.refresh();if(!params.position)cal.showAtElement(params.button||params.displayArea||params.inputField,params.align);else cal.showAt(params.position[0],params.position[1]);return false;};return cal;};