/*  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/
 *
 *--------------------------------------------------------------------------*/

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) {
    if (element.offsetParent) 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);

/*--------------------------------------------------------------------------*/


// script.aculo.us scriptaculous.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// For details, see the script.aculo.us web site: http://script.aculo.us/

var Scriptaculous = {
  Version: '1.8.2',
  require: function(libraryName) {
    // inserting via DOM fails in Safari 2.0, so brute force approach
    document.write('<script type="text/javascript" src="'+libraryName+'"><\/script>');
  },
  REQUIRED_PROTOTYPE: '1.6.0.3',
  load: function() {
    function convertVersionString(versionString) {
      var v = versionString.replace(/_.*|\./g, '');
      v = parseInt(v + '0'.times(4-v.length));
      return versionString.indexOf('_') > -1 ? v-1 : v;
    }

    if((typeof Prototype=='undefined') ||
       (typeof Element == 'undefined') ||
       (typeof Element.Methods=='undefined') ||
       (convertVersionString(Prototype.Version) <
        convertVersionString(Scriptaculous.REQUIRED_PROTOTYPE)))
       throw("script.aculo.us requires the Prototype JavaScript framework >= " +
        Scriptaculous.REQUIRED_PROTOTYPE);

    var js = /scriptaculous\.js(\?.*)?$/;
    $$('head script[src]').findAll(function(s) {
      return s.src.match(js);
    }).each(function(s) {
      var path = s.src.replace(js, ''),
      includes = s.src.match(/\?.*load=([a-z,]*)/);
      (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider,sound').split(',').each(
       function(include) { Scriptaculous.require(path+include+'.js') });
    });
  }
};

Scriptaculous.load();

// 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')];
};

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//           (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
// Contributors:
//  Richard Livsey
//  Rahul Bhargava
//  Rob Wills
//
// 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/

// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.

if(typeof Effect == 'undefined')
  throw("controls.js requires including script.aculo.us' effects.js library");

var Autocompleter = { };
Autocompleter.Base = Class.create({
  baseInitialize: function(element, update, options) {
    element          = $(element);
    this.element     = element;
    this.update      = $(update);
    this.hasFocus    = false;
    this.changed     = false;
    this.active      = false;
    this.index       = 0;
    this.entryCount  = 0;
    this.oldElementValue = this.element.value;

    if(this.setOptions)
      this.setOptions(options);
    else
      this.options = options || { };

    this.options.paramName    = this.options.paramName || this.element.name;
    this.options.tokens       = this.options.tokens || [];
    this.options.frequency    = this.options.frequency || 0.4;
    this.options.minChars     = this.options.minChars || 1;
    this.options.onShow       = this.options.onShow ||
      function(element, update){
        if(!update.style.position || update.style.position=='absolute') {
          update.style.position = 'absolute';
          Position.clone(element, update, {
            setHeight: false,
            offsetTop: element.offsetHeight
          });
        }
        Effect.Appear(update,{duration:0.15});
      };
    this.options.onHide = this.options.onHide ||
      function(element, update){ new Effect.Fade(update,{duration:0.15}) };

    if(typeof(this.options.tokens) == 'string')
      this.options.tokens = new Array(this.options.tokens);
    // Force carriage returns as token delimiters anyway
    if (!this.options.tokens.include('\n'))
      this.options.tokens.push('\n');

    this.observer = null;

    this.element.setAttribute('autocomplete','off');

    Element.hide(this.update);

    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
    Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
  },

  show: function() {
    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
    if(!this.iefix &&
      (Prototype.Browser.IE) &&
      (Element.getStyle(this.update, 'position')=='absolute')) {
      new Insertion.After(this.update,
       '<iframe id="' + this.update.id + '_iefix" '+
       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
      this.iefix = $(this.update.id+'_iefix');
    }
    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
  },

  fixIEOverlapping: function() {
    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
    this.iefix.style.zIndex = 1;
    this.update.style.zIndex = 2;
    Element.show(this.iefix);
  },

  hide: function() {
    this.stopIndicator();
    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
    if(this.iefix) Element.hide(this.iefix);
  },

  startIndicator: function() {
    if(this.options.indicator) Element.show(this.options.indicator);
  },

  stopIndicator: function() {
    if(this.options.indicator) Element.hide(this.options.indicator);
  },

  onKeyPress: function(event) {
    if(this.active)
      switch(event.keyCode) {
       case Event.KEY_TAB:
       case Event.KEY_RETURN:
         this.selectEntry();
         Event.stop(event);
       case Event.KEY_ESC:
         this.hide();
         this.active = false;
         Event.stop(event);
         return;
       case Event.KEY_LEFT:
       case Event.KEY_RIGHT:
         return;
       case Event.KEY_UP:
         this.markPrevious();
         this.render();
         Event.stop(event);
         return;
       case Event.KEY_DOWN:
         this.markNext();
         this.render();
         Event.stop(event);
         return;
      }
     else
       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;

    this.changed = true;
    this.hasFocus = true;

    if(this.observer) clearTimeout(this.observer);
      this.observer =
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },

  activate: function() {
    this.changed = false;
    this.hasFocus = true;
    this.getUpdatedChoices();
  },

  onHover: function(event) {
    var element = Event.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex)
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    Event.stop(event);
  },

  onClick: function(event) {
    var element = Event.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
    this.selectEntry();
    this.hide();
  },

  onBlur: function(event) {
    // needed to make click events working
    setTimeout(this.hide.bind(this), 250);
    this.hasFocus = false;
    this.active = false;
  },

  render: function() {
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
        this.index==i ?
          Element.addClassName(this.getEntry(i),"selected") :
          Element.removeClassName(this.getEntry(i),"selected");
      if(this.hasFocus) {
        this.show();
        this.active = true;
      }
    } else {
      this.active = false;
      this.hide();
    }
  },

  markPrevious: function() {
    if(this.index > 0) this.index--;
      else this.index = this.entryCount-1;
    this.getEntry(this.index).scrollIntoView(true);
  },

  markNext: function() {
    if(this.index < this.entryCount-1) this.index++;
      else this.index = 0;
    this.getEntry(this.index).scrollIntoView(false);
  },

  getEntry: function(index) {
    return this.update.firstChild.childNodes[index];
  },

  getCurrentEntry: function() {
    return this.getEntry(this.index);
  },

  selectEntry: function() {
    this.active = false;
    this.updateElement(this.getCurrentEntry());
  },

  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }
    var value = '';
    if (this.options.select) {
      var nodes = $(selectedElement).select('.' + this.options.select) || [];
      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
    } else
      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');

    var bounds = this.getTokenBounds();
    if (bounds[0] != -1) {
      var newValue = this.element.value.substr(0, bounds[0]);
      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
      if (whitespace)
        newValue += whitespace[0];
      this.element.value = newValue + value + this.element.value.substr(bounds[1]);
    } else {
      this.element.value = value;
    }
    this.oldElementValue = this.element.value;
    this.element.focus();

    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
  },

  updateChoices: function(choices) {
    if(!this.changed && this.hasFocus) {
      this.update.innerHTML = choices;
      Element.cleanWhitespace(this.update);
      Element.cleanWhitespace(this.update.down());

      if(this.update.firstChild && this.update.down().childNodes) {
        this.entryCount =
          this.update.down().childNodes.length;
        for (var i = 0; i < this.entryCount; i++) {
          var entry = this.getEntry(i);
          entry.autocompleteIndex = i;
          this.addObservers(entry);
        }
      } else {
        this.entryCount = 0;
      }

      this.stopIndicator();
      this.index = 0;

      if(this.entryCount==1 && this.options.autoSelect) {
        this.selectEntry();
        this.hide();
      } else {
        this.render();
      }
    }
  },

  addObservers: function(element) {
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
  },

  onObserverEvent: function() {
    this.changed = false;
    this.tokenBounds = null;
    if(this.getToken().length>=this.options.minChars) {
      this.getUpdatedChoices();
    } else {
      this.active = false;
      this.hide();
    }
    this.oldElementValue = this.element.value;
  },

  getToken: function() {
    var bounds = this.getTokenBounds();
    return this.element.value.substring(bounds[0], bounds[1]).strip();
  },

  getTokenBounds: function() {
    if (null != this.tokenBounds) return this.tokenBounds;
    var value = this.element.value;
    if (value.strip().empty()) return [-1, 0];
    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
    var offset = (diff == this.oldElementValue.length ? 1 : 0);
    var prevTokenPos = -1, nextTokenPos = value.length;
    var tp;
    for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
      if (tp > prevTokenPos) prevTokenPos = tp;
      tp = value.indexOf(this.options.tokens[index], diff + offset);
      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
    }
    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
  }
});

Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
  var boundary = Math.min(newS.length, oldS.length);
  for (var index = 0; index < boundary; ++index)
    if (newS[index] != oldS[index])
      return index;
  return boundary;
};

Ajax.Autocompleter = Class.create(Autocompleter.Base, {
  initialize: function(element, update, url, options) {
    this.baseInitialize(element, update, options);
    this.options.asynchronous  = true;
    this.options.onComplete    = this.onComplete.bind(this);
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },

  getUpdatedChoices: function() {
    this.startIndicator();

    var entry = encodeURIComponent(this.options.paramName) + '=' +
      encodeURIComponent(this.getToken());

    this.options.parameters = this.options.callback ?
      this.options.callback(this.element, entry) : entry;

    if(this.options.defaultParams)
      this.options.parameters += '&' + this.options.defaultParams;

    new Ajax.Request(this.url, this.options);
  },

  onComplete: function(request) {
    this.updateChoices(request.responseText);
  }
});

// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
//                    text only at the beginning of strings in the
//                    autocomplete array. Defaults to true, which will
//                    match text at the beginning of any *word* in the
//                    strings in the autocomplete array. If you want to
//                    search anywhere in the string, additionally set
//                    the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
//                   a partial match (unlike minChars, which defines
//                   how many characters are required to do any match
//                   at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
//                 Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.

Autocompleter.Local = Class.create(Autocompleter.Base, {
  initialize: function(element, update, array, options) {
    this.baseInitialize(element, update, options);
    this.options.array = array;
  },

  getUpdatedChoices: function() {
    this.updateChoices(this.options.selector(this));
  },

  setOptions: function(options) {
    this.options = Object.extend({
      choices: 10,
      partialSearch: true,
      partialChars: 2,
      ignoreCase: true,
      fullSearch: false,
      selector: function(instance) {
        var ret       = []; // Beginning matches
        var partial   = []; // Inside matches
        var entry     = instance.getToken();
        var count     = 0;

        for (var i = 0; i < instance.options.array.length &&
          ret.length < instance.options.choices ; i++) {

          var elem = instance.options.array[i];
          var foundPos = instance.options.ignoreCase ?
            elem.toLowerCase().indexOf(entry.toLowerCase()) :
            elem.indexOf(entry);

          while (foundPos != -1) {
            if (foundPos == 0 && elem.length != entry.length) {
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
                elem.substr(entry.length) + "</li>");
              break;
            } else if (entry.length >= instance.options.partialChars &&
              instance.options.partialSearch && foundPos != -1) {
              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
                  foundPos + entry.length) + "</li>");
                break;
              }
            }

            foundPos = instance.options.ignoreCase ?
              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
              elem.indexOf(entry, foundPos + 1);

          }
        }
        if (partial.length)
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
        return "<ul>" + ret.join('') + "</ul>";
      }
    }, options || { });
  }
});

// AJAX in-place editor and collection editor
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).

// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
  setTimeout(function() {
    Field.activate(field);
  }, 1);
};

Ajax.InPlaceEditor = Class.create({
  initialize: function(element, url, options) {
    this.url = url;
    this.element = element = $(element);
    this.prepareOptions();
    this._controls = { };
    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
    Object.extend(this.options, options || { });
    if (!this.options.formId && this.element.id) {
      this.options.formId = this.element.id + '-inplaceeditor';
      if ($(this.options.formId))
        this.options.formId = '';
    }
    if (this.options.externalControl)
      this.options.externalControl = $(this.options.externalControl);
    if (!this.options.externalControl)
      this.options.externalControlOnly = false;
    this._originalBackground = this.element.getStyle('background-color') || 'transparent';
    this.element.title = this.options.clickToEditText;
    this._boundCancelHandler = this.handleFormCancellation.bind(this);
    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
    this._boundFailureHandler = this.handleAJAXFailure.bind(this);
    this._boundSubmitHandler = this.handleFormSubmission.bind(this);
    this._boundWrapperHandler = this.wrapUp.bind(this);
    this.registerListeners();
  },
  checkForEscapeOrReturn: function(e) {
    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
    if (Event.KEY_ESC == e.keyCode)
      this.handleFormCancellation(e);
    else if (Event.KEY_RETURN == e.keyCode)
      this.handleFormSubmission(e);
  },
  createControl: function(mode, handler, extraClasses) {
    var control = this.options[mode + 'Control'];
    var text = this.options[mode + 'Text'];
    if ('button' == control) {
      var btn = document.createElement('input');
      btn.type = 'submit';
      btn.value = text;
      btn.className = 'editor_' + mode + '_button';
      if ('cancel' == mode)
        btn.onclick = this._boundCancelHandler;
      this._form.appendChild(btn);
      this._controls[mode] = btn;
    } else if ('link' == control) {
      var link = document.createElement('a');
      link.href = '#';
      link.appendChild(document.createTextNode(text));
      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
      link.className = 'editor_' + mode + '_link';
      if (extraClasses)
        link.className += ' ' + extraClasses;
      this._form.appendChild(link);
      this._controls[mode] = link;
    }
  },
  createEditField: function() {
    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
    var fld;
    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
      fld = document.createElement('input');
      fld.type = 'text';
      var size = this.options.size || this.options.cols || 0;
      if (0 < size) fld.size = size;
    } else {
      fld = document.createElement('textarea');
      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
      fld.cols = this.options.cols || 40;
    }
    fld.name = this.options.paramName;
    fld.value = text; // No HTML breaks conversion anymore
    fld.className = 'editor_field';
    if (this.options.submitOnBlur)
      fld.onblur = this._boundSubmitHandler;
    this._controls.editor = fld;
    if (this.options.loadTextURL)
      this.loadExternalText();
    this._form.appendChild(this._controls.editor);
  },
  createForm: function() {
    var ipe = this;
    function addText(mode, condition) {
      var text = ipe.options['text' + mode + 'Controls'];
      if (!text || condition === false) return;
      ipe._form.appendChild(document.createTextNode(text));
    };
    this._form = $(document.createElement('form'));
    this._form.id = this.options.formId;
    this._form.addClassName(this.options.formClassName);
    this._form.onsubmit = this._boundSubmitHandler;
    this.createEditField();
    if ('textarea' == this._controls.editor.tagName.toLowerCase())
      this._form.appendChild(document.createElement('br'));
    if (this.options.onFormCustomization)
      this.options.onFormCustomization(this, this._form);
    addText('Before', this.options.okControl || this.options.cancelControl);
    this.createControl('ok', this._boundSubmitHandler);
    addText('Between', this.options.okControl && this.options.cancelControl);
    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
    addText('After', this.options.okControl || this.options.cancelControl);
  },
  destroy: function() {
    if (this._oldInnerHTML)
      this.element.innerHTML = this._oldInnerHTML;
    this.leaveEditMode();
    this.unregisterListeners();
  },
  enterEditMode: function(e) {
    if (this._saving || this._editing) return;
    this._editing = true;
    this.triggerCallback('onEnterEditMode');
    if (this.options.externalControl)
      this.options.externalControl.hide();
    this.element.hide();
    this.createForm();
    this.element.parentNode.insertBefore(this._form, this.element);
    if (!this.options.loadTextURL)
      this.postProcessEditField();
    if (e) Event.stop(e);
  },
  enterHover: function(e) {
    if (this.options.hoverClassName)
      this.element.addClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onEnterHover');
  },
  getText: function() {
    return this.element.innerHTML.unescapeHTML();
  },
  handleAJAXFailure: function(transport) {
    this.triggerCallback('onFailure', transport);
    if (this._oldInnerHTML) {
      this.element.innerHTML = this._oldInnerHTML;
      this._oldInnerHTML = null;
    }
  },
  handleFormCancellation: function(e) {
    this.wrapUp();
    if (e) Event.stop(e);
  },
  handleFormSubmission: function(e) {
    var form = this._form;
    var value = $F(this._controls.editor);
    this.prepareSubmission();
    var params = this.options.callback(form, value) || '';
    if (Object.isString(params))
      params = params.toQueryParams();
    params.editorId = this.element.id;
    if (this.options.htmlResponse) {
      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Updater({ success: this.element }, this.url, options);
    } else {
      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Request(this.url, options);
    }
    if (e) Event.stop(e);
  },
  leaveEditMode: function() {
    this.element.removeClassName(this.options.savingClassName);
    this.removeForm();
    this.leaveHover();
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
    if (this.options.externalControl)
      this.options.externalControl.show();
    this._saving = false;
    this._editing = false;
    this._oldInnerHTML = null;
    this.triggerCallback('onLeaveEditMode');
  },
  leaveHover: function(e) {
    if (this.options.hoverClassName)
      this.element.removeClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onLeaveHover');
  },
  loadExternalText: function() {
    this._form.addClassName(this.options.loadingClassName);
    this._controls.editor.disabled = true;
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._form.removeClassName(this.options.loadingClassName);
        var text = transport.responseText;
        if (this.options.stripLoadedTextTags)
          text = text.stripTags();
        this._controls.editor.value = text;
        this._controls.editor.disabled = false;
        this.postProcessEditField();
      }.bind(this),
      onFailure: this._boundFailureHandler
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },
  postProcessEditField: function() {
    var fpc = this.options.fieldPostCreation;
    if (fpc)
      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
  },
  prepareOptions: function() {
    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
    [this._extraDefaultOptions].flatten().compact().each(function(defs) {
      Object.extend(this.options, defs);
    }.bind(this));
  },
  prepareSubmission: function() {
    this._saving = true;
    this.removeForm();
    this.leaveHover();
    this.showSaving();
  },
  registerListeners: function() {
    this._listeners = { };
    var listener;
    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
      listener = this[pair.value].bind(this);
      this._listeners[pair.key] = listener;
      if (!this.options.externalControlOnly)
        this.element.observe(pair.key, listener);
      if (this.options.externalControl)
        this.options.externalControl.observe(pair.key, listener);
    }.bind(this));
  },
  removeForm: function() {
    if (!this._form) return;
    this._form.remove();
    this._form = null;
    this._controls = { };
  },
  showSaving: function() {
    this._oldInnerHTML = this.element.innerHTML;
    this.element.innerHTML = this.options.savingText;
    this.element.addClassName(this.options.savingClassName);
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
  },
  triggerCallback: function(cbName, arg) {
    if ('function' == typeof this.options[cbName]) {
      this.options[cbName](this, arg);
    }
  },
  unregisterListeners: function() {
    $H(this._listeners).each(function(pair) {
      if (!this.options.externalControlOnly)
        this.element.stopObserving(pair.key, pair.value);
      if (this.options.externalControl)
        this.options.externalControl.stopObserving(pair.key, pair.value);
    }.bind(this));
  },
  wrapUp: function(transport) {
    this.leaveEditMode();
    // Can't use triggerCallback due to backward compatibility: requires
    // binding + direct element
    this._boundComplete(transport, this.element);
  }
});

Object.extend(Ajax.InPlaceEditor.prototype, {
  dispose: Ajax.InPlaceEditor.prototype.destroy
});

Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
  initialize: function($super, element, url, options) {
    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
    $super(element, url, options);
  },

  createEditField: function() {
    var list = document.createElement('select');
    list.name = this.options.paramName;
    list.size = 1;
    this._controls.editor = list;
    this._collection = this.options.collection || [];
    if (this.options.loadCollectionURL)
      this.loadCollection();
    else
      this.checkForExternalText();
    this._form.appendChild(this._controls.editor);
  },

  loadCollection: function() {
    this._form.addClassName(this.options.loadingClassName);
    this.showLoadingText(this.options.loadingCollectionText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        var js = transport.responseText.strip();
        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
          throw('Server returned an invalid collection representation.');
        this._collection = eval(js);
        this.checkForExternalText();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadCollectionURL, options);
  },

  showLoadingText: function(text) {
    this._controls.editor.disabled = true;
    var tempOption = this._controls.editor.firstChild;
    if (!tempOption) {
      tempOption = document.createElement('option');
      tempOption.value = '';
      this._controls.editor.appendChild(tempOption);
      tempOption.selected = true;
    }
    tempOption.update((text || '').stripScripts().stripTags());
  },

  checkForExternalText: function() {
    this._text = this.getText();
    if (this.options.loadTextURL)
      this.loadExternalText();
    else
      this.buildOptionList();
  },

  loadExternalText: function() {
    this.showLoadingText(this.options.loadingText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._text = transport.responseText.strip();
        this.buildOptionList();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },

  buildOptionList: function() {
    this._form.removeClassName(this.options.loadingClassName);
    this._collection = this._collection.map(function(entry) {
      return 2 === entry.length ? entry : [entry, entry].flatten();
    });
    var marker = ('value' in this.options) ? this.options.value : this._text;
    var textFound = this._collection.any(function(entry) {
      return entry[0] == marker;
    }.bind(this));
    this._controls.editor.update('');
    var option;
    this._collection.each(function(entry, index) {
      option = document.createElement('option');
      option.value = entry[0];
      option.selected = textFound ? entry[0] == marker : 0 == index;
      option.appendChild(document.createTextNode(entry[1]));
      this._controls.editor.appendChild(option);
    }.bind(this));
    this._controls.editor.disabled = false;
    Field.scrollFreeActivate(this._controls.editor);
  }
});

//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
//**** This only  exists for a while,  in order to  let ****
//**** users adapt to  the new API.  Read up on the new ****
//**** API and convert your code to it ASAP!            ****

Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
  if (!options) return;
  function fallback(name, expr) {
    if (name in options || expr === undefined) return;
    options[name] = expr;
  };
  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
    options.cancelLink == options.cancelButton == false ? false : undefined)));
  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
    options.okLink == options.okButton == false ? false : undefined)));
  fallback('highlightColor', options.highlightcolor);
  fallback('highlightEndColor', options.highlightendcolor);
};

Object.extend(Ajax.InPlaceEditor, {
  DefaultOptions: {
    ajaxOptions: { },
    autoRows: 3,                                // Use when multi-line w/ rows == 1
    cancelControl: 'link',                      // 'link'|'button'|false
    cancelText: 'cancel',
    clickToEditText: 'Click to edit',
    externalControl: null,                      // id|elt
    externalControlOnly: false,
    fieldPostCreation: 'activate',              // 'activate'|'focus'|false
    formClassName: 'inplaceeditor-form',
    formId: null,                               // id|elt
    highlightColor: '#ffff99',
    highlightEndColor: '#ffffff',
    hoverClassName: '',
    htmlResponse: true,
    loadingClassName: 'inplaceeditor-loading',
    loadingText: 'Loading...',
    okControl: 'button',                        // 'link'|'button'|false
    okText: 'ok',
    paramName: 'value',
    rows: 1,                                    // If 1 and multi-line, uses autoRows
    savingClassName: 'inplaceeditor-saving',
    savingText: 'Saving...',
    size: 0,
    stripLoadedTextTags: false,
    submitOnBlur: false,
    textAfterControls: '',
    textBeforeControls: '',
    textBetweenControls: ''
  },
  DefaultCallbacks: {
    callback: function(form) {
      return Form.serialize(form);
    },
    onComplete: function(transport, element) {
      // For backward compatibility, this one is bound to the IPE, and passes
      // the element directly.  It was too often customized, so we don't break it.
      new Effect.Highlight(element, {
        startcolor: this.options.highlightColor, keepBackgroundImage: true });
    },
    onEnterEditMode: null,
    onEnterHover: function(ipe) {
      ipe.element.style.backgroundColor = ipe.options.highlightColor;
      if (ipe._effect)
        ipe._effect.cancel();
    },
    onFailure: function(transport, ipe) {
      alert('Error communication with the server: ' + transport.responseText.stripTags());
    },
    onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
    onLeaveEditMode: null,
    onLeaveHover: function(ipe) {
      ipe._effect = new Effect.Highlight(ipe.element, {
        startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
        restorecolor: ipe._originalBackground, keepBackgroundImage: true
      });
    }
  },
  Listeners: {
    click: 'enterEditMode',
    keydown: 'checkForEscapeOrReturn',
    mouseover: 'enterHover',
    mouseout: 'leaveHover'
  }
});

Ajax.InPlaceCollectionEditor.DefaultOptions = {
  loadingCollectionText: 'Loading options...'
};

// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields

Form.Element.DelayedObserver = Class.create({
  initialize: function(element, delay, callback) {
    this.delay     = delay || 0.5;
    this.element   = $(element);
    this.callback  = callback;
    this.timer     = null;
    this.lastValue = $F(this.element);
    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
  },
  delayedListener: function(event) {
    if(this.lastValue == $F(this.element)) return;
    if(this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
    this.lastValue = $F(this.element);
  },
  onTimerEvent: function() {
    this.timer = null;
    this.callback(this.element, $F(this.element));
  }
});

//  Lightview 2.5.2 - 06-10-2009
//  Copyright (c) 2008-2009 Nick Stakenburg (http://www.nickstakenburg.com)
//
//  Licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License
//  http://creativecommons.org/licenses/by-nc-nd/3.0/

//  More information on this project:
//  http://www.nickstakenburg.com/projects/lightview/

var Lightview = {
  Version: '2.5.2',

  // Configuration
  options: {
    backgroundColor: '#ffffff',                            // Background color of the view
    border: 12,                                            // Size of the border
    buttons: {
      opacity: {                                           // Opacity of inner buttons
        disabled: 0.4,
        normal: 0.75,
        hover: 1
      },
      side: { display: true },                             // Toggle side buttons
      innerPreviousNext: { display: true },                // Toggle the inner previous and next button
      slideshow: { display: true },                        // Toggle slideshow button
      topclose: { side: 'right' }                          // 'right' or 'left'                    
    },
    controller: {                                          // The controller is used on sets
      backgroundColor: '#4d4d4d',
      border: 6,
      buttons: {
        innerPreviousNext: true,
        side: false
      },
      margin: 18,
      opacity: 0.7,
      radius: 6,
      setNumberTemplate: '#{position} of #{total}'
    },
    cyclic: false,                                         // Makes galleries cyclic, no end/begin
    images: '/images/lightview/',                          // The directory of the images, from this file
    imgNumberTemplate: 'Image #{position} of #{total}',    // Want a different language? change it here
    keyboard: true,                                        // Toggle keyboard buttons
    menubarPadding: 6,                                     // Space between menubar and content in px
    overlay: {                                             // Overlay
      background: '#000',                                  // Background color, Mac Firefox & Mac Safari use overlay.png
      close: true,
      opacity: 0.85,
      display: true
    },
    preloadHover: false,                                   // Preload images on mouseover
    radius: 12,                                            // Corner radius of the border
    removeTitles: true,                                    // Set to false if you want to keep title attributes intact
    resizeDuration: 0.45,                                  // The duration of the resize effect in seconds
    slideshowDelay: 5,                                     // Delay in seconds before showing the next slide
    titleSplit: '::',                                      // The characters you want to split title with
    transition: function(pos) {                            // Or your own transition
      return ((pos/=0.5) < 1 ? 0.5 * Math.pow(pos, 4) :
        -0.5 * ((pos-=2) * Math.pow(pos,3) - 2));
    },
    viewport: true,                                        // Stay within the viewport, true is recommended
    zIndex: 5000,                                          // zIndex of #lightview, #overlay is this -1

    startDimensions: {                                     // Dimensions Lightview starts at
      width: 100,
      height: 100
    },
    closeDimensions: {                                     // Modify if you've changed the close button images
      large: { width: 77, height: 22 },
      small: { width: 25, height: 22 }
    },
    sideDimensions: {                                      // Modify if you've changed the side button images
      width: 16,
      height: 22
    },

    defaultOptions: {                                      // Default options for each type of view
      image: {
        menubar: 'bottom',
        closeButton: 'large'
      },
      gallery: {
        menubar: 'bottom',
        closeButton: 'large'
      },
      ajax:   {
        width: 400,
        height: 300,
        menubar: 'top',
        closeButton: 'small',
        overflow: 'auto'
      },
      iframe: {
        width: 400,
        height: 300,
        menubar: 'top',
        scrolling: true,
        closeButton: 'small'
      },
      inline: {
        width: 400,
        height: 300,
        menubar: 'top',
        closeButton: 'small',
        overflow: 'auto'
      },
      flash: {
        width: 400,
        height: 300,
        menubar: 'bottom',
        closeButton: 'large'
      },
      quicktime: {
        width: 480,
        height: 220,
        autoplay: true,
        controls: true,
        closeButton: 'large'
      }
    }
  },
  classids: {
    quicktime: 'clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B',
    flash: 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000'
  },
  codebases: {
    quicktime: 'http://www.apple.com/qtactivex/qtplugin.cab#version=7,5,5,0',
    flash: 'http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,115,0'
  },
  errors: {
    requiresPlugin: "<div class='message'> The content your are attempting to view requires the <span class='type'>#{type}</span> plugin.</div><div class='pluginspage'><p>Please download and install the required plugin from:</p><a href='#{pluginspage}' target='_blank'>#{pluginspage}</a></div>"
  },
  mimetypes: {
    quicktime: 'video/quicktime',
    flash: 'application/x-shockwave-flash'
  },
  pluginspages: {
    quicktime: 'http://www.apple.com/quicktime/download',
    flash: 'http://www.adobe.com/go/getflashplayer'
  },
  // used with auto detection
  typeExtensions: {
    flash: 'swf',
    image: 'bmp gif jpeg jpg png',
    iframe: 'asp aspx cgi cfm htm html jsp php pl php3 php4 php5 phtml rb rhtml shtml txt',
    quicktime: 'avi mov mpg mpeg movie'
  }
};

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(){B l=!!19.aq("3y").5T,2G=1m.1Z.2F&&(n(a){B b=u 4A("9b ([\\\\d.]+)").al(a);J b?4J(b[1]):-1})(3b.4T)<7,2C=(1m.1Z.5a&&!19.45),32=3b.4T.24("6r")>-1&&4J(3b.4T.3T(/6r[\\/\\s](\\d+)/)[1])<3,4k=!!3b.4T.3T(/95/i)&&(2C||32);12.1l(Y,{aw:"1.6.1",bn:"1.8.2",R:{1a:"5u",3q:"V"},5x:n(a){m((bo 20[a]=="8M")||(9.5z(20[a].9m)<9.5z(9["8n"+a]))){9O("Y a9 "+a+" >= "+9["8n"+a]);}},5z:n(a){B v=a.2Z(/8w.*|\\./g,"");v=4w(v+"0".bq(4-v.1p));J a.24("8w")>-1?v-1:v},5G:n(){9.5x("1m");m(!!20.11&&!20.6E){9.5x("6E")}m(/^(9L?:\\/\\/|\\/)/.58(9.y.1e)){9.1e=9.y.1e}W{B b=/V(?:-[\\w\\d.]+)?\\.at(.*)/;9.1e=(($$("av[1t]").6N(n(s){J s.1t.3T(b)})||{}).1t||"").2Z(b,"")+9.y.1e}m(!l){m(19.5K>=8&&!19.6Q.3k){19.6Q.bs("3k","bA:bN-bQ-c2:c3","#5R#79")}W{19.1f("5Y:3P",n(){B a=19.9r();a.9B="3k\\\\:*{9I:3Q(#5R#79)}"})}}},60:n(){9.1z=9.y.1z;9.Q=(9.1z>9.y.Q)?9.1z:9.y.Q;9.1I=9.y.1I;9.1R=9.y.1R;9.4E()}});12.1l(Y,{7p:14,2a:n(){B a=3Z.aJ;a.61++;m(a.61==9.7p){$(19.2e).62("V:3P")}}});Y.2a.61=0;12.1l(Y,{4E:n(){9.V=u I("O",{2S:"V"});B d,3G,4N=1P(9.1R);m(2C){9.V.13=n(){9.F("1h:-3C;1b:-3C;1k:1Q;");J 9};9.V.18=n(){9.F("1k:1u");J 9};9.V.1u=n(){J(9.1H("1k")=="1u"&&4J(9.1H("1b").2Z("H",""))>-7K)}}$(19.2e).M(9.2B=u I("O",{2S:"7V"}).F({2Q:9.y.2Q-1,1a:(!(32||2G))?"4r":"35",29:4k?"3Q("+9.1e+"2B.1s) 1b 1h 3A":9.y.2B.29}).1n(4k?1:9.y.2B.1F).13()).M(9.V.F({2Q:9.y.2Q,1b:"-3C",1h:"-3C"}).1n(0).M(9.84=u I("O",{N:"bJ"}).M(9.4b=u I("3z",{N:"c1"}).M(9.8G=u I("1B",{N:"c7"}).F(3G=12.1l({1M:-1*9.1R.E+"H"},4N)).M(9.4Q=u I("O",{N:"6n"}).F(12.1l({1M:9.1R.E+"H"},4N)).M(u I("O",{N:"1D"})))).M(9.8E=u I("1B",{N:"9w"}).F(12.1l({8z:-1*9.1R.E+"H"},4N)).M(9.4O=u I("O",{N:"6n"}).F(3G).M(u I("O",{N:"1D"}))))).M(9.8x=u I("O",{N:"8v"}).M(9.4F=u I("O",{N:"6n 9Q"}).M(9.9S=u I("O",{N:"1D"})))).M(u I("3z",{N:"a8"}).M(u I("1B",{N:"8u ac"}).M(d=u I("O",{N:"ai"}).F({G:9.Q+"H"}).M(u I("3z",{N:"8r ar"}).M(u I("1B",{N:"8i"}).M(u I("O",{N:"2t"})).M(u I("O",{N:"38"}).F({1h:9.Q+"H"})))).M(u I("O",{N:"8h"})).M(u I("3z",{N:"8r az"}).M(u I("1B",{N:"8i"}).F("1N-1b: "+(-1*9.Q)+"H").M(u I("O",{N:"2t"})).M(u I("O",{N:"38"}).F("1h: "+(-1*9.Q)+"H")))))).M(9.4V=u I("1B",{N:"aP"}).F("G: "+(ba-9.Q)+"H").M(u I("O",{N:"bd"}).M(u I("O",{N:"8d"}).F("1N-1b: "+9.Q+"H").M(9.30=u I("O",{N:"bp"}).1n(0).F("3p: 0 "+9.Q+"H").M(9.85=u I("O",{N:"bz 38"})).M(9.1o=u I("O",{N:"bH 80"}).M(9.2c=u I("O",{N:"1D 7X"}).F(1P(9.y.1I.3e)).F({29:9.y.10}).1n(9.y.1A.1F.3f)).M(9.2P=u I("3z",{N:"8L"}).M(9.6b=u I("1B",{N:"94"}).M(9.1C=u I("O",{N:"97"})).M(9.2m=u I("O",{N:"9i"}))).M(9.6a=u I("O",{N:"9n"}).M(9.48=u I("1B",{N:"9u"}).M(u I("O"))).M(9.4Y=u I("1B",{N:"9x"}).M(9.9y=u I("O",{N:"1D"}).1n(9.y.1A.1F.3f).F({10:9.y.10}).1G(9.1e+"9D.1s",{10:9.y.10})).M(9.9E=u I("O",{N:"1D"}).1n(9.y.1A.1F.3f).F({10:9.y.10}).1G(9.1e+"9F.1s",{10:9.y.10}))).M(9.28=u I("1B",{N:"9K"}).M(9.34=u I("O",{N:"1D"}).1n(9.y.1A.1F.3f).F({10:9.y.10}).1G(9.1e+"7I.1s",{10:9.y.10})))))).M(9.7F=u I("O",{N:"9P "}))))).M(9.3v=u I("O",{N:"7E"}).M(9.9Y=u I("O",{N:"1D"}).F("29: 3Q("+9.1e+"3v.64) 1b 1h 4H-3A")))).M(u I("1B",{N:"8u aa"}).M(d.ab(26))).M(9.1V=u I("1B",{N:"aj"}).13().F("1N-1b: "+9.Q+"H; 29: 3Q("+9.1e+"ak.64) 1b 1h 3A"))))).M(u I("O",{2S:"41"}).13());B f=u 2f();f.1w=n(){f.1w=1m.2z;9.1R={E:f.E,G:f.G};B a=1P(9.1R),3G;9.4b.F({1X:0-(f.G/2).2o()+"H",G:f.G+"H"});9.8G.F(3G=12.1l({1M:-1*9.1R.E+"H"},a));9.4Q.F(12.1l({1M:a.E},a));9.8E.F(12.1l({8z:-1*9.1R.E+"H"},a));9.4O.F(3G);9.2a()}.U(9);f.1t=9.1e+"2u.1s";$w("30 1C 2m 48").3W(n(e){9[e].F({10:9.y.10})}.U(9));B g=9.84.2p(".2t");$w("7o 7n bl br").1d(n(a,i){m(9.1z>0){9.5Z(g[i],a)}W{g[i].M(u I("O",{N:"38"}))}g[i].F({E:9.Q+"H",G:9.Q+"H"}).7g("2t"+a.1K());9.2a()}.U(9));9.V.2p(".8h",".38",".8d").3F("F",{10:9.y.10});B S={};$w("2u 1c 2k").1d(n(s){9[s+"3i"].1J=s;B b=9.1e+s+".1s";m(s=="2k"){S[s]=u 2f();S[s].1w=n(){S[s].1w=1m.2z;9.1I[s]={E:S[s].E,G:S[s].G};B a=9.y.1A.2k.1J,27=12.1l({"5Q":a,1X:9.1I[s].G+"H"},1P(9.1I[s]));27["3p"+a.1K()]=9.Q+"H";9[s+"3i"].F(27);9.8x.F({G:S[s].G+"H",1b:-1*9.1I[s].G+"H"});9[s+"3i"].5N().1G(b).F(1P(9.1I[s]));9.2a()}.U(9);S[s].1t=9.1e+s+".1s"}W{9[s+"3i"].1G(b)}},9);B C={};$w("3e 5M").1d(n(a){C[a]=u 2f();C[a].1w=n(){C[a].1w=1m.2z;9.1I[a]={E:C[a].E,G:C[a].G};9.2a()}.U(9);C[a].1t=9.1e+"6T"+a+".1s"},9);B L=u 2f();L.1w=n(){L.1w=1m.2z;9.3v.F({E:L.E+"H",G:L.G+"H",1X:-0.5*L.G+0.5*9.Q+"H",1M:-0.5*L.E+"H"});9.2a()}.U(9);L.1t=9.1e+"3v.64";B h=u 2f();h.1w=n(a){h.1w=1m.2z;B b={E:h.E+"H",G:h.G+"H"};9.28.F(b);9.34.F(b);9.2a()}.U(9);h.1t=9.1e+"6P.1s";$w("2u 1c").1d(n(s){B S=s.1K(),i=u 2f();i.1w=n(){i.1w=1m.2z;9["3r"+S+"3s"].F({E:i.E+"H",G:i.G+"H"});9.2a()}.U(9);i.1t=9.1e+"9o"+s+".1s";9["3r"+S+"3s"].1V=s},9);$w("28 4Y 48").1d(n(c){9[c].13=9[c].13.1v(n(a,b){9.27.1a="35";a(b);J 9});9[c].18=9[c].18.1v(n(a,b){9.27.1a="9v";a(b);J 9})},9);9.V.2p("*").3F("F",{2Q:9.y.2Q+1});9.V.13();9.2a()},6K:n(){11.2J.2I("V").3W(n(e){e.6F()});9.1S=1E;m(9.q.1O()){9.6w=9.6q;m(9.X&&!9.X.1u()){9.X.F("1k:1Q").18();9.3g.1n(0)}}W{9.6w=1E;9.X.13()}m(4w(9.4F.1H("1X"))<9.1I.2k.G){9.5B(2H)}9.8H();9.8y();u 11.1i({R:9.R,1q:n(){$w("1b 3K").1d(n(a){B b=a.1K();9["3E"+b].2n();B c={};9["3E"+b]=u I("O",{N:"ad"+b}).13();c[a]=9["3E"+b];9.30.M(c)}.U(9))}.U(9)});9.5A();9.1j=1E},5y:n(){m(!9.3J||!9.3V){J}9.3V.M({2W:9.3J.F({2q:9.3J.87})});9.3V.2n();9.3V=1E},18:n(b){9.1y=1E;B c=12.7W(b);m(12.7N(b)||c){m(c&&b.3x("#")){9.18({1g:b,y:12.1l({55:26},3Z[1]||{})});J}9.1y=$(b);m(!9.1y){J}9.1y.aW();9.q=9.1y.22||u Y.3N(9.1y)}W{m(b.1g){9.1y=$(19.2e);9.q=u Y.3N(b)}W{m(12.7v(b)){9.1y=9.4j(9.q.1Y)[b];9.q=9.1y.22}}}m(!9.q.1g){J}9.6K();m(9.q.2i()||9.q.1O()){9.7r(9.q.1Y);9.1j=9.5s(9.q.1Y);m(9.q.1O()){9.2s=9.1j.1p>1?9.7e:0;9.2V=9.1j.bK(n(a){J a.2T()})}}9.3R();9.7c();m(9.q.1g!="#41"&&12.70(Y.4u).6W(" ").24(9.q.17)>=0){m(!Y.4u[9.q.17]){$("41").1x(u 4y(9.8U.8V).45({17:9.q.17.1K(),5l:9.5k[9.q.17]}));B d=$("41").2l();9.18({1g:"#41",1C:9.q.17.1K()+" 98 99",y:d});J 2H}}B e=12.1l({1o:"3K",2k:2H,5j:"9h",3X:9.q.2i()&&9.y.1A.3X.2q,5i:9.y.5i,28:(9.q.2i()&&9.y.1A.28.2q)||(9.2V),2A:"1Q",7Z:9.y.2B.9p,33:9.y.33},9.y.9t[9.q.17]||{});9.q.y=12.1l(e,9.q.y);m(9.q.1O()){9.q.y.2k=(9.1j.1p<=1)}m(!(9.q.1C||9.q.2m||(9.1j&&9.1j.1p>1))&&9.q.y.2k){9.q.y.1o=2H}9.1T="3E"+(9.q.y.1o=="1b"?"7M":"7G");m(9.q.2T()){m(!l&&!9.q.7w){9.q.7w=26;B f=u I("3k:2h",{1t:9.q.1g,2q:"9z"}).F("G:5h;E:5h;");$(19.2e).M(f);I.2n.2X(0.1,f)}m(9.q.2i()||9.q.1O()){9.1a=9.1j.24(9.q);9.74()}9.1W=9.q.4P;m(9.1W){9.4G()}W{9.5d();B f=u 2f();f.1w=n(){f.1w=1m.2z;9.4S();9.1W={E:f.E,G:f.G};9.4G()}.U(9);f.1t=9.q.1g}}W{m(9.q.1O()){9.1a=9.1j.24(9.q)}9.1W=9.q.y.6M?19.33.2l():{E:9.q.y.E,G:9.q.y.G};9.4G()}},4U:(n(){n 5c(a,b,c){a=$(a);B d=1P(c);a.1x(u I("82",{2S:"2w",1t:b,a6:"",a7:"4H"}).F(d))}B k=(n(){n 7f(a,b,c){a=$(a);B d=12.1l({"5Q":"1h"},1P(c));B e=u I("3k:2h",{1t:b,2S:"2w"}).F(d);a.1x(e);e.51=e.51}n 6Z(b,c,d){b=$(b);B f=1P(d),2h=u 2f();2h.1w=n(){3y=u I("3y",f);b.1x(3y);4c{B a=3y.5T("2d");a.ah(2h,0,0,d.E,d.G)}4e(e){5c(b,c,d)}}.U(9);2h.1t=c}m(1m.1Z.2F){J 7f}W{J 6Z}})();J n(){B c=9.8a(9.q.1g),2D=9.1S||9.1W;m(9.q.2T()){B d=1P(2D);9[9.1T].F(d);m(9.1S){k(9[9.1T],9.q.1g,2D)}W{5c(9[9.1T],9.q.1g,2D)}}W{m(9.q.5p()){59(9.q.17){2M"4f":B f=12.5f(9.q.y.4f)||{};B g=n(){9.4S();m(9.q.y.55){9[9.1T].F({E:"1L",G:"1L"});9.1W=9.5b(9[9.1T])}u 11.1i({R:9.R,1q:9.52.U(9)})}.U(9);m(f.4Z){f.4Z=f.4Z.1v(n(a,b){g();a(b)})}W{f.4Z=g}9.5d();u aF.aH(9[9.1T],9.q.1g,f);2v;2M"2x":m(9.1S){2D.G-=9.3a.G}9[9.1T].1x(9.2x=u I("2x",{b1:0,b9:0,1t:9.q.1g,2S:"2w",2b:"bc"+(6z.bf()*bg).2o(),6J:(9.q.y&&9.q.y.6J)?"1L":"4H"}).F(12.1l({Q:0,1N:0,3p:0},1P(2D))));2v;2M"4R":B h=9.q.1g,2g=$(h.5e(h.24("#")+1));m(!2g||!2g.47){J}B i=2g.2l();2g.M({by:9.3V=u I(2g.47).13()});2g.87=2g.1H("2q");9.3J=2g.18();9[9.1T].1x(9.3J);9[9.1T].2p("2p, 3t, 5g").1d(n(b){9.44.1d(n(a){m(a.1y==b){b.F({1k:a.1k})}})}.U(9));m(9.q.y.55){9.1W=i;u 11.1i({R:9.R,1q:9.52.U(9)})}2v}}W{B j={1U:"3t",2S:"2w",E:2D.E,G:2D.G};59(9.q.17){2M"40":12.1l(j,{5l:9.5k[9.q.17],3o:[{1U:"2y",2b:"88",2N:9.q.y.88},{1U:"2y",2b:"8k",2N:"8I"},{1U:"2y",2b:"X",2N:9.q.y.6p},{1U:"2y",2b:"9M",2N:26},{1U:"2y",2b:"1t",2N:9.q.1g},{1U:"2y",2b:"6s",2N:9.q.y.6s||2H}]});12.1l(j,1m.1Z.2F?{8N:9.8O[9.q.17],8P:9.8R[9.q.17]}:{2P:9.q.1g,17:9.6t[9.q.17]});2v;2M"3U":12.1l(j,{2P:9.q.1g,17:9.6t[9.q.17],8W:"8X",5j:9.q.y.5j,5l:9.5k[9.q.17],3o:[{1U:"2y",2b:"8Y",2N:9.q.1g},{1U:"2y",2b:"8Z",2N:"26"}]});m(9.q.y.6D){j.3o.3S({1U:"2y",2b:"96",2N:9.q.y.6D})}2v}9[9.1T].F(1P(2D)).1x(9.5m(j)).F("1k:1Q").18();m(9.q.4v()){(n(){4c{m("6O"6S $("2w")){$("2w").6O(9.q.y.6p)}}4e(e){}}.U(9)).9c()}}}}})(),5b:n(b){b=$(b);B d=b.9d(),5n=[],5o=[];d.3S(b);d.1d(n(c){m(c!=b&&c.1u()){J}5n.3S(c);5o.3S({2q:c.1H("2q"),1a:c.1H("1a"),1k:c.1H("1k")});c.F({2q:"9j",1a:"35",1k:"1u"})});B e={E:b.9k,G:b.9l};5n.1d(n(r,a){r.F(5o[a])});J e},4t:n(){B a=$("2w");m(a){59(a.47.4s()){2M"3t":m(1m.1Z.5a&&9.q.4v()){4c{a.71()}4e(e){}a.9q=""}m(a.72){a.2n()}W{a=1m.2z}2v;2M"2x":a.2n();m(1m.1Z.9s&&20.73.2w){5q 20.73.2w}2v;5R:a.2n();2v}}$w("7G 7M").1d(n(S){9["3E"+S].F("E:1L;G:1L;").1x("").13()},9)},77:1m.K,4G:n(){u 11.1i({R:9.R,1q:9.4o.U(9)})},4o:n(){9.3c();m(!9.q.5r()){9.4S()}m(!((9.q.y.55&&9.q.7h())||9.q.5r())){9.52()}m(!9.q.4l()){u 11.1i({R:9.R,1q:9.4U.U(9)})}m(9.q.y.2k){u 11.1i({R:9.R,1q:9.5B.U(9,26)})}},7l:n(){u 11.1i({R:9.R,1q:9.7q.U(9)});m(9.q.4l()){u 11.1i({2X:0.2,R:9.R,1q:9.4U.U(9)})}m(9.3n){u 11.1i({R:9.R,1q:9.7u.U(9)})}m(9.q.4v()||9.q.9J()){u 11.1i({R:9.R,2X:0.1,1q:I.F.U(9,9[9.1T],"1k:1u")})}},2K:n(){m(11.2J.2I(Y.R.3q).5t.1p){J}9.18(9.2O().2K)},1c:n(){m(11.2J.2I(Y.R.3q).5t.1p){J}9.18(9.2O().1c)},52:n(){9.77();B a=9.5v(),2Y=9.7P();m(9.q.y.33&&(a.E>2Y.E||a.G>2Y.G)){m(9.q.y.6M){9.1S=2Y;9.3c();a=2Y}W{B c=9.7S(),b=2Y;m(9.q.4W()){B d=[2Y.G/c.G,2Y.E/c.E,1].a4();9.1S={E:(9.1W.E*d).2o(),G:(9.1W.G*d).2o()}}W{9.1S={E:c.E>b.E?b.E:c.E,G:c.G>b.G?b.G:c.G}}9.3c();a=12.5f(9.1S);m(9.q.4W()){a.G+=9.3a.G}}}W{9.3c();9.1S=1E}9.5w(a)},3I:n(a){9.5w(a,{23:0})},5w:(n(){B e,4L,4K,8c,8e,2s,b;B f=(n(){B w,h;n 4I(p){w=(e.E+p*4L).3L(0);h=(e.G+p*4K).3L(0)}B a;m(2G){a=n(p){9.V.F({E:(e.E+p*4L).3L(0)+"H",G:(e.G+p*4K).3L(0)+"H"});9.4V.F({G:h-1*9.Q+"H"})}}W{m(32){a=n(p){B v=9.4C(),o=19.33.6o();9.V.F({1a:"35",1M:0,1X:0,E:w+"H",G:h+"H",1h:(o[0]+(v.E/2)-(w/2)).3M()+"H",1b:(o[1]+(v.G/2)-(h/2)).3M()+"H"});9.4V.F({G:h-1*9.Q+"H"})}}W{a=n(p){9.V.F({1a:"4r",E:w+"H",G:h+"H",1M:((0-w)/2).2o()+"H",1X:((0-h)/2-2s).2o()+"H"});9.4V.F({G:h-1*9.Q+"H"})}}}J n(p){4I.3w(9,p);a.3w(9,p)}})();J n(a){B c=3Z[1]||{};e=9.V.2l();b=2*9.Q;E=a.E?a.E+b:e.E;G=a.G?a.G+b:e.G;9.5C();m(e.E==E&&e.G==G){u 11.1i({R:9.R,1q:9.5D.U(9,a)});J}B d={E:E+"H",G:G+"H"};4L=E-e.E;4K=G-e.G;8c=4w(9.V.1H("1M").2Z("H",""));8e=4w(9.V.1H("1X").2Z("H",""));2s=9.X.1u()?(9.2s/2):0;m(!2G){12.1l(d,{1M:0-E/2+"H",1X:0-G/2+"H"})}m(c.23==0){f.3w(9,1)}W{9.5E=u 11.6u(9.V,0,1,12.1l({23:9.y.ax,R:9.R,6v:9.y.6v,1q:9.5D.U(9,a)},c),f.U(9))}}})(),5D:n(a){m(!9.3a){J}B b=9[9.1T],4p;m(9.q.y.2A=="1L"){4p=b.2l()}b.F({G:(a.G-9.3a.G)+"H",E:a.E+"H"});m(9.q.y.2A!="1Q"&&(9.q.5r()||9.q.7h())){m(1m.1Z.2F){m(9.q.y.2A=="1L"){B c=b.2l();b.F("2A:1u");B d={6x:"1Q",6y:"1Q"},5F=0,4n=15;m(4p.G>a.G){d.6y="1L";d.E=c.E-4n;d.aX="6A";5F=4n}m(4p.E-5F>a.E){d.6x="1L";d.G=c.G-4n;d.b2="6A"}b.F(d)}W{b.F({2A:9.q.y.2A})}}W{b.F({2A:9.q.y.2A})}}W{b.F("2A:1Q")}9.3R();9.5E=1E;9.7l()},7q:n(){u 11.1i({R:9.R,1q:9.5C.U(9)});u 11.1i({R:9.R,1q:n(){9[9.1T].18();9.3c();m(9.1o.1u()){9.1o.F("1k:1u").1n(1)}}.U(9)});u 11.b6([u 11.6B(9.30,{6C:26,4m:0,57:1}),u 11.53(9.4b,{6C:26})],{R:9.R,23:0.25,1q:n(){m(9.1y){9.1y.62("V:bh")}}.U(9)});m(9.q.2i()||(9.2V&&9.y.X.1A.1J)){u 11.1i({R:9.R,1q:9.6G.U(9)})}},8y:(n(){n 2W(){9.4t();9.4F.F({1X:9.1I.2k.G+"H"});9.5y()}n 6H(p){9.30.1n(p);9.4b.1n(p)}J n(){m(!9.V.1u()){9.30.1n(0);9.4b.1n(0);9.4t();J}u 11.6u(9.V,1,0,{23:0.2,R:9.R,1q:2W.U(9)},6H.U(9))}})(),6I:n(){$w("6a 2P 6b 1C 2m 48 4Y 28 2c").1d(n(a){I.13(9[a])},9);9.1o.F("1k:1Q").1n(0)},3c:n(){9.6I();m(!9.q.y.1o){9.3a={E:0,G:0};9.5H=0;9.1o.13()}W{9.1o.18()}m(9.q.1C||9.q.2m){9.6b.18();9.2P.18()}m(9.q.1C){9.1C.1x(9.q.1C).18()}m(9.q.2m){9.2m.1x(9.q.2m).18()}m(9.1j&&9.1j.1p>1){m(9.q.1O()){9.2r.1x(u 4y(9.y.X.6L).45({1a:9.1a+1,5I:9.1j.1p}));m(9.X.1H("1k")=="1Q"){9.X.F("1k:1u");m(9.5J){11.2J.2I("V").2n(9.5J)}9.5J=u 11.53(9.3g,{R:9.R,23:0.1})}}W{9.2P.18();m(9.q.2T()){9.6a.18();9.48.18().5N().1x(u 4y(9.y.bF).45({1a:9.1a+1,5I:9.1j.1p}));m(9.q.y.28){9.34.18();9.28.18()}}}}B a=9.q.1O();m((9.q.y.3X||a)&&9.1j.1p>1){B b={2u:(9.y.31||9.1a!=0),1c:(9.y.31||((9.q.2i()||a)&&9.2O().1c!=0))};$w("2u 1c").1d(n(z){B Z=z.1K(),3u=b[z]?"6R":"1L";m(a){9["X"+Z].F({3u:3u}).1n(b[z]?1:9.y.1A.1F.5L)}W{9["3r"+Z+"3s"].F({3u:3u}).1n(b[z]?9.y.1A.1F.3f:9.y.1A.1F.5L)}}.U(9));m(9.q.y.3X||9.y.X.3X){9.4Y.18()}}9.3O.1n(9.2V?1:9.y.1A.1F.5L).F({3u:9.2V?"6R":"1L"});9.6U();m(!9.1o.c4().6N(I.1u)){9.1o.13();9.q.y.1o=2H}9.6V()},6U:n(){B a=9.1I.5M.E,3e=9.1I.3e.E,3d=9.1S?9.1S.E:9.1W.E,4D=8J,E=0,2c=9.q.y.2c||"3e",29=9.y.8K;m(9.q.y.2k||9.q.1O()||!9.q.y.2c){29=1E}W{m(3d>=4D+a&&3d<4D+3e){29="5M";E=a}W{m(3d>=4D+3e){29=2c;E=9.1I[2c].E}}}m(E>0){9.2P.18();9.2c.F({E:E+"H"}).18()}W{9.2c.13()}m(29){9.2c.1G(9.1e+"6T"+29+".1s",{10:9.y.10})}9.5H=E},5d:n(){9.5O=u 11.53(9.3v,{23:0.2,4m:0,57:1,R:9.R})},4S:n(){m(9.5O){11.2J.2I("V").2n(9.5O)}u 11.6X(9.3v,{23:0.2,R:9.R,2X:0.2})},6Y:n(){m(!9.q.2T()){J}B a=(9.y.31||9.1a!=0),1c=(9.y.31||((9.q.2i()||9.q.1O())&&9.2O().1c!=0));9.4Q[a?"18":"13"]();9.4O[1c?"18":"13"]();B b=9.1S||9.1W;9.1V.F({G:b.G+"H",1X:9.Q+(9.q.y.1o=="1b"?9.1o.5P():0)+"H"});B c=((b.E/2-1)+9.Q).3M();m(a){9.1V.M(9.3j=u I("O",{N:"1D 8Q"}).F({E:c+"H"}));9.3j.1J="2u"}m(1c){9.1V.M(9.3h=u I("O",{N:"1D 8S"}).F({E:c+"H"}));9.3h.1J="1c"}m(a||1c){9.1V.18()}},6G:n(){m(!9.q||!9.y.1A.1J.2q||!9.q.2T()){J}9.6Y();9.1V.18()},5C:n(){9.1V.1x("").13();9.4Q.13().F({1M:9.1R.E+"H"});9.4O.13().F({1M:-1*9.1R.E+"H"})},7c:(n(){n 2W(){9.V.1n(1)}m(!2C){2W=2W.1v(n(a,b){a(b);9.V.18()})}J n(){m(9.V.1H("1F")!=0){J}m(9.y.2B.2q){u 11.53(9.2B,{23:0.2,4m:0,57:4k?1:9.y.2B.1F,R:9.R,8T:9.5S.U(9),1q:2W.U(9)})}W{2W.3w(9)}}})(),13:n(){m(1m.1Z.2F&&9.2x&&9.q.4l()){9.2x.2n()}m(2C&&9.q.4v()){B a=$$("3t#2w")[0];m(a){4c{a.71()}4e(e){}}}m(9.V.1H("1F")==0){J}9.2j();9.1V.13();m(!1m.1Z.2F||!9.q.4l()){9.30.13()}m(11.2J.2I("5U").5t.1p>0){J}11.2J.2I("V").1d(n(e){e.6F()});u 11.1i({R:9.R,1q:9.5y.U(9)});u 11.6B(9.V,{23:0.1,4m:1,57:0,R:{1a:"5u",3q:"5U"}});u 11.6X(9.2B,{23:0.16,R:{1a:"5u",3q:"5U"},1q:9.75.U(9)})},75:n(){9.4t();9.V.13();9.30.1n(0).18();9.1V.1x("").13();9.85.1x("").13();9.7F.1x("").13();9.5A();9.76();u 11.1i({R:9.R,1q:9.3I.U(9,9.y.90)});u 11.1i({R:9.R,1q:n(){m(9.1y){9.1y.62("V:1Q")}$w("1y 1j q 1S 2V 91 3E").3W(n(a){9[a]=1E}.U(9))}.U(9)})},6V:n(){9.1o.F("3p:0;");B a={},3d=9[(9.1S?"92":"i")+"93"].E;9.1o.F({E:3d+"H"});9.2P.F({E:3d-9.5H-1+"H"});a=9.5b(9.1o);m(9.q.y.1o){a.G+=9.y.5V;59(9.q.y.1o){2M"3K":9.1o.F("3p:"+9.y.5V+"H 0 0 0");2v;2M"1b":9.1o.F("3p: 0 0 "+9.y.5V+"H 0");2v}}9.1o.F({E:"78%"});9.3a=9.q.y.1o?a:{E:a.E,G:0}},3R:(n(){B a,2s;n 4I(){a=9.V.2l();2s=9.X.1u()?(9.2s/2):0}B b;m(2G){b=n(){9.V.F({1b:"50%",1h:"50%"})}}W{m(2C||32){b=n(){B v=9.4C(),o=19.33.6o();9.V.F({1M:0,1X:0,1h:(o[0]+(v.E/2)-(a.E/2)).3M()+"H",1b:(o[1]+(v.G/2)-(a.G/2)).3M()+"H"})}}W{b=n(){9.V.F({1a:"4r",1h:"50%",1b:"50%",1M:(0-a.E/2).2o()+"H",1X:(0-a.G/2-2s).2o()+"H"})}}}J n(){4I.3w(9);b.3w(9)}})(),7a:n(){9.2j();9.3n=26;9.1c.U(9).2X(0.25);9.34.1G(9.1e+"6P.1s",{10:9.y.10}).13();9.3O.1G(9.1e+"7b.1s",{10:9.y.X.10})},2j:n(){m(9.3n){9.3n=2H}m(9.5W){9a(9.5W)}9.34.1G(9.1e+"7I.1s",{10:9.y.10});9.3O.1G(9.1e+"7d.1s",{10:9.y.X.10})},5X:n(){m(9.q.1O()&&!9.2V){J}9[(9.3n?"4X":"60")+"9e"]()},7u:n(){m(9.3n){9.5W=9.1c.U(9).2X(9.y.9f)}},9g:n(){$$("a[2U~=V], 3B[2U~=V]").1d(n(a){B b=a.22;m(!b){J}m(b.3H){a.7i("1C",b.3H)}a.22=1E})},4j:n(a){B b=a.24("][");m(b>-1){a=a.5e(0,b+1)}J $$(\'a[1Y^="\'+a+\'"], 3B[1Y^="\'+a+\'"]\')},5s:n(a){J 9.4j(a).7j("22")},7k:n(){$(19.2e).1f("2L",9.7m.1r(9));$w("2R 3Y").1d(n(e){9.1V.1f(e,n(a){B b=a.3m("O");m(!b){J}m(9.3j&&9.3j==b||9.3h&&9.3h==b){9.54(a)}}.1r(9))}.U(9));9.1V.1f("2L",n(c){B d=c.3m("O");m(!d){J}B e=(9.3j&&9.3j==d)?"2K":(9.3h&&9.3h==d)?"1c":1E;m(e){9[e].1v(n(a,b){9.2j();a(b)}).U(9)()}}.1r(9));$w("2u 1c").1d(n(s){B S=s.1K(),2j=n(a,b){9.2j();a(b)},42=n(a,b){B c=b.1y().1V;m((c=="2u"&&(9.y.31||9.1a!=0))||(c=="1c"&&(9.y.31||((9.q.2i()||9.q.1O())&&9.2O().1c!=0)))){a(b)}};9[s+"3i"].1f("2R",9.54.1r(9)).1f("3Y",9.54.1r(9)).1f("2L",9[s=="1c"?s:"2K"].1v(2j).1r(9));9["3r"+S+"3s"].1f("2L",9[s=="1c"?s:"2K"].1v(42).1v(2j).1r(9)).1f("2R",I.1n.7s(9["3r"+S+"3s"],9.y.1A.1F.7t).1v(42).1r(9)).1f("3Y",I.1n.7s(9["3r"+S+"3s"],9.y.1A.1F.3f).1v(42).1r(9));9["X"+S].1f("2L",9[s=="1c"?s:"2K"].1v(42).1v(2j).1r(9))},9);B f=[9.2c,9.34];m(!2C){f.1d(n(b){b.1f("2R",I.1n.U(9,b,9.y.1A.1F.7t)).1f("3Y",I.1n.U(9,b,9.y.1A.1F.3f))},9)}W{f.3F("1n",1)}9.34.1f("2L",9.5X.1r(9));9.3O.1f("2L",9.5X.1r(9));m(2C||32){B g=n(a,b){m(9.V.1H("1b").63(0)=="-"){J}a(b)};1i.1f(20,"43",9.3R.1v(g).1r(9));1i.1f(20,"3I",9.3R.1v(g).1r(9))}m(32){1i.1f(20,"3I",9.5S.1r(9))}m(2G){n 65(){m(9.X){9.X.F({1h:((19.7x.9A||0)+19.33.7y()/2).2o()+"H"})}}1i.1f(20,"43",65.1r(9));1i.1f(20,"3I",65.1r(9))}m(9.y.9C){9.7z=n(a){B b=a.3m("a[2U~=V], 3B[2U~=V]");m(!b){J}a.4X();m(!b.22){u Y.3N(b)}9.7A(b)}.1r(9);$(19.2e).1f("2R",9.7z)}},5B:n(a){m(9.7B){11.2J.2I("9G").2n(9.9H)}B b={1X:(a?0:9.1I.2k.G)+"H"};9.7B=u 11.7C(9.4F,{27:b,23:0.16,R:9.R,2X:a?0.15:0})},7D:n(){B a={};$w("E G").1d(n(d){B D=d.1K(),4x=19.7x;a[d]=1m.1Z.2F?[4x["66"+D],4x["43"+D]].9N():1m.1Z.5a?19.2e["43"+D]:4x["43"+D]});J a},5S:n(){m(!32){J}9.2B.F(1P(9.7D()))},7m:(n(){B b=".7X, .8v .1D, .7E, .7H";J n(a){m(9.q&&9.q.y&&a.3m(b+(9.q.y.7Z?", #7V":""))){9.13()}}})(),54:n(a){B b=a.2g,1J=b.1J,w=9.1R.E,66=(a.17=="2R")?0:1J=="2u"?w:-1*w,27={1M:66+"H"};m(!9.46){9.46={}}m(9.46[1J]){11.2J.2I("7J"+1J).2n(9.46[1J])}9.46[1J]=u 11.7C(9[1J+"3i"],{27:27,23:0.2,R:{3q:"7J"+1J,9R:1},2X:(a.17=="3Y")?0.1:0})},2O:n(){m(!9.1j){J}B a=9.1a,1p=9.1j.1p;B b=(a<=0)?1p-1:a-1,1c=(a>=1p-1)?0:a+1;J{2K:b,1c:1c}},5Z:n(a,b){B c=3Z[2]||9.y,1z=c.1z,Q=c.Q;1a={1b:(b.63(0)=="t"),1h:(b.63(1)=="l")};m(l){B d=u I("3y",{N:"9T"+b.1K(),E:Q+"H",G:Q+"H"});d.F("5Q:1h");a.M(d);B e=d.5T("2d");e.9U=c.10;e.9V((1a.1h?1z:Q-1z),(1a.1b?1z:Q-1z),1z,0,6z.9W*2,26);e.9X();e.7L((1a.1h?1z:0),0,Q-1z,Q);e.7L(0,(1a.1b?1z:0),Q,Q-1z)}W{B f=u I("3k:9Z",{a0:c.10,a1:"5h",a2:c.10,a3:(1z/Q*0.5).3L(2)}).F({E:2*Q-1+"H",G:2*Q-1+"H",1a:"35",1h:(1a.1h?0:(-1*Q))+"H",1b:(1a.1b?0:(-1*Q))+"H"});a.M(f);f.51=f.51}},8H:(n(){n 67(){J $$("3t, 5g, 2p")}m(1m.1Z.2F&&19.5K>=8){67=n(){J 19.a5("3t, 5g, 2p")}}J n(){m(9.68){J}B a=67();9.44=[];7O(B i=0,1p=a.1p;i<1p;i++){B b=a[i];9.44.3S({1y:b,1k:b.27.1k});b.27.1k="1Q"}9.68=26}})(),76:n(){9.44.1d(n(a,i){a.1y.27.1k=a.1k});5q 9.44;9.68=2H},5v:n(){J{E:9.1W.E,G:9.1W.G+9.3a.G}},7S:n(){B i=9.5v(),b=2*9.Q;J{E:i.E+b,G:i.G+b}},7P:n(){B a=21,69=2*9.1R.G+a,v=9.4C();J{E:v.E-69,G:v.G-69}},4C:n(){B v=19.33.2l();m(9.X&&9.X.1u()&&9.1j&&9.1j.1p>1){v.G-=9.2s}J v}});(n(){n 7Q(a,b){m(!9.q){J}a(b)}$w("3c 4U").1d(n(a){9[a]=9[a].1v(7Q)},Y)})();n 1P(b){B c={};12.70(b).1d(n(a){c[a]=b[a]+"H"});J c}12.1l(Y,{7R:n(){m(!9.q.y.5i){J}9.4M=9.7T.1r(9);19.1f("7U",9.4M)},5A:n(){m(9.4M){19.ae("7U",9.4M)}},7T:n(a){B b=af.ag(a.2E).4s(),2E=a.2E,3D=(9.q.2i()||9.2V)&&!9.5E,28=9.q.y.28,49;m(9.q.4W()){a.4X();49=(2E==1i.7Y||["x","c"].6c(b))?"13":(2E==37&&3D&&(9.y.31||9.1a!=0))?"2K":(2E==39&&3D&&(9.y.31||9.2O().1c!=0))?"1c":(b=="p"&&28&&3D)?"7a":(b=="s"&&28&&3D)?"2j":1E;m(b!="s"){9.2j()}}W{49=(2E==1i.7Y)?"13":1E}m(49){9[49]()}m(3D){m(2E==1i.am&&9.1j.an()!=9.q){9.18(0)}m(2E==1i.ao&&9.1j.ap()!=9.q){9.18(9.1j.1p-1)}}}});Y.4o=Y.4o.1v(n(a,b){9.7R();a(b)});12.1l(Y,{7r:n(a){B b=9.4j(a);m(!b){J}b.3W(Y.4a)},74:n(){m(9.1j.1p==0){J}B a=9.2O();9.81([a.1c,a.2K])},81:n(c){B d=(9.1j&&9.1j.6c(c)||12.as(c))?9.1j:c.1Y?9.5s(c.1Y):1E;m(!d){J}B e=$A(12.7v(c)?[c]:c.17?[d.24(c)]:c).au();e.1d(n(a){B b=d[a];9.6d(b)},9)},83:n(a,b){a.4P={E:b.E,G:b.G}},6d:n(a){m(a.4P||a.4B||!a.1g){J}B P=u 2f();P.1w=n(){P.1w=1m.2z;a.4B=1E;9.83(a,P)}.U(9);a.4B=26;P.1t=a.1g},7A:n(a){B b=a.22;m(b&&b.4P||b.4B||!b.2T()){J}9.6d(b)}});I.ay({1G:n(a,b){a=$(a);B c=12.1l({86:"1b 1h",3A:"4H-3A",6e:"8k",10:""},3Z[2]||{});a.F(2G?{aA:"aB:aC.aD.aE(1t=\'"+b+"\'\', 6e=\'"+c.6e+"\')"}:{29:c.10+" 3Q("+b+") "+c.86+" "+c.3A});J a}});12.1l(Y,{6f:n(a,b){B c;$w("3U 2h 2x 40").1d(n(t){m(u 4A("\\\\.("+9.aG[t].2Z(/\\s+/g,"|")+")(\\\\?.*)?","i").58(a)){c=t}}.U(9));m(c){J c}m(a.3x("#")){J"4R"}m(19.89&&19.89!=(a).2Z(/(^.*\\/\\/)|(:.*)|(\\/.*)/g,"")){J"2x"}J"2h"},8a:n(a){B b=a.aI(/\\?.*/,"").3T(/\\.([^.]{3,4})$/);J b?b[1]:1E},5m:n(b){B c="<"+b.1U;7O(B d 6S b){m(!["3o","6g","1U"].6c(d)){c+=" "+d+\'="\'+b[d]+\'"\'}}m(u 4A("^(?:3B|aK|aL|br|aM|aN|aO|82|8b|aQ|aR|aS|2y|aT|aU|aV)$","i").58(b.1U)){c+="/>"}W{c+=">";m(b.3o){b.3o.1d(n(a){c+=9.5m(a)}.U(9))}m(b.6g){c+=b.6g}c+="</"+b.1U+">"}J c}});(n(){19.1f("5Y:3P",n(){B c=(3b.6h&&3b.6h.1p);n 4d(a){B b=2H;m(c){b=($A(3b.6h).7j("2b").6W(",").24(a)>=0)}W{4c{b=u aY(a)}4e(e){}}J!!b}m(c){20.Y.4u={3U:4d("aZ b0"),40:4d("6i")}}W{20.Y.4u={3U:4d("8f.8f"),40:4d("6i.6i")}}})})();Y.3N=b3.b4({b5:n(b){m(b.22){J}B c=12.7N(b);m(c&&!b.22){b.22=9;m(b.1C){b.22.3H=b.1C;m(Y.y.8g){b.b7("1C","")}}}9.1g=c?b.b8("1g"):b.1g;m(9.1g.24("#")>=0){9.1g=9.1g.5e(9.1g.24("#"))}B d=b.1Y;m(d){9.1Y=d;m(d.3x("4g")){9.17="4g"}W{m(d.3x("56")){m(d.bb("][")){B e=d.8j("]["),6j=e[1].3T(/([a-be-Z]*)/)[1];m(6j){9.17=6j;B f=e[0]+"]";b.7i("1Y",f);9.1Y=f}}W{9.17=Y.6f(9.1g)}}W{9.17=d}}}W{9.17=Y.6f(9.1g);9.1Y=9.17}$w("4f 3U 4g 2x 2h 4R 40 8l 8m 56").3W(n(a){B T=a.1K(),t=a.4s();m("2h 4g 8m 8l 56".24(a)<0){9["bi"+T]=n(){J 9.17==t}.U(9)}}.U(9));m(c&&b.22.3H){B g=b.22.3H.8j(Y.y.bj).3F("bk");m(g[0]){9.1C=g[0]}m(g[1]){9.2m=g[1]}B h=g[2];9.y=(h&&12.7W(h))?bm("({"+h+"})"):{}}W{9.1C=b.1C;9.2m=b.2m;9.y=b.y||{}}m(9.y.6k){9.y.4f=12.5f(9.y.6k);5q 9.y.6k}},2i:n(){J 9.17.3x("4g")},1O:n(){J 9.1Y.3x("56")},2T:n(){J(9.2i()||9.17=="2h")},5p:n(){J"2x 4R 4f".24(9.17)>=0},4W:n(){J!9.5p()}});Y.4a=n(a){B b=$(a);u Y.3N(a);J b};(n(){n 8o(a){B b=a.3m("a[2U~=V], 3B[2U~=V]");m(!b){J}a.4X();9.4a(b);9.18(b)}n 8p(a){B b=a.3m("a[2U~=V], 3B[2U~=V]");m(!b){J}9.4a(b)}n 8q(a){B b=a.2g,17=a.17,36=a.36;m(36&&36.47){m(17==="5G"||17==="bt"||(17==="2L"&&36.47.4s()==="8b"&&36.17==="bu")){b=36}}m(b.bv==bw.bx){b=b.72}J b}n 8s(a,b){m(!a){J}B c=a.N;J(c.1p>0&&(c==b||u 4A("(^|\\\\s)"+b+"(\\\\s|$)").58(c)))}n 8t(a){B b=8q(a);m(b&&8s(b,"V")){9.4a(b)}}19.1f("V:3P",n(){$(19.2e).1f("2L",8o.1r(Y));m(Y.y.8g&&1m.1Z.2F&&19.5K>=8){$(19.2e).1f("2R",8t.1r(Y))}W{$(19.2e).1f("2R",8p.1r(Y))}})})();12.1l(Y,{4z:n(){B b=9.y.X,Q=b.Q;$(19.2e).M(9.X=u I("O",{2S:"bB"}).F({2Q:9.y.2Q+1,bC:b.1N+"H",1a:"35",1k:"1Q"}).M(9.bD=u I("O",{N:"bE"}).M(u I("O",{N:"4q bG"}).F("1N-1h: "+Q+"H").M(u I("O",{N:"2t"}))).M(u I("O",{N:"6l"}).F({1N:"0 "+Q+"H",G:Q+"H"})).M(u I("O",{N:"4q bI"}).F("1N-1h: -"+Q+"H").M(u I("O",{N:"2t"})))).M(9.3l=u I("O",{N:"6m 80"}).M(9.3g=u I("3z",{N:"bL"}).F("1N: 0 "+Q+"H").M(u I("1B",{N:"bM"}).M(9.2r=u I("O"))).M(u I("1B",{N:"4h bO"}).M(9.bP=u I("O",{N:"1D"}).1G(9.1e+"8A.1s",{10:b.10}))).M(u I("1B",{N:"4h bR"}).M(9.bS=u I("O",{N:"1D"}).1G(9.1e+"bT.1s",{10:b.10}))).M(u I("1B",{N:"4h bU"}).M(9.3O=u I("O",{N:"1D"}).1G(9.1e+"7d.1s",{10:b.10}))).M(u I("1B",{N:"4h 7H"}).M(9.bV=u I("O",{N:"1D"}).1G(9.1e+"bW.1s",{10:b.10}))))).M(9.bX=u I("O",{N:"bY"}).M(u I("O",{N:"4q bZ"}).F("1N-1h: "+Q+"H").M(u I("O",{N:"2t"}))).M(u I("O",{N:"6l"}).F({1N:"0 "+Q+"H",G:Q+"H"})).M(u I("O",{N:"4q c0"}).F("1N-1h: -"+Q+"H").M(u I("O",{N:"2t"})))));$w("2u 1c").1d(n(s){B S=s.1K();9["X"+S].1V=s},9);m(2C){9.X.13=n(){9.F("1h:-3C;1b:-3C;1k:1Q;");J 9};9.X.18=n(){9.F("1k:1u");J 9};9.X.1u=n(){J(9.1H("1k")=="1u"&&4J(9.1H("1b").2Z("H",""))>-7K)}}9.X.2p(".4h O").3F("F",1P(9.8B));B c=9.X.2p(".2t");$w("7o 7n bl br").1d(n(a,i){m(b.1z>0){9.5Z(c[i],a,b)}W{c[i].M(u I("O",{N:"38"}))}c[i].F({E:b.Q+"H",G:b.Q+"H"}).7g("2t"+a.1K())},9);9.X.5N(".6m").F("E:78%;");9.X.F(2G?{1a:"35",1b:"1L",1h:""}:{1a:"4r",1b:"1L",1h:"50%"});9.X.2p(".6l",".6m",".1D",".38").3F("F",{10:b.10});9.2r.1x(u 4y(b.6L).45({1a:8C,5I:8C}));9.2r.F({E:9.2r.7y()+"H",G:9.3g.5P()+"H"});9.8D();9.2r.1x("");9.X.13().F("1k:1u");9.7k();9.2a()},8D:n(){B b,4i,X=9.y.X,Q=X.Q;m(2G){b=9.3g.2l(),4i=b.E+2*Q;9.3g.F({E:b.E+"H",1N:0});9.3l.F("E:1L;");9.3g.F({c5:Q+"H"});9.3l.F({E:4i+"H"});$w("1b 3K").1d(n(a){9["X"+a.1K()].F({E:4i+"H"})},9);9.X.F("1N-1h:-"+(4i/2).2o()+"H")}W{9.3l.F("E:1L");b=9.3l.2l();9.2r.c6().F({8F:b.G+"H",E:9.2r.2l().E+"H"});9.X.F({E:b.E+"H",1M:(0-(b.E/2).2o())+"H"});9.3l.F({E:b.E+"H"});$w("1b 3K").1d(n(a){9["X"+a.1K()].F({E:b.E+"H"})},9)}9.7e=X.1N+b.G+2*Q;9.6q=9.X.5P();9.2r.F({8F:b.G+"H"})}});Y.4z=Y.4z.1v(n(a,b){B c=u 2f();c.1w=n(){c.1w=1m.2z;9.8B={E:c.E,G:c.G};a(b)}.U(9);c.1t=9.1e+"8A.1s";B d=(u 2f()).1t=9.1e+"7b.1s"});Y.4E=Y.4E.1v(n(a,b){a(b);9.4z()});Y.13=Y.13.1v(n(a,b){m(9.q&&9.q.1O()){9.X.13();9.2r.1x("")}a(b)})})();Y.5G();19.1f("5Y:3P",Y.60.U(Y));',62,752,'|||||||||this|||||||||||||if|function|||view||||new||||options|||var|||width|setStyle|height|px|Element|return|||insert|className|div||border|queue|||bind|lightview|else|controller|Lightview||backgroundColor|Effect|Object|hide||||type|show|document|position|top|next|each|images|observe|href|left|Event|views|visibility|extend|Prototype|setOpacity|menubar|length|afterFinish|bindAsEventListener|png|src|visible|wrap|onload|update|element|radius|buttons|li|title|lv_Button|null|opacity|setPngBackground|getStyle|closeDimensions|side|capitalize|auto|marginLeft|margin|isSet|pixelClone|hidden|sideDimensions|scaledInnerDimensions|_contentPosition|tag|prevnext|innerDimensions|marginTop|rel|Browser|window||_view|duration|indexOf||true|style|slideshow|background|_lightviewLoadedEvent|name|closeButton||body|Image|target|image|isGallery|stopSlideshow|topclose|getDimensions|caption|remove|round|select|display|setNumber|controllerOffset|lv_Corner|prev|break|lightviewContent|iframe|param|emptyFunction|overflow|overlay|BROWSER_IS_WEBKIT_419|dimensions|keyCode|IE|BROWSER_IS_IE_LT7|false|get|Queues|previous|click|case|value|getSurroundingIndexes|data|zIndex|mouseover|id|isImage|class|isSetGallery|after|delay|bounds|replace|center|cyclic|BROWSER_IS_FIREFOX_LT3|viewport|slideshowButton|absolute|currentTarget||lv_Fill||menubarDimensions|navigator|fillMenuBar|imgWidth|large|normal|controllerCenter|nextButton|ButtonImage|prevButton|ns_vml|controllerMiddle|findElement|sliding|children|padding|scope|inner|Button|object|cursor|loading|call|startsWith|canvas|ul|repeat|area|9500px|staticGallery|content|invoke|sideNegativeMargin|_title|resize|inlineContent|bottom|toFixed|floor|View|controllerSlideshow|loaded|url|restoreCenter|push|match|flash|inlineMarker|_each|innerPreviousNext|mouseout|arguments|quicktime|lightviewError|blockInnerPrevNext|scroll|overlappingRestore|evaluate|sideEffect|tagName|imgNumber|action|Extend|sideButtons|try|detectPlugin|catch|ajax|gallery|lv_ButtonWrapper|finalWidth|getSet|FIX_OVERLAY_WITH_PNG|isIframe|from|scrollbarWidth|afterShow|contentDimensions|lv_controllerCornerWrapper|fixed|toLowerCase|clearContent|Plugin|isQuicktime|parseInt|ddE|Template|buildController|RegExp|isPreloading|getViewportDimensions|minimum|build|topcloseButtonImage|afterEffect|no|init|parseFloat|hdiff|wdiff|keyboardEvent|sideStyle|nextButtonImage|preloadedDimensions|prevButtonImage|inline|stopLoading|userAgent|insertContent|resizeCenter|isMedia|stop|innerPrevNext|onComplete||outerHTML|resizeWithinViewport|Appear|toggleSideButton|autosize|set|to|test|switch|WebKit|getHiddenDimensions|insertImageUsingHTML|startLoading|substr|clone|embed|1px|keyboard|wmode|pluginspages|pluginspage|createHTML|restore|styles|isExternal|delete|isAjax|getViews|effects|end|getInnerDimensions|_resize|require|restoreInlineContent|convertVersionString|disableKeyboardNavigation|toggleTopClose|hidePrevNext|_afterResize|resizing|corrected|load|closeButtonWidth|total|_controllerCenterEffect|documentMode|disabled|small|down|loadingEffect|getHeight|float|default|maxOverlay|getContext|lightview_hide|menubarPadding|slideTimer|toggleSlideshow|dom|createCorner|start|counter|fire|charAt|gif|centerControllerIELT7|offset|getOverlappingElements|preventingOverlap|safety|innerController|dataText|member|preloadImageDimensions|sizingMethod|detectType|html|plugins|QuickTime|relType|ajaxOptions|lv_controllerBetweenCorners|lv_controllerMiddle|lv_Wrapper|getScrollOffsets|controls|_controllerHeight|Firefox|loop|mimetypes|Tween|transition|controllerHeight|overflowX|overflowY|Math|15px|Opacity|sync|flashvars|Scriptaculous|cancel|showPrevNext|tween|hideData|scrolling|prepare|setNumberTemplate|fullscreen|find|SetControllerVisible|inner_slideshow_stop|namespaces|pointer|in|close_|setCloseButtons|setMenubarDimensions|join|Fade|setPrevNext|insertImageUsingCanvas|keys|Stop|parentNode|frames|preloadSurroundingImages|afterHide|showOverlapping|adjustDimensionsToView|100|VML|startSlideshow|controller_slideshow_stop|appear|controller_slideshow_play|_controllerOffset|insertImageUsingVML|addClassName|isInline|writeAttribute|pluck|addObservers|finishShow|delegateClose|tr|tl|_lightviewLoadedEvents|showContent|extendSet|curry|hover|nextSlide|isNumber|_VMLPreloaded|documentElement|getWidth|_preloadImageHover|preloadImageHover|_topCloseEffect|Morph|getScrollDimensions|lv_Loading|contentBottom|Top|lv_controllerClose|inner_slideshow_play|lightview_side|9500|fillRect|Bottom|isElement|for|getBounds|guard|enableKeyboardNavigation|getOuterDimensions|keyboardDown|keydown|lv_overlay|isString|lv_Close|KEY_ESC|overlayClose|clearfix|preloadFromSet|img|setPreloadedDimensions|container|contentTop|align|_inlineDisplayRestore|autoplay|domain|detectExtension|input|mleft|lv_WrapDown|mtop|ShockwaveFlash|removeTitles|lv_Filler|lv_CornerWrapper|split|scale|external|media|REQUIRED_|handleClick|handleMouseOver|elementIE8|lv_Half|hasClassNameIE8|handleMouseOverIE8|lv_Frame|lv_topButtons|_|topButtons|hideContent|marginRight|controller_prev|controllerButtonDimensions|999|_fixateController|nextSide|lineHeight|prevSide|hideOverlapping|tofit|180|borderColor|lv_Data|undefined|codebase|codebases|classid|lv_PrevButton|classids|lv_NextButton|beforeStart|errors|requiresPlugin|quality|high|movie|allowFullScreen|startDimensions|_openEffect|scaledI|nnerDimensions|lv_DataText|mac|FlashVars|lv_Title|plugin|required|clearTimeout|MSIE|defer|ancestors|Slideshow|slideshowDelay|updateViews|transparent|lv_Caption|block|clientWidth|clientHeight|Version|lv_innerController|inner_|close|innerHTML|createStyleSheet|Gecko|defaultOptions|lv_ImgNumber|relative|lv_NextSide|lv_innerPrevNext|innerPrevButton|none|scrollLeft|cssText|preloadHover|inner_prev|innerNextButton|inner_next|lightview_topCloseEffect|topCloseEffect|behavior|isFlash|lv_Slideshow|https|enablejavascript|max|throw|lv_contentBottom|lv_topcloseButtonImage|limit|topcloseButton|cornerCanvas|fillStyle|arc|PI|fill|loadingButton|roundrect|fillcolor|strokeWeight|strokeColor|arcSize|min|querySelectorAll|alt|galleryimg|lv_Frames|requires|lv_FrameBottom|cloneNode|lv_FrameTop|lv_content|stopObserving|String|fromCharCode|drawImage|lv_Liquid|lv_PrevNext|blank|exec|KEY_HOME|first|KEY_END|last|createElement|lv_HalfLeft|isArray|js|uniq|script|REQUIRED_Prototype|resizeDuration|addMethods|lv_HalfRight|filter|progid|DXImageTransform|Microsoft|AlphaImageLoader|Ajax|typeExtensions|Updater|gsub|callee|base|basefont|col|frame|hr|lv_Center|link|isindex|meta|range|spacer|wbr|blur|paddingRight|ActiveXObject|Shockwave|Flash|frameBorder|paddingBottom|Class|create|initialize|Parallel|setAttribute|getAttribute|hspace|150|include|lightviewContent_|lv_WrapUp|zA|random|99999|opened|is|titleSplit|strip||eval|REQUIRED_Scriptaculous|typeof|lv_WrapCenter|times||add|error|radio|nodeType|Node|TEXT_NODE|before|lv_contentTop|urn|lightviewController|marginBottom|controllerTop|lv_controllerTop|imgNumberTemplate|lv_controllerCornerWrapperTopLeft|lv_MenuBar|lv_controllerCornerWrapperTopRight|lv_Container|all|lv_controllerCenter|lv_controllerSetNumber|schemas|lv_controllerPrev|controllerPrev|microsoft|lv_controllerNext|controllerNext|controller_next|lv_controllerSlideshow|controllerClose|controller_close|controllerBottom|lv_controllerBottom|lv_controllerCornerWrapperBottomLeft|lv_controllerCornerWrapperBottomRight|lv_Sides|com|vml|childElements|paddingLeft|up|lv_PrevSide'.split('|'),0,{}));


//  Prototip 2.0.5 - 28-10-2008
//  Copyright (c) 2008 Nick Stakenburg (http://www.nickstakenburg.com)
//
//  Licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License
//  http://creativecommons.org/licenses/by-nc-nd/3.0/

//  More information on this project:
//  http://www.nickstakenburg.com/projects/prototip2/

var Prototip = {
  Version: '2.0.5'
};

var Tips = {
  options: {
    images: '../images/prototip/', // image path, can be relative to this file or an absolute url
    zIndex: 6000                   // raise if required
  }
};

Prototip.Styles = {
  // The default style every other style will inherit from.
  // Used when no style is set through the options on a tooltip.
  'default': {
    border: 6,
    borderColor: '#c7c7c7',
    className: 'default',
    closeButton: false,
    hideAfter: false,
    hideOn: 'mouseleave',
    hook: false,
	//images: 'styles/creamy/',    // Example: different images. An absolute url or relative to the images url defined above.
    radius: 6,
	showOn: 'mousemove',
    stem: {
      //position: 'topLeft',       // Example: optional default stem position, this will also enable the stem
      height: 12,
      width: 15
    }
  },

  'protoblue': {
    className: 'protoblue',
    border: 6,
    borderColor: '#116497',
    radius: 6,
    stem: { height: 12, width: 15 }
  },

  'darkgrey': {
    className: 'darkgrey',
    border: 6,
    borderColor: '#363636',
    radius: 6,
    stem: { height: 12, width: 15 }
  },

  'creamy': {
    className: 'creamy',
    border: 6,
    borderColor: '#ebe4b4',
    radius: 6,
    stem: { height: 12, width: 15 }
  },

  'protogrey': {
    className: 'protogrey',
    border: 6,
    borderColor: '#606060',
    radius: 6,
    stem: { height: 12, width: 15 }
  },
  
  'zd_comment': {
    className: 'zd_comment',
    border: 6,
    borderColor: '#363636',
    radius: 6,
    stem: { height: 12, width: 15 }
  }  
  
};

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}('P.11(X,{5V:"1.6.0.3",3U:c(){8.3C("25");b(/^(6x?:\\/\\/|\\/)/.6i(e.9.W)){e.W=e.9.W}13{h A=/1P(?:-[\\w\\d.]+)?\\.4G(.*)/;e.W=(($$("4C 4y[2b]").3t(c(B){O B.2b.2k(A)})||{}).2b||"").3j(A,"")+e.9.W}b(25.2r.3e&&!17.3X.v){17.3X.34("v","5L:5y-5r-5k:5d");17.1f("3G:32",c(){17.4P().4I("v\\\\:*","4H: 30(#2Z#4D);")})}e.2p();r.1f(2S,"2R",8.2R)},3C:c(A){b((4v 2S[A]=="4p")||(8.2P(2S[A].4l)<8.2P(8["4i"+A]))){4g("X 6p "+A+" >= "+8["4i"+A]);}},2P:c(A){h B=A.3j(/4c.*|\\./g,"");B=6h(B+"0".6e(4-B.3g));O A.66("4c")>-1?B-1:B},62:$w("43 60"),1U:c(A){b(25.2r.3e){O A}A=A.2s(c(E,D){h B=P.2A(8)?8:8.m,C=D.5J;5E(C&&C!=B){5x{C=C.5t}5q(F){C=B}}b(C==B){O}E(D)});O A},37:c(A){O(A>0)?(-1*A):(A).5g()},2R:c(){e.4j()}});P.11(e,{1D:[],1c:[],2p:c(){8.2G=8.1t},1p:(c(A){O{1k:(A?"29":"1k"),1a:(A?"1S":"1a"),29:(A?"29":"1k"),1S:(A?"1S":"1a")}})(25.2r.3e),3D:{1k:"1k",1a:"1a",29:"1k",1S:"1a"},2f:{k:"31",31:"k",i:"1s",1s:"i",1Y:"1Y",1e:"1h",1h:"1e"},3A:{q:"1e",p:"1h"},2U:c(A){O!!23[1]?8.2f[A]:A},1n:(c(B){h A=s 4x("4w ([\\\\d.]+)").4u(B);O A?(3u(A[1])<7):10})(4n.4m),2N:(25.2r.4k&&!17.6w),34:c(A){8.1D.2L(A)},1J:c(A){h B=8.1D.3t(c(C){O C.m==$(A)});b(B){B.4f();b(B.1b){B.o.1J();b(e.1n){B.1v.1J()}}8.1D=8.1D.4b(B)}A.1P=2a},4j:c(){8.1D.3m(c(A){8.1J(A.m)}.1j(8))},2J:c(C){b(C==8.49){O}b(8.1c.3g===0){8.2G=8.9.1t;3i(h B=0,A=8.1D.3g;B<A;B++){8.1D[B].o.f({1t:8.9.1t})}}C.o.f({1t:8.2G++});b(C.T){C.T.f({1t:8.2G})}8.49=C},47:c(A){8.3f(A);8.1c.2L(A)},3f:c(A){8.1c=8.1c.4b(A)},46:c(){e.1c.1Q("V")},Y:c(B,F){B=$(B),F=$(F);h K=P.11({1g:{x:0,y:0},R:10},23[2]||{});h D=K.1z||F.2t();D.k+=K.1g.x;D.i+=K.1g.y;h C=K.1z?[0,0]:F.3H(),A=17.1E.2D(),G=K.1z?"20":"15";D.k+=(-1*(C[0]-A[0]));D.i+=(-1*(C[1]-A[1]));b(K.1z){h E=[0,0];E.q=0;E.p=0}h I={m:B.21()},J={m:P.2c(D)};I[G]=K.1z?E:F.21();J[G]=P.2c(D);3i(h H 3Q J){3O(K[H]){U"5w":U"5u":J[H].k+=I[H].q;18;U"5s":J[H].k+=(I[H].q/2);18;U"5p":J[H].k+=I[H].q;J[H].i+=(I[H].p/2);18;U"5o":U"5m":J[H].i+=I[H].p;18;U"5l":U"5j":J[H].k+=I[H].q;J[H].i+=I[H].p;18;U"5h":J[H].k+=(I[H].q/2);J[H].i+=I[H].p;18;U"5f":J[H].i+=(I[H].p/2);18}}D.k+=-1*(J.m.k-J[G].k);D.i+=-1*(J.m.i-J[G].i);b(K.R){B.f({k:D.k+"j",i:D.i+"j"})}O D}});e.2p();h 5c=59.3J({2p:c(C,E){8.m=$(C);b(!8.m){4g("X: r 58 56, 55 3J a 1b.");O}e.1J(8.m);h A=(P.2F(E)||P.2A(E)),B=A?23[2]||[]:E;8.1u=A?E:2a;b(B.28){B=P.11(P.2c(X.33[B.28]),B)}8.9=P.11(P.11({1m:10,1i:0,3k:"#4R",1o:0,u:e.9.u,19:e.9.4L,1B:!(B.1d&&B.1d=="1Z")?0.14:10,1C:10,1x:"1S",3B:10,Y:B.Y,1g:B.Y?{x:0,y:0}:{x:16,y:16},1K:(B.Y&&!B.Y.1z)?1l:10,1d:"2q",n:10,28:"2Z",15:8.m,12:10,1E:(B.Y&&!B.Y.1z)?10:1l,q:10},X.33["2Z"]),B);8.15=$(8.9.15);8.1o=8.9.1o;8.1i=(8.1o>8.9.1i)?8.1o:8.9.1i;b(8.9.W){8.W=8.9.W.2Y("://")?8.9.W:e.W+8.9.W}13{8.W=e.W+"4F/"+(8.9.28||"")+"/"}b(!8.W.4E("/")){8.W+="/"}b(P.2F(8.9.n)){8.9.n={R:8.9.n}}b(8.9.n.R){8.9.n=P.11(P.2c(X.33[8.9.28].n)||{},8.9.n);8.9.n.R=[8.9.n.R.2k(/[a-z]+/)[0].2e(),8.9.n.R.2k(/[A-Z][a-z]+/)[0].2e()];8.9.n.1I=["k","31"].3z(8.9.n.R[0])?"1e":"1h";8.1r={1e:10,1h:10}}b(8.9.1m){8.9.1m.9=P.11({2V:25.4B},8.9.1m.9||{})}8.1p=$w("4A 43").3z(8.m.4z.2e())?e.3D:e.1p;b(8.9.Y.1z){h D=8.9.Y.1q.2k(/[a-z]+/)[0].2e();8.20=e.2f[D]+e.2f[8.9.Y.1q.2k(/[A-Z][a-z]+/)[0].2e()].2B()}8.3y=(e.2N&&8.1o);8.3x();e.34(8);8.3w();X.11(8)},3x:c(){8.o=s r("S",{u:"1P"}).f({1t:e.9.1t});b(8.3y){8.o.V=c(){8.f("k:-3v;i:-3v;1O:2o;");O 8};8.o.Q=c(){8.f("1O:1c");O 8};8.o.1c=c(){O(8.2Q("1O")=="1c"&&3u(8.2Q("i").3j("j",""))>-4t)}}8.o.V();b(e.1n){8.1v=s r("4s",{u:"1v",2b:"4r:10;",4q:0}).f({2m:"2i",1t:e.9.1t-1,4o:0})}b(8.9.1m){8.24=8.24.2s(8.2O)}8.1q=s r("S",{u:"1u"});8.12=s r("S",{u:"12"}).V();b(8.9.19||(8.9.1x.m&&8.9.1x.m=="19")){8.19=s r("S",{u:"2j"}).26(8.W+"2j.2l")}},2H:c(){b(17.32){8.3r();8.3s=1l;O 1l}13{b(!8.3s){17.1f("3G:32",8.3r);O 10}}},3r:c(){$(17.2M).N(8.o);b(e.1n){$(17.2M).N(8.1v)}b(8.9.1m){$(17.2M).N(8.T=s r("S",{u:"6v"}).26(8.W+"T.6t").V())}h G="o";b(8.9.n.R){8.n=s r("S",{u:"6r"}).f({p:8.9.n[8.9.n.1I=="1h"?"p":"q"]+"j"});h B=8.9.n.1I=="1e";8[G].N(8.3p=s r("S",{u:"6q 2K"}).N(8.4e=s r("S",{u:"6o 2K"})));8.n.N(8.1T=s r("S",{u:"6n"}).f({p:8.9.n[B?"q":"p"]+"j",q:8.9.n[B?"p":"q"]+"j"}));b(e.1n&&!8.9.n.R[1].4d().2Y("6m")){8.1T.f({2m:"6l"})}G="4e"}b(8.1i){h D=8.1i,F;8[G].N(8.1W=s r("6j",{u:"1W"}).N(8.1V=s r("3n",{u:"1V 3l"}).f("p: "+D+"j").N(s r("S",{u:"2n 6g"}).N(s r("S",{u:"1X"}))).N(F=s r("S",{u:"6f"}).f({p:D+"j"}).N(s r("S",{u:"4a"}).f({1w:"0 "+D+"j",p:D+"j"}))).N(s r("S",{u:"2n 6d"}).N(s r("S",{u:"1X"})))).N(8.2W=s r("3n",{u:"2W 3l"}).N(8.2T=s r("S",{u:"2T"}).f("2I: 0 "+D+"j"))).N(8.48=s r("3n",{u:"48 3l"}).f("p: "+D+"j").N(s r("S",{u:"2n 6c"}).N(s r("S",{u:"1X"}))).N(F.6b(1l)).N(s r("S",{u:"2n 69"}).N(s r("S",{u:"1X"})))));G="2T";h C=8.1W.2X(".1X");$w("68 67 65 63").3m(c(I,H){b(8.1o>0){X.45(C[H],I,{1L:8.9.3k,1i:D,1o:8.9.1o})}13{C[H].2E("44")}C[H].f({q:D+"j",p:D+"j"}).2E("1X"+I.2B())}.1j(8));8.1W.2X(".4a",".2W",".44").1Q("f",{1L:8.9.3k})}8[G].N(8.1b=s r("S",{u:"1b "+8.9.u}).N(8.27=s r("S",{u:"27"}).N(8.12)));b(8.9.q){h E=8.9.q;b(P.61(E)){E+="j"}8.1b.f("q:"+E)}b(8.n){h A={};A[8.9.n.1I=="1e"?"i":"1s"]=8.n;8.o.N(A);8.2g()}8.1b.N(8.1q);b(!8.9.1m){8.3d({12:8.9.12,1u:8.1u})}},3d:c(E){h A=8.o.2Q("1O");8.o.f("p:1M;q:1M;1O:2o").Q();b(8.1i){8.1V.f("p:0");8.1V.f("p:0")}b(E.12){8.12.Q().42(E.12);8.27.Q()}13{b(!8.19){8.12.V();8.27.V()}}b(P.2A(E.1u)){E.1u.Q()}b(P.2F(E.1u)||P.2A(E.1u)){8.1q.42(E.1u)}8.1b.f({q:8.1b.41()+"j"});8.o.f("1O:1c").Q();8.1b.Q();h C=8.1b.21(),B={q:C.q+"j"},D=[8.o];b(e.1n){D.2L(8.1v)}b(8.19){8.12.Q().N({i:8.19});8.27.Q()}b(E.12||8.19){8.27.f("q: 3c%")}B.p=2a;8.o.f({1O:A});8.1q.2E("2K");b(E.12||8.19){8.12.2E("2K")}b(8.1i){8.1V.f("p:"+8.1i+"j");8.1V.f("p:"+8.1i+"j");B="q: "+(C.q+2*8.1i)+"j";D.2L(8.1W)}D.1Q("f",B);b(8.n){8.2g();b(8.9.n.1I=="1e"){8.o.f({q:8.o.41()+8.9.n.p+"j"})}}8.o.V()},3w:c(){8.3b=8.24.1y(8);8.40=8.V.1y(8);b(8.9.1K&&8.9.1d=="2q"){8.9.1d="1k"}b(8.9.1d==8.9.1x){8.1R=8.3Z.1y(8);8.m.1f(8.9.1d,8.1R)}b(8.19){8.19.1f("1k",c(E){E.26(8.W+"5Y.2l")}.1j(8,8.19)).1f("1a",c(E){E.26(8.W+"2j.2l")}.1j(8,8.19))}h C={m:8.1R?[]:[8.m],15:8.1R?[]:[8.15],1q:8.1R?[]:[8.o],19:[],2i:[]},A=8.9.1x.m;8.39=A||(!8.9.1x?"2i":"m");8.1N=C[8.39];b(!8.1N&&A&&P.2F(A)){8.1N=8.1q.2X(A)}h D={29:"1k",1S:"1a"};$w("Q V").3m(c(H){h G=H.2B(),F=(8.9[H+"3Y"].38||8.9[H+"3Y"]);8[H+"3W"]=F;b(["29","1S","1k","1a"].2Y(F)){8[H+"3W"]=(8.1p[F]||F);8["38"+G]=X.1U(8["38"+G])}}.1j(8));b(!8.1R){8.m.1f(8.9.1d,8.3b)}b(8.1N){8.1N.1Q("1f",8.5X,8.40)}b(!8.9.1K&&8.9.1d=="1Z"){8.2u=8.R.1y(8);8.m.1f("2q",8.2u)}8.3V=8.V.2s(c(G,F){h E=F.5P(".2j");b(E){E.5N();F.5M();G(F)}}).1y(8);b(8.19||(8.9.1x&&(8.9.1x.m==".2j"))){8.o.1f("1Z",8.3V)}b(8.9.1d!="1Z"&&(8.39!="m")){8.2C=X.1U(c(){8.1G("Q")}).1y(8);8.m.1f(8.1p.1a,8.2C)}h B=[8.m,8.o];8.36=X.1U(c(){e.2J(8);8.2v()}).1y(8);8.35=X.1U(8.1C).1y(8);B.1Q("1f",8.1p.1k,8.36).1Q("1f",8.1p.1a,8.35);b(8.9.1m&&8.9.1d!="1Z"){8.2z=X.1U(8.3T).1y(8);8.m.1f(8.1p.1a,8.2z)}},4f:c(){b(8.9.1d==8.9.1x){8.m.1A(8.9.1d,8.1R)}13{8.m.1A(8.9.1d,8.3b);b(8.1N){8.1N.1Q("1A")}}b(8.2u){8.m.1A("2q",8.2u)}b(8.2C){8.m.1A("1a",8.2C)}8.o.1A();8.m.1A(8.1p.1k,8.36).1A(8.1p.1a,8.35);b(8.2z){8.m.1A(8.1p.1a,8.2z)}},2O:c(C,B){b(!8.1b){b(!8.2H()){O}}8.R(B);b(8.2y){O}13{b(8.3S){C(B);O}}8.2y=1l;h D={2h:{1F:22.1F(B),1H:22.1H(B)}};h A=P.2c(8.9.1m.9);A.2V=A.2V.2s(c(F,E){8.3d({12:8.9.12,1u:E.5I});8.R(D);(c(){F(E);h G=(8.T&&8.T.1c());b(8.T){8.1G("T");8.T.1J();8.T=2a}b(G){8.Q()}8.3S=1l;8.2y=2a}.1j(8)).1B(0.6)}.1j(8));8.5H=r.Q.1B(8.9.1B,8.T);8.o.V();8.2y=1l;8.T.Q();8.5F=(c(){s 5B.5A(8.9.1m.30,A)}.1j(8)).1B(8.9.1B);O 10},3T:c(){8.1G("T")},24:c(A){b(!8.1b){b(!8.2H()){O}}8.R(A);b(8.o.1c()){O}8.1G("Q");8.5z=8.Q.1j(8).1B(8.9.1B)},1G:c(A){b(8[A+"3N"]){5v(8[A+"3N"])}},Q:c(){b(8.o.1c()){O}b(e.1n){8.1v.Q()}b(8.9.3B){e.46()}e.47(8);8.1b.Q();8.o.Q();b(8.n){8.n.Q()}8.m.3M("1P:5C")},1C:c(A){b(8.9.1m){b(8.T&&8.9.1d!="1Z"){8.T.V()}}b(!8.9.1C){O}8.2v();8.5D=8.V.1j(8).1B(8.9.1C)},2v:c(){b(8.9.1C){8.1G("1C")}},V:c(){8.1G("Q");8.1G("T");b(!8.o.1c()){O}8.3L()},3L:c(){b(e.1n){8.1v.V()}b(8.T){8.T.V()}8.o.V();(8.1W||8.1b).Q();e.3f(8);8.m.3M("1P:2o")},3Z:c(A){b(8.o&&8.o.1c()){8.V(A)}13{8.24(A)}},2g:c(){h C=8.9.n,B=23[0]||8.1r,D=e.2U(C.R[0],B[C.1I]),F=e.2U(C.R[1],B[e.2f[C.1I]]),A=8.1o||0;8.1T.26(8.W+D+F+".2l");b(C.1I=="1e"){h E=(D=="k")?C.p:0;8.3p.f("k: "+E+"j;");8.1T.f({"2w":D});8.n.f({k:0,i:(F=="1s"?"3c%":F=="1Y"?"50%":0),5G:(F=="1s"?-1*C.q:F=="1Y"?-0.5*C.q:0)+(F=="1s"?-1*A:F=="i"?A:0)+"j"})}13{8.3p.f(D=="i"?"1w: 0; 2I: "+C.p+"j 0 0 0;":"2I: 0; 1w: 0 0 "+C.p+"j 0;");8.n.f(D=="i"?"i: 0; 1s: 1M;":"i: 1M; 1s: 0;");8.1T.f({1w:0,"2w":F!="1Y"?F:"2i"});b(F=="1Y"){8.1T.f("1w: 0 1M;")}13{8.1T.f("1w-"+F+": "+A+"j;")}b(e.2N){b(D=="1s"){8.n.f({R:"3P",5n:"5K",i:"1M",1s:"1M","2w":"k",q:"3c%",1w:(-1*C.p)+"j 0 0 0"});8.n.28.2m="3K"}13{8.n.f({R:"3R","2w":"2i",1w:0})}}}8.1r=B},R:c(B){b(!8.1b){b(!8.2H()){O}}e.2J(8);b(e.1n){h A=8.o.21();b(!8.2x||8.2x.p!=A.p||8.2x.q!=A.q){8.1v.f({q:A.q+"j",p:A.p+"j"})}8.2x=A}b(8.9.Y){h J,H;b(8.20){h K=17.1E.2D(),C=B.2h||{};h G,I=2;3O(8.20.4d()){U"5O":U"5i":G={x:0-I,y:0-I};18;U"5Q":G={x:0,y:0-I};18;U"5R":U"5S":G={x:I,y:0-I};18;U"5T":G={x:I,y:0};18;U"5U":U"5e":G={x:I,y:I};18;U"5W":G={x:0,y:I};18;U"5b":U"5a":G={x:0-I,y:I};18;U"5Z":G={x:0-I,y:0};18}G.x+=8.9.1g.x;G.y+=8.9.1g.y;J=P.11({1g:G},{m:8.9.Y.1q,20:8.20,1z:{i:C.1H||22.1H(B)-K.i,k:C.1F||22.1F(B)-K.k}});H=e.Y(8.o,8.15,J);b(8.9.1E){h M=8.3a(H),L=M.1r;H=M.R;H.k+=L.1h?2*X.37(G.x-8.9.1g.x):0;H.i+=L.1h?2*X.37(G.y-8.9.1g.y):0;b(8.n&&(8.1r.1e!=L.1e||8.1r.1h!=L.1h)){8.2g(L)}}H={k:H.k+"j",i:H.i+"j"};8.o.f(H)}13{J=P.11({1g:8.9.1g},{m:8.9.Y.1q,15:8.9.Y.15});H=e.Y(8.o,8.15,P.11({R:1l},J));H={k:H.k+"j",i:H.i+"j"}}b(8.T){h E=e.Y(8.T,8.15,P.11({R:1l},J))}b(e.1n){8.1v.f(H)}}13{h F=8.15.2t(),C=B.2h||{},H={k:((8.9.1K)?F[0]:C.1F||22.1F(B))+8.9.1g.x,i:((8.9.1K)?F[1]:C.1H||22.1H(B))+8.9.1g.y};b(!8.9.1K&&8.m!==8.15){h D=8.m.2t();H.k+=-1*(D[0]-F[0]);H.i+=-1*(D[1]-F[1])}b(!8.9.1K&&8.9.1E){h M=8.3a(H),L=M.1r;H=M.R;b(8.n&&(8.1r.1e!=L.1e||8.1r.1h!=L.1h)){8.2g(L)}}H={k:H.k+"j",i:H.i+"j"};8.o.f(H);b(8.T){8.T.f(H)}b(e.1n){8.1v.f(H)}}},3a:c(C){h E={1e:10,1h:10},D=8.o.21(),B=17.1E.2D(),A=17.1E.21(),G={k:"q",i:"p"};3i(h F 3Q G){b((C[F]+D[G[F]]-B[F])>A[G[F]]){C[F]=C[F]-(D[G[F]]+(2*8.9.1g[F=="k"?"x":"y"]));b(8.n){E[e.3A[G[F]]]=1l}}}O{R:C,1r:E}}});P.11(X,{45:c(G,H){h F=23[2]||8.9,B=F.1o,E=F.1i,D=s r("57",{u:"64"+H.2B(),q:E+"j",p:E+"j"}),A={i:(H.3I(0)=="t"),k:(H.3I(1)=="l")};b(D&&D.3h&&D.3h("2d")){G.N(D);h C=D.3h("2d");C.54=F.1L;C.53((A.k?B:E-B),(A.i?B:E-B),B,0,6a.52*2,1l);C.51();C.3F((A.k?B:0),0,E-B,E);C.3F(0,(A.i?B:0),E,E-B)}13{G.N(s r("S").f({q:E+"j",p:E+"j",1w:0,2I:0,2m:"3K",R:"3P",4Z:"2o"}).N(s r("v:4Y",{4X:F.1L,4W:"4V",4U:F.1L,4T:(B/E*0.5).4S(2)}).f({q:2*E-1+"j",p:2*E-1+"j",R:"3R",k:(A.k?0:(-1*E))+"j",i:(A.i?0:(-1*E))+"j"})))}}});r.6k({26:c(C,B){C=$(C);h A=P.11({3E:"i k",3q:"4Q-3q",3o:"4O",1L:""},23[2]||{});C.f(e.1n?{4N:"4M:6s.4K.6u(2b=\'"+B+"\'\', 3o=\'"+A.3o+"\')"}:{4J:A.1L+" 30("+B+") "+A.3E+" "+A.3q});O C}});X.4h={Q:c(){e.2J(8);8.2v();h D={};b(8.9.Y){D.2h={1F:0,1H:0}}13{h A=8.15.2t(),C=8.15.3H(),B=17.1E.2D();A.k+=(-1*(C[0]-B[0]));A.i+=(-1*(C[1]-B[1]));D.2h={1F:A.k,1H:A.i}}b(8.9.1m){8.2O(D)}13{8.24(D)}8.1C()}};X.11=c(A){A.m.1P={};P.11(A.m.1P,{Q:X.4h.Q.1j(A),V:A.V.1j(A),1J:e.1J.1j(e,A.m)})};X.3U();',62,406,'||||||||this|options||if|function||Tips|setStyle||var|top|px|left||element|stem|wrapper|height|width|Element|new||className|||||||||||||||||||insert|return|Object|show|position|div|loader|case|hide|images|Prototip|hook||false|extend|title|else||target||document|break|closeButton|mouseout|tooltip|visible|showOn|horizontal|observe|offset|vertical|border|bind|mouseover|true|ajax|fixIE|radius|useEvent|tip|stemInverse|bottom|zIndex|content|iframeShim|margin|hideOn|bindAsEventListener|mouse|stopObserving|delay|hideAfter|tips|viewport|pointerX|clearTimer|pointerY|orientation|remove|fixed|backgroundColor|auto|hideTargets|visibility|prototip|invoke|eventToggle|mouseleave|stemImage|capture|borderTop|borderFrame|prototip_Corner|middle|click|mouseHook|getDimensions|Event|arguments|showDelayed|Prototype|setPngBackground|toolbar|style|mouseenter|null|src|clone||toLowerCase|_inverse|positionStem|fakePointer|none|close|match|png|display|prototip_CornerWrapper|hidden|initialize|mousemove|Browser|wrap|cumulativeOffset|eventPosition|cancelHideAfter|float|iframeShimDimensions|ajaxContentLoading|ajaxHideEvent|isElement|capitalize|eventCheckDelay|getScrollOffsets|addClassName|isString|zIndexTop|build|padding|raise|clearfix|push|body|WebKit419|ajaxShow|convertVersionString|getStyle|unload|window|borderCenter|inverseStem|onComplete|borderMiddle|select|include|default|url|right|loaded|Styles|add|activityLeave|activityEnter|toggleInt|event|hideElement|getPositionWithinViewport|eventShow|100|_update|IE|removeVisible|length|getContext|for|replace|borderColor|borderRow|each|li|sizingMethod|stemWrapper|repeat|_build|_isBuilding|find|parseFloat|9500px|activate|setup|fixSafari2|member|_stemTranslation|hideOthers|require|specialEvent|align|fillRect|dom|cumulativeScrollOffset|charAt|create|block|afterHide|fire|Timer|switch|relative|in|absolute|ajaxContentLoaded|ajaxHide|start|buttonEvent|Action|namespaces|On|toggle|eventHide|getWidth|update|input|prototip_Fill|createCorner|hideAll|addVisibile|borderBottom|_highest|prototip_Between|without|_|toUpperCase|stemBox|deactivate|throw|Methods|REQUIRED_|removeAll|WebKit|Version|userAgent|navigator|opacity|undefined|frameBorder|javascript|iframe|9500|exec|typeof|MSIE|RegExp|script|tagName|area|emptyFunction|head|VML|endsWith|styles|js|behavior|addRule|background|Microsoft|closeButtons|progid|filter|scale|createStyleSheet|no|000000|toFixed|arcSize|strokeColor|1px|strokeWeight|fillcolor|roundrect|overflow||fill|PI|arc|fillStyle|cannot|available|canvas|not|Class|LEFTBOTTOM|BOTTOMLEFT|Tip|vml|BOTTOMRIGHT|leftMiddle|abs|bottomMiddle|TOPLEFT|rightBottom|com|bottomRight|leftBottom|clear|bottomLeft|rightMiddle|catch|microsoft|topMiddle|parentNode|rightTop|clearTimeout|topRight|try|schemas|showTimer|Request|Ajax|shown|hideAfterTimer|while|ajaxTimer|marginTop|loaderTimer|responseText|relatedTarget|both|urn|stop|blur|LEFTTOP|findElement|TOPMIDDLE|TOPRIGHT|RIGHTTOP|RIGHTMIDDLE|RIGHTBOTTOM|REQUIRED_Prototype|BOTTOMMIDDLE|hideAction|close_hover|LEFTMIDDLE|textarea|isNumber|_captureTroubleElements|br|cornerCanvas|bl|indexOf|tr|tl|prototip_CornerWrapperBottomRight|Math|cloneNode|prototip_CornerWrapperBottomLeft|prototip_CornerWrapperTopRight|times|prototip_BetweenCorners|prototip_CornerWrapperTopLeft|parseInt|test|ul|addMethods|inline|MIDDLE|prototip_StemImage|prototip_StemBox|requires|prototip_StemWrapper|prototip_Stem|DXImageTransform|gif|AlphaImageLoader|prototipLoader|evaluate|https'.split('|'),0,{}));

Autocompleter.Json = Class.create(Autocompleter.Base, {
  initialize: function(element, update, lookupFunction, options) {
    options = options || {};
    this.baseInitialize(element, update, options);
    this.lookupFunction = lookupFunction;
    this.options.choices = options.choices || 10;
  },
  
  getUpdatedChoices: function() {
    this.lookupFunction(this.getToken().toLowerCase(), this.updateJsonChoices.bind(this));
  },
  
  updateJsonChoices: function(choices) {
    this.updateChoices('<ul>' + choices.slice(0, this.options.choices).map(this.jsonChoiceToListChoice.bind(this)).join('') + '</ul>');
  },
  
  jsonChoiceToListChoice: function(choice, mark) {
    return '<li>' + choice.escapeHTML() + '</li>';
  }
});

Autocompleter.Cache = Class.create({
  initialize: function(backendLookup, options) {
    this.cache = new Hash();
    this.backendLookup = backendLookup;
    this.options = Object.extend({
      choices: 10,
      fuzzySearch: false
    }, options || {});
  },
  
  lookup: function(term, callback) {
    return this._lookupInCache(term, null, callback) || this.backendLookup(term, this._storeInCache.curry(term, callback).bind(this));
  },
  
  _lookupInCache: function(fullTerm, partialTerm, callback) {
    var partialTerm = partialTerm || fullTerm;
    var result = this.cache.get(partialTerm);
    
    if (result == null) {
      if (partialTerm.length > 1) {
        return this._lookupInCache(fullTerm, partialTerm.substr(0, partialTerm.length - 1), callback);
      } else {
        return false;
      };
    } else {
      if (fullTerm != partialTerm) {
        result = this._localSearch(result, fullTerm);
        this._storeInCache(fullTerm, null, result);
      };
      callback(result.slice(0, this.options.choices));
      return true;
    };
  },
  
  _localSearch: function(data, term) {
    var exp = this.options.fuzzySearch ? new RegExp(term.gsub(/./, ".*#{0}"), 'i') : new RegExp(term, 'i');
    var foundItems = new Array();
    
    //optimized for speed:
    var item = null;
    var name = null;
    for (var i = 0, len = data.length; i < len; ++i) {
      item = data[i];
      if (exp.test(item)) {
        foundItems.push(item);
      };
    }
    
    return foundItems;
  },
  
  _storeInCache: function(term, callback, data) {
    this.cache.set(term, data);
    if (callback) {
      callback(data.slice(0, this.options.choices));
    };
  }
});

Autocompleter.MultiValue = Class.create({
  options: $H({}),
  element: null,
  dataFetcher: null,

  createSelectedElement: function(id, title) {
    var closeLink = new Element('a', {className: 'close'}).update('×');
    closeLink.observe('click', function(e) {
      this.removeEntry(e.element().up('li'));
      e.stop();
    }.bind(this));
    var hiddenValueField = new Element('input', {type: 'hidden', name: this.name + '[]', value: id, style: 'display: none;'});
    return new Element('li', { className:'choice', choice_id: id }).insert(('' + title).escapeHTML()).insert(closeLink).insert(hiddenValueField);
  },
  
  initialize: function(element, dataFetcher, values, options) {
    var self = this;
    this.options = options || { };
    var outputElement = $(element);
    this.name = outputElement.name;
    this.form = outputElement.up('form');
    this.dataFetcher = dataFetcher;
    this.active = false;
    this.acceptNewValues      = this.options.acceptNewValues || false;
    this.options.frequency    = this.options.frequency || 0.4;
    this.options.minChars     = this.options.minChars || 2;
    this.options.onShow       = this.options.onShow ||
      function(element, update) {
        if(!update.style.position || update.style.position=='absolute') {
          update.style.position = 'absolute';
          try {
            update.clonePosition(element, {setHeight: false, offsetTop: element.offsetHeight});
          } catch(e) {
          }
        }
        Effect.Appear(update,{duration: 0.15});
      };
    this.options.onHide = this.options.onHide ||
      function(element, update){ new Effect.Fade(update,{duration: 0.15}) };
    
    this.searchField = new Element('input', {type: 'text', autocomplete: 'off'});
    this.searchField.observe('keydown', function(event){// Delete the tags if you backspace
      if (event.keyCode === Event.KEY_BACKSPACE && this.getValue().blank()) {
        var tag = this.up('li.search_field_item').previous('li.choice');
        if (typeof(tag) === 'object') {
          self.removeEntry(tag);
        }
      }
    });
    this.searchFieldItem = new Element('li', {className: 'search_field_item'}).update(this.searchField);
    this.holder = new Element('ul', {className: 'multi_value_field', style: outputElement.readAttribute('style')}).update(this.searchFieldItem);

    outputElement.insert({before: this.holder});
    outputElement.remove();
    
    this.choicesHolderList = new Element('ul');
    this.choicesHolder = new Element('div', {className: 'autocomplete', style: 'position: absolute;'}).update(this.choicesHolderList);
    this.holder.insert({after: this.choicesHolder});
    this.choicesHolder.hide();
    
    Event.observe(this.holder, 'click', Form.Element.focus.curry(this.searchField));
    Event.observe(this.searchField, 'keydown', this.onSearchFieldKeyPress.bindAsEventListener(this));
    if (this.acceptNewValues) {
      Event.observe(this.searchField, 'keyup', this.onSearchFieldKeyUp.bindAsEventListener(this));
      Event.observe(this.searchField, 'blur', this.onSearchFieldBlur.bindAsEventListener(this));
    };
    
    Event.observe(this.searchField, 'focus', this.getUpdatedChoices.bindAsEventListener(this));
    Event.observe(this.searchField, 'focus', this.show.bindAsEventListener(this));
    Event.observe(this.searchField, 'blur', this.hide.bindAsEventListener(this));
    
    this.setEmptyValue();
    (values || []).each(function(value) {
      this.addEntry(this.getValue(value), this.getTitle(value));
    }, this);
  },
  
  show: function() {
    if (!this.choicesHolderList.empty()) {
      if(Element.getStyle(this.choicesHolder, 'display')=='none') {
        this.options.onShow(this.holder, this.choicesHolder);
      }
    };
  },

  hide: function() {
    this.stopIndicator();
    if(Element.getStyle(this.choicesHolder, 'display')!='none') {
      this.options.onHide(this.element, this.choicesHolder);
    }
    if(this.iefix) Element.hide(this.iefix);
  },
  
  onSearchFieldKeyPress: function(event) {
    if(this.active) {
      switch(event.keyCode) {
       case Event.KEY_TAB:
       case Event.KEY_RETURN:
         this.selectEntry();
         event.stop();
       case Event.KEY_ESC:
         this.hide();
         this.active = false;
         event.stop();
         return;
       case Event.KEY_LEFT:
       case Event.KEY_RIGHT:
         return;
       case Event.KEY_UP:
         this.markPrevious();
         this.render();
         event.stop();
         return;
       case Event.KEY_DOWN:
         this.markNext();
         this.render();
         event.stop();
         return;
      }
    } else if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
              (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) {
      return;
    }

    this.changed = true;
    this.hasFocus = true;

    if(this.observer) clearTimeout(this.observer);
      this.observer =
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },
  
  onSearchFieldKeyUp: function(event) {
    var newValue = '';
    if(event.keyCode == 188 || event.keyCode == 32) {
      var fieldValue = $F(event.element());
      var separatorIndex = 0;
      if (event.keyCode == 188) {
        separatorIndex = fieldValue.indexOf(',');
      } else if (event.keyCode == 32) {
        separatorIndex = fieldValue.indexOf(' ');
      };
      newValue = fieldValue.substr(0, separatorIndex).toLowerCase().strip();
    }

    if (!newValue.blank()) {
      this.addEntry(newValue, newValue);
      event.element().value = fieldValue.substring(separatorIndex + 1, fieldValue.length);
    };
  },
  
  onSearchFieldBlur: function(event) {
    this.addNewValueFromSearchField.bind(this).delay(0.1, event.element());
  },
  
  addNewValueFromSearchField: function(searchFieldElement) {
    var newValue = $F(searchFieldElement).strip();
    if (!newValue.blank()) {
      this.addEntry(newValue, newValue);
      searchFieldElement.value = '';
    };
  },

  onObserverEvent: function() {
    this.changed = false;
    this.tokenBounds = null;
    if(this.getToken().length>=this.options.minChars) {
      this.getUpdatedChoices();
    } else {
      this.active = false;
      this.hide();
    }
  },
  
  getToken: function() {
    return this.searchField.value;
  },

  markPrevious: function() {
    if(this.index > 0) this.index--;
      else this.index = this.entryCount-1;
  },

  markNext: function() {
    if(this.index < this.entryCount-1) this.index++;
      else this.index = 0;
  },

  getEntry: function(index) {
    return this.choicesHolderList.childNodes[index];
  },

  getCurrentEntry: function() {
    return this.getEntry(this.index);
  },
  
  selectEntry: function() {
    this.active = false;
    var element = this.getCurrentEntry();
    this.addEntry(element.choiceId, element.textContent || element.innerText);
    this.searchField.clear();
    this.searchField.focus();
  },
  
  addEntry: function(id, title) {
    title = title || id;
    if (!this.selectedEntries().include('' + id)) {
      this.searchFieldItem.insert({before: this.createSelectedElement(id, title)});
    };
    var emptyValueField = this.emptyValueElement();
    if (emptyValueField) {
      emptyValueField.remove();
    };
  },
  
  removeEntry: function(entryElement) {
    entryElement = Object.isElement(entryElement) ? entryElement : this.holder.down("li[choice_id=" + entryElement + "]");
    if (entryElement) {
      entryElement.remove();
      if (this.selectedEntries().length == 0) {
        this.setEmptyValue();
      };
    };
  },
  
  clear: function() {
    this.holder.select('li.choice').each(function(e) { this.removeEntry(e); }, this);
  },
  
  setEmptyValue: function() {
    if (!this.emptyValueElement()) {
      this.form.insert(new Element('input', {type: 'hidden', name: this.name, className: 'emptyValueField'}));
    };
  },
  
  emptyValueElement: function() {
    return this.form.down("input.emptyValueField[name='" + this.name + "']");
  },
  
  selectedEntries: function() {
    return this.form.select("input[type=hidden][name='" + this.name + "[]']").map(function(entry) {return entry.value});
  },

  startIndicator: function() {},
  stopIndicator: function() {},

  getUpdatedChoices: function() {
    this.startIndicator();
    var term = this.getToken();
    if (term.length > 0) {
      this.dataFetcher(term, this.updateChoices.curry(term).bind(this));
    } else {
      this.choicesHolderList.update();
    };
  },
  
  updateChoices: function(term, choices) {
    if(!this.changed && this.hasFocus) {
      this.entryCount = choices.length;
      
      this.choicesHolderList.innerHTML = '';
      choices.each(function(choice, choiceIndex) {
        this.choicesHolderList.insert(this.createChoiceElement(this.getValue(choice), this.getTitle(choice), choiceIndex, term));
      }.bind(this));
      
      for (var i = 0; i < this.entryCount; i++) {
        var entry = this.getEntry(i);
        entry.choiceIndex = i;
        this.addObservers(entry);
      }
      
      this.stopIndicator();
      this.index = 0;

      if(this.entryCount==1 && this.options.autoSelect) {
        this.selectEntry();
        this.hide();
      } else {
        this.render();
      }
    }
  },
  
  addObservers: function(element) {
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
  },

  onHover: function(event) {
    var element = Event.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex)
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    Event.stop(event);
  },

  onClick: function(event) {
    var element = Event.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
    this.selectEntry();
    this.hide();
  },

  createChoiceElement: function(id, title, choiceIndex, searchTerm) {
    var node = new Element('li', { choice_id: id });
    node.innerHTML = ('' + title).escapeHTML();
    node.choiceId = id;
    node.autocompleteIndex = choiceIndex;
    return node;
  },
  
  render: function() {
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
        this.index==i ?
          Element.addClassName(this.getEntry(i),"selected") :
          Element.removeClassName(this.getEntry(i),"selected");
      if(this.hasFocus) {
        this.show();
        this.active = true;
      }
    } else {
      this.active = false;
      this.hide();
    }
  },
  
  getTitle: function(obj) {
    return Object.isArray(obj) ? obj[0] : obj;
  },
  
  getValue: function(obj) {
    return Object.isArray(obj) ? obj[1] : obj;
  }, 

  beforeFormSubmit: function() { 
    this.addNewValueFromSearchField(this.searchField);
  } 
  
});


// CalendarDateSelect version 1.15 - a prototype based date picker
// Questions, comments, bugs? - see the project page: http://code.google.com/p/calendardateselect
if (typeof Prototype == 'undefined') alert("CalendarDateSelect Error: Prototype could not be found. Please make sure that your application's layout includes prototype.js (.g. <%= javascript_include_tag :defaults %>) *before* it includes calendar_date_select.js (.g. <%= calendar_date_select_includes %>).");
if (Prototype.Version < "1.6") alert("Prototype 1.6.0 is required.  If using earlier version of prototype, please use calendar_date_select version 1.8.3");

Element.addMethods({
  purgeChildren: function(element) { $A(element.childNodes).each(function(e){$(e).remove();}); },
  build: function(element, type, options, style) {
    var newElement = Element.buildAndAppend(type, options, style);
    element.appendChild(newElement);
    return newElement;
  }
});

Element.buildAndAppend = function(type, options, style)
{
  var e = $(document.createElement(type));
  $H(options).each(function(pair) { e[pair.key] = pair.value });
  if (style) e.setStyle(style);
  return e;
};
nil = null;

Date.one_day = 24*60*60*1000;
Date.weekdays = $w("S M T W T F S");
Date.first_day_of_week = 0;
Date.months = $w("January February March April May June July August September October November December" );
Date.padded2 = function(hour) { var padded2 = parseInt(hour, 10); if (hour < 10) padded2 = "0" + padded2; return padded2; }
Date.prototype.getPaddedMinutes = function() { return Date.padded2(this.getMinutes()); }
Date.prototype.getAMPMHour = function() { var hour = this.getHours(); return (hour == 0) ? 12 : (hour > 12 ? hour - 12 : hour ) }
Date.prototype.getAMPM = function() { return (this.getHours() < 12) ? "AM" : "PM"; }
Date.prototype.stripTime = function() { return new Date(this.getFullYear(), this.getMonth(), this.getDate());};
Date.prototype.daysDistance = function(compare_date) { return Math.round((compare_date - this) / Date.one_day); };
Date.prototype.toFormattedString = function(include_time){
  var hour, str;
  str = Date.months[this.getMonth()] + " " + this.getDate() + ", " + this.getFullYear();
  
  if (include_time) { hour = this.getHours(); str += " " + this.getAMPMHour() + ":" + this.getPaddedMinutes() + " " + this.getAMPM() }
  return str;
}
Date.parseFormattedString = function(string) { return new Date(string);}
Math.floor_to_interval = function(n, i) { return Math.floor(n/i) * i;}
window.f_height = function() { return( [window.innerHeight ? window.innerHeight : null, document.documentElement ? document.documentElement.clientHeight : null, document.body ? document.body.clientHeight : null].select(function(x){return x>0}).first()||0); }
window.f_scrollTop = function() { return ([window.pageYOffset ? window.pageYOffset : null, document.documentElement ? document.documentElement.scrollTop : null, document.body ? document.body.scrollTop : null].select(function(x){return x>0}).first()||0 ); }

_translations = {
  "OK": "OK",
  "Now": "Now",
  "Today": "Today",
  "Clear": "Clear"
}
SelectBox = Class.create();
SelectBox.prototype = {
  initialize: function(parent_element, values, html_options, style_options) {
    this.element = $(parent_element).build("select", html_options, style_options);
    this.populate(values);
  },
  populate: function(values) {
    this.element.purgeChildren();
    var that = this; $A(values).each(function(pair) { if (typeof(pair)!="object") {pair = [pair, pair]}; that.element.build("option", { value: pair[1], innerHTML: pair[0]}) });
  },
  setValue: function(value) {
    var e = this.element;
    var matched = false;
    $R(0, e.options.length - 1 ).each(function(i) { if(e.options[i].value==value.toString()) {e.selectedIndex = i; matched = true;}; } );
    return matched;
  },
  getValue: function() { return $F(this.element)}
}
CalendarDateSelect = Class.create();
CalendarDateSelect.prototype = {
  initialize: function(target_element, options) {
    this.target_element = $(target_element); // make sure it's an element, not a string
    if (!this.target_element) { alert("Target element " + target_element + " not found!"); return false;}
    if (this.target_element.tagName != "INPUT") this.target_element = this.target_element.down("INPUT")
    
    this.target_element.calendar_date_select = this;
    this.last_click_at = 0;
    // initialize the date control
    this.options = $H({
      embedded: false,
      popup: nil,
      time: false,
      buttons: true,
      clear_button: true,
      year_range: 10,
      close_on_click: nil,
      minute_interval: 5,
      popup_by: this.target_element,
      month_year: "dropdowns",
      onchange: this.target_element.onchange,
      valid_date_check: nil
    }).merge(options || {});
    this.use_time = this.options.get("time");
    this.parseDate();
    this.callback("before_show")
    this.initCalendarDiv();
    if(!this.options.get("embedded")) {
      this.positionCalendarDiv()
      // set the click handler to check if a user has clicked away from the document
      Event.observe(document, "mousedown", this.closeIfClickedOut_handler = this.closeIfClickedOut.bindAsEventListener(this));
      Event.observe(document, "keypress", this.keyPress_handler = this.keyPress.bindAsEventListener(this));
    }
    this.callback("after_show")
  },
  positionCalendarDiv: function() {
    var above = false;
    var c_pos = this.calendar_div.cumulativeOffset(), c_left = c_pos[0], c_top = c_pos[1], c_dim = this.calendar_div.getDimensions(), c_height = c_dim.height, c_width = c_dim.width; 
    var w_top = window.f_scrollTop(), w_height = window.f_height();
    var e_dim = $(this.options.get("popup_by")).cumulativeOffset(), e_top = e_dim[1], e_left = e_dim[0], e_height = $(this.options.get("popup_by")).getDimensions().height, e_bottom = e_top + e_height;
    
    if ( (( e_bottom + c_height ) > (w_top + w_height)) && ( e_bottom - c_height > w_top )) above = true;
    var left_px = e_left.toString() + "px", top_px = (above ? (e_top - c_height ) : ( e_top + e_height )).toString() + "px";
    
    this.calendar_div.style.left = left_px;  this.calendar_div.style.top = top_px;
    
    this.calendar_div.setStyle({visibility:""});
    
    // draw an iframe behind the calendar -- ugly hack to make IE 6 happy
    if(navigator.appName=="Microsoft Internet Explorer") this.iframe = $(document.body).build("iframe", {src: "javascript:false", className: "ie6_blocker"}, { left: left_px, top: top_px, height: c_height.toString()+"px", width: c_width.toString()+"px", border: "0px"})
  },
  initCalendarDiv: function() {
    if (this.options.get("embedded")) {
      var parent = this.target_element.parentNode;
      var style = {}
    } else {
      var parent = document.body
      var style = { position:"absolute", visibility: "hidden", left:0, top:0 }
    }
    this.calendar_div = $(parent).build('div', {className: "calendar_date_select"}, style);
    
    var that = this;
    // create the divs
    $w("top header body buttons footer bottom").each(function(name) {
      eval("var " + name + "_div = that." + name + "_div = that.calendar_div.build('div', { className: 'cds_"+name+"' }, { clear: 'left'} ); ");
    });
    
    this.initHeaderDiv();
    this.initButtonsDiv();
    this.initCalendarGrid();
    this.updateFooter("&#160;");
    
    this.refresh();
    this.setUseTime(this.use_time);
  },
  initHeaderDiv: function() {
    var header_div = this.header_div;
    this.close_button = header_div.build("a", { innerHTML: "x", href:"#", onclick:function () { this.close(); return false; }.bindAsEventListener(this), className: "close" });
    this.next_month_button = header_div.build("a", { innerHTML: "&gt;", href:"#", onclick:function () { this.navMonth(this.date.getMonth() + 1 ); return false; }.bindAsEventListener(this), className: "next" });
    this.prev_month_button = header_div.build("a", { innerHTML: "&lt;", href:"#", onclick:function () { this.navMonth(this.date.getMonth() - 1 ); return false; }.bindAsEventListener(this), className: "prev" });
    
    if (this.options.get("month_year")=="dropdowns") {
      this.month_select = new SelectBox(header_div, $R(0,11).map(function(m){return [Date.months[m], m]}), {className: "month", onchange: function () { this.navMonth(this.month_select.getValue()) }.bindAsEventListener(this)}); 
      this.year_select = new SelectBox(header_div, [], {className: "year", onchange: function () { this.navYear(this.year_select.getValue()) }.bindAsEventListener(this)}); 
      this.populateYearRange();
    } else {
      this.month_year_label = header_div.build("span")
    }
  },
  initCalendarGrid: function() {
    var body_div = this.body_div;
    this.calendar_day_grid = [];
    var days_table = body_div.build("table", { cellPadding: "0px", cellSpacing: "0px", width: "100%" })
    // make the weekdays!
    var weekdays_row = days_table.build("thead").build("tr");
    Date.weekdays.each( function(weekday) { 
      weekdays_row.build("th", {innerHTML: weekday});
    });
    
    var days_tbody = days_table.build("tbody")
    // Make the days!
    var row_number = 0, weekday;
    for(var cell_index = 0; cell_index<42; cell_index++)
    {
      weekday = (cell_index+Date.first_day_of_week ) % 7;
      if ( cell_index % 7==0 ) days_row = days_tbody.build("tr", {className: 'row_'+row_number++});
      (this.calendar_day_grid[cell_index] = days_row.build("td", {
          calendar_date_select: this,
          onmouseover: function () { this.calendar_date_select.dayHover(this); },
          onmouseout: function () { this.calendar_date_select.dayHoverOut(this) },
          onclick: function() { this.calendar_date_select.updateSelectedDate(this, true); },
          className: (weekday==0) || (weekday==6) ? " weekend" : "" //clear the class
        },
        { cursor: "pointer" }
      )).build("div");
      this.calendar_day_grid[cell_index];
    }
  },
  initButtonsDiv: function()
  {
    var buttons_div = this.buttons_div;
    if (this.options.get("time"))
    {
      var blank_time = $A(this.options.get("time")=="mixed" ? [[" - ", ""]] : []);
      buttons_div.build("span", {innerHTML:"@", className: "at_sign"});
      
      var t = new Date();
      this.hour_select = new SelectBox(buttons_div,
        blank_time.concat($R(0,23).map(function(x) {t.setHours(x); return $A([t.getAMPMHour()+ " " + t.getAMPM(),x])} )),
        { 
          calendar_date_select: this, 
          onchange: function() { this.calendar_date_select.updateSelectedDate( { hour: this.value });},
          className: "hour" 
        }
      );
      buttons_div.build("span", {innerHTML:":", className: "seperator"});
      var that = this;
      this.minute_select = new SelectBox(buttons_div,
        blank_time.concat($R(0,59).select(function(x){return (x % that.options.get('minute_interval')==0)}).map(function(x){ return $A([ Date.padded2(x), x]); } ) ),
        { 
          calendar_date_select: this, 
          onchange: function() { this.calendar_date_select.updateSelectedDate( {minute: this.value }) }, 
          className: "minute" 
        }
      );
      
    } else if (! this.options.get("buttons")) buttons_div.remove();
    
    if (this.options.get("buttons")) {
      buttons_div.build("span", {innerHTML: "&#160;"});
      if (this.options.get("time")=="mixed" || !this.options.get("time")) b = buttons_div.build("a", {
          innerHTML: _translations["Today"],
          href: "#",
          onclick: function() {this.today(false); return false;}.bindAsEventListener(this)
        });
      
      if (this.options.get("time")=="mixed") buttons_div.build("span", {innerHTML: "&#160;|&#160;", className:"button_seperator"})
      
      if (this.options.get("time")) b = buttons_div.build("a", {
        innerHTML: _translations["Now"],
        href: "#",
        onclick: function() {this.today(true); return false}.bindAsEventListener(this)
      });
      
      if (!this.options.get("embedded") && !this.closeOnClick())
      {
        buttons_div.build("span", {innerHTML: "&#160;|&#160;", className:"button_seperator"})
        buttons_div.build("a", { innerHTML: _translations["OK"], href: "#", onclick: function() {this.close(); return false;}.bindAsEventListener(this) });
      }
      if (this.options.get('clear_button')) {
        buttons_div.build("span", {innerHTML: "&#160;|&#160;", className:"button_seperator"})
        buttons_div.build("a", { innerHTML: _translations["Clear"], href: "#", onclick: function() {this.clearDate(); if (!this.options.get("embedded")) this.close(); return false;}.bindAsEventListener(this) });
      }
    }
  },
  refresh: function ()
  {
    this.refreshMonthYear();
    this.refreshCalendarGrid();
    
    this.setSelectedClass();
    this.updateFooter();
  },
  refreshCalendarGrid: function () {
    this.beginning_date = new Date(this.date).stripTime();
    this.beginning_date.setDate(1);
    this.beginning_date.setHours(12); // Prevent daylight savings time boundaries from showing a duplicate day
    var pre_days = this.beginning_date.getDay() // draw some days before the fact
    if (pre_days < 3) pre_days += 7;
    this.beginning_date.setDate(1 - pre_days + Date.first_day_of_week);
    
    var iterator = new Date(this.beginning_date);
    
    var today = new Date().stripTime();
    var this_month = this.date.getMonth();
    vdc = this.options.get("valid_date_check");
    for (var cell_index = 0;cell_index<42; cell_index++)
    {
      day = iterator.getDate(); month = iterator.getMonth();
      cell = this.calendar_day_grid[cell_index];
      Element.remove(cell.childNodes[0]); div = cell.build("div", {innerHTML:day});
      if (month!=this_month) div.className = "other";
      cell.day = day; cell.month = month; cell.year = iterator.getFullYear();
      if (vdc) { if (vdc(iterator.stripTime())) cell.removeClassName("disabled"); else cell.addClassName("disabled") };
      iterator.setDate( day + 1);
    }
    
    if (this.today_cell) this.today_cell.removeClassName("today");
    
    if ( $R( 0, 41 ).include(days_until = this.beginning_date.stripTime().daysDistance(today)) ) {
      this.today_cell = this.calendar_day_grid[days_until];
      this.today_cell.addClassName("today");
    }
  },
  refreshMonthYear: function() {
    var m = this.date.getMonth();
    var y = this.date.getFullYear();
    // set the month
    if (this.options.get("month_year") == "dropdowns") 
    {
      this.month_select.setValue(m, false);
      
      var e = this.year_select.element; 
      if (this.flexibleYearRange() && (!(this.year_select.setValue(y, false)) || e.selectedIndex <= 1 || e.selectedIndex >= e.options.length - 2 )) this.populateYearRange();
      
      this.year_select.setValue(y);
      
    } else {
      this.month_year_label.update( Date.months[m] + " " + y.toString()  );
    }
  },
  populateYearRange: function() {
    this.year_select.populate(this.yearRange().toArray());
  },
  yearRange: function() {
    if (!this.flexibleYearRange())
      return $R(this.options.get("year_range")[0], this.options.get("year_range")[1]);
      
    var y = this.date.getFullYear();
    return $R(y - this.options.get("year_range"), y + this.options.get("year_range"));
  },
  flexibleYearRange: function() { return (typeof(this.options.get("year_range")) == "number"); },
  validYear: function(year) { if (this.flexibleYearRange()) { return true;} else { return this.yearRange().include(year);}  },
  dayHover: function(element) {
    var hover_date = new Date(this.selected_date);
    hover_date.setYear(element.year); hover_date.setMonth(element.month); hover_date.setDate(element.day);
    this.updateFooter(hover_date.toFormattedString(this.use_time));
  },
  dayHoverOut: function(element) { this.updateFooter(); },
  clearSelectedClass: function() {if (this.selected_cell) this.selected_cell.removeClassName("selected");},
  setSelectedClass: function() {
    if (!this.selection_made) return;
    this.clearSelectedClass()
    if ($R(0,42).include( days_until = this.beginning_date.stripTime().daysDistance(this.selected_date.stripTime()) )) {
      this.selected_cell = this.calendar_day_grid[days_until];
      this.selected_cell.addClassName("selected");
    }
  },
  reparse: function() { this.parseDate(); this.refresh(); },
  dateString: function() {
    return (this.selection_made) ? this.selected_date.toFormattedString(this.use_time) : "&#160;";
  },
  parseDate: function()
  {
    var value = $F(this.target_element).strip()
    this.selection_made = (value != "");
    this.date = value=="" ? NaN : Date.parseFormattedString(this.options.get("date") || value);
    if (isNaN(this.date)) this.date = new Date();
    if (!this.validYear(this.date.getFullYear())) this.date.setYear( (this.date.getFullYear() < this.yearRange().start) ? this.yearRange().start : this.yearRange().end);
    this.selected_date = new Date(this.date);
    this.use_time = /[0-9]:[0-9]{2}/.exec(value) ? true : false;
    this.date.setDate(1);
  },
  updateFooter:function(text) { if (!text) text = this.dateString(); this.footer_div.purgeChildren(); this.footer_div.build("span", {innerHTML: text }); },
  clearDate:function() {
    if ((this.target_element.disabled || this.target_element.readOnly) && this.options.get("popup") != "force") return false;
    var last_value = this.target_element.value;
    this.target_element.value = "";
    this.clearSelectedClass();
    this.updateFooter('&#160;');
    if (last_value!=this.target_element.value) this.callback("onchange");
  },
  updateSelectedDate:function(partsOrElement, via_click) {
    var parts = $H(partsOrElement);
    if ((this.target_element.disabled || this.target_element.readOnly) && this.options.get("popup") != "force") return false;
    if (parts.get("day")) {
      var t_selected_date = this.selected_date, vdc = this.options.get("valid_date_check");
      for (var x = 0; x<=3; x++) t_selected_date.setDate(parts.get("day"));
      t_selected_date.setYear(parts.get("year"));
      t_selected_date.setMonth(parts.get("month"));
      
      if (vdc && ! vdc(t_selected_date.stripTime())) { return false; }
      this.selected_date = t_selected_date;
      this.selection_made = true;
    }
    
    if (!isNaN(parts.get("hour"))) this.selected_date.setHours(parts.get("hour"));
    if (!isNaN(parts.get("minute"))) this.selected_date.setMinutes( Math.floor_to_interval(parts.get("minute"), this.options.get("minute_interval")) );
    if (parts.get("hour") === "" || parts.get("minute") === "") 
      this.setUseTime(false);
    else if (!isNaN(parts.get("hour")) || !isNaN(parts.get("minute")))
      this.setUseTime(true);
    
    this.updateFooter();
    this.setSelectedClass();
    
    if (this.selection_made) this.updateValue();
    if (this.closeOnClick()) { this.close(); }
    if (via_click && !this.options.get("embedded")) {
      if ((new Date() - this.last_click_at) < 333) this.close();
      this.last_click_at = new Date();
    }
  },
  closeOnClick: function() {
    if (this.options.get("embedded")) return false;
    if (this.options.get("close_on_click")===nil )
      return (this.options.get("time")) ? false : true
    else
      return (this.options.get("close_on_click"))
  },
  navMonth: function(month) { (target_date = new Date(this.date)).setMonth(month); return (this.navTo(target_date)); },
  navYear: function(year) { (target_date = new Date(this.date)).setYear(year); return (this.navTo(target_date)); },
  navTo: function(date) {
    if (!this.validYear(date.getFullYear())) return false;
    this.date = date;
    this.date.setDate(1);
    this.refresh();
    this.callback("after_navigate", this.date);
    return true;
  },
  setUseTime: function(turn_on) {
    this.use_time = this.options.get("time") && (this.options.get("time")=="mixed" ? turn_on : true) // force use_time to true if time==true && time!="mixed"
    if (this.use_time && this.selected_date) { // only set hour/minute if a date is already selected
      var minute = Math.floor_to_interval(this.selected_date.getMinutes(), this.options.get("minute_interval"));
      var hour = this.selected_date.getHours();
      
      this.hour_select.setValue(hour);
      this.minute_select.setValue(minute)
    } else if (this.options.get("time")=="mixed") {
      this.hour_select.setValue(""); this.minute_select.setValue("");
    }
  },
  updateValue: function() {
    var last_value = this.target_element.value;
    this.target_element.value = this.dateString();
    if (last_value!=this.target_element.value) this.callback("onchange");
  },
  today: function(now) {
    var d = new Date(); this.date = new Date();
    var o = $H({ day: d.getDate(), month: d.getMonth(), year: d.getFullYear(), hour: d.getHours(), minute: d.getMinutes()});
    if ( ! now ) o = o.merge({hour: "", minute:""}); 
    this.updateSelectedDate(o, true);
    this.refresh();
  },
  close: function() {
    if (this.closed) return false;
    this.callback("before_close");
    this.target_element.calendar_date_select = nil;
    Event.stopObserving(document, "mousedown", this.closeIfClickedOut_handler);
    Event.stopObserving(document, "keypress", this.keyPress_handler);
    this.calendar_div.remove(); this.closed = true;
    if (this.iframe) this.iframe.remove();
    if (this.target_element.type != "hidden" && ! this.target_element.disabled) this.target_element.focus();
    this.callback("after_close");
  },
  closeIfClickedOut: function(e) {
    if (! $(Event.element(e)).descendantOf(this.calendar_div) ) this.close();
  },
  keyPress: function(e) {
    if (e.keyCode==Event.KEY_ESC) this.close();
  },
  callback: function(name, param) { if (this.options.get(name)) { this.options.get(name).bind(this.target_element)(param); } }
}

// fix for https://prototype.lighthouseapp.com/projects/8886/tickets/365-elementgetstyle-problem-with-ie-6-7
Element.Methods.originalGetStyle = Element.Methods.getStyle;

Element.addMethods({
  getStyle: function(element, style) {
    try {
      return Element.Methods.originalGetStyle(element, style);
    } catch(e) {
      return 'static';
    }
  }
});


// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults

var standard_blind_options = {duration: "0.25"};

var items = new Object(); // For rule data

// Ticket functions - create/edit

//function zd_highlight(p) {
//	if (p) { p.className = (p.value==0 || p.value=='') ? "highlight" : "" }
//}

function render_per_type() {
	if ($("ticket_ticket_type_id")) {
  	if ($("ticket_linked_id")) { set_visibility("ticket_link", ($F("ticket_ticket_type_id")=="2")) }
  	if ($("ticket_date")) { set_visibility("ticket_date", ($F("ticket_ticket_type_id")=="4")) }  
  	//if ($("ticket_due_date_1i")) { set_visibility("ticket_date", ($F("ticket_ticket_type_id")=="4")) }  
  }
}   

// Assignee select - ticket create/edit
function assignee_select(group_id, user_id) {
  $('ticket_assignee_id').update(); // empty the select of all options

  $('ticket_assignee_id').insert(new Element('option'));
  if (Number(group_id) > -1) {
    // Create an option for every agent of the selected group
    var selected_option;
    for (var agent_id in agents) {
      if (agents.hasOwnProperty(agent_id)) { // Don't iterate through inherited properties, per Douglas Crockford
        if (groups[group_id].include('#' + agent_id + '#')) {
          var option = new Element('option', {value: agent_id}).update(agents[agent_id]);
          if (String(user_id) === agent_id) {
            option.writeAttribute({selected: 'selected'}); // select the current user if he's one of the potential assignees
          }
          $('ticket_assignee_id').insert(option);
        }
      }
    }
    //zd_highlight($('ticket_assignee_id'));
    $('ticket_assignee_id').enable();
  }
  else {  
    $("ticket_assignee_id").options.selectedIndex = 0;
    $("ticket_assignee_id").disable();
  }
}

function update_properties(assignee_id) {
	// Highlight missing properties on ticket form                      
  if ($("ticket_group_id") && $("ticket_group_id").type=="select-one") {      
    if ($F("ticket_group_id") == '') {
      //zd_highlight($("ticket_group_id")) ; $("ticket_assignee_id").disabled='disabled' 
			} 
    else {
      if ($F("ticket_assignee_id")=='') { assignee_select($F("ticket_group_id"), assignee_id) } } 
  }      
  //zd_highlight($("ticket_priority_id"))  
  //zd_highlight($("ticket_ticket_type_id")) 
  render_per_type();           
  return true;         
}

// Highlights agents in ticket cc list
function highlightAgents(){
  // Parse CC-List
  var ccusers = $$('#facebook-list li.choice');
  for(index = 0; index < ccusers.length; index++){
    var choiceId = ccusers[index].readAttribute('choice_id');

    if(agents[choiceId] != null){
      $$('#facebook-list li.choice')[index].addClassName('agent');
    }
  }

  // Observe changes
	var first_ul = $$('#facebook-list ul.multi_value_field')[0];
	if (first_ul){
		first_ul.observe('DOMNodeInserted', highlightAgents);
	}
};

// Generic functions

function set_visibility(id, want_visible) {
  if (want_visible != $(id).visible()) { (want_visible) ? $(id).show() : $(id).hide() }	
}

function toggle_visibility(/* id1, id2, ...*/) {
  for (var i = 0 ; i < arguments.length ; ++i) {
    var id = arguments[i];
		($(id).visible()) ? $(id).hide() : $(id).show()    
  }
}

function toggle_comment(){
	toggle_visibility('comment_full', 'comment_partial'); 
	toggle_visibility('comment_up', 'comment_down');     		
	return false;
}

function toggle_comment_type_for_ticket() {
	$('comment_type').className = ($('comment_type').className == 'say public') ? 'say private' : 'say public'
}

// For user admin page
function toggleFilter(id) {
  $('options').hide();
  toggle_visibility(id);
  return false;
}

function submitUpload() {
	var filename = $('attachment_content').value.strip();	
	if(filename != null && filename.length > 0) {
	  $('uploadform').submit();	
	  $('uploadinput').style.display = "none";
	  $('uploadbar').style.display = "block";		
	}	
}

function notifyOnError(name) {
  alert("Failed to upload file '"+name+"', verify the path");
  resetView();	
}

function addAttachment(name, size, id) {
  new Insertion.Bottom("attachmentlist", "<div class='attachment' id='attachment_"+id+"'>"+name+" <span class='size'>"+size+"</span> <a href='#' onclick='deleteAttachment("+id+");return false'>x</a>");
  resetView();
}

function deleteAttachment(id) {
  new Ajax.Updater({success:'attachmentDeleted('+id+')'}, '/attachment/delete/'+id, { asynchronous:true, evalScripts:true});
}

function attachmentDeleted(id) {
	Element.remove('attachment_'+id);
}

var attachmentLists = new Array();

function registerAttachmentList(listId, size) {
  attachmentLists[listId] = size;	
}

function deleteFromAttachmentList(listId, itemId) {
  Element.remove($(itemId));
	attachmentLists[listId] = attachmentLists[listId]-1;
	if(attachmentLists[listId] <= 0) {
	   Element.remove($(listId));
	   delete attachmentLists[listId];
	}
}

function resetView() {
  $('attachment_content').value = '';
  $('uploadbar').style.display = "none";
  $('uploadinput').style.display = "block";
}
  
  
// **** Upload photo stuff
function pictureDeleted(key) {
  $('cancel-block-'+key).hide();
  if (key == 'header_logo') { $('website_url').hide(); }
  selectPicture(key);
}

function selectPicture(key) {
  $('image-block-'+key).hide();
  $('upload-block-'+key).show();
  $('ignore-upload-'+key).value = 0;
}     
  
function resetPicture(key) {
  $('upload-block-'+key).hide();
  $('image-block-'+key).show();
  $('ignore-upload-'+key).value = 1;      
}


function checkTicketDelete() {
	if ($F('submit_type') == 'delete') 
		alert('Warning - selecting this option will delete the ticket when you click submit. Deleted tickets cannot be recovered.');
}

function comfirmTicketDelete() {
	return confirm('You are about to delete this ticket. Deleted tickets will not figure in reports and cannot be recovered. Are you sure you wish to continue?');
}

function check_ticket(ticket_id) {
	
	if (!$('comment_type') && $F('ticket_requester_name') == '') {    
    if (!confirm('You haven\'t entered a requester for the ticket. Submitting this form will register you as the requester.'))
      return false;
	}

  $('submit-button').value = 'Submitting...';
  $('submit-button').disabled = true;	 
  
	if (typeof(collaboratorList) != 'undefined') 
	  collaboratorList.update(); 

	// Remove requester/cc if it is not visible - so we don't need to update it in the ticket model
	if ($F('submit_type') != 'merge' && $('edit_requester').style.display == 'none')	
	  $('edit_requester').remove()

	return true			
}

function copySubmitType() {
  $('ticket-chat').submit_type.value = $('submit_form').submit_type.value;
}

function submitTicketForm(ticket_id) {
  if (check_ticket()) {
    copySubmitType();
    if ($F('submit_type') == 'merge') {
      showMergeWizard(ticket_id);
    } else {
			if (typeof(ticketTagField) != 'undefined') {ticketTagField.beforeFormSubmit();}
      $('ticket-chat').submit();
    }
  }
}

function submitBulkUpdateForm() {
  if (fetch_tickets_to_bulk_update()) {
    if ($F('submit_type') == 'merge') {
      showMergeWizard($('tickets_to_bulk_update').value);
    } else {
      $('ticket-chat').submit();
    }
  }
}

function showMergeWizard(ticket_id) {
  var unchecked = '';
  if ($$('input.tickets_to_bulk_update')) {
    unchecked = $$('input.tickets_to_bulk_update').collect(function(s) { if (!s.checked) return s.value;}).compact().join(',');
  }
  Lightview.show({
    href: '/merge/new',
    rel: 'ajax',
    options: {
      topclose: true,
      autosize: true,
      backgroundColor: '#999',
      ajax: {
        parameters: { source_ids: ticket_id, unchecked: unchecked }, 
        method: 'get'
      }
    }
  });
}

function submitSelectedMerge() {
  var nice_id = $('target_id').value.strip();
  if (!nice_id) {
    alert('Please enter a ticket ID');
    return;
  }

  Lightview.show({
    href: '/merge',
    rel: 'ajax',
    options: {
      topclose: true,
      autosize: true,
      backgroundColor: '#999',
      ajax: {
        parameters: Form.serialize('merge_form'), // the parameters from the form
        method: 'get'
      }
    }
  });

}

function submitNewUserFromTicket(addedAs) {
  new Ajax.Request('/people/users/find_or_create.json', {
    method: 'post',
    parameters: Form.serialize('new_user_form'),
    onSuccess : function(response) {
      addUserToTicket(response.responseJSON, addedAs); 
    }, 
    onFailure : function(response) {
      $('user_email').focus();
      new Effect.Highlight('user_email', { duration: 3 });
    }
  });
}

function addUserToTicket(data, addedAs) {
  value = data.email_address_with_name_without_quotes;
  if (addedAs == 'requester') {
    $('ticket_requester_name').value = value;
  } else {
    collaboratorsInput.addEntry(data.id, value);
  }
  Lightview.hide();
}

function observeMergeWizardEvents() {
  document.observe('lightview:hidden', function(event) {
    $('submit-button').value = 'Submit';
    $('submit-button').disabled = false;
  });

  document.observe('lightview:opened', function(event) {
    // Check if we're in step2 first and if the user wrote something as a comment
    if ($('target_comment') && $('comment_value').value.length > 0) {
      // Add current text in comment area as last comment in target ticket
      $('target_comment').value = $('target_comment').value + '\n\nComment during merge: ' + $('comment_value').value;
      $('source_comment').value = $('source_comment').value + '\n\nComment during merge: ' + $('comment_value').value;
    }
  });
}

function copySolvedState() {
  if ( $('submit_form').solved_true ) {
    $('ticketform').ticket_force_status_change.value = $('submit_form').solved_true.checked;
  }
}

function submitRequestForm() {
  $('ticketform').submit();
}

function toggleAttachForm() {
  $('uploads_link').toggle();
  $('uploads_field').toggle();
}

function submitAttachForm(uploading_msg) {
  $('submit-button').value = uploading_msg;
  $('submit-button').disabled = true;
  document.attach_form.submit();
  $('uploads_form').hide();
  $('uploading_message').show();
  document.attach_form.reset();
}

// TODO use prototype instead
function set_selected_for_select(select, selected_value) {
	if (select) {
		for (var i=0; i<select.options.length; i++) {	
			select.options[i].selected = (select.options[i].value == selected_value);    				         
		}				     
	}			
}

// Autotagging for ticket
function autotag_ticket(element) {
  if(!$F(element).blank() && ticketTagField && ticketTagField.selectedEntries().length == 0) {
    new Ajax.Request('/tags/autotag', {
      parameters: { text: $F(element), target: 'ticketTagField' }
    });
  }
}

// ***** Prototip tips

function comment_tip(element, title, body) {
  new Tip(element, ' ' + body, {
    title: title,
    style: 'zd_comment',
    delay: 0,
    border: 4,
    radius: 4,
    width: 600,
    viewport: true,
    hook: { tip: 'topLeft', mouse: true },
    offset: { x: 12, y: 8 }
  });
}

function help_tip(element, title, body) {
  new Tip(element, ' ' + body, {
    title: title,
    style: 'protogrey',
    border: 5,
    radius: 5,
    stem: 'topLeft',
    hook: { tip: 'topLeft', mouse: true },
    offset: { x: 12, y: 12 }
  });
}

// Tool tips for help labels on ticket form
function create_help_tips() {
  $$('label[tip="help"]').each(function(element) {
    help_tip(element, element.id, element.title);
    element.title = '';
});
}

// ***** 

function stv(value, title, tagger_id, evt, this_elm) {
  if ($('ticket_fields_' + tagger_id)) {
    $('ticket_fields_' + tagger_id).value = value;
    $('title-tagger-' + tagger_id).innerHTML = title.split("").reverse().join("").truncate(26).split("").reverse().join("");
    var e=(evt)?evt:window.event;
    Event.stop(e);
		var li = e.findElement();
		selection_feedback(li);
		
		return(false);
  }
}

// ***** Macro stuff

function apply_macro(case_id, evt){
  if (case_id != '') {
    if (typeof(ticket_id) == 'undefined')
      this_id = '0'
    else
      this_id = ticket_id
		new Ajax.Updater('case', '/rules/apply_macro/' + case_id + '?ticket_id=' + this_id, 
    	{asynchronous:true, evalScripts:true, insertion:Insertion.After, parameters:Form.serialize('ticket-chat')}); 
    $('case').hide();
    var e=(evt)?evt:window.event;
    Event.stop(e);
		var li = e.findElement();
		selection_feedback(li);

  	return(false);
	}
}

var hide_macro_li = function(li){
	// using element storage to retrieve the currently open li and close it
	var current_selection = li.up().retrieve('selection');
	if (current_selection) {
		category_set(current_selection, 'none', 0, 'static');
	}
};

// ***** Topic stuff...

var TopicForm = {
  editNewTitle: function(txtField) {
    $('new_topic').innerHTML = (txtField.value.length > 5) ? txtField.value : 'New Topic';
  }
}

var LoginForm = {
  checkLogin: function(txt) {
    if(txt.value.match(/^https?:\/\//)) {
      $('password_fields').hide();
    } else {
      $('password_fields').show();
    }
  }
}

var EditForm = {
  // show the form
  init: function(postId) {
    $('edit-post-' + postId + '_spinner').show();
    this.clearReplyId();
  },

  // sets the current post id we're editing
  setReplyId: function(postId) {
    $('edit').setAttribute('post_id', postId.toString());
    $('posts-' + postId + '-row').addClassName('editing');
    if($('reply')) $('reply').hide();
  },
  
  // clears the current post id
  clearReplyId: function() {
    var currentId = this.currentReplyId()
    if(!currentId || currentId == '') return;

    var row = $('posts-' + currentId + '-row');
    if(row) row.removeClassName('editing');
    $('edit').setAttribute('post_id', '');
  },
  
  // gets the current post id we're editing
  currentReplyId: function() {
    return $('edit').getAttribute('post_id');
  },
  
  // checks whether we're editing this post already
  isEditing: function(postId) {
    if (this.currentReplyId() == postId.toString())
    {
      $('edit').show();
      //$('edit_post_body').focus();
      return true;
    }
    if($('reply')) $('reply').hide(); 
    return false;
  },

  // close reply, clear current reply id
  cancel: function() {
    this.clearReplyId();
    $('edit').hide()
  }
}

var ReplyForm = {
  init: function() {
    EditForm.cancel();
    $('reply').toggle();
   // $('post_body').focus();
    // for Safari which is sometime weird
//    setTimeout('$(\"post_body\").focus();',50);
  }
}

function showOrHideBulkForm() {
  fetch_tickets_to_bulk_update();
  if ($F('tickets_to_bulk_update').blank()) {
    $('bulk-update').hide();
  }
  else {
    $('bulk-update').show();
  }
}

function fetch_tickets_to_bulk_update() {
	$('tickets_to_bulk_update').value = $$('input.tickets_to_bulk_update').collect(function(s) { if (s.checked) return s.value;}).compact().join(',')
 	return true;
}

function select_all_tickets_for_bulk_update(elm) {
	state = elm.checked
	if ($('bulk-update')) {
	  state ? $('bulk-update').show() : $('bulk-update').hide()   	
	  $$('input.tickets_to_bulk_update').each(function(s) {s.checked = state}) 
	} 	
}

// Color picking and calculations for account
var rgb_css_to_hex = function(str) { // e.g. rgb(0, 122, 255)
	var color_components = str.match(/\d+/g);
	var hex_color = '#';
	str.match(/\d+/g).each(function(val){
		hex_color += Number(val).toColorPart();
	});
	return(hex_color);
}

function hsv2hex(hsv_new) {
	rgb = YAHOO.util.Color.hsv2rgb(hsv_new[0], hsv_new[1], hsv_new[2])
	return YAHOO.util.Color.rgb2hex(rgb[0], rgb[1], rgb[2])
}

function calculateColors(){	
	color = $F('branding_header_color')
	if (fncIsValidColor(color)) {		
		rgb = YAHOO.util.Color.hex2rgb(color)
  	if (!YAHOO.util.Color.isValidRGB(rgb)) return;				
		hsv = YAHOO.util.Color.rgb2hsv(rgb[0], rgb[1], rgb[2])				
		hsv_new = hsv[0] * 1.03				
		if (hsv_new > 1.0) hsv_new = 1.0
		$('branding_tab_background_color').value = hsv2hex([hsv_new, hsv[1] * 0.87, hsv[2] * 0.83])		
		hsv_new = hsv[2] * 1.02	
		if (hsv_new > 1.0) hsv_new = 1.0		
		$('branding_tab_hover_color').value =	hsv2hex([hsv[0], hsv[1] * 0.62, hsv_new])			
		if ($('branding_tab_hover_color').value == 'FFFFFF')
			$('branding_tab_hover_color').value	= 'E8E8E8'				
		$('branding_text_color').value = (hsv[2] > 0.85) ? "2A2A2A" : "FFFFFF";		
		return true;}
	else {
		alert('You have not entered a valid hex color');
		return false;
	}	
}

function fncIsValidColor(hexcolor) {
	var strPattern = /^([0-9a-f]{1,2}){3}$/i;
	return strPattern.test(hexcolor);
}

// ********* Sorting
Ordering = {};
						
Ordering.SetOrder = function(table){
	Element.hide(table);
	Element.show(table + '_sort');
}

// Cookie stuff

var Cookie = {
  set: function(name, value, daysToExpire) {
    var expire = '';
    if (daysToExpire != undefined) {
      var d = new Date();
      d.setTime(d.getTime() + (86400000 * parseFloat(daysToExpire)));
      expire = '; expires=' + d.toGMTString();
    }
    return (document.cookie = escape(name) + '=' + escape(value || '') + expire + '; path=/');
  },
  get: function(name) {
    var cookie = document.cookie.match(new RegExp('(^|;)\\s*' + escape(name) + '=([^;\\s]*)'));
    return (cookie ? unescape(cookie[2]) : null);
  },
  erase: function(name) {
    var cookie = Cookie.get(name) || true;
    Cookie.set(name, '', -1);
    return cookie;
  },
  accept: function() {
    if (typeof navigator.cookieEnabled == 'boolean') {
      return navigator.cookieEnabled;
    }
    Cookie.set('_test', '1');
    return (Cookie.erase('_test') === '1');
  }
};

/* Textarea auto-resize. http://www.felgall.com/jstip45.htm */
function textarea_resize(t) {
	a = t.value.split('\n');
	b=1;
	for (x=0;x < a.length; x++) {
 		if (a[x].length >= t.cols) b+= Math.floor(a[x].length/t.cols);
 	}
	b += a.length;
	if (b > t.rows) t.rows = b;
}

//Javacript difference in time in words - http://nullstyle.com/2007/06/02/caching-time_ago_in_words/
function time_ago_in_words(from) {
  return distance_of_time_in_words(new Date(), new Date(from)) 
}

function i18n_time_ago_in_words(from) {
  return i18n_distance_of_time_in_words(new Date(), new Date(from)) 
}

function distance_of_time_in_words(to, from) {
  seconds_ago = ((to  - from) / 1000);
  minutes_ago = Math.floor(seconds_ago / 60)

  if(minutes_ago <= 0) { return "less than a minute";}
  if(minutes_ago == 1) { return "a minute";}
  if(minutes_ago < 45) { return minutes_ago + " minutes";}
  if(minutes_ago < 90) { return "1 hour";}
  hours_ago  = Math.round(minutes_ago / 60);
  if(minutes_ago < 1440) { return hours_ago + " hours";}
  if(minutes_ago < 2880) { return "1 day";}
  days_ago  = Math.round(minutes_ago / 1440);
  if(minutes_ago < 43200) { return days_ago + " days";}
  if(minutes_ago < 86400) { return "1 month";}
  months_ago  = Math.round(minutes_ago / 43200);
  if(minutes_ago < 525960) { return months_ago + " months";}
  if(minutes_ago < 1051920) { return "1 year";}
  years_ago  = Math.round(minutes_ago / 525960);
  return "over " + years_ago + " years" 
}

/*
  Object factory. Used to allow multiple occurences of the same object type on the same page
  Use like so:

  ObjectFactory.create(TextWidget, { 
    id: <%= @widget.id %>, params: <%= @widget.params.to_json %>
  });
   
  This instantiates and returns a new instance of the class specified. The instance gets initialized
  with an args parameter, containing the property list (eg. { id: 4, params: 'horse' }). The id must be 
  set as it is used to lookup the instance later. So, to get the instance reference:
  
  ObjectFactory.get(<%= @widget.id%>).dance_the_funky_chicken_dance();
*/
var ObjectFactory       = Class.create({});
ObjectFactory.instances = new Hash();

ObjectFactory.create    = function(type, args) {
  instance = new type(args);
  ObjectFactory.instances.set(args.id, instance);
  return instance;
};

ObjectFactory.get = function(id) {
  return ObjectFactory.instances.get(id);
};

ObjectFactory.remove = function(id) {
  return ObjectFactory.instances.unset(id);
};

var usableChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghiklmnopqrstuvwxyz";

function randomString() {
	var result = '';

	for (var i=0; i< 48; i++) {
		var index = Math.floor(Math.random() * usableChars.length);
		result += usableChars.substring(index, index+1);
	}
	
	return result;
}

document.getUrlParameter = function(name) {
  name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
  var regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
  var results = regex.exec(window.location.href);
  return results ? unescape(results[1]) : null;
}


// Will format strings in human readable format... addCommas(1234567890) -> 1,234,567,890
function addCommas(nStr)
{
  nStr += '';
  var rgx = /(\d+)(\d{3})/;
  while (rgx.test(nStr)) {
    nStr = nStr.replace(rgx, '$1' + ',' + '$2');
  }
  return nStr;
}


function Start() { 				
	//if ($('views-drop') && ($$('ul#views-drop>li').length == 0)) 
	//	$('views-drop').innerHTML = '<li class="drop-error">There are no tickets in your views currently</li>';


  // Mouse over for nested category menues
	var ul = $$('div.content ul.drop-list').each(function(ul){
		document.observe('click', function(event){ // close the menu if the user clicks outside of it
			if (!event.findElement().descendantOf(ul)){
				ul.select('li').each(function(li){
					category_set(li, 'none', 0, 'static');
				});
			}
		});
		var applicator = ul.firstDescendant('li');
		applicator.observe('click', function(){
			category_set(applicator, 'block', 5, 'relative');
		});

		ul.select('ul.first-drop li').each(function(li){
			li.observe('mouseover', function(event){ // adding mouseover handler
				hide_macro_li(li);
				li.up().store('selection', li); // store the currently selected li
				category_set(li, 'block', 5, 'relative'); // open this sub-menu
			});
		});
	});
	
	if (!window.XMLHttpRequest) startList(); // Only for IE6
}

function createRef(instance,method) {
  return function(){  method.apply(instance,arguments); };
}

function category_set(element, visibility, z_index, position) {
  if (elm = element.down('ul')) {
//    var elm_a = elm
    elm.style.display = visibility;
/*    if (elm = element.up('div.select')) {
      // Agent form 
      if (elm_z = elm.down('label')) {
        elm_z.style.zIndex = z_index;
        elm_z.style.position = position;
      }
	  if (elm_z = elm.down('span.drop')) elm_z.style.zIndex = z_index;
    }*/
  }
}

var selection_feedback = function(li){
	new Effect.Highlight(li, {
		queue: 'end',
		beforeStart: function(){
				li.store('selection_feedback.style', li.readAttribute('style')); // make sure to set it to an empty string if it's null, otherwise you get problems in e.g. Safari
				li.setStyle({color: li.getStyle('background-color')}); // to temporarily invert the colors
		},
		startcolor: rgb_css_to_hex(li.getStyle('color')),
		duration: 0.2,
		afterFinish: function(){
			li.writeAttribute('style', li.retrieve('selection_feedback.style') || '');
		}
	});
	new Effect.Fade(li.up('ul.first-drop'), { queue: 'end', duration: 0.3, afterFinish: hide_macro_li(li) });
}

/* Make IE 6 do (h)over */
function startList() {
	iterate_startList($("green"));
	if (document.getElementById("gray")) {
		iterate_startList($("gray"));	}
}

function iterate_startList(navRoot) {	
	if (!navRoot) {return;}		
	for (i=0; i<navRoot.childNodes.length; i++) {
		node = navRoot.childNodes[i];
		if (node.nodeName=="LI") {
			node.onmouseover=function() {
				this.className+=" over";
				if (this.childNodes[2] && this.childNodes[2].nodeName=='UL')
					ie6_show_iframe_mask(this.childNodes[2]);					
			}
			node.onmouseout=function() {
				this.className=this.className.replace(" over", "");
				ie6_hide_iframe_mask(this.childNodes[2]);
			}
		}
	}
}

function ie6_show_iframe_mask(el) {
  if (el) 
  	el.style.display = 'block';
	//if (!window.XMLHttpRequest) {
	var	ieMat = document.createElement('iframe');
	if(document.location.protocol == "https:")
		ieMat.src="//0";
	else if(window.opera != "undefined")
		ieMat.src="";
	else
		ieMat.src="javascript:false";

	ieMat.id = 'mIframe';
	ieMat.scrolling="no";
	ieMat.frameBorder="0";
	ieMat.style.top = -1; 
	ieMat.style.left= -1;
	ieMat.style.width= el.offsetWidth+"px";
	ieMat.style.height= el.offsetHeight+"px";
	ieMat.style.zIndex= -1;		
	ieMat.style.filter='alpha(opacity=0)';
	el.insertBefore(ieMat, el.childNodes[0]);
	ieMat.style.position = 'absolute';				
	//}
}

function ie6_hide_iframe_mask(el) {
  if (el)
  	el.style.display = 'none';
	if (iframe = document.getElementById('mIframe'))
		iframe.style.display = 'none';	
}

var ticket_id;

if(!Zendesk) var Zendesk = {};
Zendesk.resources = [];
Zendesk.Resource = Class.create();
	
Zendesk.Resource.prototype = {
  initialize : function(options) {  		  	
    this.options      = options || {};   
    this.options.user = options.user || Cookie.get(this.options.anchor + '_user')
    this.options.pass = options.pass || Cookie.get(this.options.anchor + '_pass') 
    this.content_anchor = $$('#' + this.options.anchor + ' #content')[0]
    this.title_anchor   = $$('#' + this.options.anchor + ' #title')[0]  
     
 		Ajax.Responders.register({ onException: function(r, e) { alert("Exception:\n\n" + e) }});          

		if (this.options.title) {
		  this.title_anchor.innerHTML = this.options.title;
		}
		
    this.render();             		     				           
  },
  submit_credentials : function(form) {  
		this.options.user = form['username'].value;
		this.options.pass = form['password'].value;  	
		if (this.options.user.blank() && this.options.pass.blank())
			alert('Please provide both a username and a password');
		else {
  		Cookie.set(this.options.anchor + '_user', this.options.user);
  		Cookie.set(this.options.anchor + '_pass', this.options.pass); 
    	this.render();   		
  	}  	
  },  
  logout : function() { 
    options = this.options;
    Cookie.erase(options.anchor + '_user');
    Cookie.erase(options.anchor + '_pass');
    this.options.user = this.options.pass = null;
    this.render();
  },
  submit_data : function(form) {
  	control = this;
  	var parameters = Form.serialize(form);
  	parameters += "&user=" + control.options.user + "&pass=" + control.options.pass + "&domain=" + control.options.domain + "&use_ssl=" + control.options.use_ssl;
  	if (ticket_id) {
  	  parameters += "&ticket_id=" + ticket_id;
	  }
    if ( parameters.indexOf("media_type=") == 0 ) {
      var media_type = control.options.media_type || 'application/xml';
      parameters += "&media_type=" + media_type;
    }
		new Ajax.Request('/proxy.js', {asynchronous:true, evalScripts:true, parameters:parameters,
			onSuccess: function(transport) { control.resource_write_success(transport, control); enable_submit(form); },
    	onFailure: function(transport) { control.resource_failure(transport, control); enable_submit(form); }
    }); 
    disable_submit(form)
  },
  render : function() { 
  	control = this;	
  	options = control.options; 	
  	if (options.login_content != null && !(options.user && options.pass)) 
  		control.content_anchor.innerHTML = options.login_content();
		else {					  				
			control.content_anchor.innerHTML =	options.application_content();		  	
  		options.application_resources.each(function(resource){control.request(resource)});
		}		
  },
  request : function(options) {
  	control = this; 		
  	if (options.resource == null) {
  		if (options.on_success != null) {
  		  options.on_success();
  		}
  	}
  	else {	  		
	  	media_type = options.media_type || 'application/json';
			new Ajax.Request('/proxy.js', {
	  		method: 'get',
				parameters: {user: control.options.user, pass: control.options.pass, 
										 domain: control.options.domain, use_ssl: control.options.use_ssl, 
										 resource: options.resource, media_type: media_type, cache_gets: control.options.cache_gets},
	  		onSuccess: function(transport) { control.resource_read_success(transport, control, options); },
	    	onFailure: function(transport){ control.resource_failure(transport, control); }
	    });	
    }	
  },
  resource_failure : function(transport, control) {
    if (transport.status == 401) {  
			control.options.user = null; control.options.pass = null;					 		
			Cookie.erase(control.options.anchor + '_user'); Cookie.erase(control.options.anchor + '_pass'); 		
			if (control.content_anchor.innerHTML != control.options.login_content())			
				alert("Username and password are not correct. Please re-enter.");
			control.render();	}
    else {
    	alert(transport.responseJSON.error)
  	}
  },
  //Called when data has been read
  resource_read_success : function(transport, control, options) {
    if(options != null && options.on_success != null) {
      options.on_success(transport.responseJSON);      
    }
  },
  //Called when data has been written
  resource_write_success : function(transport, control) {
    if(transport.responseJSON.audit != null) {
		  new Ajax.Request('/events/create.js', {
  		  method: 'post', asynchronous:true,
			  parameters: { ticket_id: transport.responseJSON.audit.ticket_id,
			                value:     transport.responseJSON.audit.value,
			                reference: transport.responseJSON.audit.reference,
			                via_id:    transport.responseJSON.audit.via_id },
  		  onSuccess: function(transport) { },
    	  onFailure: function(transport){ alert('Failed to register event: '+transport.responseJSON.error); }
      });	
    }
  }  
}	


function enable_submit(form) {
	form['submit'].value = form['submit'].getAttribute('originalValue'); 
	form.enable();		
	form.reset();
}

function disable_submit(form) {
	form['submit'].setAttribute('originalValue', form['submit'].value);	
	form.disable();		
	form['submit'].value = 'Submitting...';		
}




Prototype.Browser.Mac = navigator.userAgent.toLowerCase().indexOf('mac') != -1;

var Widget = Class.create({
  initialize: function(args) {
    this.id = args.id;
    this.locationId = args.locationId;
    this.isEmpty = args.isEmpty;
    if (Prototype.Browser.Mac) {
      if (this._closeButton()) {
        this._closeButton().addClassName('mac_widget_close_button');
      }
    }
    if (this.isEmpty) {
      this.containingElement().hide();
    };
  },
  containingElement: function() {
    return $('widget_' + this.id);
  },
  show: function() {
    this.containingElement().blindDown({duration: 0.2});
  },
  hide: function(afterFinishFunction) {
    this.containingElement().blindUp({duration: 0.2, afterFinish: afterFinishFunction});
  },
  destroy: function() {
    this.containingElement().remove();
  },
  _closeButton: function() {
    return this.containingElement().down('.widget_close_button');
  },
  _editButton: function() {
    return this.containingElement().down('.widget_edit_button');
  },
  showControls: function() {
    if (this.isEmpty) {
      this.containingElement().show();
    };
    this._closeButton().appear({duration: 0.3});
    if (this._editButton()) {
      this._editButton().appear({duration: 0.3});
    };
    this.containingElement().addClassName('widget_edit_mode');
  },
  hideControls: function() {
    this._closeButton().fade({duration: 0.2});
    if (this._editButton()) {
      this._editButton().fade({duration: 0.3});
    };
    this.containingElement().removeClassName('widget_edit_mode');
    if (this.isEmpty) {
      this.containingElement().hide();
    };
  }
});

Object.extend(Widget, {
  asyncInsertion: function(ajax_response) {
    var fixedWidget = $('widget_fixed');
    ajax_response.responseJSON.each(function(json_widget) {
      if (json_widget.location < 0 && fixedWidget) {
        fixedWidget.insert({before: json_widget.inner_html})
      } else {
        $('sidebar').insert({bottom: json_widget.inner_html})
      }
    });

    Widget.redraw();
    Widget.fireLoadEvents.delay(0.1);
  },
  
  redraw: function() {
    if (Widget.hasVisibleWidgets() || ($('add_widget_select_widget').visible())) {
      $('container').removeClassName('full_width');
      $('container').removeClassName('minimized_widgets');
    } else if (widgetManager) {
      $('container').removeClassName('full_width');
      $('container').addClassName('minimized_widgets');
    } else {
      $('container').addClassName('full_width');
      $('container').removeClassName('minimized_widgets');
    };
  },
  
  hasVisibleWidgets: function() {
    return $$('#sidebar .draggable').any(function(w) {return w.visible();});
  },
  
  pendingWidgets: new Array(),
  
  registeredWidgets: new Array(),
  
  register: function(widgetClass, options) {
    Widget.pendingWidgets.push([widgetClass, options || {}]);
  },
  
  activateWidgets: function() {
    var classAndOptions;
    while (classAndOptions = Widget.pendingWidgets.shift()) {
      Widget.registeredWidgets.push(ObjectFactory.create(classAndOptions[0], classAndOptions[1]));
    }
  }
});



Widget.ScriptLoadingQueue = Class.create({
  initialize: function($super) {
    this.QUEUED = 10;
    this.LOADING = 20;
    this.LOADED = 30;
    this.queue = new Array();
    this.running = false;
  },
  
  setExternalScriptState: function(src, state) {
    var entry = this.queue.find(function(e) {return e.src == src});
    if (entry.state < state) {
      switch(state) {
        case this.LOADING:
          entry.loadingStartTime = new Date().getTime();
          break;
        case this.LOADED:
          entry.loadingEndTime = new Date().getTime();
          break;
      }
      entry.state = state;
    };
  },
  
  loading: function() {
    return this.queue.find(function(e) {return e.state == this.LOADING}, this);
  },
  
  isDone: function() {
    return this.queue.all(function(e) {return e.state == this.LOADED}, this);
  },
  
  next: function() {
    return this.queue.find(function(e) {return e.state == this.QUEUED}, this);
  },
  
  push: function(theSrc, theOptions) {
    if (!this.include(theSrc)) {
      this.queue.push({ src: theSrc,
                        state: this.QUEUED,
                        options: theOptions || {},
                        loadingStartTime: null,
                        loadingEndTime: null});
    };
  },
  
  include: function(src) {
    return this.queue.any(function(e) {return e.src == src;});
  },
  
  loadScripts: function(callback) {
    if (this.isDone()) {
      //console.log('done loading scripts');
      this.running = false;
      if (callback) {
        callback();
      };
    } else {
      this.running = true;
      
      if (this.loading()) {
        //console.log('delay loading next script')
      } else {
        var nextScript = this.next();
        this.setExternalScriptState(nextScript.src, this.LOADING);
        //console.log('loading ' + nextScript.src);
        
        var attribs = $H({type: 'text/javascript', charset: 'utf-8'}).merge(nextScript.options);
        var script = new Element('script');
        script.src = nextScript.src;
        attribs.each(function(pair) {
          script.setAttribute(pair.key, pair.value);
        });
        var head = document.getElementsByTagName('head')[0];
        if (!head) {
          head = document.body.parentNode.appendChild(new Element('head'));
        }

        script.onload = this.setExternalScriptState.curry(nextScript.src, this.LOADED).bind(this);

        script.onreadystatechange = function() {
          if (this.readyState == 'loaded' || this.readyState == 'complete') {
            this.onload();
          }
        }.bind(script);

        head.appendChild(script);
      };
      this.loadScripts.bind(this).delay(0.1, callback);
    };
  }
});

Widget.scriptLoadingQueue = new Widget.ScriptLoadingQueue();

Object.extend(Widget, {
  //fancy external dependency loading queue is disabled for now.
  require: Widget.scriptLoadingQueue.push.bind(Widget.scriptLoadingQueue),
  require_old: function(src, options) {
    var attribs = $H({type: 'text/javascript', charset: 'utf-8'}).merge(options);
    var script = new Element('script');
    script.src = src;
    attribs.each(function(pair) {
      script.setAttribute(pair.key, pair.value);
    });
    var head = document.getElementsByTagName('head')[0];
    if (!head) {
      head = document.body.parentNode.appendChild(new Element('head'));
    }
    head.appendChild(script);
  },
  
  fireLoadEvents: function() {
    Widget.activateWidgets();
    Widget.scriptLoadingQueue.loadScripts(document.fire.curry('widgets:load').bind(document));
    //document.fire('widgets:load');
  }
});

var ReportWidget = Class.create(Widget, {
  initialize: function($super, args) {
    $super(args);
    this.output  = args.output;
    this.graphs  = args.graphs;
    this.pointer = 0;
  },
  displayGraph: function() {
     if($(this.output) == null) {
       return;
     }
     this.pointer = this.pointer % this.graphs.length;
     $(this.output).src = this.graphs[this.pointer];
  },
  next: function() {
    this.pointer = this.pointer + 1;
    this.displayGraph();
  },
  start: function() {
    this.displayGraph();
  }
});

var TextWidget = Class.create(Widget, {
  initialize: function($super, args) {
    $super(args);
    this.output  = args.output;
    this.texts   = args.texts;
    this.delay   = args.delay;
    this.pointer = 0;
    this.callout = null;
  },
  displayText: function(position, target) {
     if($(target) == null) {
       return;
     }

     if(position < 0) {
       position = this.texts.length - 1;
     }
     else {
       position = position % this.texts.length;
     }

     this.pointer = position;

     $(target).hide();
     $(target).update(this.texts[position]);
     $(target).appear({ duration: 1.5 });

     if(this.texts.length > 1) {
       this.callout = this.displayText.bind(this).delay(this.delay, position+1, target);
     }
  },
  next: function() {
    window.clearTimeout(this.callout);
    this.displayText(this.pointer+1, this.output);
  },
  prev: function() {
    window.clearTimeout(this.callout);
    this.displayText(this.pointer-1, this.output);
  },
  start: function() {
    if(this.texts.length > 0) {
      this.displayText(this.pointer, this.output);
    }
  }
});

var CustomWidget = Class.create(Widget, {});

var BuddhaMachineWidget = Class.create(Widget, {});

var TopPerformersWidget = Class.create(Widget, {});

var CampfireWidget = Class.create(Widget, {});

var OrganizationWidget = Class.create(Widget, {});

var EmbedWidget = Class.create(Widget, {});

var RelatedEntriesWidget = Class.create(Widget, {});

var TopForumEntriesWidget = Class.create(Widget, {});

var ViewsWidget = Class.create(Widget, {});

var GetSatisfactionWidget = Class.create(Widget, {});

var ForumsWidget = Class.create(Widget, {
  initialize: function($super, args) {
    $super(args);
    
    var selectedForum = document.getUrlParameter('forum_id')
    if (selectedForum) {
      set_selected_for_select($('forum_id'), selectedForum);
    };
    
    var query = document.getUrlParameter('query');
    if (query && ($('query').value == '')) {
      $('query').value = query;
    };
  }
});

var HarvestWidget = Class.create(Widget, {
  initialize: function($super, args) {
    $super(args);
    this.harvest_domain = args.domain;
    this.use_ssl = args.use_ssl;
    this.ticket_id = args.ticket_id;
    this.ticket_nice_id = args.ticket_nice_id;
    if (this.ticket_nice_id == undefined)
      this.ticket_nice_id = this.ticket_id
    if (this.harvest_domain && this.ticket_id) {
      this.harvest_resource = new Zendesk.Resource(
        {anchor: 'widget_' + this.id,
         domain: this.harvest_domain,
         use_ssl: this.use_ssl || 'false',
         login_content: this.login.bind(this),
         application_content: this.application.bind(this),
         application_resources: [ {resource: 'daily', on_success: this.projectsSelector.bind(this)} ]
        }
      );
    } else {
      this.containingElement().down('div#content').update(args.not_available || 'Only available when viewing tickets');
    }
  },

  login: function() {
    return  '<form onsubmit="ObjectFactory.get(' + this.id + ').harvest_resource.submit_credentials(this); return false;" class="form">' +
            '<label>Username</label><input type="text" id="username"/>' +
            '<label>Password</label><input type="password" id="password"/>' +
            '<input type="submit" value="Login" id="submit" style="float:left">' +
            '</form>'
  },

  application: function() {
    return '<form onsubmit="if (parseFloat(this[\'request[hours]\'].value)==NaN) {alert(\'Please enter a valid value for hours\'); return false;}; this[\'request[notes]\'].value +=\' (ticket #' + this.ticket_nice_id + ')\';  ObjectFactory.get(' + this.id + ').harvest_resource.submit_data(this); return false;" class="form">' +
           /* Form fields to POST to Harvest */
           '<label>Select project</label><select name="request[project_id]" id="harvest-form-projects" onchange="ObjectFactory.get(' + this.id + ').tasksSelector(this.options[this.selectedIndex].value)"></select>' +
           '<label>Select task</label><select name="request[task_id]" id="harvest-form-tasks"></select>' +
           '<label>Notes</label><input type="text" name="request[notes]"/>' +
           '<label><b>Hours</b></label><input type="text" name="request[hours]" size="5" maxlength="5"/><br/>' +
           '<input type="hidden" name="request[spent_at]" value="' + Date('dd/mm/yyyy') + '">' +
           /* Request parameter to pluck from form fields for POST*/
           '<input type="hidden" name="pluck_param" value="request">' +
           /* REST resource to POST to */
           '<input type="hidden" name="resource" value="daily/add">' +
           /* Format for REST resource POST (json or xml) */
           '<input type="hidden" name="media_type" value="application/xml">' +
           /* Title on ticket event */
           '<input type="hidden" name="event_reference" value="Harvest time tracking">' +
           /* Return values from POST to log on ticket event */
           '<input type="hidden" name="event_log" value="Project,Task,Notes,Hours,location,ID">' +
           '<input type="submit" value="Submit" id="submit">' +
           '<span class="link" style="font-weight:normal;margin-left:20px;" onclick="ObjectFactory.get(' + this.id + ').harvest_resource.logout()">(logout)</span>' +
           '</form>'
  },

  // Harvest client selector
  projectsSelector: function(response) {
    // load options into the project dropdown
    var currentClient = ''
    var projectInput = $('harvest-form-projects');
    var optGroup;
    projectInput.update();
    projects = $A(response.projects);
    if (projects.length == 0) return;
    projects.sortBy(function(p){return p.client + p.name}).each(function(p){
      // if this is a new client, create the option group for it
      if (currentClient != p.client)  {
        // add the last option group to the input selector if one has been created
        if (currentClient != '')
          projectInput.appendChild(optGroup);
          optGroup = new Element("optgroup");
          optGroup.setAttribute("label", p.client);
          currentClient = p.client;
        }

        // keep adding this to the option group
        var option = new Element("option");
        option.value = p.id;
        option.update(p.name);
        optGroup.appendChild(option);
    });

    // tack on the final optgroup
    projectInput.appendChild(optGroup);

    // display the default
    projectInput.selectedIndex = 0;

    // load default task list
    if (projects.length>0) this.tasksSelector(projects[0].id);
  },

  // Harvest task selector - tasks are dependant on project selected
  tasksSelector: function(project_id) {
    // we always start with the billable group
    var billable = false;
    var currentGroup = '';
    $('harvest-form-tasks').update();

    projects.each(function(p){
      // if we have a match, print out the tasks
      if (p.id == project_id) {
        $A(p.tasks).sortBy(function(t){return !t.billable + t.name}).each(function(t){
          var option = new Element("option");
          option.value = t.id;
          option.update(t.name);
          // if the current group matches this task's billable status, add it in
          currentTask = t.billable ? "Billable" : "Non-billable";
          if (currentGroup != currentTask) {
            // wrap up the first option group
            if (currentGroup != '') $('harvest-form-tasks').insert(optGroup);
            // start the next one
            optGroup = new Element("optgroup");
            optGroup.setAttribute("label", currentTask);
            currentGroup = currentTask;
          }
          optGroup.appendChild(option);
        });
        // add last group to option group
        $('harvest-form-tasks').insert(optGroup);
      }
    });
  }
});

var HighriseWidget = Class.create(Widget, {
  initialize: function($super, args) {
    $super(args);
    this.highrise_domain = args.domain;
    this.use_ssl = args.use_ssl;
    this.initial_name = args.name;
    this.ticket_id = args.ticket_id;
    this.ticket_nice_id = args.ticket_nice_id;
    if (this.ticket_nice_id == undefined)
      this.ticket_nice_id = this.ticket_id
    this.highrise_resource = new Zendesk.Resource(
      {anchor: 'widget_' + this.id,
       domain: this.highrise_domain,
       use_ssl: this.use_ssl || 'false',
       user: args.username,
       pass: 'X',
       login_content: this.login.bind(this),
       application_content: this.application.bind(this),
       application_resources: [ {resource: null, on_success: this.application.bind(this)} ]
      }
    );

  },

  login: function() {
    return  '<form onsubmit="ObjectFactory.get(' + this.id + ').highrise_resource.submit_credentials(this); return false;" class="form">' +
            '<label>Enter your highrise token</label><input type="text" class="text" id="username"/>' +
            '<input type="hidden" id="password" value="X"/>' +
            '<input type="submit" value="Login" id="submit" style="float:left">' +
            '</form>'
  },

  application: function() {
    return '<div id="highrise-lookup-updating"><form id="lookup-form" onsubmit="disable_submit($(\'lookup-form\')); ObjectFactory.get(' + this.id + ').lookup(); return false;">' +
              '<input type="text" class="text" style="margin-bottom:5px;" id="h_lookup" value="' + (this.initial_name || '') + '"/>' +
              '<input type="submit" id="submit" value="Lookup user"/>' +
            '</form></div>' +
           '<div id="highrise-lookup-result" style="display:none;"></div>'
  },

  lookup: function() {
    hName = $F('h_lookup').split(' ');
    if (hName.length > 1) {
      name = hName.first() + ' ' + hName.last();
    } else {
      name = hName.first();
    }
    if (name.blank()) {
      alert("Please enter a name");
      enable_submit($('lookup-form'));
    }
    resource = '/people/search.js?term=' + escape(name);
    this.highrise_resource.request({resource: resource, on_success: this.render_highrise_lookup_result.bind(this)});
  },

  lookup_id: function(id) {
    person = this.people.detect(function(p) {return p.id == id;})
    result = this.format_person_data(person)
    $('highrise-lookup-result').update(result);
  },

  people: null,

  render_highrise_lookup_result: function(response) {
    this.people = response.people;
    if (typeof(this.people) != 'undefined') {
      if (this.people.length > 1) {
        result = '<h4>' + this.people.length + " matching people found in Highrise. Click on a person for more information</h4>";
        result += this.people.collect(function(p) {return ('<span class="link" onclick="ObjectFactory.get(' + this.id + ').lookup_id(' + p.id + ')">' + (p.first_name == null ? "" : p.first_name) + ' ' + (p.last_name == null ? "" : p.last_name) + '</span>')}.bind(this)).join('<br/>');
        result += '<br><br><a href="javascript:ObjectFactory.get(' + this.id + ').reset_form();">New search</a>';
      } else {
        result = this.format_person_data(this.people.first());
      }
      toggle_visibility('highrise-lookup-updating', 'highrise-lookup-result');
      $('highrise-lookup-result').update(result);
    } else {
      alert("No match found in Highrise");
      enable_submit($('lookup-form'));
    }
  },

  format_person_data: function(person) {
      host  = 'http' + (this.use_ssl ? 's' : '') + '://' + this.highrise_domain;
      result = '<h4 style="font-size:16px"><a href="' + host + '/people/' + person.id + '" target="_new">' + (person.first_name == null ? "" : person.first_name) + ' ' + (person.last_name == null ? "" : person.last_name) + '</a></h4>';
      if (person.title) result += '<p class="minimum">' + person.title + '</p>';
      if (person.background) result += '<h4>Background</h4><p class="minimum">' + person.background + '</p>';

      // Phones
      if (person.contact_data.phone_numbers) {
        items = person.contact_data.phone_numbers.collect(function(s) {return (s.number + ' <span class="sub">' + s.location.toLowerCase() + '</span>')}).join('<br/>');
        if (items) result += '<h4>Phone</h4><p class="minimum">' + items + '</p>';
      }

      // Emails
      if (person.contact_data.email_addresses) {
        items = person.contact_data.email_addresses.collect(function(s) {return '<a href="mailto:' + s.address + '">' + s.address + '</a> <span class="sub">' + s.location.toLowerCase() + '</span>'}).join('<br/>');
        if (items) result += '<h4>Email</h4><p class="minimum">' + items + '</p>' ;
      }

      // Web adresses
      if (person.contact_data.web_addresses) {
        items = person.contact_data.web_addresses.collect(function(s) {return '<a href="' + s.url + '" target="_new">' + s.url + '</a> <span class="sub">' + s.location.toLowerCase() + '</span>'}).join('<br/>');
        if (items) result += '<h4>Web</h4><p class="minimum">' + items + '</p>';
      }

      // Instant messengers
      if (person.contact_data.instant_messengers) {
        items = person.contact_data.instant_messengers.collect(function(s) {return s.address + ' <span class="sub">' + s.protocol + ', ' + s.location.toLowerCase() + '</span>'}).join('<br/>');
        if (items) result += '<h4>IM</h4><p class="minimum">' + items + '</p>';
      }

      // Addresses?
      //items = person.contact_data.addresses.collect(function(s) {return s.street + '<br/>' + s.city + ' '  + s.zip + '<br/>' + s.state + '<br/>'  + s.country + ' <span class="sub">' + s.location + '</span>'}).join('<br/><br/>');
      //if (items) result += '<h4>Addresses(s)</h4><p class="minimum">' + items + '</p>';

      result += '<h4>Add a note to this person in Highrise</h4>';
      result += '<form onsubmit="if (this[\'note[body]\'].value==\'\') {alert(\'Please enter a note\'); return false;}; ObjectFactory.get(' + this.id + ').add_ticket_to_note(this); ObjectFactory.get(' + this.id + ').highrise_resource.submit_data(this); return false;" class="form">' +
           /* Form fields to POST to Highrise */
           '<textarea name="note[body]"></textarea>' +
           '<input type="hidden" name="note[subject-id]" value="' + person.id + '"/>' +
           '<input type="hidden" name="note[subject-type]" value="Person"/>' +
           /* Request parameter to pluck from form fields for POST*/
           '<input type="hidden" name="pluck_param" value="note">' +
           /* REST resource to POST to */
           '<input type="hidden" name="resource" value="notes">' +
           /* Format for REST resource POST (json or xml) */
           '<input type="hidden" name="media_type" value="application/xml">' +
           /* Title on ticket event */
           '<input type="hidden" name="event_reference" value="Highrise requester note">' +
           /* Return values from POST to log on ticket event */
           '<input type="hidden" name="event_log" value="Body">' +
           '<input type="submit" value="Submit" id="submit">' +
           ' <a href="javascript:ObjectFactory.get(' + this.id + ').reset_form();">Lookup user</a>' +
           '</form>';
      return result;
  },

  reset_form: function() {
    enable_submit($('lookup-form'));
    toggle_visibility('highrise-lookup-result', 'highrise-lookup-updating');
  }, 

  add_ticket_to_note: function(form) {
    if(ticket_id) {
      form['note[body]'].value += ' (Zendesk ticket #' + this.ticket_nice_id + ')';
    }
  }
});

/*
  The SalesforceHelper provides a series of static templates for evaluation and a selection
  of convenience methods to call from the widget implementation.
*/
var SalesforceHelper = Class.create();

SalesforceHelper.SOAP_LOGIN = new Template(
  '<?xml version="1.0" encoding="UTF-8"?>' +
  '<SOAP-ENV:Envelope ' +
  '    xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' +
  '    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +
  '    xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" ' +
  '    SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" ' +
  '    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">' +
  '      <SOAP-ENV:Body>' +
  '        <login xmlns="urn:enterprise.soap.sforce.com">' +
  '          <username xsi:type="xsd:string">#{username}</username>' +
  '          <password xsi:type="xsd:string">#{password}</password>' +
  '        </login>' +
  '      </SOAP-ENV:Body>' +
  '</SOAP-ENV:Envelope>'
);

SalesforceHelper.SOAP_QUERY = new Template(
  '<?xml version="1.0" encoding="utf-8"?>' +
  '   <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
  '    xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
  '  <soap:Header>' +
  '    <SessionHeader xmlns="urn:enterprise.soap.sforce.com">' +
  '      <sessionId>#{session}</sessionId>' +
  '    </SessionHeader>' +
  '  </soap:Header>' +
  '  <soap:Body>' +
  '    <query xmlns="urn:enterprise.soap.sforce.com">' +
  '      <queryString>#{query}</queryString>' +
  '    </query>' +
  '  </soap:Body>' +
  '</soap:Envelope>'
);

SalesforceHelper.SELECT = new Template(
  "SELECT department, email, fax, firstname, homephone, lastname, mailingcity, " +
  "       mailingcountry, mailingpostalcode, mailingstreet, mobilephone, name, " +
  "       othercity, othercountry, otherphone, otherpostalcode, otherstate, " +
  "       otherstreet, phone, salutation, title " +
  "FROM contact WHERE email LIKE '%#{search}%' OR name LIKE '%#{search}%'"
);

SalesforceHelper.LOGIN_URL     = '/proxy/direct.xml?url=https://www.salesforce.com/services/Soap/c/13.0&SOAPAction=login&body=';
SalesforceHelper.QUERY_URL     = new Template('/proxy/direct.xml?url=#{serverUrl}&SOAPAction=query&body=#{xml}');
SalesforceHelper.SESSION_MATCH = new RegExp("/.*<sessionId>(.*)</sessionId>.*/");
SalesforceHelper.SERVER_MATCH  = new RegExp("/.*<serverUrl>(.*)</serverUrl>.*/");

SalesforceHelper.authorizationUrl = function(username, password) {
  return this.LOGIN_URL + escape(this.SOAP_LOGIN.evaluate({ username: username, password: password }));
}

SalesforceHelper.getNodeValue = function(document, nodeName) {
  if(Prototype.Browser.WebKit) {
    nodeName = nodeName.replace("sf:","");
  }

  var node = document.getElementsByTagName(nodeName);
  if(node == null || node.length == 0) {
    return null;
  }
  return node[0].childNodes[0].nodeValue;
}

SalesforceHelper.getNodeString = function(document, nodeName) {
  var value = this.getNodeValue(document, nodeName);
  if(value == null) {
    return '';
  }
  else {
    return value;
  }
}

SalesforceHelper.getErrorCode = function(document) {
   return this.getNodeValue(document, 'faultcode');
}

SalesforceHelper.getRecords = function(document) {
  var results = document.getElementsByTagName('records')
  if(results == null) {
    return new Array();
  }
  return results;
}

SalesforceHelper.renderLine = function(record, elements, template) {
  var output = '';
  for(var i=0; i<elements.length; i++) {
    output += this.getNodeString(record, elements[i]);
    if(output != '') {
      output += ' ';
    }
  }
  if(output != '') {
    return template.evaluate({ output: output });
  }
  else {
    return '';
  }
}

SalesforceHelper.renderRecord = function(record) {
  var result = '';

  result += this.renderLine(record, ['sf:Salutation', 'sf:FirstName', 'sf:LastName'], new Template("<h4 style='font-size: 16px;'>#{output}</h4>"))
  result += this.renderLine(record, ['sf:Title'], new Template("<p class='minimum'>#{output}</p>"));
  result += this.renderLine(record, ['sf:Department'], new Template("<p class='minimum'>#{output}</p>"));

  var phones = '';

  phones += this.renderLine(record, [ 'sf:Phone' ], new Template("<p class='minimum'>#{output}</p>"));
  phones += this.renderLine(record, [ 'sf:MobilePhone' ], new Template("<p class='minimum'>#{output}<span class='sub'>mobile</span></p>"));

  if(phones != '') {
    result += '<h4>Phone</h4>';
    result += phones;
  }

  result += this.renderLine(record, [ 'sf:Email' ], new Template("<p class='minimum'><a href='mailto:#{output}'>#{output}</a></p>"));

  var address = this.renderLine(record, [ 'sf:MailingStreet' ], new Template("#{output}")).replace(',', '<br />');
  if(address != '') {
    subaddress = this.renderLine(record, [ 'sf:MailingPostalCode', 'sf:MailingCity'], new Template("#{output} "));
    if(subaddress != '') {
      address += '<br /> ' + subaddress;
    }
    address += this.renderLine(record, ['sf:MailingCountry'], new Template("<br />#{output}"));
  }

  result += address;
  return result;
}

SalesforceHelper.buildQueryUrl = function(sessionId, serverUrl, input) {
  var query = this.SELECT.evaluate({ search: input });

  var xml = this.SOAP_QUERY.evaluate({ query: query, session: sessionId });
  var url = this.QUERY_URL.evaluate({ serverUrl: serverUrl, xml: escape(xml) });

  return url;
}

/*
  The salesforce widget is a mechanism for looking up person data on salesforce. The widget must be
  configured with valid user login and password, after which it can be deployed and used on pages.
*/
var SalesforceWidget = Class.create(Widget, {
  initialize: function($super, args) {
    $super(args);
    this.username     = args.username;
    this.password     = args.password;
    this.destination  = args.destination;
    this.formInstance = null;
  },
  disableForm: function() {
    this.formInstance.button.value    = 'Looking...';
    this.formInstance.button.disabled = true;
  },
  enableForm: function() {
    this.formInstance.button.value    = 'Lookup user';
    this.formInstance.button.disabled = false;
  },
  lookup: function(submittedForm) {
    this.formInstance = submittedForm;
    this.disableForm();

    var url = SalesforceHelper.authorizationUrl(this.username, this.password);

    new Ajax.Request(url, {
      method:'post',
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        this.executeAuthorizedQuery(transport.responseXML);
      }.bind(this),
      onFailure: function() {
        alert("Salesforce login failed. Try again in a short moment.");
        this.enableForm();
      }.bind(this)
    });
  },
  executeAuthorizedQuery: function(responseXML) {
    var sessionId = SalesforceHelper.getNodeValue(responseXML, 'sessionId');
    var serverUrl = SalesforceHelper.getNodeValue(responseXML, 'serverUrl');

    if (sessionId != null && serverUrl != null) {
      var url = SalesforceHelper.buildQueryUrl(sessionId, serverUrl, this.formInstance.email.value);
      new Ajax.Request(url, {
        method:'post',
        requestHeaders: { Accept: 'application/xml' },
        onSuccess: function(transport) {
          this.displayResult(transport.responseXML);
        }.bind(this),
        onFailure: function() {
          alert("Salesforce query failed.");
          this.enableForm();
        }.bind(this)
      });
    }
    else {
      var errorCode = SalesforceHelper.getNodeValue(responseXML, 'faultcode');

      if (errorCode == 'sf:LOGIN_MUST_USE_SECURITY_TOKEN') {
        alert("Salesforce login failed - Salesforce network access needs configuration.");
      }
      else if (errorCode == 'sf:API_DISABLED_FOR_ORG') {
        alert("Salesforce login failed - Your Salesforce edition does not support API access.");
      }
      else {
        alert("Salesforce login failed. Failed to find server or session. Code: "+errorCode);
      }
      this.enableForm();
    }
  },
  displayResult: function(responseXML)  {

    var records = SalesforceHelper.getRecords(responseXML);

    if(records.length == 0) {
      alert("No match found in Salesforce.");
      this.enableForm();
      return;
    }

    var output = '';

    for(var i=0; i<records.length; i++) {
      output += SalesforceHelper.renderRecord(records[i]);
    }

    output += '<br />';
    $(this.destination).update(output);
    this.enableForm();
  }
});




/*
 * Freshbooks
 *
 *
 */
var FreshbooksHelper = {
  STAFF_FORM: new Template(
    '<form class="form" id="freshbooks-form-#{widget_id}">' +
    ' <label>Select user</label><select name="staff_id" id="freshbooks-form-staff-id" onchange="ObjectFactory.get(#{widget_id}).staffSelector(this.options[this.selectedIndex].value)"></select>' +
    ' <br/><input type="submit" disabled="disabled" id="freshbooks-submit" value="Sign in" onclick="ObjectFactory.get(#{widget_id}).saveStaffId($(\'freshbooks-form-#{widget_id}\'));return false;">' +
    '</form>'
  ),

  SUBMIT_FORM: new Template(
    '<form class="form" id="freshbooks-form-#{widget_id}">' +
    ' <label>Select project</label><select name="project_id" id="freshbooks-form-projects" onchange="ObjectFactory.get(#{widget_id}).projectsSelector(this.options[this.selectedIndex].value)"></select>' +
    ' <label>Select task</label><select disabled name="task_id" id="freshbooks-form-tasks" onchange="ObjectFactory.get(#{widget_id}).tasksSelector(this.options[this.selectedIndex].value)"></select>' +
    ' <label>Notes</label><textarea disabled name="freshbooks-form-notes" id="freshbooks-form-notes" wrap="virtual" style="width:190px; height: 50px;">#{default_note}</textarea>' +
    ' <label>Hours</label><input type="text" disabled name="freshbooks-form-hours" id="freshbooks-form-hours" style="width:50px">' +
    ' <br/><input type="submit" disabled id="freshbooks-form-submit" style="margin-top: 10px;" value="Submit" onclick="ObjectFactory.get(#{widget_id}).submitTimeSpent($(\'freshbooks-form-#{widget_id}\'));return false;">' +
    ' <span class="link" style="font-weight:normal;margin-left:20px;" onclick="ObjectFactory.get(#{widget_id}).logout()">(logout)</span>' +
    '</form>'
  ),

  GET_TASKS: new Template(
    '<?xml version="1.0" encoding="utf-8"?>'+
    ' <request method="task.list" >' +
    '   <project_id>#{project_id}</project_id>' +
    ' </request>'
  ),

  GET_PROJECTS: new Template(
    '<?xml version="1.0" encoding="utf-8"?>' +
    '<request method="project.list" />'
  ),

  GET_STAFF: new Template(
    '<?xml version="1.0" encoding="utf-8"?>' +
    '<request method="staff.list">' +
    '</request>'
  ),

	GET_CLIENTS: new Template(
		'<?xml version="1.0" encoding="utf-8"?>' +
		'<request method="client.list">' +
		'  <per_page>200</per_page>' +
		'</request>'
	),

  POST_HOURS: new Template(
    '<?xml version="1.0" encoding="ISO-8859-1"?>' +
    '<request method="time_entry.create">' +
    '  <time_entry>' +
    '    <project_id>#{project_id}</project_id>' +
    '    <task_id>#{task_id}</task_id>' +
    '    <hours>#{hours}</hours>' +
    '    <notes><![CDATA[#{notes}]]></notes>' +
    '    <staff_id>#{staff_id}</staff_id>' +
    '  </time_entry>' +
    '</request>'
  ),

  ERROR_MESSAGE: new Template(
    'An error occured:' +
    '\n\n#{error}\n\n' +
    'See https://support.zendesk.com/forums/25876 for troubleshooting\n\n'+
    'or contact Freshbooks support at support@freshbooks.com'
  ),

  getTasksRequest: function(project_id) {
    return escape(this.GET_TASKS.evaluate({ project_id: project_id }));
  },

  getProjectsRequest: function() {
    return escape(this.GET_PROJECTS.evaluate({}));
  },

	getClientsRequest: function() {
		return escape(this.GET_CLIENTS.evaluate({}));
	},

  postHoursRequest: function(staff_id, project_id, task_id, notes, hours) {
    return escape(this.POST_HOURS.evaluate({ staff_id: staff_id, project_id: project_id, task_id: task_id, notes: notes, hours: hours }));
  },

  getStaffRequest: function() {
    return escape(this.GET_STAFF.evaluate({}));
  },

  populateProjects: function(records, clients) {
		$('freshbooks-form-projects').update("");
		$('freshbooks-form-projects').appendChild(new Element("option"));
		
    for (i=0; i<records.length; i++) {
			var option = new Element("option");

			var client_id = FreshbooksHelper.getNodeString(records[i], "client_id");
			option.value = FreshbooksHelper.getNodeString(records[i], "project_id");
			option.innerHTML = (client_id ? clients[client_id] + " - " : "")  + FreshbooksHelper.getNodeString(records[i], "name");
			$('freshbooks-form-projects').appendChild(option);
    }
  },

  populateStaff: function(records) {
		$('freshbooks-form-staff-id').update("");
		$('freshbooks-form-staff-id').appendChild(new Element("option"));

    for (i=0; i<records.length; i++) {
			var option = new Element("option");

			option.value = FreshbooksHelper.getNodeString(records[i], "staff_id");
			option.innerHTML = FreshbooksHelper.getNodeString(records[i], "first_name") + ' ' + FreshbooksHelper.getNodeString(records[i], "last_name");
			$('freshbooks-form-staff-id').appendChild(option);
    }
  },

  populateTasks: function(records) {
		$('freshbooks-form-tasks').update("");
		$('freshbooks-form-tasks').appendChild(new Element("option"));

    for (i=0; i<records.length; i++) {
			var option = new Element("option");

			option.value = FreshbooksHelper.getNodeString(records[i], "task_id");
			option.innerHTML = FreshbooksHelper.getNodeString(records[i], "name");
			$('freshbooks-form-tasks').appendChild(option);
    }

    $('freshbooks-form-tasks').enable();
  },

  PROXY_URL: new Template(
    '/proxy/direct?basic_user=#{username}&basic_pass=#{password}&url=#{url}'
  ),

  PROXY_POST_URL: new Template(
    '/proxy/direct?basic_user=#{username}&basic_pass=#{password}&url=#{url}&body=#{body}'
  ),

  getProxyUrl: function(apikey, url) {
    return this.PROXY_URL.evaluate({ username: apikey, password: '', url: url});
  },

  getProxyPostUrl: function(apikey, url, body) {
    return this.PROXY_POST_URL.evaluate({ username: apikey, password: '', url: url, body: body});
  },

  getNodeValue: function(document, nodeName) {
    var node = document.getElementsByTagName(nodeName);
    if(node == null || node.length == 0) {
      return null;
    }
    
    if(node[0].childNodes[0] == null) {
      return "";
    }
    
    return node[0].childNodes[0].nodeValue;
  },


  getNodeString: function(document, nodeName) {
    return this.getNodeValue(document, nodeName) || '';
  },

  getRecords: function(document, tagname) {
    return document.getElementsByTagName(tagname) || new Array();
  },

  getCookie: function(name) {
    if (document.cookie.length > 0) {
      var c_start = document.cookie.indexOf(name + "=");
      if (c_start != -1) {
        c_start = c_start + name.length + 1;
        var c_end = document.cookie.indexOf(";", c_start);
        if (c_end == -1) {
          c_end = document.cookie.length;
        }
        return unescape(document.cookie.substring(c_start, c_end));
      }
    }
    return "";
  },

  setCookie: function(name, value, expiredays) {
    var exdate = new Date();
    exdate.setDate(exdate.getDate() + expiredays);
    document.cookie = name + "=" + escape(value)+((expiredays==null) ? "" : ";expires=" + exdate.toGMTString());
  },

  checkStatus: function(xml) {
    var records = FreshbooksHelper.getRecords(xml, "response");
    if (records.length > 0) {
      var text = FreshbooksHelper.getNodeString(records[0], "error");
      if (text != "") {
        alert(FreshbooksHelper.ERROR_MESSAGE.evaluate({error: text}));
        return false;
      }
    }
    return true;
  },

	parseClients: function(xml) {
    var clients = FreshbooksHelper.getRecords(xml, "client");
		var results = [];

    for (var i=0; i<clients.length; i++)
    {
			results[XmlHelper.getTextContentByKey(clients[i], 'client_id')] = XmlHelper.getTextContentByKey(clients[i], 'organization');
    }

		return results;
  }
}

var FreshbooksWidget = Class.create(Widget, {

  initialize: function($super, args) {
    $super(args);
    this.url = args.url;
    this.formInstance = null;
    this.ticket_id = args.ticket_id;
    this.default_note = args.default_note;
    this.apikey = args.apikey;
    this.staff_id = '';
		this.subject = args.subject;
    this.contentRoot = this.containingElement().down('div.freshbooks-content');
		this.clients = null;
		
    this.setup();
  },

  setup: function() {
    this.staff_id = FreshbooksHelper.getCookie("freshbooks_staff_id");
    if (this.staff_id == "") {
      this.contentRoot.update(FreshbooksHelper.STAFF_FORM.evaluate({widget_id: this.id}));
      this.loadStaff();
    } else
      this.buildForm();
  },

  saveApiKey: function(submittedForm) {
    this.formInstance = submittedForm;
    FreshbooksHelper.setCookie("freshbooks_api_key", $('freshbooks-form-apikey').value, 14);
    this.setup();
  },

  logout: function() {
    FreshbooksHelper.setCookie("freshbooks_staff_id", "", 180);
    this.staff_id = '';
    this.setup();
  },

  buildForm: function() {
    var default_note = new Template(this.default_note).evaluate({ticket_id: this.ticket_id, subject: this.subject});
    this.contentRoot.update(FreshbooksHelper.SUBMIT_FORM.evaluate({widget_id: this.id, default_note: default_note, ticket_id: this.ticket_id }));
		this.loadClients();
  },

  loadStaff: function() {
    var url = FreshbooksHelper.getProxyPostUrl(this.apikey, this.url);
		var body = "body=" + FreshbooksHelper.getStaffRequest();
		
    new Ajax.Request(url, {
      method: 'post',
			postBody: body,
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        if (FreshbooksHelper.checkStatus(transport.responseXML)) {
          this.showStaff(transport.responseXML);
        }
      }.bind(this),
      onFailure: function() {
        alert("Failed");
      }
    });
  },

  loadClients: function() {
    var url = FreshbooksHelper.getProxyPostUrl(this.apikey, this.url);
		var body = "body=" + FreshbooksHelper.getClientsRequest();

    new Ajax.Request(url, {
      method:'post',
			postBody: body,
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        if (FreshbooksHelper.checkStatus(transport.responseXML)) {
          this.clients = FreshbooksHelper.parseClients(transport.responseXML);
					this.loadProjects();
        }
      }.bind(this),
      onFailure: function() {
        alert("Failed");
      }.bind(this)
    });
  },

  loadProjects: function() {
    var url = FreshbooksHelper.getProxyPostUrl(this.apikey, this.url);
		var body = "body=" + FreshbooksHelper.getProjectsRequest()
		
    new Ajax.Request(url, {
      method:'post',
			postBody: body,
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        if (FreshbooksHelper.checkStatus(transport.responseXML)) {
          this.showProjects(transport.responseXML, this.clients);
        }
      }.bind(this),
      onFailure: function() {
        alert("Failed");
      }.bind(this)
    });
  },

  projectsSelector: function(id) {
    this.loadTasks(id);
  },

  staffSelector: function(id) {
    if ($('freshbooks-form-staff-id').value > "") {
      $('freshbooks-submit').enable();
    } else {
      $('freshbooks-submit').disable();
    }
  },

  saveStaffId: function(submitted_form) {
    FreshbooksHelper.setCookie("freshbooks_staff_id", $('freshbooks-form-staff-id').value, 180);
    this.setup();
  },

  loadTasks: function(project_id) {
    var url = FreshbooksHelper.getProxyPostUrl(this.apikey, this.url);
		var body = "body=" + FreshbooksHelper.getTasksRequest(project_id)

    new Ajax.Request(url, {
      method:'post',
			postBody: body,
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        if (FreshbooksHelper.checkStatus(transport.responseXML)) {
          this.showTasks(transport.responseXML);
        }
      }.bind(this),
      onFailure: function() {
        alert("Failed");
      }
    });
  },

  tasksSelector: function(id) {
    $('freshbooks-form-notes').enable();
    $('freshbooks-form-hours').enable();
    $('freshbooks-form-submit').enable();
  },

  showStaff: function(xml) {
    if ($('freshbooks-form-staff-id').options.length == 0) {
      var records = FreshbooksHelper.getRecords(xml, "member");
      FreshbooksHelper.populateStaff(records);
    }
  },

  showProjects: function(xml, clients) {
    if ($('freshbooks-form-projects').options.length == 0) {
      var records = FreshbooksHelper.getRecords(xml, "project");
      FreshbooksHelper.populateProjects(records, clients);
    }
  },

  showTasks: function(xml) {
    var records = FreshbooksHelper.getRecords(xml, "task");
    FreshbooksHelper.populateTasks(records);
  },

  submitTimeSpent: function(submittedForm) {
    var hours = parseFloat(submittedForm['freshbooks-form-hours'].value);
    if (isNaN(hours)) {
      alert('Please enter a valid value for hours');
      return false;
    }

    var url = FreshbooksHelper.getProxyPostUrl(this.apikey, this.url);
		var body = "body=" + FreshbooksHelper.postHoursRequest(this.staff_id, $('freshbooks-form-projects').value, $('freshbooks-form-tasks').value, $('freshbooks-form-notes').value, $('freshbooks-form-hours').value);

    new Ajax.Request(url, {
      method:'post',
			postBody: body,
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        if (FreshbooksHelper.checkStatus(transport.responseXML)) {
          this.resetForm();
        }
      }.bind(this),
      onFailure: function() {
        alert("Failed");
      }
    });

    return false;
  },

  resetForm: function() {
    var default_note = new Template(this.default_note).evaluate({ticket_id: this.ticket_id, subject: this.subject});

    $('freshbooks-form-hours').value = '';
    $('freshbooks-form-notes').value = default_note;
    $('freshbooks-form-notes').focus();
  }
});




/*
 * JIRA
 *
 *
 */

var JiraHelper = {

  GET_SESSION: new Template(
    '<?xml version="1.0" encoding="utf-8"?>' + 
    '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="#{url}rpc/soap/jirasoapservice-v2" xmlns:types="#{url}rpc/soap/jirasoapservice-v2/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
    '  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
    '    <q1:login xmlns:q1="http://soap.rpc.jira.atlassian.com">' +
    '      <in0 xsi:type="xsd:string">#{username}</in0>' +
    '      <in1 xsi:type="xsd:string">#{password}</in1>' +
    '    </q1:login>' + 
    '  </soap:Body>' +
    '</soap:Envelope>'
  ),

  GET_PROJECTS: new Template(
    '<?xml version="1.0" encoding="UTF-8"?>' + 
    '<SOAP-ENV:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">' +
    '  <SOAP-ENV:Body>' + 
    '    <m:getProjectsNoSchemes xmlns:m="#{url}rpc/soap/jirasoapservice-v2">' +
    '      <in0 xsi:type="xsd:string">#{sessionId}</in0>' +
    '    </m:getProjectsNoSchemes>' +
    '  </SOAP-ENV:Body>' + 
    '</SOAP-ENV:Envelope>'
  ),

  GET_ASSIGNEES: new Template(
    '<?xml version="1.0" encoding="utf-8"?>' +
    '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="#{url}rpc/soap/agilossoapservice-v1" xmlns:types="#{url}rpc/soap/agilossoapservice-v1/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
    '  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
    '    <q1:getAssignableUsers xmlns:q1="#{url}">' +
    '      <in0 xsi:type="xsd:string">#{sessionId}</in0>' +
    '      <in1 xsi:type="xsd:string">#{projectKey}</in1>' +
    '    </q1:getAssignableUsers>' +
    '  </soap:Body>' +
    '</soap:Envelope>'
  ),

  GET_ISSUE_TYPES2: new Template(
    '<?xml version="1.0" encoding="utf-8"?>' +
    '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="#{url}rpc/soap/jirasoapservice-v2" xmlns:types="#{url}rpc/soap/jirasoapservice-v2/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
    '<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
    '  <q1:getIssueTypesForProject xmlns:q1="http://soap.rpc.jira.atlassian.com">' +
    '   <in0 xsi:type="xsd:string">#{sessionId}</in0>' +
    '   <in1 xsi:type="xsd:string">#{projectId}</in1>' +
    '  </q1:getIssueTypesForProject>' +
    '</soap:Body>' +
    '</soap:Envelope>'
  ),
  
  GET_ISSUE_TYPES: new Template(
    '<?xml version="1.0" encoding="utf-8"?>' +
    '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="#{url}rpc/soap/jirasoapservice-v2" xmlns:types="#{url}rpc/soap/jirasoapservice-v2/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
    '<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
    '<q1:getIssueTypesForProject xmlns:q1="http://soap.rpc.jira.atlassian.com">' +
    '<in0 xsi:type="xsd:string">#{sessionId}</in0>' +
    '<in1 xsi:type="xsd:string">#{projectId}</in1>' +
    '</q1:getIssueTypesForProject>' +
    '</soap:Body>' +
    '</soap:Envelope>'
  ),
  
  SUBMIT_ISSUE: new Template(
    '<?xml version="1.0" encoding="ISO-8859-1"?>' +
    '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="#{url}rpc/soap/jirasoapservice-v2" xmlns:types="#{url}rpc/soap/jirasoapservice-v2/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
    '  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
    '    <q1:createIssue xmlns:q1="http://soap.rpc.jira.atlassian.com">' +
    '      <in0 xsi:type="xsd:string">#{sessionId}</in0>' +
    '      <in1 href="#id1" />' +
    '    </q1:createIssue>' +
    '    <q2:RemoteIssue id="id1" xsi:type="q2:RemoteIssue" xmlns:q2="http://beans.soap.rpc.jira.atlassian.com">' +
    '      <id xsi:nil="true" />' +
    '      <affectsVersions xsi:nil="true" />' +
    '      <assignee xsi:type="xsd:string">#{assignee}</assignee>' +
    '      <attachmentNames xsi:nil="true" />' +
    '      <components xsi:nil="true" />' +
    '      <created xsi:nil="true" />' +
    '      <customFieldValues xsi:nil="true" />' +
    '      <description>#{description}</description>' +
    '      <duedate xsi:nil="true" />' +
    '      <environment xsi:nil="true" />' +
    '      <fixVersions xsi:nil="true" />' +
    '      <key xsi:nil="true" />' +
    '      <priority xsi:nil="true" />' +
    '      <project xsi:type="xsd:string">#{projectKey}</project>' +
    '      <reporter xsi:nil="true" />' +
    '      <resolution xsi:nil="true" />' +
    '      <status xsi:nil="true" />' +
    '      <summary xsi:type="xsd:string">#{summary}</summary>' +
    '      <type xsi:type="xsd:string">#{type}</type>' +
    '      <updated xsi:nil="true" />' +
    '      <votes xsi:nil="true" />' +
    '      <customFieldValues href="#id2" />' +
    '    </q2:RemoteIssue>' +
    '    <soapenc:Array id="id2" xmlns:q3="http://beans.soap.rpc.jira.atlassian.com" soapenc:arrayType="q3:RemoteCustomFieldValue[1]">' +
    '      <Item href="#id3" />' +
    '    </soapenc:Array>' +
    '    <q4:RemoteCustomFieldValue id="id3" xsi:type="q4:RemoteCustomFieldValue" xmlns:q4="http://beans.soap.rpc.jira.atlassian.com">' +
    '      <customfieldId xsi:type="xsd:string">customfield_#{customFieldId}</customfieldId>' +
    '      <key xsi:nil="true" />' +
    '      <values href="#id4" />' +
    '    </q4:RemoteCustomFieldValue>' +
    '    <soapenc:Array id="id4" soapenc:arrayType="xsd:string[1]">' +
    '      <Item>#{ticketId}</Item>' +
    '    </soapenc:Array>' +
    '  </soap:Body>' +
    '</soap:Envelope>'
  ),
  
  SUBMIT_FORM: new Template(
    '<form class="form" id="jira-form-#{widgetId}">' +
    ' Submit this ticket as a new issue' + 
    ' <label>Project</label><select name="projectId" id="jiraProjectSelector" class="jiraProjectSelector" onchange="ObjectFactory.get(#{widgetId}).projectsSelector(this.options[this.selectedIndex].value)"></select>' + 
    ' <label>Type</label><select name="typeId" id="jiraTypeSelector" class="jiraTypeSelector" onchange="ObjectFactory.get(#{widgetId}).typeSelector(this.options[this.selectedIndex].value)"></select>' + 
    ' <label>Assignee ID</label><select name="jiraAssigneeSelector" class="jiraAssigneeSelector" id="jiraAssigneeSelector"></select>' + 
    ' <label></label><input type="button" value="Submit" id="submit" class="jiraSubmit" onclick="ObjectFactory.get(#{widgetId}).submitIssue($(\'jiraForm-#{widgetId}\'));return false;">' +              
    '</form>'
  ),

  EXTERNAL_ID: new Template(
    '<ticket><external-id>#{externalId}</external-id><additional-tags>jira</additional-tags></ticket>'
  ),

  NOTICE: new Template(
    'This tickets is related to the following issue in JIRA:<br /><br /><b>#{externalId}</b>' + 
    '<br />&nbsp;'
  ),
  
  MISSING_TICKET: new Template(
    'Only available when viewing tickets.' +
    '<br />&nbsp;'
  ),

  getSessionRequest: function(username, password, url) {
    return escape(this.GET_SESSION.evaluate({ username: username, password: password, url: url }));
  },

  getProjectsRequest: function(sessionId, url) {
    return escape(this.GET_PROJECTS.evaluate({ sessionId: sessionId, url: url }));
  },
  
  getProjectsAssigneeRequest: function(sessionId, url, projectKey) {
    return escape(this.GET_ASSIGNEES.evaluate({ sessionId: sessionId, url: url, projectKey: projectKey}));
  },

  getProjectsIssueTypesRequest: function(sessionId, url, projectId) {
    return escape(this.GET_ISSUE_TYPES.evaluate({ sessionId: sessionId, url: url, projectId: projectId}));
  },

  getSubmitRequest: function(sessionId, url, assignee, type, projectKey, summary, description, ticketId, customFieldId) { 
    return escape(this.SUBMIT_ISSUE.evaluate({ sessionId: sessionId, url: url, assignee: assignee, projectKey: projectKey, type: type, summary: XmlHelper.encodeString(summary), description: XmlHelper.encodeString(description), ticketId: ticketId, customFieldId: customFieldId }));
  },

  getSubmitForm: function(widgetId) {
    return (this.SUBMIT_FORM.evaluate({widgetId: widgetId}));
  },

  getInJiraNotice: function(externalId) {
    return (this.NOTICE.evaluate({externalId: externalId}));
  },
  
  getMissingTicketMessage: function () {
    return (this.MISSING_TICKET.evaluate({}));
  },

  setExternalIdRequest: function (externalId) {
    return this.EXTERNAL_ID.evaluate({ externalId: externalId })
  },

  PROXY_URL1: new Template(
    '/proxy/direct?log=1&url=#{url}rpc/soap/jirasoapservice-v2&json=1&SOAPAction='
  ),

  PROXY_URL2: new Template(
    '/proxy/direct?log=1&url=#{url}rpc/soap/agilossoapservice-v1?&SOAPAction='
  ),

  getProxyUrl: function(url, body) {
    return escape(this.PROXY_URL1.evaluate({ url: url, body: body }));
  },

  getProxyUrl2: function(url, body) {
    return escape(this.PROXY_URL2.evaluate({ url: url, body: body }));
  },

  getSessionUrl: function(username, password, url) {
    doc = JiraHelper.getSessionRequest(username, password, url);
    url = JiraHelper.getProxyUrl(url, doc);
    return url;
  },

  getNodeValue: function(document, nodeName) {
    if(Prototype.Browser.WebKit) {
      nodeName = nodeName.replace("sf:","");
    }
  
    node = document.getElementsByTagName(nodeName);
    if(node == null || node.length == 0) {
      return null;
    }
    return node[0].childNodes[0].nodeValue;
  },

  getNodeString: function(document, nodeName) {
    value = this.getNodeValue(document, nodeName);

    if(value == null) {
      return '';
    }
    else {
      return value;
    }
  },

  getRecords: function(document, ns, tagName) {
    results = document.getElementsByTagName(tagName);
    if (results.length == 0)
      results = document.getElementsByTagName(ns + ':' + tagName);
      
    if(results == null) {
      return new Array();
    }
    return results;
  },
  
  populateProjects: function(contentRoot, responseXML) {
    var keys = [];
    
    if ($('jiraProjectSelector').options.length == 0) {
      projects = JiraHelper.getRecords(responseXML, "", "multiRef");
      ddlProjects = contentRoot.down("select.jiraProjectSelector");
      ddlProjects.appendChild(new Element("option"));

      for (var i=0; i<projects.length; i++)
      {
        var option = new Element("option");
        option.value = XmlHelper.encodeString(XmlHelper.getTextContentByKey(projects[i], 'key'));
        option.innerHTML = XmlHelper.encodeString(XmlHelper.getTextContentByKey(projects[i], 'name'));
        ddlProjects.appendChild(option);

        keys[option.value] = XmlHelper.getTextContentByKey(projects[i], 'id');
      }
    }

    return keys;
  },

  populateProjectIssueTypes: function(contentRoot, responseXML) {
    types = JiraHelper.getRecords(responseXML, "", "multiRef");
    
    ddlTypes = contentRoot.down("select.jiraTypeSelector");
    ddlTypes.update("");
    
    ddlTypes.appendChild(new Element("option"));

    for (var i=0; i<types.length; i++) {
      var option = new Element("option");
      option.value = XmlHelper.encodeString(XmlHelper.getTextContentByKey(types[i], 'id'));
      option.innerHTML = XmlHelper.encodeString(XmlHelper.getTextContentByKey(types[i], 'name'));
      ddlTypes.appendChild(option);
    }
  },
    
  populateAssignees: function(contentRoot, responseXML) {
    assignees = JiraHelper.getRecords(responseXML, "", "multiRef");

    ddlAssignees = contentRoot.down("select.jiraAssigneeSelector");
    ddlAssignees.update("");
    
    ddlAssignees.appendChild(new Element("option"));

    for (var i=0; i<assignees.length; i++)
    {
      var option = new Element("option");
      option.value = XmlHelper.encodeString(XmlHelper.getTextContentByKey(assignees[i], 'name'));
      option.innerHTML = XmlHelper.encodeString(XmlHelper.getTextContentByKey(assignees[i], 'fullname'));
      ddlAssignees.appendChild(option);
    }
  }
  
}

var XmlHelper = {
  getTextContentByKey: function(document, key) {
    if (Prototype.Browser.IE)
      return document.getElementsByTagName(key)[0].childNodes[0].nodeValue;

    // all other browsers
    return document.getElementsByTagName(key)[0].textContent;
  },

  encodeString: function(st) {
    var result = st;
    
    result = result.replace(/&/g, '&amp;');
    result = result.replace(/</g, '&lt;');
    result = result.replace(/>/g, '&gt;');
    result = result.replace(/'/g, '&apos;');
    result = result.replace(/"/g, '&quot;');

    return result;
  },
  
  getRecords: function(document, ns, tagName) {
    results = document.getElementsByTagName(tagName);
    if (results.length == 0)
      results = document.getElementsByTagName(ns + ':' + tagName);
      
    if(results == null) {
      return new Array();
    }
    return results;
  }

}

var JiraWidget = Class.create(Widget, {
  initialize: function($super, args) {
    $super(args);
    this.url = args.url;
    this.username = args.username;
    this.password = args.password;
    this.formInstance = null;
    this.ticketId = args.ticketId;
    this.externalId = args.externalId;
    this.customFieldId = args.customFieldId;
    this.widgetId = args.id;
    this.subject = args.subject;
    this.description = args.description;
    this.projectIds = null;

    this.contentRoot = this.containingElement().down("div.jiraContent" + this.widgetId);

    if(!this.url.endsWith('/')) {
      this.url += '/';      
    }
    
    this.start();
  },
  
  start: function() {
    if (this.ticketId) {
      if (this.externalId) {
        this.contentRoot.update(JiraHelper.getInJiraNotice(this.externalId));
      } else {
        this.contentRoot.update(JiraHelper.getSubmitForm(this.id));

        this.proxyUrl = JiraHelper.PROXY_URL1.evaluate({ url: this.url });
        this.proxyUrl2 = JiraHelper.PROXY_URL2.evaluate({ url: this.url });

        this.getSession();
      }
    } else {
      this.contentRoot.update(JiraHelper.getMissingTicketMessage());
    }
  },
  
  getSession: function() {
    var doc = "body=" + JiraHelper.getSessionRequest(this.username, this.password, this.url);

    new Ajax.Request(this.proxyUrl, {
      method:'post',
      postBody: doc,
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        this.extractSessionID(transport.responseXML);
      }.bind(this),
      onFailure: function() {
        alert("Failed");
      }.bind(this)
     });
  },
  
  extractSessionID: function(responseXML) {
    var arr = JiraHelper.getRecords(responseXML, "ns1", "loginResponse");

    if (arr.length == 1) {
      this.sessionId = JiraHelper.getNodeString(arr[0], "loginReturn");
      this.getProjects();
    } else {
      alert("Login failed");
    }
  },
  
  getProjects: function() {
    var doc = "body=" + JiraHelper.getProjectsRequest(this.sessionId, this.url);

    new Ajax.Request(this.proxyUrl, {
      method:'post',
      postBody: doc, 
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        this.projectIds = JiraHelper.populateProjects(this.contentRoot, transport.responseXML);
      }.bind(this),
      onFailure: function() {
        alert("Failed to get projects");
      }.bind(this)
     });
  },
  
  projectsSelector: function(projectkey) {
    var doc = "body=" + JiraHelper.getProjectsAssigneeRequest(this.sessionId, this.url, projectkey);

    new Ajax.Request(this.proxyUrl2 , {
      method:'post',
      postBody: doc,
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        JiraHelper.populateAssignees(this.contentRoot, transport.responseXML);
      }.bind(this),
      onFailure: function() {
        alert("Failed to get projects");
      }.bind(this)
    });

    doc = "body=" + JiraHelper.getProjectsIssueTypesRequest(this.sessionId, this.url, this.projectIds[projectkey] );

    new Ajax.Request(this.proxyUrl , {
      method:'post',
      postBody: doc,
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
        JiraHelper.populateProjectIssueTypes(this.contentRoot, transport.responseXML);
      }.bind(this),
      onFailure: function() {
        alert("Failed to get projects");
      }.bind(this)
    });
  },
  
  submitIssue: function(form) {
		var url = "/tickets/" + this.ticketId + ".xml";

    new Ajax.Request(url, {
      method:'get',
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
				var subject = XmlHelper.getTextContentByKey(transport.responseXML, "subject");
				var description = XmlHelper.getTextContentByKey(transport.responseXML, "description");
				this.submitIssue2(subject, description)
      }.bind(this),
      onFailure: function() {
        alert("Failed to submit issue.");
      }.bind(this)
    });
    return false;
  },

  submitIssue2: function(subject, description) {
    var doc = "body=" + JiraHelper.getSubmitRequest(this.sessionId, this.url, this.contentRoot.down('select.jiraAssigneeSelector').getValue(), this.contentRoot.down('select.jiraTypeSelector').getValue(), this.contentRoot.down("select.jiraProjectSelector").getValue(), subject, description, this.ticketId, this.customFieldId);
    
    new Ajax.Request(this.proxyUrl, {
      method:'post',
      postBody: doc,
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) { 
        this.setExternalId(transport.responseXML);
        this.addTag("jira");
      }.bind(this),
      onFailure: function() {
        alert("Failed to submit issue.");
      }.bind(this)
    });
    return false;
  },
  
  addTag: function(tag) {
	  if(typeof(ticketTagField) != 'undefined') {
	    ticketTagField.addEntry("jira", "jira");
	  };
  },
  
  setExternalId: function(responseXML) {
    var issues = JiraHelper.getRecords(responseXML, "", "multiRef");

    if (issues.length > 0) {
      var key = XmlHelper.getTextContentByKey(issues[0],'key');
      var doc = JiraHelper.setExternalIdRequest(key);
      var url = '/tickets/' + this.ticketId + '.xml?_method=put';

      new Ajax.Request(url, {
        contentType: 'application/xml',
        method:'put',
        encoding: '',
        postBody: doc,
        onSuccess: function(transport) { this.contentRoot.update(JiraHelper.getInJiraNotice(key)); }.bind(this),
        onFailure: function() { alert('failed'); }
      });
    } else {
      var faultString = XmlHelper.getTextContentByKey(responseXML, 'faultstring');
      alert("Failed to retrieve JIRA issue key.\n\n(" + faultString + ")");
    }
  }
});




/*
 *
 * TactileCRM
 *
 */
var TactileHelper = {
  PROXY_URL: new Template(
    '/proxy/direct?log=1&url=#{url}'
  ),

  QUERYSTRING_PEOPLE: new Template(
    'people/?api_token=#{apiToken}&email=#{email}'
  ),

  QUERYSTRING_CONTACTMETHODS: new Template(
    'people/contact_methods/?api_token=#{apiToken}&id=#{id}'
  ),

  QUERYSTRING_PERSON: new Template(
    'people/view/?api_token=#{apiToken}&id=#{id}'
  ),

  getQueryURL: function(url) {
    return TactileHelper.PROXY_URL.evaluate({url: url});
  },

  getPeopleQueryString: function(apiToken, email) {
    return TactileHelper.QUERYSTRING_PEOPLE.evaluate({ apiToken: apiToken, email: email });
  },

  getContactMethodsQueryString: function(apiToken, id) {
    return TactileHelper.QUERYSTRING_CONTACTMETHODS.evaluate({ apiToken: apiToken, id: id });
  },
  
  getPersonQueryString: function(apiToken, id) {
    return TactileHelper.QUERYSTRING_PERSON.evaluate({ apiToken: apiToken, id: id });
  },
  
  renderLine: function(values, template) {
    var output = '';

    for(var i=0; i<values.length; i++) {
      if (values[i] > "") {
        if (output != '')
          output += ' ';
        output += values[i];
      }
    }

    return output > '' ? template.evaluate({ output: output }) : '';
  },

  renderRecord: function(record, contact) {
    var output = '';
    var companyInfo = '';
    var address = '';
    var template = new Template("#{output}<br />");

    output += this.renderLine([record.title, record.name], new Template("<h4 style='font-size: 16px;'>#{output}</h4>")); 

    companyInfo += this.renderLine([record.jobtitle], template);
    companyInfo += this.renderLine([record.organisation], template);
    output += this.renderLine([companyInfo], new Template("<p class=\"minimum\">#{output}</p>"));

    address += this.renderLine([record.street1], template);
    address += this.renderLine([record.street2], template);
    address += this.renderLine([record.street3], template);
    address += this.renderLine([record.town], template);
    address += this.renderLine([record.county], template);
    address += this.renderLine([record.postcode], template);
    address += this.renderLine([record.country], template);
    output += this.renderLine([address], new Template("<p class=\"minimum\">#{output}</p>"));

    output += this.renderContactMethods(contact, "T", "Telephone");
    output += this.renderContactMethods(contact, "M", "Mobile");
    output += this.renderContactMethods(contact, "E", "Email");
    
    output += this.renderLine([record.description], new Template("<h4>Description</h4>#{output}<br />"));

    return output + "<br />";
  },

  renderContactMethods: function(contact, type, label) {
    var output = '';
    var template = new Template("#{value} <span class=\"sub\">#{name}</span><br />");
    
    for (var i=0; i<contact.contact_methods.length; i++) {
      var method = contact.contact_methods[i];
      if (method.type == type) {
        output += template.evaluate({ value: method.contact, name: method.name});
      }
    }
    
    return output > "" ? "<h4>" + label + "</h4>" + output : "";
  },
  
  renderList: function(people, id) {
    var result = '';

    result = '<h4>' + people.length + " matching people found in TactileCRM. Click on a person for more information</h4>";
    result += people.collect(function(p) {return ('<span class="link" onclick="ObjectFactory.get(' + id + ').lookupPerson(' + p.id + ')">' + p.name + '</span>')}.bind(this)).join('<br/>')+"<br />&nbsp;";

    return result;
  }
};

var TactileWidget = Class.create(Widget, {
  initialize: function($super, args) {
    $super(args);
    this.widgetId     = args.id;
    this.url          = args.url;
    this.apiToken     = args.apiToken;
    this.formInstance = null;
    this.contentRoot  = this.containingElement().down("div.tactileContent" + this.widgetId);

    if(!this.url.endsWith('/')) {
      this.url += '/';      
    }
  },

  lookup: function(submittedForm) {
    var email = submittedForm.down('input.email').value;
    var url = TactileHelper.getQueryURL(this.url) + escape(TactileHelper.getPeopleQueryString(this.apiToken, email));

    new Ajax.Request(url, {
      method:'get',
      encoding: '',
      onSuccess: function(transport) {
        this.showList(transport.responseText.evalJSON());
      }.bind(this),
      onFailure: function() {
        alert("Failed to lookup.");
      }.bind(this)
    });
    return false;
  },
  
  showList: function(result) {
    if (result.status == "error") {
      alert("Error: Unable to login to TactileCRM");
      return false;
    }     
    if (result.people.length == 0) {
      alert("No match found in TactileCRM");
    } else {
      if (result.people.length > 1) {
        this.contentRoot.update(TactileHelper.renderList(result.people, this.id));
      } else {
        this.lookupPerson(result.people[0].id)
      }
    }
  },
  
  lookupPerson: function(id) {
    var url = TactileHelper.getQueryURL(this.url) + escape(TactileHelper.getPersonQueryString(this.apiToken, id));

    new Ajax.Request(url, {
      method:'get',
      encoding: '',
      onSuccess: function(transport) {
        this.result1 = transport.responseText.evalJSON();
        this.lookupContactMethods(id);
      }.bind(this),
      onFailure: function() {
        alert("Failed to lookup.");
      }.bind(this)
    });
  },
  
  lookupContactMethods: function(id) {
    var url = TactileHelper.getQueryURL(this.url) + escape(TactileHelper.getContactMethodsQueryString(this.apiToken, id));
    new Ajax.Request(url, {
      method:'get',
      encoding: '',
      onSuccess: function(transport) {
        this.result2 = transport.responseText.evalJSON();
        this.show();
      }.bind(this),
      onFailure: function() {
        alert("Failed to lookup.");
      }.bind(this)
    });
  },
  
  show: function() {
    var output = TactileHelper.renderRecord(this.result1.person, this.result2);
    this.contentRoot.update(output);
  }
});




/*
 * Paglo widget
 *
 *
 */
var PagloHelper = {
	SEARCH_FORM: new Template(
		'<div class="asset"></div>' +
		'<div class="software"></div>' +
    '<form class="form" id="paglo-form-#{widgetId}">' +
    ' <label>Search</label><input type="text" name="token" id="token" value="#{name}"/	>' +
    ' <br/><input type="submit" id="submit" value="Search" onclick="ObjectFactory.get(#{widgetId}).doSearch($(\'paglo-form-#{widgetId}\').token.value);return false;">' +
    '</form>'
	),

	getSearchForm: function(widgetId, name) {
		return PagloHelper.SEARCH_FORM.evaluate({ widgetId: widgetId, name: name });
	},

	QUERY: new Template(
		'select nvl(system/dns_name, first(interface/inet/ip_address)) as name, ' +
		'first(interface/inet/ip_address) as ip_address from /network/device ' +
		'where system/dns_name like \'%#{token}%\' or interface/inet/ip_address = \'#{token}%\' ' +
		'or wmi/win32_computersystem/username = \'#{token}\' or wmi/win32_computersystem/username like \'%#{token2}\' ' +
		'order by 1 limit 10'
	),

	getQuery: function(token) {
		return PagloHelper.QUERY.evaluate({ token: token, token2: '\\' + token });
	},

	SOFTWARE_QUERY: new Template(
		'select Name, Version from /network/device[system/dns_name = \'#{name}\']/wmi/win32_product order by 1,2'
	),

	getSoftwareQuery: function(name) {
		return PagloHelper.SOFTWARE_QUERY.evaluate({ name: name });
	},

	URL: new Template(
		'/proxy/direct?url=https://api.paglo.com/api&contenttype=application/x-www-form-urlencoded&body=#{body}'
	),

	getURL: function(body) {
		return PagloHelper.URL.evaluate({ body: escape(body) });
	},

	doc: new Template(
			'method=paglo.query&v=1.0&api_key=#{apiKey}&query=#{query}'
	),

	getDoc: function(apiKey, query) {
		return PagloHelper.doc.evaluate({ apiKey: apiKey, query: query });
	},

	ITEM: new Template(
		'<span class="link" onClick="ObjectFactory.get(#{widgetId}).#{functionName}(\'#{name}\')">#{name}</span><br />'
	),

	renderItem: function(name, functionName, widgetId) {
		return PagloHelper.ITEM.evaluate({ name: name, functionName: functionName, widgetId: widgetId });
	},

	LIST_TEMPLATE: new Template(
		'<b>#{title}</b><br /><br />#{content}'
	),

	getList: function(title, content) {
		return PagloHelper.LIST_TEMPLATE.evaluate({ title: title, content: content });
	},

	SOFTWARE_SELECTION: new Template(
		'<b>Select software package:</b><br /><select id="paglo_software" class="software" onChange="ObjectFactory.get(#{widgetId}).selectSoftware(this.options[this.selectedIndex].value)"></select>'
	),

	getSoftwareSelection: function(widgetId) {
		return PagloHelper.SOFTWARE_SELECTION.evaluate({ widgetId: widgetId });
	},

	ASSET: new Template(
		'select system/dns_name, interface/inet/ip_address, nvl(system_profile/spsoftwaredatatype/os_version, os/version, ' +
		'wmi/win32_operatingsystem/caption || \' \' || wmi/win32_operatingsystem/csdversion) as os, ' +
		'nvl(system/class, system/computed_class) as class, nvl(system/vendor, ' +
		'wmi/win32_computersystemproduct/vendor, system/computed_vendor) as vendor, ' +
		'nvl(system/model, wmi/win32_computersystemproduct/name, system/computed_model) as model, ' +
		'nvl(wmi/win32_computersystem/username, system_profile/spsoftwaredatatype/user_name) as username ' +
		'from /network/device where system/(dns_name = \'#{name}\')'
	),

	getAssetQuery: function(name) {
		return PagloHelper.ASSET.evaluate({ name: name });
	},

	ASSET_INFO_SECTION: new Template(
		'<b>#{title}:</b><br /> #{value}<br /><br />'
	),

	getAssetInfoSection: function(title, value) {
		return (value != '' ? PagloHelper.ASSET_INFO_SECTION.evaluate({ title: title, value: value }) : ''); 
	},

	ASSET_HEADER: new Template(
			'<a href="https://app.paglo.com/app/inventory/lookup?key=system/dns_name&value=#{name}" target="_new"><img src="/images/widgets/paglo/asset.png">&nbsp;&nbsp;<b>#{name}</b></a> <br /><br />'
	),

	getAssetHeader: function(name) {
		return PagloHelper.ASSET_HEADER.evaluate({ name: name});
	},

	getAssetInfo: function(records, name) {
		var output = '';
		var reg = new RegExp("\\S");

		for (var i=0; i<records.length; i++)
			if (records[i].attributes.length > 0 && records[i].childNodes.length > 0)
 				if (records[i].getAttribute('name') == name && records[i].firstChild.nodeValue.match(reg)) 
					output += (output != '' ? '<br />' : '') + records[i].firstChild.nodeValue;

		return output;
	},

	renderAssetInfo: function(xml) {
		var output = '';
		var records = XmlHelper.getRecords(xml, "", "value");

		output += PagloHelper.getAssetHeader(PagloHelper.getAssetInfo(records, "system/dns_name"));
		output += PagloHelper.getAssetInfoSection("Class", PagloHelper.getAssetInfo(records, "class"));
		output += PagloHelper.getAssetInfoSection("Vendor", PagloHelper.getAssetInfo(records, "vendor"));
		output += PagloHelper.getAssetInfoSection("Model", PagloHelper.getAssetInfo(records, "model"));
		output += PagloHelper.getAssetInfoSection("Operating system", PagloHelper.getAssetInfo(records, "os"));
		output += PagloHelper.getAssetInfoSection("Username", PagloHelper.getAssetInfo(records, "username"));
		output += PagloHelper.getAssetInfoSection("Network", PagloHelper.getAssetInfo(records, "interface/inet/ip_address"));

		return output;
	}
}


var PagloWidget = Class.create(Widget, {
  initialize: function($super, args) {
    $super(args);
    this.apiKey = args.apikey;
    this.formInstance = null;
    this.widgetId = args.id;
		this.isEmpty = args.isEmpty;
		this.hardwareFieldId = args.hardwareFieldId;
		this.softwareFieldId = args.softwareFieldId;
		this.externalId = args.externalId;
		this.email = args.email;
    this.contentRoot = this.containingElement().down("div.paglo-content");

		this.startup();
  },

	startup: function() {
		// check if the custom fields are in place
		if (!($('ticket_fields_' + this.hardwareFieldId) && $('ticket_fields_' + this.softwareFieldId))) {
			this.contentRoot.update('Not a ticket, or missing custom fields.');
			return;
		}
		this.contentRoot.update( PagloHelper.getSearchForm(this.widgetId, ''));
		
		// if the hardware ID is present in the custom field, retrieve hardware info
		var name = $('ticket_fields_' + this.hardwareFieldId).value;
		if (name) {
			this.getAsset(name);	
		} else {
			// if not hardware name found, use externalID
			name = (name ? name : this.externalId);

			// if no externalId found, use mailbox name from email
			if (!name && this.email) {
				var reg = /(\S*)@\S*/;
				var arr = reg.exec(this.email);
				if (arr.length > 1)
					name = arr[1];
			} 
			if (name)
				this.doSearch(name);	
		}

		this.contentRoot.down('form').token.value = name;
	},

	doSearch: function(token) {
		var query = PagloHelper.getQuery(token);
		var doc = PagloHelper.getDoc(this.apiKey, query);
		var url = PagloHelper.getURL(doc);

		this.contentRoot.down("div.asset").update('Searching...');
		this.contentRoot.down("div.software").update('');

    new Ajax.Request(url, {
      method:'post',
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
				this.showResults(transport.responseXML);
      }.bind(this),
      onFailure: function() {
        alert("Failed");
      }
    });
	},

	showResults: function(xml) {
		var output = '';

		// loop records
		var records = XmlHelper.getRecords(xml, '', 'tree');
		if (records.length > 0) {
			for (var i=0; i<records.length; i++) {

				// loop values
				var arr = new Array();
				var values = XmlHelper.getRecords(records[i], '','value');
				for (var u=0; u<values.length; u++) {
					arr[values[u].getAttribute('name')] = values[u].firstChild.nodeValue;
				}
				if (arr["name"])
					output += PagloHelper.renderItem(arr["name"], "selectAsset", this.widgetId);
			}
			this.contentRoot.down("div.asset").update(PagloHelper.getList('Select hardware asset:', output));
			this.contentRoot.down("div.software").update('');
		} else {
			this.contentRoot.down("div.asset").update('Nothing found');
		}
	},

	selectAsset: function(name) {
		if (name != '') {
			$('ticket_fields_' + this.hardwareFieldId).value = name;
			$('ticket_fields_' + this.softwareFieldId).value = '';
			this.getAsset(name);
			this.getSoftware(name);
		}
	},

	getSoftware: function(name) {
		var query = PagloHelper.getSoftwareQuery(name);
		var doc = PagloHelper.getDoc(this.apiKey, query);
		var url = PagloHelper.getURL(doc);

    new Ajax.Request(url, {
      method:'post',
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
				this.showSoftware(transport.responseXML);
      }.bind(this),
      onFailure: function() {
        alert("Failed");
      }
    });
	},

	showSoftware: function(xml) {
		var output = '';
		var ddlSoftware;

		var records = XmlHelper.getRecords(xml, "", "tree");

		if (records.length > 0) {
			this.contentRoot.down("div.software").update(PagloHelper.getSoftwareSelection(this.widgetId));
			ddlSoftware = this.contentRoot.down("select.software");
			ddlSoftware.appendChild(new Element("option"));

			for (var i=0; i<records.length; i++) {
				var values = XmlHelper.getRecords(records[i], "", "value");
				for (var u=0; u<values.length; u++) {
					if (values[u].getAttribute('name') == 'Name') {
						var option = new Element("option");
						option.value=values[u].firstChild.nodeValue;
						option.innerHTML = option.value;
						ddlSoftware.appendChild(option);
					}
				}
			}
		} else {
			this.contentRoot.down("div.software").update('');
		}
	},

	selectSoftware: function(name) {
		$('ticket_fields_' + this.softwareFieldId).value = name;
	},

	getAsset: function(name) {
		var query = PagloHelper.getAssetQuery(name);
		var doc = PagloHelper.getDoc(this.apiKey, query);
		var url = PagloHelper.getURL(doc);

		this.contentRoot.down("div.asset").update('Retrieving info...');

    new Ajax.Request(url, {
      method:'post',
      requestHeaders: { Accept: 'application/xml' },
      onSuccess: function(transport) {
				this.contentRoot.down("div.asset").update(PagloHelper.renderAssetInfo(transport.responseXML));
      }.bind(this),
      onFailure: function() {
        alert("Failed");
      }
    });
	}

});




/*
 * Batchbook widget
 *
 * Enables lookup of contact details in batchbook
 */

/*
  The BatchBookHelper provides a selection of convenience methods to call from the widget implementation.
*/
var BatchBookHelper = Class.create();

BatchBookHelper.getNodeValue = function(document, nodeName) {
  var node = document.getElementsByTagName(nodeName);
  if(node == null || node.length == 0 || node[0].childNodes.length == 0) {
    return null;
  }
  return node[0].childNodes[0].nodeValue;
}

BatchBookHelper.getNodeString = function(document, nodeName) {
  var value = this.getNodeValue(document, nodeName);
  if(value == null) {
    return '';
  }
  else {
    return value;
  }
}

BatchBookHelper.getArrayByTagName = function(tag_name, document) {
  var results = document.getElementsByTagName(tag_name)
  if(results == null) {
    return new Array();
  }
  return results;
}

BatchBookHelper.getRecords = BatchBookHelper.getArrayByTagName.curry('person')
BatchBookHelper.getLocations = BatchBookHelper.getArrayByTagName.curry('location')



/*
  The BatchBookQuery provides a template and function for building BatchBook API query URLs.
*/

var BatchBookQuery = Class.create(); 

BatchBookQuery.SERVER_URL = new Template('https://#{account_name}.batchbook.com/service/people.xml?email=#{email}');
BatchBookQuery.PROXY_URL  = new Template('/proxy/direct.xml?basic_user=#{token}&basic_pass=x&url=#{server_url}');
BatchBookQuery.CONTACT_URL = new Template('https://#{account_name}.batchbook.com/contacts/show/#{record_id}');

BatchBookQuery.buildUrl = function(account_name, token, email) {
  var serverUrl = this.SERVER_URL.evaluate({ account_name: account_name, email: email });
  var proxyUrl = this.PROXY_URL.evaluate({ server_url: escape(serverUrl), token: token });
  return proxyUrl;
}




/*
  The BatchBookWidgetView provides functions for turning fetched API results into markup.
*/

var BatchBookWidgetView = Class.create();

BatchBookWidgetView.HEADLINE_TEMPLATE = new Template('<h4 style="font-size: 16px"><a href="#{contact_url}" target="_new">#{first_name} #{last_name}</a></h4>');
BatchBookWidgetView.LOCATION_TEMPLATE = new Template('<h4>#{label}</h4>#{items}');
BatchBookWidgetView.LOCATION_ADDRESS_TEMPLATE = new Template("<span class=\"sub\">#{item_label}</span><br />#{item_value}<br />");
BatchBookWidgetView.LOCATION_ITEM_TEMPLATE = new Template("#{item_value} <span class=\"sub\">#{item_label}</span><br />");
BatchBookWidgetView.ADDRESS_TEMPLATE = new Template('#{street_1}</h4><p class="minimal">#{items}</p>');

BatchBookWidgetView.renderLine = function(record, elements, template) {
  var output = '';
  for(var i=0; i<elements.length; i++) {
    output += BatchBookHelper.getNodeString(record, elements[i]);
    if(output != '') {
      output += ' ';
    }
  }
  if(output != '') {
    return template.evaluate({ output: output });
  }
  else {
    return '';
  }
}

BatchBookWidgetView.renderAddress = function(l) {
  var s1 = BatchBookHelper.getNodeString(l, 'street_1');
  var s2 = BatchBookHelper.getNodeString(l, 'street_2');
  var ct = BatchBookHelper.getNodeString(l, 'city');
  var st = BatchBookHelper.getNodeString(l, 'state');
  var pc = BatchBookHelper.getNodeString(l, 'postal_code');
  var cn = BatchBookHelper.getNodeString(l, 'country');

  var a = '';

  if (s1 != '') { a += s1 + '<br />'; }
  if (s2 != '') { a += s2 + '<br />'; }

  if (ct != '') {
	a += ct;
    if (st != '' || pc != '') { a += ', '; }
  }

  if (st != '' || pc != '') {
    a += st;
    if (pc != '') { a += ' '; }
  }

  if (ct != '' || st != '' || pc != '') {
    a += pc + '<br />'
  }

  if (cn != '') { a += cn; }

  return a;
}


BatchBookWidgetView.renderRecord = function(record, accountName) {
  var result = '';

  var record_id = BatchBookHelper.getNodeString(record, 'id');
  var first_name = BatchBookHelper.getNodeString(record, 'first_name');
  var last_name = BatchBookHelper.getNodeString(record, 'last_name');

  var contact_url = BatchBookWidgetView.contact_url(record_id, accountName);

  result += BatchBookWidgetView.HEADLINE_TEMPLATE.evaluate({ contact_url: contact_url, 
	                                                         first_name: first_name,
	                                                         last_name: last_name});

  result += this.renderLine(record, ['title'], new Template("<p class='minimum'>#{output}</p>"));
  result += this.renderLine(record, ['company'], new Template("<p class='minimum'>#{output}</p>"));

  var locations = BatchBookHelper.getLocations(record);
  var location_string = {'address': '', 'email': '', 'website': '', 'phone': '', 'cell': '', 'fax': ''};
  var location_keys = ['address', 'email', 'website', 'phone', 'cell', 'fax']

  for (var i=0; i<locations.length; i++) {
		var l = locations[i];
		var loc_label = l.attributes['label'].value;

		for (var j=0; j<location_keys.length; j++) {
			var lk = location_keys[j];
			var val = '';

			if (lk == 'address') {
				val = this.renderAddress(l);
			} else {
				val = BatchBookHelper.getNodeString(l, lk);
			}

			if (val != '') {
				if (lk == 'address') {
					if (location_string[lk])
						location_string[lk] += "<br />";
					location_string[lk] += BatchBookWidgetView.LOCATION_ADDRESS_TEMPLATE.evaluate({ item_label: loc_label, item_value: val });
				} else {
					location_string[lk] += BatchBookWidgetView.LOCATION_ITEM_TEMPLATE.evaluate({ item_label: loc_label, item_value: val });
				}
			}
		}
	}

  for (var j=0; j<location_keys.length; j++) {
    var lk = location_keys[j];
    if (location_string[lk] != '') {
      result += BatchBookWidgetView.LOCATION_TEMPLATE.evaluate({ label: lk, items: location_string[lk] });
    }
  }

  return result + "<br />";
}

BatchBookWidgetView.contact_url = function(record_id, accountName) {
	return BatchBookQuery.CONTACT_URL.evaluate({ record_id: record_id, account_name: accountName });
}



/*
  The BatchBook widget is a mechanism for looking up person data on BatchBook. The widget must be
  configured with valid account_name and token, after which it can be deployed and used on pages.
*/

var BatchBookWidget = Class.create(Widget, {

	lookup: function(form) {
	  if (this.accountName != null && this.token != null) {
	    var url = BatchBookQuery.buildUrl(this.accountName, this.token, form.email.value);

	    new Ajax.Request(url, {
	     	method:'get',
	      onSuccess: function(transport) {					
	        this.displayResult(transport.responseXML);
	      }.bind(this),
	      onFailure: function() {
		    	$(this.contentRoot).update("<h4>Unable to search your BatchBook account at this time.  Please try again later.</h4>");
	      }.bind(this)
	    });
	  }
	  else {
	    $(this.contentRoot).update("<h4>BatchBook login failed - BatchBook account needs configuration.</h4>");
	  }
	},

	displayResult: function(responseXML) {
	  var records = BatchBookHelper.getRecords(responseXML);

	  if(records.length == 0) {
	    $(this.contentRoot).update("<h4>No match found in your BatchBook account.</h4>");
	    return;
	  } else {
	    // only render first result in sidebar.  can link to BB to search for more hits.
	    var m = BatchBookWidgetView.renderRecord(records[0], this.accountName);
	    $(this.contentRoot).update(m);
	  }
	},

	initialize: function($super, args) {
		$super(args);

		this.token = args.token;
		this.formInstance = null;
		this.widgetId = args.id;
		this.isEmpty = args.isEmpty;
		this.accountName = args.accountName;
		this.requesterEmail = args.requester_email;
		this.contentRoot = this.containingElement().down("div.batchbook-content-" + this.widgetId);
	}
	
} );




/*
*
* Contactology Widget
*
*/

var ContactologyHelper = {
  GET_ACTIVE_LISTS: new Template(
		'<?xml version="1.0" encoding="utf-8"?>' + 
		'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://soapserver.emailcampaigns.net/1.1/" xmlns:types="http://soapserver.emailcampaigns.net/1.1/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
		'  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
		'    <tns:GetActiveLists>' + 
		'      <sortDir xsi:type="xsd:string">U</sortDir>' + 
		'      <sortID xsi:type="xsd:string">Name</sortID>' + 
		'      <startRecord xsi:type="xsd:int">0</startRecord>' +
		'      <pageSize xsi:type="xsd:int">0</pageSize>' +
		'    </tns:GetActiveLists>' +
		'  </soap:Body>' +
		'</soap:Envelope>'
  ),
/*
	GET_ADDRESS_SUBSCRIPTIONS: new Template(
		'<?xml version="1.0" encoding="utf-8"?>' +
		'  <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://soapserver.emailcampaigns.net/1.1/" xmlns:types="http://soapserver.emailcampaigns.net/1.1/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
		'    <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
		'      <tns:GetAddressSubscriptions>' +
		'        <email xsi:type="xsd:string">#{email}</email>' + 
		'      </tns:GetAddressSubscriptions>' +
		'    </soap:Body>' +
		'</soap:Envelope>'
	),
*/
	GET_CONTACT_SUBSCRIPTIONS: new Template(
		'<?xml version="1.0" encoding="utf-8"?>' +
		'  <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://soapserver.emailcampaigns.net/1.1/" xmlns:types="http://soapserver.emailcampaigns.net/1.1/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
		'    <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
		'      <tns:GetContactSubscriptions>' +
		'        <email xsi:type="xsd:string">#{email}</email>' + 
		'      </tns:GetContactSubscriptions>' +
		'    </soap:Body>' +
		'</soap:Envelope>'
	),
	
	SUBSCRIBE_TO_LIST: new Template(
		'<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://soapserver.emailcampaigns.net/1.1/" xmlns:types="http://soapserver.emailcampaigns.net/1.1/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
		'  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
		'    <tns:SubscribeToList>' +
		'      <email xsi:type="xsd:string">#{email}</email>' +
		'      <listId xsi:type="xsd:int">#{id}</listId>' +
		'    </tns:SubscribeToList>' +
		'  </soap:Body>' +
		'</soap:Envelope>'
	),
	
	UNSUBSCRIBE_FROM_LIST: new Template(
		'<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://soapserver.emailcampaigns.net/1.1/" xmlns:types="http://soapserver.emailcampaigns.net/1.1/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
		'  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
		'    <tns:UnsubscribeFromList>' +
		'      <email xsi:type="xsd:string">#{email}</email>' +
		'      <listId xsi:type="xsd:int">#{id}</listId>' +
		'    </tns:UnsubscribeFromList>' +
		'  </soap:Body>' +
		'</soap:Envelope>'
	),
	
	GET_CONTACT: new Template(
		'<?xml version="1.0" encoding="utf-8"?>' +
		'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://soapserver.emailcampaigns.net/1.1/" xmlns:types="http://soapserver.emailcampaigns.net/1.1/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
		'  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
		'    <tns:GetContactByEmail>' +
		'      <emailAddress xsi:type="xsd:string">#{email}</emailAddress>' +
		'    </tns:GetContactByEmail>' +
		'  </soap:Body>' +
		'</soap:Envelope>'
	),
	
	REACTIVATE: new Template(
		'<?xml version="1.0" encoding="utf-8"?>' +
		'<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://soapserver.emailcampaigns.net/1.1/" xmlns:types="http://soapserver.emailcampaigns.net/1.1/encodedTypes" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
		'  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' +
		'    <tns:UpdateContact>' +
		'      <addressId xsi:type="xsd:int">#{id}</addressId>' +
		'      <emailAddress xsi:type="xsd:string" />' +
		'      <status xsi:type="xsd:int">0</status>' +
		'    </tns:UpdateContact>' +
		'  </soap:Body>' +
		'</soap:Envelope>'
	),

  PROXY_URL: new Template(
    '/proxy/direct?basic_user=#{username}&basic_pass=#{password}&log=1&url=http://soapserver.emailcampaigns.net/1.1/index.php&SOAPAction=http://soapserver.emailcampaigns.net/1.1/index.php/#{action}'
  ),

  getActiveListsRequest: function() {
    return ContactologyHelper.GET_ACTIVE_LISTS.evaluate({ })
  },
/*
	getAddressSubscriptionsRequest: function(email) {
		return ContactologyHelper.GET_ADDRESS_SUBSCRIPTIONS.evaluate({ email: email});
	},
*/
	getContactSubscriptionsRequest: function(email) {
		return ContactologyHelper.GET_CONTACT_SUBSCRIPTIONS.evaluate({ email: email});
	},
	
	getSubscribeToListRequest: function(email, id) {
		return ContactologyHelper.SUBSCRIBE_TO_LIST.evaluate({ email: email, id: id });
	},

	getGetContactRequest: function(email) {
		return ContactologyHelper.GET_CONTACT.evaluate({ email: email });
	},
	
	getReactivateRequest: function(id) {
		return ContactologyHelper.REACTIVATE.evaluate({ id: id });
	},

	getUnsubscribeFromListRequest: function(email, id) {
		return ContactologyHelper.UNSUBSCRIBE_FROM_LIST.evaluate({ email: email, id: id });
	},

	getProxyURL: function(username, password, action) {
    return this.PROXY_URL.evaluate({ username: username, password: password, action: action })
	},

	STATUS_SECTION: new Template(
		'<b>Status:</b><div class="contact_status">#{status}</div><br />'
	),
		
	LIST_SECTION: new Template(
		'<b>#{header}</b><br />#{items}<br />'
	),

	LIST_ELEMENT: new Template (
		'<input type="checkbox" style="vertical-align: top;" value="#{id}" #{checked} #{disabled} onClick="ObjectFactory.get(#{widgetId}).changeSubscription(this);"> #{name} <span class="sub">#{status}</span><br />'
	),
	
	REACTIVATE_LINK: new Template (
		'<a href="javascript:ObjectFactory.get(#{widgetId}).reactivate();">Click to reactivate</a><br/><br/>'
	),
	
	renderView: function(responseXML, newSubscriber, widgetId) {
		var output = '';
		var active = true;
		
		var addressSubscriptions = XmlHelper.getRecords(responseXML, "", "item");

		if (newSubscriber)
			status_code = "NEW";
			
		else {
			var status_code = ContactologyHelper.getStatus(responseXML);
			active = (status_code == 'GLOBALY_UNSUBSCRIBED' || status_code == 'GLOBALLY_UNSUBSCRIBED' || status_code == 'BOUNCED' || status_code == 'DELETED') ? false : true;
		}
		
		output += ContactologyHelper.renderStatus(status_code, newSubscriber);
		
		if (status_code == 'BOUNCED')
			output += ContactologyHelper.REACTIVATE_LINK.evaluate({ widgetId: widgetId });
			
		output += ContactologyHelper.renderListsByType(addressSubscriptions, "INTERNAL", "Internal", newSubscriber, widgetId, active);
		output += ContactologyHelper.renderListsByType(addressSubscriptions, "PUBLIC", "Public", newSubscriber, widgetId, active);
		output += ContactologyHelper.renderListsByType(addressSubscriptions, "PRIVATE", "Private", newSubscriber, widgetId, active);
		return output;
	},
	
	renderListsByType: function(addressSubscriptions, typeName, typeLabel, newSubscriber, widgetId, enabled) {
		var output = '';

		for (var i=0; i<addressSubscriptions.length; i++) {	
			if (XmlHelper.getTextContentByKey(addressSubscriptions[i], newSubscriber? "type_label" : "type").toUpperCase() == typeName) {
				var name = XmlHelper.getTextContentByKey(addressSubscriptions[i], newSubscriber? "list_name" : "name");
				var id = XmlHelper.getTextContentByKey(addressSubscriptions[i], newSubscriber? "list_id" : "id");
				var status = newSubscriber ? "NONE" : XmlHelper.getTextContentByKey(addressSubscriptions[i], "subscription_status");
				var checked = '';
				var substatus = '';
				
				if (status == "SUBSCRIBED" || status == "AWAITING_CONFIRMATION")
					checked = "checked";
					
				if (status == "AWAITING_CONFIRMATION")
					substatus = "pending";

				output += ContactologyHelper.LIST_ELEMENT.evaluate({ name: name, id: id, checked: checked, widgetId: widgetId, status: substatus, disabled: (enabled? '' : 'DISABLED') });
			}
		}

		if (output > "")
			return ContactologyHelper.LIST_SECTION.evaluate({ header: typeLabel, items: output });
		else
			return "";
	},
	
	renderStatus: function(status, newSubscriber) {
		if (newSubscriber)
			return ContactologyHelper.STATUS_SECTION.evaluate({ status: 'NEW' });		
			
		return ContactologyHelper.STATUS_SECTION.evaluate({ status: status });
	},
	
	getStatus: function(responseXML) {
		var element = responseXML.getElementsByTagName('return')[0];
		var status = XmlHelper.getTextContentByKey(element, "contact_status");
		return status;
	},
	
	errorFree: function(xml) {
		var element = xml.getElementsByTagName('faultstring');
		return(element.length > 0 ? false : true);
	}
};

var ContactologyWidget = Class.create(Widget, {

  initialize: function($super, args) {
    $super(args);
    this.widgetId 		= args.id;
    this.username	    = args.username;
    this.password     = args.password;
		this.email				= args.email;
    this.formInstance = null;
    this.contentRoot 	= this.containingElement().down("div.contactology-content" + this.widgetId);
		this.lastChange		= null;
		this.contactId		= null;
		this.startup();
  },

	startup: function() {
		if (this.email == null)
			this.contentRoot.update('No requester found');
		else
			this.getSubscriptions();
	},

	getSubscriptions: function(change) {
		var doc = "body=" + ContactologyHelper.getContactSubscriptionsRequest(this.email);
		var url = ContactologyHelper.getProxyURL( this.username, this.password, 'GetContactSubscriptions' );

    new Ajax.Request(url, {
      method:'post',
      encoding: '',
			postBody: doc, 
      onSuccess: function(transport) {	
				if (XmlHelper.getRecords(transport.responseXML, "SOAP-ENV", "Fault").length > 0)
					this.getActiveLists();
				else if (change == this.lastChange)
					this.contentRoot.update(ContactologyHelper.renderView(transport.responseXML, false, this.widgetId));
      }.bind(this),
      onFailure: function() {
        alert("Failed to lookup.");
      }.bind(this)
    });
	},
	
	getActiveLists: function() {
		var doc = "body=" + ContactologyHelper.getActiveListsRequest();
		var url = ContactologyHelper.getProxyURL(this.username, this.password, 'GetActiveLists');
		
    new Ajax.Request(url, {
      method:'post',
      encoding: '',
			postBody: doc, 
      onSuccess: function(transport) {
				this.contentRoot.update(ContactologyHelper.renderView(transport.responseXML, true, this.widgetId));
      }.bind(this),
      onFailure: function() {
        alert("Failed to lookup.");
      }.bind(this)
    });
	},
		
	changeSubscription: function(item) {
		var doc = '';
		var action = '';
		var checked = item.checked;
		
		item.disabled = true;
		this.lastChange = item.value;
		
		if (checked) {
			doc = "body=" + ContactologyHelper.SUBSCRIBE_TO_LIST.evaluate({ email: this.email, id: item.value });
			action = 'SubscribeToList';
		} else {
			doc = "body=" + ContactologyHelper.UNSUBSCRIBE_FROM_LIST.evaluate({ email: this.email, id: item.value });
			action = 'UnsubscribeFromList';
		}
		
		var url = ContactologyHelper.getProxyURL(this.username, this.password, action);

		this.showStatus("UPDATING...");
    new Ajax.Request(url, {
      method:'post',
      encoding: '',
			postBody: doc, 
      onSuccess: function(transport) {
				if (ContactologyHelper.errorFree(transport.responseXML)) {
					if (XmlHelper.getTextContentByKey(transport.responseXML, "return") != 1) {
					}
					if (this.lastChange == item.value)
						this.getSubscriptions(item.value);
				} else {
					this.changeFailed(item, checked);
					return;
					
				}
      }.bind(this),
      onFailure: function() {
				alert("Failure!");
				this.changeFailed(item, checked);
      }.bind(this)
    });
	},
	
	reactivate: function() {
		var doc = "body=" + ContactologyHelper.getGetContactRequest(this.email);
		this.showStatus("UPDATING...");
		
    new Ajax.Request(ContactologyHelper.getProxyURL(this.username, this.password, "GetContactByEmail"), {
      method:'post',
      encoding: '',
			postBody: doc, 
      onSuccess: function(transport) {
				var element = transport.responseXML.getElementsByTagName('return')[0];
				var id = XmlHelper.getTextContentByKey(element, "ID");
				var doc2 = "body=" + ContactologyHelper.getReactivateRequest(id);
		    new Ajax.Request(ContactologyHelper.getProxyURL(this.username, this.password, "UpdateContact"), {
		      method:'post',
		      encoding: '',
					postBody: doc2, 
		      onSuccess: function(transport) {
						this.lastChange = null;
						this.getSubscriptions(null);
		      }.bind(this),
		      onFailure: function() {
						this.refreshStatus();
						alert("Failed to reactivate");
		      }.bind(this)
		    });
      }.bind(this),
      onFailure: function() {
				this.showStatus("UNKNOWN");
				alert("Failed to reactivate");
      }.bind(this)
    });
	},	
	
	showStatus: function(status) {
		this.containingElement().down("div.contact_status").update(status);
	},
	
	refreshStatus: function() {
		var doc = "body=" + ContactologyHelper.getGetContactRequest(this.email);
    new Ajax.Request(ContactologyHelper.getProxyURL(this.username, this.password, "GetContactByEmail"), {
      method:'post',
      encoding: '',
			postBody: doc, 
      onSuccess: function(transport) {
				var element = transport.responseXML.getElementsByTagName('return')[0];
				var status = XmlHelper.getTextContentByKey(element, "StatusLabel");
				this.showStatus(status.toUpperCase());
      }.bind(this),
      onFailure: function() {
				this.showStatus("ERROR");
      }.bind(this)
    });
	},
	
	changeFailed: function(item, checked) {
    alert("Failed to change subscription.");
		item.checked = !checked;
		item.disabled = false;
		this.refreshStatus();
	}

});







var WidgetManagerWidget = Class.create({
  initialize: function() {
    widgetManager = this;
  },
  widgets: function() {
    var foundWidgets = $$('#sidebar div.widget');
    foundWidgets = foundWidgets.without($$('#preview-output .widget')[0]);
    return foundWidgets.map(function(elm) {
      return ObjectFactory.get(parseInt(elm.id.substring(7)));
    });
  },
  show: function() {
    $('add_widget_select_widget').show();
    $('add_widget_button').hide();
    this.widgets().each(function(widget) {
      widget.showControls();
      widget.containingElement().title = 'You can drag and drop widgets to rearrange them';
    });
    if ($('widget_fixed')) {
      $('widget_fixed').addClassName('widget_edit_mode');
    };
    Sortable.create('sidebar', {tag: 'div',
                                only: 'draggable',
                                scroll: window,
                                onUpdate: this.updateOrder});
    Widget.redraw();
  },
  hide: function() {
    $('add_widget_button').show();
    $('add_widget_select_widget').hide();
    this.widgets().each(function(widget) {
      widget.hideControls();
      widget.containingElement().title = '';
    });
    if ($('widget_fixed')) {
      $('widget_fixed').removeClassName('widget_edit_mode');
    };
    Sortable.destroy('sidebar');
    Widget.redraw();
  },
  updateOrder: function(container) {
    var locations = Sortable.sequence(container).map(function(widgetId) {
      if (widgetId == 'fixed') {
        return 'fixed';
      } else {
        return ObjectFactory.get(widgetId).locationId;
      }
    });
    new Ajax.Request('/widgets/reorder_locations', {
      method: 'put',
      parameters: {'locations[]': locations}
    });
  },
  addWidgetToPage: function(controllerName, actionName, widgetId) {
    new Ajax.Request('/widgets/' + widgetId + '/add_to_page', {
      method: 'post',
      parameters: {controller_name: controllerName,
                   action_name: actionName}
    });
  },
  removeWidgetFromPage: function(controllerName, actionName, widgetId) {
    new Ajax.Request('/widgets/' + widgetId + '/remove_from_page', {
      method: 'delete',
      parameters: {controller_name: controllerName,
                   action_name: actionName}
    });
  },
  removeWidget: function(widget_id) {
    var widget = ObjectFactory.get(widget_id);
    widget.hide(widget.destroy.bind(widget));
    ObjectFactory.remove(widget_id);
    $('add_widget_' + widget_id).show();
  },
  displayNewWidget: function() {
    $('widget_manager_widget').insert({before: 'hello'});
  }
});

var widgetManager = null;


var InputTracking = {
  trackedElements: new Array(),
  enabled: true,

  trackElement: function(e) {
    if(!InputTracking.trackedElements.include(this)) {
      this.originalValue = this.value;
      InputTracking.trackedElements.push(this);
      Event.stopObserving(this, 'keydown', InputTracking.trackElement);
    }
  },

  disable: function(e) {
    InputTracking.enabled = false;
    return true;
  },

  fixSubmit: function(element, eventName) {
    //I would do it like "Event.observe(form, 'submit', InputTracking.disable)",
    //but that does not work in IE
    var eventHandler = element[eventName];
    if (Object.isString(eventHandler)) {
      element[eventName] = "InputTracking.disable() && " + eventHandler;
    } else if (Object.isFunction(eventHandler)) {
      element.onsubmitWithoutInputTracking = eventHandler;
      element[eventName] = function(e) {
        return InputTracking.disable() && element.onsubmitWithoutInputTracking();
      };
    } else {
      element[eventName] = InputTracking.disable;
    }
  }
};

var InitialHelpValue = Class.create({
  initialize: function(formElement) {
    this.element = formElement;
    this.helpValue = formElement.getValue();

    Event.observe(this.element, 'focus', this.onFocus.bindAsEventListener(this));
    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
  },

  onFocus: function(e) {
    if (this.element.getValue() == this.helpValue) {
      this.element.value = '';
    };
    this.element.removeClassName('initial_help_value');
  },

  onBlur: function(e) {
    if (this.element.getValue().blank()) {
      this.element.value = this.helpValue;
      this.element.addClassName('initial_help_value');
    };
  }
});

var CountrySelectField = Class.create({
  initialize: function(selectElement) {
    this.element = selectElement;
    this.container = new Element('div', {className: 'country_select_field'});
    this.closedView = new Element('div', {className: 'closed'});
    this.container.insert(this.closedView);
    this.openedView = new Element('div', {className: 'opened', style: 'display: none;'}).update('<ul/>');
    this.container.insert(this.openedView);
    this.renderOpenedView();
    this.updateOpenedView();
    this.updateClosedView();
    this.element.hide();
    this.element.insert({after: this.container});

    this.closedView.observe('click', this.toggle.bind(this))
  },

  renderOpenedView: function() {
    var list = this.openedView.down('ul');
    list.update();
    var item;
    this.element.select('option').each(function(option) {
      item = new Element('li', {country: option.value});
      this.renderOption(item, option);
      list.insert(item);
      item.observe('click', this.clickOption.bindAsEventListener(this));
    }, this);
  },

  updateOpenedView: function() {
    this.openedView.select('li.selected').each(function(item) {
      item.removeClassName('selected');
    });

    var selectedOption = this.element.down("option[value=" + this.element.getValue() + "]");

    this.openedView.down('li[country=' + selectedOption.value + ']').addClassName('selected');
  },

  updateClosedView: function() {
    var selectedOption = this.element.down("option[value=" + this.element.getValue() + "]");
    this.renderOption(this.closedView, selectedOption);
  },

  renderOption: function(element, option) {
    element.update();
    element.insert(new Element('img', {src: '/images/flags/' + option.value + '.gif'}));
    element.insert(' ' + option.innerHTML);
  },

  open: function() {
    //this.closedView.hide();
    this.openedView.show();
    this.element.focus();
  },

  close: function() {
    this.openedView.hide();
    //this.closedView.show();
  },

  toggle: function() {
    this.openedView.toggle();
    this.element.focus();
  },

  clickOption: function(e) {
    this.element.down("option[value=" + e.findElement('li').getAttribute('country').toLowerCase() + "]").selected = true;
    this.updateOpenedView();
    this.updateClosedView();
    this.close();
  }
});

var CheckboxMultiSelect = Class.create({
  initialize: function(selectElement) {
    this.element = selectElement;
    this.container = new Element('div', {className: 'checkbox_multi_select'});
    this.render();
    Element.replace(this.element, this.container);
  },

  render: function() {
    var item;
    var input;
    this.element.select('option').each(function(option) {
      input = new Element('input', {type: 'checkbox',
                                    value: option.value,
                                    name: this.element.name,
                                    id: this.element.identify() + '_' + option.value});
      input.checked = option.selected;

      item = new Element('div');
      item.insert(input);
      item.insert(new Element('label', {'for': input.id}).update(option.innerHTML));
      if (input.id != "account_allowed_translation_locale_ids_1") { //special case for english by zendesk
        var results = input.id.split("_");
        item.insert(new Element('a', { 'class': 'lightview',
                                       href: '#more_translation_info_' + results[5],
                                       title: ' :: :: topclose: true, autosize: true' }).update('(more info)'));
      }
      this.container.insert(item);
    }, this);
  }
});

Event.observe(document, 'dom:loaded', function() {
  $$('textarea').each(function(textarea) {
    Event.observe(textarea, 'keydown', InputTracking.trackElement);
  });

  $$('form').each(function(form) {
    InputTracking.fixSubmit(form, 'onsubmit');
  });

  $$('form input[type=button]').each(function(button) {
    InputTracking.fixSubmit(button, 'onclick');
  });

  $$('input.initial_help_value').each(function(formElement) {
    new InitialHelpValue(formElement);
  });

  $$('select.country_select_field').each(function(selectElement) {
    new CountrySelectField(selectElement);
  });

  $$('select.checkbox_multi_select').each(function(selectElement) {
    new CheckboxMultiSelect(selectElement);
  });
});

//Somehow this does not work when done with Event.observe
window.onbeforeunload = function() {
  if (InputTracking.enabled) {
    for (var i = 0; i < InputTracking.trackedElements.length; i++) {
      if (InputTracking.trackedElements[i].value != InputTracking.trackedElements[i].originalValue) {
        return "You have entered some text on this page. If you choose to proceed you will lose your changes.";
      };
    };
  };
};


document.observe('dom:loaded', function() {
  if(window.fluid) {
    document.body.insert("<div id='notifications' style='display:none'></div>");

    //Synchronous poller
    new Ajax.PeriodicalUpdater('notifications', '/notifications/status', {
      asynchronous: false, method: 'get', frequency: 120, decay: 10,
      requestHeaders: {Accept: 'application/json'},
      onSuccess: function(transport) {
        var json = transport.responseText.evalJSON();

        if(json.count != null && json.count > 0) {
          window.fluid.dockBadge = json.count;
        }

        if(json.message != null) {
          window.fluid.showGrowlNotification({
            title: "Zendesk update",
            description: json.message.toString(),
            priority: 1,
            sticky: false,
            identifier: "Zendesk " + Math.random() * 10000
          });
        }
      }
    });
  }
});


/* User */
Autocompleter.lookupUser = function(multi_user, searchTerm, callback) {
  var url = multi_user ? '/people/users/multivalue_autocomplete' : '/people/users/autocomplete';
  new Ajax.Request(url, { parameters: {name: searchTerm, rand: (new Date()).getTime()},
                          onSuccess: function(response) {
                            callback(response.responseJSON);
                          }
                        });
};

Autocompleter.UserCache = new Autocompleter.Cache(Autocompleter.lookupUser.curry(false));
Autocompleter.cachedLookupUser = Autocompleter.UserCache.lookup.bind(Autocompleter.UserCache);

Autocompleter.MultiUserCache = new Autocompleter.Cache(Autocompleter.lookupUser.curry(true));
Autocompleter.cachedLookupMultiUser = Autocompleter.MultiUserCache.lookup.bind(Autocompleter.MultiUserCache);

/* Tag */
Autocompleter.lookupTag = function(searchTerm, callback) {
  new Ajax.Request('/tags/autocomplete', { parameters: {name: searchTerm, rand: (new Date()).getTime()},
                                           onSuccess: function(response) {
                                             callback(response.responseJSON);
                                           }
                                         });
};

Autocompleter.TagCache = new Autocompleter.Cache(Autocompleter.lookupTag);
Autocompleter.cachedLookupTag = Autocompleter.TagCache.lookup.bind(Autocompleter.TagCache);

/* Account */
Autocompleter.lookupAccount = function(searchTerm, callback) {
  new Ajax.Request('/monitor/children/lookup', { parameters: {query: searchTerm, rand: (new Date()).getTime()},
                                           onSuccess: function(response) {
                                             callback(response.responseJSON);
                                           }
                                         });
};
Autocompleter.AccountCache = new Autocompleter.Cache(Autocompleter.lookupAccount, {choices: 50});
Autocompleter.cachedLookupAccount = Autocompleter.AccountCache.lookup.bind(Autocompleter.AccountCache);

/* Organization */
Autocompleter.lookupOrganization = function(searchTerm, callback) {
  new Ajax.Request('/organizations/autocomplete', { parameters: {name: searchTerm, rand: (new Date()).getTime()},
                                           onSuccess: function(response) {
                                             callback(response.responseJSON);
                                           }
                                         });
};
Autocompleter.OrganizationCache = new Autocompleter.Cache(Autocompleter.lookupOrganization);
Autocompleter.cachedLookupOrganization = Autocompleter.OrganizationCache.lookup.bind(Autocompleter.OrganizationCache);



var ZdSubscription = {
  initialize : function(currentPlanId) {
    Page.setCurrentPlan(currentPlanId);
    Page.selectPlan(Page.currentPlan());
    $('plan-' + Page.currentPlan() + '-vars-inner').innerHTML = $('plan-vars-org').innerHTML;
    $('plan-vars-org').remove();
  }, 

  updatePrice: function() {
    new Ajax.Request('/account/subscription/calculate', { 
      method: 'post',
      parameters: { 
        current_plan       : ZdSubscription.currentPlan(),
        max_agents         : ZdSubscription.maxAgents(), 
        plan_type          : ZdSubscription.planType(),
        billing_cycle_type : ZdSubscription.billingCycleType() 
      },
 
      // TODO: do we really need a spinner/waiting indication? 
      // onLoading : ZdSubscription.beforeCalculateBilling,

      onSuccess : function(response) { 
        ZdSubscription.calculateBillingCallback(response.responseJSON); 
      }
    }); 
  }, // updatePrice

  maxAgents        : function() { return ZdSubscription.adjustAgents($F('subscription_max_agents')); },  // maxAgents
  planType         : function() { return $F('subscription_plan_type'); }, // planType
  billingCycleType : function() { return $F('subscription_billing_cycle_type'); }, // billingCycleType
  currentPlan      : function() { return Page.currentPlanId; } , // currentPlan
  setCurrentPlan   : function(planId) { Page.currentPlanId = planId; } , // setCurrentPlan

  beforeCalculateBilling : function() {
    $('billing-cycle-price').innerHTML = '-';
    $('billing-cycle-discounted-amount').innerHTML  = '-';
    $('billing-cycle-undiscounted-price').innerHTML = '-';
  }, // beforeCalculateBilling

  calculateBillingCallback : function(data) { 
    $('billing-cycle-desc').innerHTML  = "Billed " + ZdSubscription.billingCycleName();
    $('billing-cycle-price').innerHTML = '$' + addCommas(data.discounted_price.toFixed(2));
    $('billing-cycle-discounted-amount').innerHTML  = '-&nbsp;$' + addCommas(data.discounted_amount.toFixed(2));
    $('billing-cycle-undiscounted-price').innerHTML = '$' + addCommas(data.undiscounted_price.toFixed(2));
  }, // cb_calculateBilling

  billingCycleName : function() {
    switch(ZdSubscription.billingCycleType()) { 
      case('1') : { return "monthly";   };
      case('2') : { return "quarterly"; };
      case('4') : { return "annually";  };
    }; 
  }, // billingCycleName 

  adjustAgents : function(agents) {
    var currentPlanId = ZdSubscription.currentPlan(); 
    agents = parseInt(agents)
    if (isNaN(agents) || currentPlanId == 1)   { agents = 1; }
    else if (currentPlanId == 2 && agents < 3) { agents = 3; }
    else if (currentPlanId == 3 && agents < 5) { agents = 5; }
    
    $('subscription_max_agents').value = agents;
    return agents;
  }, // adjustAgents

  selectPlan : function(selectedPlan) {
    thisPlan = ZdSubscription.currentPlan();
    ZdSubscription.setCurrentPlan(selectedPlan);
    $('subscription_plan_type').value = selectedPlan;

    [1, 2, 3].each(function(s) {
      selectButton = $('plan-' + s + '-select')
      divPlanVars  = $('plan-' + s + '-vars')
    
      if (selectedPlan == s) {
        $('plan-' + s).style.border = '3px solid #A4D1DB';
        selectButton.disabled = true;
        selectButton.value = "Selected plan!";
        if (s != thisPlan) {
          plan_vars = $('plan-' + thisPlan + '-vars-inner') //.innerHTML;
          plan_vars.id = 'plan-' + s + '-vars-inner'
          divPlanVars.insert({ top: plan_vars }); 
        } // !thisPlan
        
        divPlanVars.style.padding = '5px 10px';
        divPlanVars.style.borderTop = '1px dashed #CCC';

        ZdSubscription.updatePrice();
      
        $('subscription_max_agents').disabled = (selectedPlan == 1);
      } else { // selectedPlan != s
      
        $('plan-' + s).style.border = '3px solid #DDD';
        selectButton.disabled = false;
        selectButton.value = "Select this plan";
        divPlanVars.style.padding = '';
        divPlanVars.style.borderTop = 'none';
      } // if / else
    }); // [1,2,3].each
  }, // selectPlan

  updateSubscription : function() {
    Lightview.show({
      href: '/account/subscription/confirm',
      rel: 'ajax',
      options: {
        topclose: true,
        autosize: true,
        backgroundColor: '#999',
        ajax: {
          parameters: { 
            plan_type: $F('subscription_plan_type'), max_agents: $F('subscription_max_agents'), 
            billing_cycle_type: $F('subscription_billing_cycle_type'), 
            card_number_length: $F('subscription_credit_card_number').strip().length
          },  
          method: 'get'
        }
      } // options
    }); // Lightview.show
  } // updateSubscription
}; // "class" ZdSubscription


var SuggestionsSearch = Class.create({
  initialize: function(options) {
    this.options = options;
    
    var moreLink = new Element('a').update(this.options.more);
    this.more = new Element('li', {id: 'more_suggestions'}).update(moreLink);
  },

  searchComplete: function() {
    $('topic_suggestions').removeClassName('loading');

    Try.these(this.more.remove);

    //clear if this is a new search
    if (!this.searcher.cursor || this.searcher.cursor.currentPageIndex == 0) {
      $('topic_suggestions').update('');
    }

    if (this.searcher.results && this.searcher.results.length > 0) {
      var list = $('suggestions');
      if (!list) {
        $('topic_suggestions').insert(new Element('h2').update(this.options.header));
        list = new Element('ul', {id: 'suggestions'});
        $('topic_suggestions').insert(list);
      };

      this.searcher.results.each(function(result) {
        //removes the account name prefix in titles
        var title = result.titleNoFormatting.gsub(/.* :: /, '');
        var link = new Element('a', {href: result.url}).update(title);

        var item = new Element('li', {className: 'suggestion'});
        item.insert(new Element('h3').update(link));
        item.insert(new Element('div').update(result.content));

        this.insert(item);
      }, list);

      if (this.searcher.cursor && this.searcher.cursor.currentPageIndex < this.searcher.cursor.pages.length - 1) {
        list.insert(this.more);
      };
    } else {
      $('topic_suggestions').insert(new Element('h2').update(this.options.noResults));
    };
  },

  onSubmit: function() {
    $('topic_suggestions').update();
    $('topic_suggestions').addClassName('loading');

    this.searcher.execute($F('q'));

    return false;
  },

  onShowMore: function() {
    this.more.remove();
    $('topic_suggestions').addClassName('loading');
    this.searcher.gotoPage(this.searcher.cursor.currentPageIndex + 1);
    return false;
  },

  setup: function() {
    this.searcher = new google.search.WebSearch();
    this.searcher.setSiteRestriction(this.options.site);
    this.searcher.setResultSetSize(google.search.Search.SMALL_RESULTSET);
    this.searcher.setRestriction(google.search.Search.RESTRICT_SAFESEARCH, google.search.Search.SAFESEARCH_OFF);
    this.searcher.setSearchCompleteCallback(this, this.searchComplete, null);

    $('suggest_form').onsubmit = this.onSubmit.bind(this);

    this.more.observe('click', this.onShowMore.bind(this));
  }
});
