
/* /assets/ctx/0.8/js/scriptaculous/prototype.js */;
/*  Prototype JavaScript framework, version 1.6.1_rc3
 *  (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_rc3',

  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() {

  function getClass(object) {
    return Object.prototype.toString.call(object)
     .match(/^\[object\s(.*)\]$/)[1];
  }

  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 getClass(object) === "Array";
  }


  function isHash(object) {
    return object instanceof Hash;
  }

  function isFunction(object) {
    return typeof object === "function";
  }

  function isString(object) {
    return getClass(object) === "String";
  }

  function isNumber(object) {
    return getClass(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();
      } catch(e) {
        /* empty catch for clients that don't support try/finally */
      }
      finally {
        this.currentlyExecuting = false;
      }
    }
  }
});
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() {
    escapeHTML.text.data = this;
    return escapeHTML.div.innerHTML;
  }

  function unescapeHTML() {
    var div = document.createElement('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? (div.childNodes.length > 1 ?
      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
      div.childNodes[0].nodeValue) : '';
  }


  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.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
  }

  function dasherize() {
    return this.gsub(/_/,'-');
  }

  function inspect(useDoubleQuotes) {
    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
      var character = String.specialChar[match[0]];
      return character ? character : '\\u00' + match[0].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.sub(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
  };
})());

Object.extend(String.prototype.escapeHTML, {
  div:  document.createElement('div'),
  text: document.createTextNode('')
});

String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);

if ('<\n>'.escapeHTML() !== '&lt;\n&gt;') {
  String.prototype.escapeHTML = function() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  };
}

if ('&lt;\n&gt;'.unescapeHTML() !== '<\n>') {
  String.prototype.unescapeHTML = function() {
    return this.stripTags().replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&');
  };
}
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].gsub('\\\\]', ']') : 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(){

    var iframeGetAttributeThrowsError = (function(){
      var el = document.createElement('iframe'),
          isBuggy = false;

      document.documentElement.appendChild(el);
      try {
        el.getAttribute('type', 2);
      } catch(e) {
        isBuggy = true;
      }
      document.documentElement.removeChild(el);
      el = null;
      return isBuggy;
    })();

    return function(element, name) {
      element = $(element);
      if (iframeGetAttributeThrowsError &&
          name === 'type' &&
          element.tagName.toUpperCase() == 'IFRAME') {
        return element.getAttribute('type');
      }
      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, 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._getAttr,
      src:         v._getAttr,
      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');
  var HTMLAPPLETELEMENT_PROTOTYPE_BUGGY = checkDeficiency('applet');

  if (Prototype.BrowserFeatures.SpecificElementExtensions) {
    if (HTMLOBJECTELEMENT_PROTOTYPE_BUGGY &&
        HTMLAPPLETELEMENT_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, "\\$0");
          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);

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

/* /assets/ctx/0.8/js/scriptaculous/scriptaculous.js */;
// 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);

// Tapestry turns off this mechanism, and replaces it with RenderSupport.addScriptLink().

//    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();
/* /assets/ctx/0.8/js/scriptaculous/effects.js */;
// script.aculo.us effects.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)
// 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);
/* /assets/tapestry/5.1.0.8-SNAPSHOT/tapestry.js */;
// Copyright 2007, 2008, 2009 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var Tapestry = {

    /** Event that allows observers to perform cross-form validation after individual
     *  fields have performed their validation. The form element is passed as the
     *  event memo. Observers may set the validationError property of the Form's Tapestry object to true (which
     *  will prevent form submission).
     */
    FORM_VALIDATE_EVENT : "tapestry:formvalidate",

    /** Event fired just before the form submits, to allow observers to make
     *  final preparations for the submission, such as updating hidden form fields.
     *  The form element is passed as the event memo.
     */
    FORM_PREPARE_FOR_SUBMIT_EVENT : "tapestry:formprepareforsubmit",

    /**
     *  Form event fired after prepare.
     */
    FORM_PROCESS_SUBMIT_EVENT : "tapestry:formprocesssubmit",

    /** Event, fired on a field element, to cause observers to validate the input. Passes a memo object with
     * two keys: "value" (the raw input value) and "translated" (the parsed value, usually meaning a number
     * parsed from a string).  Observers may invoke Element.showValidationMessage()
     *  to identify that the field is in error (and decorate the field and show a popup error message).
     */
    FIELD_VALIDATE_EVENT : "tapestry:fieldvalidate",

    /** Event, fired on the document object, which identifies the current focus input element. */
    FOCUS_CHANGE_EVENT : "tapestry:focuschange",

    /** Event, fired on a zone element when the zone is updated with new content. */
    ZONE_UPDATED_EVENT : "tapestry:zoneupdated",

    /** When false, the default, the Tapestry.debug() function will be a no-op. */
    DEBUG_ENABLED : false,

    /** Time, in seconds, that console messages are visible. */
    CONSOLE_DURATION : 10,

    // Initially, false, set to true once the page is fully loaded.

    pageLoaded : false,

    /**
     * Invoked from onclick event handlers built into links and forms. Raises a dialog
     * if the page is not yet fully loaded.
     */
    waitForPage : function(event)
    {
        if (Tapestry.pageLoaded) return true;

        Event.extend(event || window.event).stop();

        var body = $(document.body);

        // The overlay is stretched to cover the full screen (including scrolling areas)
        // and is used to fade out the background ... and prevent keypresses (its z-order helps there).

        var overlay = new Element("div", {
            'class' : 't-dialog-overlay'
        });
        overlay.setOpacity(0.0);

        body.insert({
            top: overlay
        });

        new Effect.Appear(overlay, {
            duration: 0.2,
            from: 0.0
        });

        var messageDiv = new Element("div", {
            'class' : 't-page-loading-banner'
        }).update(Tapestry.Messages.pageIsLoading);
        overlay.insert({
            top: messageDiv
        });

        var hideDialog = function()
        {
            new Effect.Fade(overlay, {
                duration: 0.2,
                afterFinish: function()
                {
                    overlay.remove();
                }
            });
        };

        document.observe("dom:loaded", hideDialog);

        // A rare race condition.

        if (Tapestry.pageLoaded)
        {
            hideDialog.call(null);

            return true;
        }
        else
        {
            return false;
        }

    },


    // Adds a callback function that will be invoked when the DOM is loaded (which
    // occurs *before* window.onload, which has to wait for images and such to load
    // first.  This simply observes the dom:loaded event on the document object (support for
    // which is provided by Prototype).
    onDOMLoaded : function(callback)
    {
        document.observe("dom:loaded", callback);
    },

    /** Find all elements marked with the "t-invisible" CSS class and hide()s them, so that
     * Prototype's visible() method operates correctly. In addition,
     * finds form control elements and adds additional listeners to them to support
     * form field input validation.
     *
     * <p>This is invoked when the
     * DOM is first loaded, and AGAIN whenever dynamic content is loaded via the Zone
     * mechanism.
     */
    onDomLoadedCallback : function()
    {
        // Turn off click & submit protection inside Tapestry.waitForPage().

        Tapestry.pageLoaded = true;

        Tapestry.ScriptManager.initialize();

        $$(".t-invisible").each(function(element)
        {
            element.hide();
            element.removeClassName("t-invisible");
        });

        // Adds a focus observer that fades all error popups except for the
        // field in question.

        $$("INPUT", "SELECT", "TEXTAREA").each(function(element)
        {
            // Due to Ajax, we may execute the callback multiple times,
            // and we don't want to add multiple listeners to the same
            // element.

            var t = $T(element);

            if (! t.observingFocusChange)
            {
                element.observe("focus", function()
                {
                    if (element != Tapestry.currentFocusField)
                    {
                        document.fire(Tapestry.FOCUS_CHANGE_EVENT, element);

                        Tapestry.currentFocusField = element;
                    }
                });

                t.observingFocusChange = true;
            }
        });

        // When a submit element is clicked, record the name of the element
        // on the associated form. This is necessary for some Ajax processing,
        // see TAPESTRY-2324.

        $$("INPUT[type=submit]").each(function(element)
        {
            var t = $T(element);

            if (!t.trackingClicks)
            {
                element.observe("click", function()
                {
                    $T(element.form).lastSubmit = element;
                });

                t.trackingClicks = true;
            }
        });
    },

    /* Generalized initialize function for Tapestry, used to help minimize the amount of JavaScript
     * for the page by removing redundancies such as repeated Object and method names. The spec
     * is a hash whose keys are the names of methods of the Tapestry.Initializer object.
     * The value is an array of arrays.  The outer arrays represent invocations
     * of the method.  The inner array are the parameters for each invocation.
     * As an optimization, the inner value may not be an array but instead
     * a single value.
     */
    init : function(spec)
    {
        $H(spec).each(function(pair)
        {
            var functionName = pair.key;

            var initf = Tapestry.Initializer[functionName];

            if (initf == undefined)
            {
                Tapestry.error(Tapestry.Messages.missingInitializer, {
                    name:functionName
                });
                return;
            }

            pair.value.each(function(parameterList)
            {
                if (! Object.isArray(parameterList))
                {
                    parameterList = [
                        parameterList
                    ];
                }

                initf.apply(this, parameterList);
            });
        });
    },

    /** Formats and displays an error message on the console. */
    error : function (message, substitutions)
    {
        Tapestry.invokeLogger(message, substitutions, Tapestry.Logging.error);
    },

    /** Formats and displays a warning on the console. */
    warn : function (message, substitutions)
    {
        Tapestry.invokeLogger(message, substitutions, Tapestry.Logging.warn);
    },

    /** Formats and displays a debug message on the console. */
    debug : function (message, substitutions)
    {
        Tapestry.invokeLogger(message, substitutions, Tapestry.Logging.debug);
    },

    invokeLogger : function(message, substitutions, loggingFunction)
    {
        if (substitutions != undefined)
            message = message.interpolate(substitutions);

        loggingFunction.call(this, message);
    },

    /**
     * Passed the JSON content of a Tapestry partial markup response, extracts
     * the script and stylesheet information.  JavaScript libraries and stylesheets are loaded,
     * then the callback is invoked.  All three keys are optional:
     * <dl>
     * <dt>redirectURL</dt> <dd>URL to redirect to (in which case, the callback is not invoked)</dd>
     * <dt>scripts</dt><dd>Array of strings (URIs of scripts)</dd>
     * <dt>stylesheets</dt><dd>Array of hashes, each hash has key href and optional key media</dd>
     *
     * @param reply JSON response object from the server
     * @param callback function invoked after the scripts have all loaded (presumably, to update the DOM)
     */
    loadScriptsInReply : function(reply, callback)
    {
        var redirectURL = reply.redirectURL;

        if (redirectURL)
        {
            // Check for complete URL.
            if (/^https?:/.test(redirectURL))
            {
                window.location = redirectURL;
                return;
            }

            window.location.pathname = redirectURL;

            // Don't bother loading scripts or invoking the callback.

            return;
        }

        Tapestry.ScriptManager.addStylesheets(reply.stylesheets);

        Tapestry.ScriptManager.addScripts(reply.scripts,
                function()
                {
                    callback.call(this);

                    // After the callback updates the DOM
                    // (presumably), continue on with
                    // evaluating the reply.script
                    // and other final steps.

                    if (reply.script) eval(reply.script);

                    Tapestry.onDomLoadedCallback();

                });
    },

    /**
     * Default function for handling Ajax-related failures.
     */
    ajaxFailureHandler : function(response)
    {
        var message = response.getHeader("X-Tapestry-ErrorMessage");

        Tapestry.error(Tapestry.Messages.communicationFailed + message);

        Tapestry.debug(Tapestry.Messages.ajaxFailure + message, response);
    },

    /**
     * Processes a typical Ajax request for a URL invoking the provided handler on success.
     * On failure, error() is invoked to inform the user.
     *
     * @param url of Ajax request
     * @param successHandler to invoke on success
     * @return the Ajax.Request object
     */
    ajaxRequest : function(url, successHandler)
    {
        return new Ajax.Request(url, {
            onSuccess: function(response, jsonResponse)
            {
                // When the page is unloaded, pending Ajax requests appear to terminate
                // as succesful (but with no reply value). Since we're trying to navigate
                // to a new page anyway, we just ignore those false success callbacks.
                // We have a listener for the window's "beforeunload" event that sets
                // this flag.

                if (Tapestry.windowUnloaded)
                    return;

                if (! response.request.success())
                {
                    Tapestry.error(Tapestry.Messages.ajaxRequestUnsuccessful);
                    return;
                }

                try
                {
                    // Re-invoke the success handler, capturing any exceptions.
                    successHandler.call(this, response, jsonResponse);
                }
                catch (e)
                {
                    Tapestry.error(Tapestry.Messages.clientException + e);
                }
            },
            onException: Tapestry.ajaxFailureHandler,
            onFailure: Tapestry.ajaxFailureHandler
        });
    },

    /** Obtains the Tapestry.ZoneManager object associated with a triggering element
     * (an <a> or <form>) configured to update a zone. Writes errors to the AjaxConsole
     * if the zone and ZoneManager can not be resolved.
     *
     * @param element   triggering element (id or instance)
     * @return Tapestry.ZoneManager instance for updated zone, or null if not found.
     */
    findZoneManager : function(element)
    {
        var zoneId = $T(element).zoneId;

        return Tapestry.findZoneManagerForZone(zoneId);
    },

    /**
     * Obtains the Tapestry.ZoneManager object associated with a zone element (usually
     * a <div>). Writes errors to the Ajax console if the element or manager
     * can not be resolved.
     * @param zoneElement  zone element (id or instance)
     * @return Tapestry.ZoneManager instance for zone, or null if not found
     */
    findZoneManagerForZone : function(zoneElement)
    {
        var element = $(zoneElement);

        if (!zoneElement)
        {
            Tapestry.error(Tapestry.Messages.missingZone, {
                id:zoneElement
            });
            return null;
        }

        var manager = $T(zoneElement).zoneManager;

        if (!manager)
        {
            Tapestry.error(Tapestry.Messages.noZoneManager, element);
            return null;
        }

        return manager;
    },

    /**
     * Used to reconstruct a complete URL from a path that is (or may be) relative to window.location.
     * This is used when determining if a JavaScript library or CSS stylesheet has already been loaded.
     * Recognizes complete URLs (which are returned unchanged), otherwise the URLs are expected to be
     * absolute paths.
     *
     * @param path
     * @return complete URL as string
     */
    rebuildURL : function(path)
    {
        if (path.match(/^https?:/))
        {
            return path;
        }

        if (! path.startsWith("/"))
        {
            Tapestry.error(Tapestry.Messages.pathDoesNotStartWithSlash, {
                path: path
            });

            return path;
        }

        var l = window.location;
        return l.protocol + "//" + l.host + path;
    },

    stripToLastSlash : function(URL)
    {
        var slashx = URL.lastIndexOf("/");

        return URL.substring(0, slashx + 1);
    },

    /**
     * Convert a user-provided localized number to an ordinary number (not a string).
     * Removes seperators and leading/trailing whitespace. Disallows the decimal point if isInteger is true.
     * @param number string provided by user
     * @param isInteger if true, disallow decimal point
     */
    formatLocalizedNumber : function(number, isInteger)
    {
        // We convert from localized string to a canonical string,
        // stripping out  group seperators (normally commas). If isInteger is
        // true, we don't allow a decimal point.

        var minus = Tapestry.decimalFormatSymbols.minusSign;
        var grouping = Tapestry.decimalFormatSymbols.groupingSeparator;
        var decimal = Tapestry.decimalFormatSymbols.decimalSeparator;

        var canonical = "";

        number.strip().toArray().each(function(ch)
        {
            if (ch == minus)
            {
                canonical += "-";
                return;
            }

            if (ch == grouping)
            {
                return;
            }

            if (ch == decimal)
            {
                if (isInteger) throw Tapestry.Messages.notAnInteger;

                ch = ".";
            }
            else if (ch < "0" || ch > "9") throw Tapestry.Messages.invalidCharacter;

            canonical += ch;
        });

        return Number(canonical);
    },

    /**
     * Marks a number of script libraries as loaded; this is used with virtual scripts (which combine multiple
     * actual scripts). This is necessary so that subsequent Ajax requests do not load scripts that have
     * already been loaded
     * @param scripts     array of script paths
     */
    markScriptLibrariesLoaded : function(scripts)
    {
        $(scripts).each(function (script)
        {
            var complete = Tapestry.rebuildURL(script);
            Tapestry.ScriptManager.virtualScripts.push(complete);
        });
    }
};


Element.addMethods(
{

    /**
     * Works upward from the element, checking to see if the element is visible. Returns false
     * if it finds an invisible container. Returns true if it makes it as far as a (visible) FORM element.
     *
     * Note that this only applies to the CSS definition of visible; it doesn't check that the element
     * is scolled into view.
     *
     * @param element to search up from
     * @return true if visible (and containers visible), false if it or container are not visible
     */
    isDeepVisible : function(element)
    {
        var current = $(element);

        while (true)
        {
            if (! current.visible()) return false;

            if (current.tagName == "FORM") break;

            current = $(current.parentNode)
        }

        return true;
    }
});

Element.addMethods('FORM',
{
    /**
     * Gets or creates the Tapestry.FormEventManager for the form.
     *
     * @param form form element
     */
    getFormEventManager : function(form)
    {
        form = $(form);
        var t = $T(form);

        var manager = t.formEventManager;

        if (manager == undefined)
        {
            manager = new Tapestry.FormEventManager(form);
            t.formEventManager = manager;
        }

        return manager;
    },

    /**
     * Sends an Ajax request to the Form's action. This encapsulates
     * a few things, such as a default onFailure handler, and working
     * around bugs/features in Prototype concerning how
     * submit buttons are processed.
     *
     * @param form used to define the data to be sent in the request
     * @param options      standard Prototype Ajax Options
     * @return Ajax.Request the Ajax.Request created for the request
     */
    sendAjaxRequest : function (form, url, options)
    {
        form = $(form);

        // Generally, options should not be null or missing,
        // because otherwise there's no way to provide any callbacks!

        options = Object.clone(options || { });

        // Set a default failure handler if none is provided.

        options.onFailure |= Tapestry.ajaxFailureHandler;

        // Find the elements, skipping over any submit buttons.
        // This works around bugs in Prototype 1.6.0.2.

        var elements = form.getElements().reject(function(e)
        {
            return e.tagName == "INPUT" && e.type == "submit";
        });

        var hash = Form.serializeElements(elements, true);

        var lastSubmit = $T(form).lastSubmit;

        // Put the last submit clicked into the hash, emulating
        // what a normal form submit would do.

        if (lastSubmit && lastSubmit.name)
        {
            hash[lastSubmit.name] = $F(lastSubmit);
        }


        // Copy the parameters in, overwriting field values,
        // because Prototype 1.6.0.2 does not.

        Object.extend(hash, options.parameters);

        options.parameters = hash;

        // Ajax.Request will convert the hash into a query string and post it.

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

Element.addMethods([
    'INPUT',
    'SELECT',
    'TEXTAREA'
],
{
    /**
     * Invoked on a form element (INPUT, SELECT, etc.), gets or creates the
     * Tapestry.FieldEventManager for that field.
     *
     * @param field field element
     */
    getFieldEventManager : function(field)
    {
        field = $(field);
        var t = $T(field);

        var manager = t.fieldEventManager;

        if (manager == undefined)
        {
            manager = new Tapestry.FieldEventManager(field);
            t.fieldEventManager = manager;
        }

        return manager;
    },

    /**
     * Obtains the Tapestry.FieldEventManager and asks it to show
     * the validation message.   Sets the  validationError property of the elements tapestry object to true.
     * @param element
     * @param message to display
     */
    showValidationMessage : function(element, message)
    {
        element = $(element);

        element.getFieldEventManager().showValidationMessage(message);

        return element;
    },

    /**
     * Removes any validation decorations on the field, and
     * hides the error popup (if any) for the field.
     */
    removeDecorations : function(element)
    {
        $(element).getFieldEventManager().removeDecorations();

        return element;
    },


    /**
     * Adds a standard validator for the element, an observer of
     * Tapestry.FIELD_VALIDATE_EVENT. The validator function will be
     * passed the current field value and should throw an error message if
     * the field's value is not valid.
     * @param element field element to validate
     * @param validator function to be passed the field value
     */
    addValidator : function(element, validator)
    {
        element.observe(Tapestry.FIELD_VALIDATE_EVENT, function(event)
        {
            try
            {
                validator.call(this, event.memo.translated);
            }
            catch (message)
            {
                element.showValidationMessage(message);
            }
        });

        return element;
    }
});

/** Container of functions that may be invoked by the Tapestry.init() function. */
Tapestry.Initializer = {

    ajaxFormLoop : function(spec)
    {
        var rowInjector = $(spec.rowInjector);

        $(spec.addRowTriggers).each(function(triggerId)
        {
            $(triggerId).observe("click", function(event)
            {
                $(rowInjector).trigger();

                Event.stop(event);
            })
        });
    },

    formLoopRemoveLink : function(spec)
    {
        var link = $(spec.link);
        var fragmentId = spec.fragment;

        link.observe("click", function(event)
        {
            Event.stop(event);

            var successHandler = function(transport)
            {
                var container = $(fragmentId);
                var fragment = $T(container).formFragment;

                if (fragment != undefined)
                {
                    fragment.hideAndRemove();
                }
                else
                {
                    var effect = Tapestry.ElementEffect.fade(container);

                    effect.options.afterFinish = function()
                    {
                        container.remove();
                    };
                }
            }

            Tapestry.ajaxRequest(spec.url, successHandler);
        });
    },


    /**
     * Convert a form or link into a trigger of an Ajax update that
     * updates the indicated Zone.
     * @param element id or instance of <form> or <a> element
     * @param zoneId id of the element to update when link clicked or form submitted
     * @param url absolute component event request URL
     */
    linkZone : function(element, zoneId, url)
    {
    	Tapestry.Initializer.updateZoneOnEvent("click", element, zoneId, url);
    },
    
    updateZoneOnEvent : function(eventName, element, zoneId, url)
    {
        element = $(element);

        // Update the element with the id of zone div. This may be changed dynamically on the client
        // side.

        $T(element).zoneId = zoneId;

        if (element.tagName == "FORM")
        {
            // Turn normal form submission off.

            element.getFormEventManager().preventSubmission = true;

            // After the form is validated and prepared, this code will
            // process the form submission via an Ajax call.  The original submit event
            // will have been cancelled.

            element.observe(Tapestry.FORM_PROCESS_SUBMIT_EVENT, function()
            {
                var zoneManager = Tapestry.findZoneManager(element);

                if (!zoneManager) return;

                var successHandler = function(transport)
                {
                    zoneManager.processReply(transport.responseJSON);
                };

                element.sendAjaxRequest(url, {
                    onSuccess : successHandler
                });
            });

            return;
        }

        // Otherwise, assume it's just an ordinary link or input field.

        element.observe(eventName, function(event)
        {
            Event.stop(event);

            var zoneObject = Tapestry.findZoneManager(element);

            if (!zoneObject) return;
            
            var newUrl = url;

            if(element.tagName == "SELECT" && element.value)
            {
            	newUrl+='&t:selectvalue='+element.value;
            }

            zoneObject.updateFromURL(newUrl);
        });
    },

    validate : function (field, specs)
    {
        field = $(field);

        // Force the creation of the form and field event managers.

        $(field.form).getFormEventManager();
        $(field).getFieldEventManager();

        specs.each(function(spec)
        {
            // spec is a 2 or 3 element array.
            // validator function name, message, optional constraint

            var name = spec[0];
            var message = spec[1];
            var constraint = spec[2];

            var vfunc = Tapestry.Validator[name];

            if (vfunc == undefined)
            {
                Tapestry.error(Tapestry.Messages.missingValidator, {
                    name:name,
                    fieldName:field.id
                });
                return;
            }

            // Pass the extend field, the provided message, and the constraint object
            // to the Tapestry.Validator function, so that it can, typically, invoke
            // field.addValidator().

            vfunc.call(this, field, message, constraint);
        });
    },

    zone : function(spec)
    {
        new Tapestry.ZoneManager(spec);
    },

    formFragment : function(spec)
    {
        new Tapestry.FormFragment(spec)
    },

    formInjector : function(spec)
    {
        new Tapestry.FormInjector(spec);
    },

    // Links a FormFragment to a trigger (a radio or a checkbox), such that changing the trigger will hide
    // or show the FormFragment. Care should be taken to render the page with the
    // checkbox and the FormFragment('s visibility) in agreement.

    linkTriggerToFormFragment : function(trigger, element)
    {
        trigger = $(trigger);

        if (trigger.type == "radio")
        {
            $(trigger.form).observe("click", function()
            {
                $T(element).formFragment.setVisible(trigger.checked);
            });

            return;
        }

        // Otherwise, we assume it is a checkbox.  The difference is
        // that we can observe just the single checkbox element,
        // rather than handling clicks anywhere in the form (as with
        // the radio).

        trigger.observe("click", function()
        {
            $T(element).formFragment.setVisible(trigger.checked);
        });

    }
};

// Collection of field based functions related to validation. Each
// function takes a field, a message and an optional constraint value.
// Some functions are related to Translators and work on the format event,
// other's are from Validators and work on the validate event.

Tapestry.Validator = {

    required : function(field, message)
    {
        $(field).getFieldEventManager().requiredCheck = function(value)
        {
        	if ((Object.isString(value) && value.strip() == '') || value == null)
                $(field).showValidationMessage(message);
        };
    },

    /** Supplies a client-side numeric translator for the field. */
    numericformat : function (field, message, isInteger)
    {
        $(field).getFieldEventManager().translator = function(input)
        {
            try
            {
                return Tapestry.formatLocalizedNumber(input, isInteger);
            }
            catch (e)
            {
                $(field).showValidationMessage(message);
            }
        };
    },

    minlength : function(field, message, length)
    {
        field.addValidator(function(value)
        {
            if (value.length < length) throw message;
        });
    },

    maxlength : function(field, message, maxlength)
    {
        field.addValidator(function(value)
        {
            if (value.length > maxlength) throw message;
        });
    },

    min : function(field, message, minValue)
    {
        field.addValidator(function(value)
        {
            if (value < minValue) throw message;
        });
    },

    max : function(field, message, maxValue)
    {
        field.addValidator(function(value)
        {
            if (value > maxValue) throw message;
        });
    },

    regexp : function(field, message, pattern)
    {
        var regexp = new RegExp(pattern);

        field.addValidator(function(value)
        {
            if (! regexp.test(value)) throw message;
        });
    }
};

Tapestry.ErrorPopup = Class.create({

    // If the images associated with the error popup are overridden (by overriding Tapestry's default.css stylesheet),
    // then some of these values may also need to be adjusted.

    BUBBLE_VERT_OFFSET : -34,

    BUBBLE_HORIZONTAL_OFFSET : -20,

    BUBBLE_WIDTH: "auto",

    BUBBLE_HEIGHT: "39px",

    initialize : function(field)
    {
        this.field = $(field);

        this.innerSpan = new Element("span");
        this.outerDiv = $(new Element("div", {
            'id' : this.field.id + ":errorpopup",
            'class' : 't-error-popup'
        })).update(this.innerSpan).hide();

        var body = $(document.body);

        body.insert({
            bottom: this.outerDiv
        });

        this.outerDiv.absolutize();

        this.outerDiv.observe("click", function(event)
        {
            this.ignoreNextFocus = true;

            this.stopAnimation();

            this.outerDiv.hide();

            this.field.activate();

            Event.stop(event);  // Should be domevent.stop(), but that fails under IE
        }.bindAsEventListener(this));

        this.queue = {
            position: 'end',
            scope: this.field.id
        };

        Event.observe(window, "resize", this.repositionBubble.bind(this));

        document.observe(Tapestry.FOCUS_CHANGE_EVENT, function(event)
        {
            if (this.ignoreNextFocus)
            {
                this.ignoreNextFocus = false;
                return;
            }

            if (event.memo == this.field)
            {
                this.fadeIn();
                return;
            }

            // If this field is not the focus field after a focus change, then it's bubble,
            // if visible, should fade out. This covers tabbing from one form to another. 
            this.fadeOut();

        }.bind(this));
    },

    showMessage : function(message)
    {
        this.stopAnimation();

        this.innerSpan.update(message);

        this.hasMessage = true;

        this.fadeIn();
    },

    repositionBubble : function()
    {
        var fieldPos = this.field.cumulativeOffset();

        this.outerDiv.setStyle({
            top: (fieldPos[1] + this.BUBBLE_VERT_OFFSET) + "px",
            left: (fieldPos[0] + this.BUBBLE_HORIZONTAL_OFFSET) + "px",
            width: this.BUBBLE_WIDTH,
            height: this.BUBBLE_HEIGHT
        });
    },

    fadeIn : function()
    {
        if (! this.hasMessage) return;

        this.repositionBubble();

        if (this.animation) return;

        this.animation = new Effect.Appear(this.outerDiv, {
            queue: this.queue,
            afterFinish: function()
            {
                this.animation = null;

                if (this.field != Tapestry.currentFocusField)
                    this.fadeOut();
            }.bind(this)
        });
    },

    stopAnimation : function()
    {
        if (this.animation) this.animation.cancel();

        this.animation = null;
    },

    fadeOut : function ()
    {
        if (this.animation) return;

        this.animation = new Effect.Fade(this.outerDiv, {
            queue : this.queue,
            afterFinish: function()
            {
                this.animation = null;
            }.bind(this)
        });
    },

    hide : function()
    {
        this.hasMessage = false;

        this.stopAnimation();

        this.outerDiv.hide();
    }
});

Tapestry.FormEventManager = Class.create({

    initialize : function(form)
    {
        this.form = $(form);

        this.form.onsubmit = this.handleSubmit.bindAsEventListener(this);
    },

    handleSubmit : function(domevent)
    {
        var t = $T(this.form);

        t.validationError = false;

        var firstErrorField = null;

        // Locate elements that have an event manager (and therefore, validations)
        // and let those validations execute, which may result in calls to recordError().


        this.form.getElements().each(function(element)
        {
            var fem = $T(element).fieldEventManager;

            if (fem != undefined)
            {
                // Ask the FEM to validate input for the field, which fires
                // a number of events.
                var error = fem.validateInput();

                if (error && ! firstErrorField)
                {
                    firstErrorField = element;
                }
            }
        });

        // Allow observers to validate the form as a whole.  The FormEvent will be visible
        // as event.memo.  The Form will not be submitted if event.result is set to false (it defaults
        // to true).  Still trying to figure out what should get focus from this
        // kind of event.

        this.form.fire(Tapestry.FORM_VALIDATE_EVENT, this.form);

        if (t.validationError)
        {
            Event.stop(domevent); // Should be domevent.stop(), but that fails under IE

            if (firstErrorField) firstErrorField.activate();

            // Because the submission failed, the last submit property is cleared,
            // since the form may be submitted for some other reason later.

            t.lastSubmit = null;

            return false;
        }

        this.form.fire(Tapestry.FORM_PREPARE_FOR_SUBMIT_EVENT, this.form);

        // This flag can be set to prevent the form from submitting normally.
        // This is used for some Ajax cases where the form submission must
        // run via Ajax.Request.

        if (this.preventSubmission)
        {
            // Prevent the normal submission.

            Event.stop(domevent);

            // Instead ...

            this.form.fire(Tapestry.FORM_PROCESS_SUBMIT_EVENT);

            return false;
        }

        // Validation is OK, not doing Ajax, continue as planned.

        return true;
    }
});

Tapestry.FieldEventManager = Class.create({

    initialize : function(field)
    {
        this.field = $(field);

        var id = this.field.id;
        this.label = $(id + '-label');
        this.icon = $(id + '-icon');

        this.translator = Prototype.K;

        document.observe(Tapestry.FOCUS_CHANGE_EVENT, function(event)
        {
            // If changing focus *within the same form* then
            // perform validation.  Note that Tapestry.currentFocusField does not change
            // until after the FOCUS_CHANGE_EVENT notification.

            if (Tapestry.currentFocusField == this.field &&
                this.field.form == event.memo.form)
                this.validateInput();

        }.bindAsEventListener(this));
    },


    /** Removes validation decorations if present. Hides the ErrorPopup,
     *  if it exists.
     */
    removeDecorations : function()
    {
        this.field.removeClassName("t-error");

        if (this.label)
            this.label.removeClassName("t-error");

        if (this.icon)
            this.icon.hide();

        if (this.errorPopup)
            this.errorPopup.hide();
    },


    /**
     * Show a validation error message, which will add decorations to the
     * field and it label, make the icon visible, and raise the
     * field's Tapestry.ErrorPopup to show the message.
     * @param message validation message to display
     */
    showValidationMessage : function(message)
    {
        $T(this.field).validationError = true;
        $T(this.field.form).validationError = true;

        this.field.addClassName("t-error");

        if (this.label)
            this.label.addClassName("t-error");

        if (this.icon)
        {
            if (! this.icon.visible())
                new Effect.Appear(this.icon);
        }

        if (this.errorPopup == undefined)
            this.errorPopup = new Tapestry.ErrorPopup(this.field);

        this.errorPopup.showMessage(message);
    },

    /**
     * Invoked when a form is submitted, or when leaving a field, to perform
     * field validations. Field validations are skipped for disabled fields.
     * If all validations are succesful, any decorations are removed. If any validation
     * fails, an error popup is raised for the field, to display the validation
     * error message.
     *
     * @return true if the field has a validation error
     */
    validateInput : function()
    {
        if (this.field.disabled) return false;

        if (! this.field.isDeepVisible()) return false;

        var t = $T(this.field);

        var value = $F(this.field);

        t.validationError = false;

        if (this.requiredCheck)
            this.requiredCheck.call(this, value);

        // Don't try to validate blank values; if the field is required, that error is already
        // noted and presented to the user.

        if (!t.validationError && ! (Object.isString(value) && value.blank()))
        {
            var translated = this.translator(value);

            // If Format went ok, perhaps do the other validations.

            if (! t.validationError)
            {
                this.field.fire(Tapestry.FIELD_VALIDATE_EVENT, {
                    value: value,
                    translated: translated
                });
            }
        }

        // Lastly, if no validation errors were found, remove the decorations.

        if (! t.validationError)
            this.field.removeDecorations();

        return t.validationError;
    }
});

// Wrappers around Prototype and Scriptaculous effects.
// All the functions of this object should have all-lowercase names.
// The methods all return the Effect object they create.

Tapestry.ElementEffect = {

    /** Fades in the element. */
    show : function(element)
    {
        return new Effect.Appear(element);
    },

    /** The classic yellow background fade. */
    highlight : function(element, color)
    {
        if(color)
            return new Effect.Highlight(element, { endcolor: color, restorecolor: color});
        
        return new Effect.Highlight(element);
    },

    /** Scrolls the content down. */
    slidedown : function (element)
    {
        return new Effect.SlideDown(element);
    },

    /** Slids the content back up (opposite of slidedown). */
    slideup : function(element)
    {
        return new Effect.SlideUp(element);
    },

    /** Fades the content out (opposite of show). */
    fade : function(element)
    {
        return new Effect.Fade(element);
    }
};


/**
 * Manages a &lt;div&lt; (or other element) for dynamic updates.
 *
 */
Tapestry.ZoneManager = Class.create({
    // spec are the parameters for the Zone:
    // trigger: required -- name or instance of link.
    // element: required -- name or instance of div element to be shown, hidden and updated
    // show: name of Tapestry.ElementEffect function used to reveal the zone if hidden
    // update: name of Tapestry.ElementEffect function used to highlight the zone after it is updated
    initialize: function(spec)
    {
        if (Object.isString(spec))
            spec = {
                element: spec
            }

        this.element = $(spec.element);
        this.showFunc = Tapestry.ElementEffect[spec.show] || Tapestry.ElementEffect.show;
        this.updateFunc = Tapestry.ElementEffect[spec.update] || Tapestry.ElementEffect.highlight;

        // TAP5-707: store the old background color of the element or take white as a default
        this.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
        
        // Link the div back to this zone.

        $T(this.element).zoneManager = this;

        // Look inside the managed element for another element with the CSS class "t-zone-update".
        // If present, then this is the element whose content will be changed, rather
        // then the entire zone's element.  This allows a Zone element to contain "wrapper" markup
        // (borders and such).  Typically, such a Zone element will initially be invisible.
        // The show and update functions apply to the Zone element, not the update element.

        var updates = this.element.select(".t-zone-update");

        this.updateElement = updates.first() || this.element;
    },

    // Updates the content of the div controlled by this Zone, then
    // invokes the show function (if not visible) or the update function (if visible),

    /**
     * Updates the zone's content, and invokes either the update function (to highlight the change)
     * or the show function (to reveal a hidden element). Lastly, fires the Tapestry.ZONE_UPDATED_EVENT
     * to let listeners know that the zone was updated.
     * @param content
     */
    show: function(content)
    {
        this.updateElement.update(content);

        var func = this.element.visible() ? this.updateFunc : this.showFunc;

        func.call(this, this.element, this.endcolor);

        this.element.fire(Tapestry.ZONE_UPDATED_EVENT);
    },

    /**
     * Invoked with a reply (i.e., transport.responseJSON), this updates the managed element
     * and processes any JavaScript in the reply.  The response should have a
     * content key, and may have  script, scripts and stylesheets keys.
     * @param reply response in JSON format appropriate to a Tapestry.Zone
     */
    processReply : function(reply)
    {
        Tapestry.loadScriptsInReply(reply, function()
        {
            // In a multi-zone update, the reply.content may be blank or missing.

            reply.content && this.show(reply.content);

            // zones is an object of zone ids and zone content that will be present
            // in a multi-zone update response.

            Object.keys(reply.zones).each(function (zoneId)
            {
                var manager = Tapestry.findZoneManagerForZone(zoneId);

                if (manager)
                {
                    var zoneContent = reply.zones[zoneId];
                    manager.show(zoneContent);
                }
            });
        }.bind(this));
    },

    /** Initiates an Ajax request to update this zone by sending a request
     *  to the URL. Expects the correct JSON reply (wth keys content, etc.).
     * @param URL component event request URL
     */
    updateFromURL : function (URL)
    {
        var successHandler = function(transport)
        {
            this.processReply(transport.responseJSON);
        }.bind(this);

        Tapestry.ajaxRequest(URL, successHandler);
    }
});

// A class that managed an element (usually a <div>) that is conditionally visible and
// part of the form when visible.

Tapestry.FormFragment = Class.create({

    initialize: function(spec)
    {
        if (Object.isString(spec))
            spec = {
                element: spec
            };

        this.element = $(spec.element);

        $T(this.element).formFragment = this;

        this.hidden = $(spec.element + "-hidden");

        this.showFunc = Tapestry.ElementEffect[spec.show] || Tapestry.ElementEffect.slidedown;
        this.hideFunc = Tapestry.ElementEffect[spec.hide] || Tapestry.ElementEffect.slideup;

        var form = $(this.hidden.form);

        // TAP5-283: Force creation of the FormEventManager if it does not already exist.

        form.getFormEventManager();

        $(form).observe(Tapestry.FORM_PREPARE_FOR_SUBMIT_EVENT, function()
        {
            // On a submission, if the fragment is not visible, then wipe out its
            // form submission data, so that no processing or validation occurs on the server.

            if (! this.element.isDeepVisible())
                this.hidden.value = "";
        }.bind(this));
    },

    hide : function()
    {
        if (this.element.visible())
            this.hideFunc(this.element);
    },

    hideAndRemove : function()
    {
        var effect = this.hideFunc(this.element);

        effect.options.afterFinish = function()
        {
            this.element.remove();
        }.bind(this);
    },

    show : function()
    {
        if (! this.element.visible())
            this.showFunc(this.element);
    },

    toggle : function()
    {
        this.setVisible(! this.element.visible());
    },

    setVisible : function(visible)
    {
        if (visible)
        {
            this.show();
            return;
        }

        this.hide();
    }
});

Tapestry.FormInjector = Class.create({

    initialize: function(spec)
    {
        this.element = $(spec.element);
        this.url = spec.url;
        this.below = spec.below;

        this.showFunc = Tapestry.ElementEffect[spec.show] || Tapestry.ElementEffect.highlight;

        this.element.trigger = function()
        {
            var successHandler = function(transport)
            {
                var reply = transport.responseJSON;

                // Clone the FormInjector element (usually a div)
                // to create the new element, that gets inserted
                // before or after the FormInjector's element.

                var newElement = new Element(this.element.tagName, {
                    'class' : this.element.className
                });

                // Insert the new element before or after the existing element.

                var param = { };
                param[this.below ? "after" : "before"] = newElement;

                Tapestry.loadScriptsInReply(reply, function()
                {
                    // Add the new element with the downloaded content.

                    this.element.insert(param);

                    // Update the empty element with the content from the server

                    newElement.update(reply.content);

                    newElement.id = reply.elementId;

                    // Add some animation to reveal it all.

                    this.showFunc(newElement);
                }.bind(this));
            }.bind(this);

            Tapestry.ajaxRequest(this.url, successHandler);

            return false;
        }.bind(this);
    }
});

/**
 * Wait for a set of JavaScript libraries to load (in terms of DOM script elements), then invokes a callback function.
 */
Tapestry.ScriptLoadMonitor = Class.create({

    initialize : function(scriptElements, callback)
    {
        this.callback = callback;
        this.loaded = 0;
        this.toload = scriptElements.length;

        var executor = this;

        scriptElements.each(function (scriptElement)
        {
            if (Prototype.Browser.IE)
            {
                var loaded = false;

                scriptElement.onreadystatechange = function ()
                {
                    // IE may fire either loaded or complete, or perhaps even both.
                    if (! loaded && (this.readyState == 'loaded' || this.readyState == 'complete'))
                    {
                        loaded = true;
                        executor.loadComplete(scriptElement);
                    }
                };
            }
            else
            {
                // Much simpler in FF, Safari, etc.
                scriptElement.onload = executor.loadComplete.bindAsEventListener(executor, scriptElement);
            }
        });

        // If no scripts to actually load, call the callback immediately.

        if (this.toload == 0) this.callback.call(this);
    },

    loadComplete : function()
    {
        this.loaded++;

        // Evaluated the dependent script only once all the elements have loaded.

        if (this.loaded == this.toload)
            this.callback.call(this);
    }
});

Tapestry.ScriptManager = {

    /** Complete URLs of virtually loaded scripts (combined scripts loaded as a single virtual asset). */
    virtualScripts : $A([]),

    initialize : function()
    {

        // Check to see if document.scripts is supported; if not (for example, FireFox),
        // we can fake it.

        this.emulated = false;

        if (! document.scripts)
        {
            this.emulated = true;

            document.scripts = new Array();

            $$('script').each(function (s)
            {
                document.scripts.push(s);
            });
        }
    },

    /**
     * Checks to see if the given collection (of <script> or <style> elements) contains the given asset URL.
     * @param collection
     * @param prop      property to check ('src' for script, 'href' to style).
     * @param assetURL        complete URL (i.e., with protocol, host and port) to the asset
     */
    contains : function (collection, prop, assetURL)
    {
        return $A(collection).any(function (element)
        {
            var existing = element[prop];

            if (! existing || existing.blank()) return false;

            var complete =
                    Prototype.Browser.IE ? Tapestry.rebuildURL(existing) : existing;

            return complete == assetURL;
        });

        return false;
    },

    /**
     * Add scripts, as needed, to the document, then waits for them all to load, and finally, calls
     * the callback function.
     * @param scripts        Array of scripts to load
     * @param callback invoked after scripts are loaded
     */
    addScripts: function(scripts, callback)
    {
        var added = new Array();

        if (scripts)
        {
            var emulated = this.emulated;
            // Looks like IE really needs the new <script> tag to be
            // in the <head>. FF doesn't seem to care.
            // See http://unixpapa.com/js/dyna.html
            var head = $$("head").first();

            scripts.each(function(s)
            {
                var assetURL = Tapestry.rebuildURL(s);

                // Check to see if the script is already loaded, either as a virtual script, or as
                // an individual <script src=""> element.

                if (Tapestry.ScriptManager.virtualScripts.member(assetURL)) return;
                if (Tapestry.ScriptManager.contains(document.scripts, "src", assetURL)) return;

                // IE needs the type="text/javascript" as well.

                var element = new Element('script', {
                    src: assetURL,
                    type: 'text/javascript'
                });

                head.insert({
                    bottom:element
                });

                added.push(element);

                if (emulated) document.scripts.push(element);
            });

        }

        new Tapestry.ScriptLoadMonitor(added, callback);
    },

    addStylesheets : function(stylesheets)
    {
        if (!stylesheets) return;

        var head = $$('head').first();

        $(stylesheets).each(function(s)
        {
            var assetURL = Tapestry.rebuildURL(s.href);

            if (Tapestry.ScriptManager.contains(document.styleSheets, 'href', assetURL)) return; // continue

            var element = new Element('link', {
                type: 'text/css',
                rel: 'stylesheet',
                href: assetURL
            });

            // Careful about media types, some browser will break if it ends up as 'null'.

            if (s.media != undefined)
                element.writeAttribute('media', s.media);

            head.insert({
                bottom: element
            });

        });
    }
};

/**
 * In the spirit of $(), $T() exists to access the <em>Tapestry object</em> for the element. The Tapestry object
 * is used to store additional values related to the element; it is simply an annoymous object stored as property
 * <code>_tapestry</code> of the element, created the first time it is accessed.
 * <p>This mechanism acts as a namespace, and so helps prevent name
 * conflicts that would occur if properties were stored directly on DOM elements, and makes debugging a bit easier
 * (the Tapestry-specific properties are all in one place!).
 * For the moment, added methods are stored directly on the object, and are not prefixed in any way, valueing
 * readability over preventing naming conflicts.
 *
 * @param element an element instance or element id
 * @return object Tapestry object for the element
 */
function $T(element)
{
    var e = $(element);
    var t = e._tapestry;

    if (!t)
    {
        t = { };
        e._tapestry = t;
    }

    return t;
}

Tapestry.onDOMLoaded(Tapestry.onDomLoadedCallback);

// Ajax code needs to know to do nothing after the window is unloaded.
Event.observe(window, "beforeunload", function()
{
    Tapestry.windowUnloaded = true;
});


/* /assets/blackbird/5.1.0.8-SNAPSHOT/blackbird.js */;
/*
 Blackbird - Open Source JavaScript Logging Utility
 Author: G Scott Olson
 Web: http://blackbirdjs.googlecode.com/
 http://www.gscottolson.com/blackbirdjs/
 Version: 1.0

 The MIT License - Copyright (c) 2008 Blackbird Project

 Heavily modified for Tapestry to rename namespace, make use of Prototype: March 2009
 */
( function()
{
    var IE6_POSITION_FIXED = true; // enable IE6 {position:fixed}

    var bbird, checkbox, filters, controls, size;
    var outputList;
    var cache = [];

    var state = getState();
    var classes = {};
    var profiler = {};

    var messageTypes = { //order of these properties imply render order of filter controls
        debug: true,  // May be set to false based on Tapestry.DEBUG_ENABLED
        info: true,
        warn: true,
        error: true,
        profile: true
    };

    function constructUI()
    {
        bbird = new Element("div", { 'id': 't-console', 'title': 'F2 toggles / Shift-F2 moves' }).hide();
        var header = new Element("div", { 'class': 't-header' });
        var left = new Element("div", { 'class' : 't-left' });

        left.insert(filters = new Element("div", { 'class': 't-filters' }));

        for (var type in messageTypes)
        {
            var className = messageTypes[type] ? type : type + "Disabled";

            filters.insert(new Element("span", {'class': className, 'title': "filter by " + type, 'type': type }));
        }

        var right = new Element("div", { 'class': 't-right'});

        right.insert(controls = new Element("div", { 'class': 't-controls'}));

        controls.insert(size = new Element("span", { 'title': 'contract', 'op': 'resize' }));

        controls.insert(new Element("span", { 'class': 't-clear', 'title': 'clear', 'op': 'clear' }));
        controls.insert(new Element("span", { 'class': 't-close', 'title': 'close', 'op': 'close' }));

        header.insert(left);
        header.insert(right);

        bbird.insert(header);

        var main = new Element("div", { 'class': 't-main' });
        main.insert(new Element("div", {'class': 't-left'}));

        var mainBody = new Element("div", { 'class': 't-mainBody' });

        mainBody.insert(outputList = new Element("ol"));

        $A(cache).each(function(element)
        {
            outputList.insert(element);
        });

        cache = undefined;

        main.insert(mainBody);
        main.insert(new Element("div", { 'class': 't-right' }));

        bbird.insert(main);

        var footer = new Element("div", { 'class': 't-footer'});

        footer.insert(left = new Element("div", { 'class': 't-left' }));

        left.insert();

        var label = new Element("label")
        label.insert(checkbox = new Element("input", { 'type': 'checkbox' }));
        label.insert("Visible on page load");

        left.insert(label);

        footer.insert(new Element("div", { 'class': 't-right' }));

        bbird.insert(footer);

        $(document.body).insert(bbird);
    }

    function backgroundImage()
    { //(IE6 only) change <BODY> tag's background to resolve {position:fixed} support
        var bodyTag = $(document.body);

        if (bodyTag.currentStyle && IE6_POSITION_FIXED)
        {
            if (bodyTag.currentStyle.backgroundImage == 'none')
            {
                bodyTag.addClassName('t-fix-ie6-background');
            }
            if (bodyTag.currentStyle.backgroundAttachment == 'scroll')
            {
                bodyTag.style.backgroundAttachment = 'fixed';
            }
        }
    }

    function addMessage(type, content)
    { //adds a message to the output list

        content = ( content.constructor == Array ) ? content.join('') : content;

        var newMsg = new Element("li", { 'class': type});

        newMsg.insert(new Element("span", { 'class': 'icon'}));
        newMsg.insert(content);

        if (outputList)
        {
            outputList.insert(newMsg);

            // If the added message is not being filtered out, then
            // make sure it is visible to the user.
            if (messageTypes[type] && !isVisible())
            {
                scrollToBottom();
                show();
            }
        }
        else
        {
            cache.push(newMsg);
        }

    }

    function clear()
    { //clear list output
        outputList.update();
    }

    function clickControl(evt)
    {
        var el = evt.element();

        if (el.tagName == 'SPAN')
        {
            switch (el.getAttributeNode('op').nodeValue)
                    {
                case 'resize': resize(); break;
                case 'clear':  clear();  break;
                case 'close':  hide();   break;
            }
        }
    }

    function clickFilter(evt)
    { //show/hide a specific message type
        var span = evt.element();

        if (span && span.tagName == 'SPAN')
        {
            var type = span.readAttribute('type');

            if (evt.altKey)
            {
                var active = 0;
                for (var entry in messageTypes)
                {
                    if (messageTypes[ entry ]) active++;
                }

                var oneActiveFilter = ( active == 1 && messageTypes[ type ] );

                filters.childElements().each(function (child)
                {
                    var childType = child.readAttribute('type');

                    var enabled = oneActiveFilter || (childType == type);

                    messageTypes[ childType ] = enabled;

                    child.className = enabled ? childType : childType + 'Disabled';
                });
            }
            else
            {
                messageTypes[ type ] = ! messageTypes[ type ];
                span.className = ( messageTypes[ type ] ) ? type : type + 'Disabled';
            }

            rebuildOutputListClassName();

            scrollToBottom();
        }
    }

    function rebuildOutputListClassName()
    {
        //build outputList's class from messageTypes object
        var disabledTypes = [];
        for (type in messageTypes)
        {
            if (! messageTypes[ type ]) disabledTypes.push(type);
        }
        disabledTypes.push('');
        outputList.className = disabledTypes.join('Hidden ');
    }

    function clickVis(evt)
    {
        var el = evt.element();

        state.load = el.checked;
        saveState();
    }


    function scrollToBottom()
    { //scroll list output to the bottom
        outputList.scrollTop = outputList.scrollHeight;
    }

    function isVisible()
    {
        return bbird.visible();
    }

    function hide()
    {
        bbird.style.display = 'none';
    }

    function show()
    {
        var body = $(document.body);

        body.removeChild(bbird);
        body.appendChild(bbird);

        bbird.style.display = 'block';
    }

    //sets the position
    function reposition(position)
    {
        if (position === undefined || position == null)
        {
            position = ( state && state.pos === null ) ? 1 : ( state.pos + 1 ) % 4; //set to initial position ('topRight') or move to next position
        }

        switch (position)
                {
            case 0: classes[ 0 ] = 'bbTopLeft'; break;
            case 1: classes[ 0 ] = 'bbTopRight'; break;
            case 2: classes[ 0 ] = 'bbBottomLeft'; break;
            case 3: classes[ 0 ] = 'bbBottomRight'; break;
        }
        state.pos = position;
        saveState();
    }

    function resize(big)
    {
        if (big === undefined || big === null)
        {
            big = ( state && state.size == null ) ? 0 : ( state.size + 1 ) % 2;
        }

        classes[ 1 ] = ( big === 0 ) ? 'bbSmall' : 'bbLarge'

        size.title = ( big === 1 ) ? 'small' : 'large';
        size.className = "t-" + size.title;

        state.size = big;

        saveState();
        scrollToBottom();
    }

    function saveState()
    {
        var expiration = new Date();
        expiration.setDate(expiration.getDate() + 14);
        document.cookie =
        [ 'blackbird=', Object.toJSON(state), '; expires=', expiration.toUTCString() ,';' ].join('');

        var newClass = [];
        for (word in classes)
        {
            newClass.push(classes[ word ]);
        }
        bbird.className = newClass.join(' ');
    }

    function getState()
    {
        var re = new RegExp(/blackbird=({[^;]+})(;|\b|$)/);
        var match = re.exec(document.cookie);
        return ( match && match[ 1 ] ) ? eval('(' + match[ 1 ] + ')') : { pos:null, size:null, load:null };
    }

    //event handler for 'keyup' event for window
    function readKey(evt)
    {
        var code = 113; //F2 key

        if (evt && evt.keyCode == code)
        {

            var visible = isVisible();

            if (visible && evt.shiftKey && evt.altKey) clear();
            else if (visible && evt.shiftKey) reposition();
            else if (!evt.shiftKey && !evt.altKey)
                {
                    ( visible ) ? hide() : show();
                }
        }
    }


    Tapestry.Logging = {
        toggle:
                function()
                {
                    ( isVisible() ) ? hide() : show();
                },
        hide:
                function()
                {
                    hide();
                },
        resize:
                function()
                {
                    resize();
                },
        clear:
                function()
                {
                    clear();
                },
        move:
                function()
                {
                    reposition();
                },
        debug:
                function(msg)
                {
                    addMessage('debug', msg);
                },
        warn:
                function(msg)
                {
                    addMessage('warn', msg);
                },
        info:
                function(msg)
                {
                    addMessage('info', msg);
                },
        error:
                function(msg)
                {
                    addMessage('error', msg);
                },
        profile:
                function(label)
                {
                    var currentTime = new Date(); //record the current time when profile() is executed

                    if (label == undefined || label == '')
                    {
                        addMessage('error', '<b>ERROR:</b> Please specify a label for your profile statement');
                    }
                    else if (profiler[ label ])
                    {
                        addMessage('profile', [ label, ': ', currentTime - profiler[ label ],    'ms' ].join(''));
                        delete profiler[ label ];
                    }
                    else
                    {
                        profiler[ label ] = currentTime;
                        addMessage('profile', label);
                    }
                    return currentTime;
                }
    }

    Tapestry.onDOMLoaded(
        /* initialize Blackbird when the page loads */
            function()
            {
                messageTypes.debug = Tapestry.DEBUG_ENABLED;

                constructUI();

                rebuildOutputListClassName();

                backgroundImage();

                checkbox.observe("click", clickVis.bindAsEventListener());

                filters.observe("click", clickFilter.bindAsEventListener());
                controls.observe("click", clickControl.bindAsEventListener());

                document.observe("keyup", readKey.bindAsEventListener());

                resize(state.size);
                reposition(state.pos);

                if (state.load)
                {
                    show();
                    $(checkbox).checked = true;
                }

                scrollToBottom();

                // The original Blackbird code would unregister the events, but I believe that's not
                // necessary due to Prototype.
            }.bind(this));
})();

/* /assets/tapestry/5.1.0.8-SNAPSHOT/tapestry-messages.js */;
// Copyright 2009 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

Tapestry.Messages = {

    pageIsLoading : "Please wait for the page to finish loading ...",

    missingInitializer : "Function Tapestry.Initializer.#{name}() does not exist.",

    missingValidator :      "Function Tapestry.Validator.#{name}() does not exist for field '#{fieldName}'.",

    ajaxFailure : "Ajax failure: Status #{status} for #{request.url}: ",

    ajaxRequestUnsuccessful : "Server request was unsuccessful. There may be a problem accessing the server.",

    clientException :     "Client exception processing response: ",

    missingZone :   "Unable to locate Ajax Zone '#{id}' for dynamic update.",

    noZoneManager :   "Ajax Zone '#{id}' does not have an associated Tapestry.ZoneManager object." ,

    pathDoesNotStartWithSlash : "External path #{path} does not start with a leading slash.",

    notAnInteger : "Not an integer",

    invalidCharacter : "Invalid character",

    communicationFailed : "Communication with the server failed: "
};
/* /assets/ctx/0.8/js/scriptaculous/dragdrop.js */;
// script.aculo.us dragdrop.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)
//           (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')];
};
/* /assets/ctx/0.8/js/Cropper.js */;
/**
 * Image Cropper (v. 1.2.1 - 2009-10-06 )
 * Copyright (c) 2006-2009 David Spurr (http://www.defusion.org.uk/)
 * 
 * The image cropper provides a way to draw a crop area on an image and capture
 * the coordinates of the drawn crop area.
 * 
 * Features include:
 *   - Based on Prototype and Scriptaculous
 *   - Image editing package styling, the crop area functions and looks 
 *     like those found in popular image editing software
 *   - Dynamic inclusion of required styles
 *   - Drag to draw areas
 *   - Shift drag to draw/resize areas as squares
 *   - Selection area can be moved 
 *   - Seleciton area can be resized using resize handles
 *   - Allows dimension ratio limited crop areas
 *   - Allows minimum dimension crop areas
 *   - Allows maximum dimesion crop areas
 *   - If both min & max dimension options set to the same value for a single axis,then the cropper will not 
 *     display the resize handles as appropriate (when min & max dimensions are passed for both axes this
 *     results in a 'fixed size' crop area)
 *   - Allows dynamic preview of resultant crop ( if minimum width & height are provided ), this is
 *     implemented as a subclass so can be excluded when not required
 *   - Movement of selection area by arrow keys ( shift + arrow key will move selection area by
 *     10 pixels )
 *   - All operations stay within bounds of image
 *   - All functionality & display compatible with most popular browsers supported by Prototype:
 *       PC:  IE 8, 7, 6 & 5.5, Firefox 2, 3, 3.5, Opera 8.5 (see known issues) & 9.0b
 *       MAC: Camino 1.0, Firefox 2, 3, 3.5, Safari 3.x, 4
 * 
 * Requires:
 *   - Prototype v. 1.5.0_rc0 > (as packaged with Scriptaculous 1.6.1)
 *   - Scriptaculous v. 1.6.1 > modules: dragdrop 
 *   
 * Known issues:
 *   - Safari animated gifs, only one of each will animate, this seems to be a known Safari issue
 *   
 *   - After drawing an area and then clicking to start a new drag in IE 5.5 the rendered height 
 *       appears as the last height until the user drags, this appears to be the related to the error 
 *       that the forceReRender() method fixes for IE 6, i.e. IE 5.5 is not redrawing the box properly.
 *   
 *   - Lack of CSS opacity support in Opera before version 9 mean we disable those style rules, these 
 *     could be fixed by using PNGs with transparency if Opera 8.5 support is high priority for you
 *   
 *   - Marching ants keep reloading in IE <6, it is a known issue in IE and I have 
 *       found no viable workarounds that can be included in the release. If this really is an issue for you
 *       either try this post: http://mir.aculo.us/articles/2005/08/28/internet-explorer-and-ajax-image-caching-woes
 *       or uncomment the 'FIX MARCHING ANTS IN IE' rules in the CSS file
 *   
 *   - Styling & borders on image, any CSS styling applied directly to the image itself (floats, borders, padding, margin, etc.) will 
 *     cause problems with the cropper. The use of a wrapper element to apply these styles to is recommended.
 *   
 *   - overflow: auto or overflow: scroll on parent will cause cropper to burst out of parent in IE and Opera (maybe Mac browsers too)
 *     I'm not sure why yet.
 *   
 * Usage:
 *   See Cropper.Img & Cropper.ImgWithPreview for usage details
 * 
 * Changelog:
 * v1.2.1 - 2009-10-06
 *    + Added support for latest versions of Prototype & script.aculo.us
 *      (1.6.1.0 & 1.8.2 respectively). Changes provided by Tom Hirashima.
 *    - No-longer package prototype & script.aculo.us with the release
 *    * Changed tests to use google ajax libraries api to load prototype & script.aculo.us
 *    + Added option to not auto include the cropper CSS file
 *    * #00008 - Fixed bug: Dynamic include of cropper CSS expected cropper.js and failed when using cropper.uncompressed.js
 *    * #00028 - Fixed bug: Doesn't work with latest script.aculo.us - Fix by Tom Hirashima
 *    * #00030 - Fixed bug: Doesn't work in Firefox 3.5 (CSS include issue)
 *    * #00007 - Fixed bug: onEndCrop isn't called when moving with keys
 *    * #00011 - Fixed bug: The image that is to be cropped does not show in IE6.0 -- included CSS fix
 *    * Tidied up source code & fixed issues that jslint found so it will compress better
 * 
 * v1.2.0 - 2006-10-30
 *    + Added id to the preview image element using 'imgCrop_[originalImageID]'
 *      * #00001 - Fixed bug: Doesn't account for scroll offsets
 *      * #00009 - Fixed bug: Placing the cropper inside differently positioned elements causes incorrect co-ordinates and display
 *      * #00013 - Fixed bug: I-bar cursor appears on drag plane
 *      * #00014 - Fixed bug: If ID for image tag is not found in document script throws error
 *      * Fixed bug with drag start co-ordinates if wrapper element has moved in browser (e.g. dragged to a new position)
 *      * Fixed bug with drag start co-ordinates if image contained in a wrapper with scrolling - this may be buggy if image 
 *      has other ancestors with scrolling applied (except the body)
 *      * #00015 - Fixed bug: When cropper removed and then reapplied onEndCrop callback gets called multiple times, solution suggestion from Bill Smith
 *      * Various speed increases & code cleanup which meant improved performance in Mac - which allowed removal of different overlay methods for
 *        IE and all other browsers, which led to a fix for:
 *    * #00010 - Fixed bug: Select area doesn't adhere to image size when image resized using img attributes
 *      - #00006 - Removed default behaviour of automatically setting a ratio when both min width & height passed, the ratioDimensions must be passed in
 *    + #00005 - Added ability to set maximum crop dimensions, if both min & max set as the same value then we'll get a fixed cropper size on the axes as appropriate
 *        and the resize handles will not be displayed as appropriate
 *    * Switched keydown for keypress for moving select area with cursor keys (makes for nicer action) - doesn't appear to work in Safari
 *    
 * v1.1.3 - 2006-08-21
 *    * Fixed wrong cursor on western handle in CSS
 *    + #00008 & #00003 - Added feature: Allow to set dimensions & position for cropper on load
 *      * #00002 - Fixed bug: Pressing 'remove cropper' twice removes image in IE
 *    
 * v1.1.2 - 2006-06-09
 *    * Fixed bugs with ratios when GCD is low (patch submitted by Andy Skelton)
 * 
 * v1.1.1 - 2006-06-03
 *    * Fixed bug with rendering issues fix in IE 5.5
 *    * Fixed bug with endCrop callback issues once cropper had been removed & reset in IE
 * 
 * v1.1.0 - 2006-06-02
 *    * Fixed bug with IE constantly trying to reload select area background image
 *    * Applied more robust fix to Safari & IE rendering issues
 *    + Added method to reset parameters - useful for when dynamically changing img cropper attached to
 *    + Added method to remove cropper from image
 *    
 * v1.0.0 - 2006-05-18 
 *    + Initial verison
 * 
 * 
 * Copyright (c) 2006-2009, David Spurr (http://www.defusion.org.uk/)
 * 
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 *     * Neither the name of the David Spurr nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * http://www.opensource.org/licenses/bsd-license.php
 * 
 * See scriptaculous.js for full scriptaculous licence
 */
 
/**
 * Extend the Draggable class to allow us to pass the rendering
 * down to the Cropper object.
 */
var CropDraggable = Class.create(Draggable, {
	
	initialize: function(element) {
		this.options = Object.extend(
			{
				/**
				 * The draw method to defer drawing to
				 */
				drawMethod: function() {}
			}, 
			arguments[1] || {}
		);

		this.element = $(element);

		this.handle = this.element;

		this.delta    = this.currentDelta();
		this.dragging = false;   

		this.eventMouseDown = this.initDrag.bindAsEventListener(this);
		Event.observe(this.handle, "mousedown", this.eventMouseDown);

		Draggables.register(this);
	},
	
	/**
	 * Defers the drawing of the draggable to the supplied method
	 */
	draw: function(point) {
		var pos = Element.cumulativeOffset(this.element),
		    d = this.currentDelta();
		pos[0] -= d[0]; 
		pos[1] -= d[1];
				
		var p = [0,1].map(function(i) { 
			return (point[i]-pos[i]-this.offset[i]);
		}.bind(this));
				
		this.options.drawMethod( p );
	}
	
});


/**
 * The Cropper object, this will attach itself to the provided image by wrapping it with 
 * the generated xHTML structure required by the cropper.
 * 
 * Usage:
 *  @param obj Image element to attach to
 *  @param obj Optional options:
 *     - ratioDim obj 
 *       The pixel dimensions to apply as a restrictive ratio, with properties x & y
 *     
 *     - minWidth int 
 *       The minimum width for the select area in pixels
 *     
 *     - minHeight	int 
 *       The mimimum height for the select area in pixels
 *     
 *     - maxWidth int
 *       The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed)
 *     
 *     - maxHeight int
 *       The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed)
 *     
 *     - displayOnInit int 
 *       Whether to display the select area on initialisation, only used when providing minimum width & height or ratio
 *     
 *     - onEndCrop func
 *       The callback function to provide the crop details to on end of a crop (see below)
 *     
 *     - captureKeys boolean
 *       Whether to capture the keys for moving the select area, as these can cause some problems at the moment
 *     
 *     - onloadCoords obj
 *       A coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area to display onload
 *     
 *     - autoIncludeCSS boolean
 *       Whether to automatically include the stylesheet (assumes it lives in the same location as the cropper JS file)
 *-    ---------------------------------------------
 * 
 * The callback function provided via the onEndCrop option should accept the following parameters:
 *   - coords obj
 *     The coordinates object with properties x1, y1, x2 & y2; for the coordinates of the select area
 *   
 *   - dimensions obj
 *     The dimensions object with properites width & height; for the dimensions of the select area
 *   
 *   
 *   Example:
 *     function onEndCrop( coords, dimensions ) {
 *         $( 'x1' ).value = coords.x1;
 *         $( 'y1' ).value = coords.y1;
 *         $( 'x2' ).value = coords.x2;
 *         $( 'y2' ).value = coords.y2;
 *         $( 'width' ).value = dimensions.width;
 *         $( 'height' ).value = dimensions.height;
 *     }
 * 
 */
var Cropper = {};
Cropper.Img = Class.create({
	
	/**
	 * Initialises the class
	 * 
	 * @access public
	 * @param obj Image element to attach to
	 * @param obj Options
	 * @return void
	 */
	initialize: function(element, options) {
		this.options = Object.extend(
			{
				/**
				 * @var obj
				 * The pixel dimensions to apply as a restrictive ratio
				 */
				ratioDim: { x: 0, y: 0 },
				/**
				 * @var int
				 * The minimum pixel width, also used as restrictive ratio if min height passed too
				 */
				minWidth:		0,
				/**
				 * @var int
				 * The minimum pixel height, also used as restrictive ratio if min width passed too
				 */
				minHeight:		0,
				/**
				 * @var boolean
				 * Whether to display the select area on initialisation, only used when providing minimum width & height or ratio
				 */
				displayOnInit:	false,
				/**
				 * @var function
				 * The call back function to pass the final values to
				 */
				onEndCrop: Prototype.emptyFunction,
				/**
				 * @var boolean
				 * Whether to capture key presses or not
				 */
				captureKeys: true,
				/**
				 * @var obj Coordinate object x1, y1, x2, y2
				 * The coordinates to optionally display the select area at onload
				 */
				onloadCoords: null,
				/**
				 * @var int
				 * The maximum width for the select areas in pixels (if both minWidth & maxWidth set to same the width of the cropper will be fixed)
				 */
				maxWidth: 0,
				/**
				 * @var int
				 * The maximum height for the select areas in pixels (if both minHeight & maxHeight set to same the height of the cropper will be fixed)
				 */
				maxHeight: 0,
				/**
				 * @var boolean - default true
				 * Whether to automatically include the stylesheet (assumes it lives in the same location as the cropper JS file)
				 */
				autoIncludeCSS: true
			}, 
			options || {}
		);				
		/**
		 * @var obj
		 * The img node to attach to
		 */
		this.img = $( element );
		/**
		 * @var obj
		 * The x & y coordinates of the click point
		 */
		this.clickCoords = { x: 0, y: 0 };
		/**
		 * @var boolean
		 * Whether the user is dragging
		 */
		this.dragging = false;
		/**
		 * @var boolean
		 * Whether the user is resizing
		 */
		this.resizing = false;
		/**
		 * @var boolean
		 * Whether the user is on a webKit browser
		 */
		this.isWebKit = /Konqueror|Safari|KHTML/.test( navigator.userAgent );
		/**
		 * @var boolean
		 * Whether the user is on IE
		 */
		this.isIE = /MSIE/.test( navigator.userAgent );
		/**
		 * @var boolean
		 * Whether the user is on Opera below version 9
		 */
		this.isOpera8 = /Opera\s[1-8]/.test( navigator.userAgent );
		/**
		 * @var int
		 * The x ratio 
		 */
		this.ratioX = 0;
		/**
		 * @var int
		 * The y ratio
		 */
		this.ratioY = 0;
		/**
		 * @var boolean
		 * Whether we've attached sucessfully
		 */
		this.attached = false;
		/**
		 * @var boolean
		 * Whether we've got a fixed width (if minWidth EQ or GT maxWidth then we have a fixed width
		 * in the case of minWidth > maxWidth maxWidth wins as the fixed width)
		 */
		this.fixedWidth = ( this.options.maxWidth > 0 && ( this.options.minWidth >= this.options.maxWidth ) );
		/**
		 * @var boolean
		 * Whether we've got a fixed height (if minHeight EQ or GT maxHeight then we have a fixed height
		 * in the case of minHeight > maxHeight maxHeight wins as the fixed height)
		 */
		this.fixedHeight = ( this.options.maxHeight > 0 && ( this.options.minHeight >= this.options.maxHeight ) );
		
		// quit if the image element doesn't exist
		if( typeof this.img == 'undefined' ) { return; }
		
		// include the stylesheet
		if( this.options.autoIncludeCSS ) {
			//$$('script').each(function(s) {
			//	if( s.src.match( /\/cropper([^\/]*)\.js/ ) ) {
			//		var path    = s.src.replace( /\/cropper([^\/]*)\.js.*/, '' ),
					style = document.createElement( 'link' );
					style.rel   = 'stylesheet';
					style.type  = 'text/css';
					style.href  = '/css/cropper.css';
					style.media = 'screen';
					document.getElementsByTagName( 'head' )[0].appendChild( style );
			//	}
			//});   
		}
	
		// calculate the ratio when neccessary
		if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) {
			var gcd = this.getGCD( this.options.ratioDim.x, this.options.ratioDim.y );
			this.ratioX = this.options.ratioDim.x / gcd;
			this.ratioY = this.options.ratioDim.y / gcd;
			// dump( 'RATIO : ' + this.ratioX + ':' + this.ratioY + '\n' );
		}
							
		// initialise sub classes
		this.subInitialize();

		// only load the event observers etc. once the image is loaded
		// this is done after the subInitialize() call just in case the sub class does anything
		// that will affect the result of the call to onLoad()
		if( this.img.complete || this.isWebKit ) {
			this.onLoad(); // for some reason Safari seems to support img.complete but returns 'undefined' on the this.img object
		} else {
			Event.observe( this.img, 'load', this.onLoad.bindAsEventListener( this) );
		}
	},
	
	/**
	 * The Euclidean algorithm used to find the greatest common divisor
	 * 
	 * @acces private
	 * @param int Value 1
	 * @param int Value 2
	 * @return int
	 */
	getGCD : function( a , b ) {
		if( b === 0 ) { return a; }
		return this.getGCD(b, a % b );
	},
	
	/**
	 * Attaches the cropper to the image once it has loaded
	 * 
	 * @access private
	 * @return void
	 */
	onLoad: function( ) {
		/*
		 * Build the container and all related elements, will result in the following
		 *
		 * <div class="imgCrop_wrap">
		 *   <img ... this.img ... />
		 *   <div class="imgCrop_dragArea">
		 *     <!-- the inner spans are only required for IE to stop it making the divs 1px high/wide -->
		 *     <div class="imgCrop_overlay imageCrop_north"><span></span></div>
		 *     <div class="imgCrop_overlay imageCrop_east"><span></span></div>
		 *     <div class="imgCrop_overlay imageCrop_south"><span></span></div>
		 *     <div class="imgCrop_overlay imageCrop_west"><span></span></div>
		 *     <div class="imgCrop_selArea">
		 *       <!-- marquees -->
		 *       <!-- the inner spans are only required for IE to stop it making the divs 1px high/wide -->
		 *       <div class="imgCrop_marqueeHoriz imgCrop_marqueeNorth"><span></span></div>
		 *       <div class="imgCrop_marqueeVert imgCrop_marqueeEast"><span></span></div>
		 *       <div class="imgCrop_marqueeHoriz imgCrop_marqueeSouth"><span></span></div>
		 *       <div class="imgCrop_marqueeVert imgCrop_marqueeWest"><span></span></div>
		 *       <!-- handles -->
		 *       <div class="imgCrop_handle imgCrop_handleN"></div>
		 *       <div class="imgCrop_handle imgCrop_handleNE"></div>
		 *       <div class="imgCrop_handle imgCrop_handleE"></div>
		 *       <div class="imgCrop_handle imgCrop_handleSE"></div>
		 *       <div class="imgCrop_handle imgCrop_handleS"></div>
		 *       <div class="imgCrop_handle imgCrop_handleSW"></div>
		 *       <div class="imgCrop_handle imgCrop_handleW"></div>
		 *       <div class="imgCrop_handle imgCrop_handleNW"></div>
		 *       <div class="imgCrop_clickArea"></div>
		 *     </div>
		 *     <div class="imgCrop_clickArea"></div>
		 *   </div>
		 * </div>
		 */
		var cNamePrefix = 'imgCrop_';
		
		// get the point to insert the container
		var insertPoint = this.img.parentNode;
		
		// apply an extra class to the wrapper to fix Opera below version 9
		var fixOperaClass = '';
		if( this.isOpera8 ) { fixOperaClass = ' opera8'; }
		this.imgWrap = new Element( 'div', { 'class': cNamePrefix + 'wrap' + fixOperaClass } );
		
		this.north = new Element( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'north' }).insert(new Element( 'span' ));
		this.east  = new Element( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'east' }).insert(new Element( 'span' ));
		this.south = new Element( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'south' }).insert(new Element( 'span' ));
		this.west  = new Element( 'div', { 'class': cNamePrefix + 'overlay ' + cNamePrefix + 'west' }).insert(new Element( 'span' ));
		
		var overlays = [ this.north, this.east, this.south, this.west ];

		this.dragArea = new Element( 'div', { 'class': cNamePrefix + 'dragArea' } );
		
		overlays.each(function(o){this.dragArea.insert(o);}, this);
		
		this.handleN  = new Element( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleN' } );
		this.handleNE = new Element( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNE' } );
		this.handleE  = new Element( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleE' } );
		this.handleSE = new Element( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSE' } );
		this.handleS  = new Element( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleS' } );
		this.handleSW = new Element( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleSW' } );
		this.handleW  = new Element( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleW' } );
		this.handleNW = new Element( 'div', { 'class': cNamePrefix + 'handle ' + cNamePrefix + 'handleNW' } );
				
		this.selArea = new Element( 'div', { 'class': cNamePrefix + 'selArea' });
			[
				new Element( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeNorth' }).insert(new Element( 'span' )),
				new Element( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeEast' }).insert(new Element( 'span' )),
				new Element( 'div', { 'class': cNamePrefix + 'marqueeHoriz ' + cNamePrefix + 'marqueeSouth' }).insert(new Element( 'span' )),
				new Element( 'div', { 'class': cNamePrefix + 'marqueeVert ' + cNamePrefix + 'marqueeWest' }).insert(new Element( 'span' )),
				this.handleN,
				this.handleNE,
				this.handleE,
				this.handleSE,
				this.handleS,
				this.handleSW,
				this.handleW,
				this.handleNW,
				new Element( 'div', { 'class': cNamePrefix + 'clickArea' } )
			].each(function(o){this.selArea.insert(o);}, this);
		
				
		this.imgWrap.appendChild( this.img );
		this.imgWrap.appendChild( this.dragArea );
		this.dragArea.appendChild( this.selArea );
		this.dragArea.appendChild( new Element( 'div', { 'class': cNamePrefix + 'clickArea' } ) );

		insertPoint.appendChild( this.imgWrap );

		// add event observers
		this.startDragBind = this.startDrag.bindAsEventListener( this );
		Event.observe( this.dragArea, 'mousedown', this.startDragBind );
		
		this.onDragBind = this.onDrag.bindAsEventListener( this );
		Event.observe( document, 'mousemove', this.onDragBind );
		
		this.endCropBind = this.endCrop.bindAsEventListener( this );
		Event.observe( document, 'mouseup', this.endCropBind );
		
		this.resizeBind = this.startResize.bindAsEventListener( this );
		this.handles = [ this.handleN, this.handleNE, this.handleE, this.handleSE, this.handleS, this.handleSW, this.handleW, this.handleNW ];
		this.registerHandles( true );
		
		if( this.options.captureKeys ) {
			this.keysBind = this.handleKeys.bindAsEventListener( this );
			Event.observe( document, 'keypress', this.keysBind );
		}

		// attach the dragable to the select area
		var x = new CropDraggable( this.selArea, { drawMethod: this.moveArea.bindAsEventListener( this ) } );
		
		this.setParams();
	},
	
	/**
	 * Manages adding or removing the handle event handler and hiding or displaying them as appropriate
	 * 
	 * @access private
	 * @param boolean registration true = add, false = remove
	 * @return void
	 */
	registerHandles: function( registration ) {	
		for( var i = 0; i < this.handles.length; i++ ) {
			var handle = $( this.handles[i] );
			
			if( registration ) {
				var hideHandle	= false;	// whether to hide the handle
				
				// disable handles asappropriate if we've got fixed dimensions
				// if both dimensions are fixed we don't need to do much
				if( this.fixedWidth && this.fixedHeight ) {
					hideHandle = true;
				} else if( this.fixedWidth || this.fixedHeight ) {
					// if one of the dimensions is fixed then just hide those handles
					var isCornerHandle = handle.className.match( /([S|N][E|W])$/ ),
					    isWidthHandle  = handle.className.match( /(E|W)$/ ),
					    isHeightHandle = handle.className.match( /(N|S)$/ );
					if( isCornerHandle || ( this.fixedWidth && isWidthHandle ) || ( this.fixedHeight && isHeightHandle ) ) {
						hideHandle = true;
					}
				}
				if( hideHandle ) { 
					handle.hide(); 
				} else {
					Event.observe( handle, 'mousedown', this.resizeBind );
				}
			} else {
				handle.show();
				Event.stopObserving( handle, 'mousedown', this.resizeBind );
			}
		}
	},
		
	/**
	 * Sets up all the cropper parameters, this can be used to reset the cropper when dynamically
	 * changing the images
	 * 
	 * @access private
	 * @return void
	 */
	setParams: function() {
		/**
		 * @var int
		 * The image width
		 */
		this.imgW = this.img.width;
		/**
		 * @var int
		 * The image height
		 */
		this.imgH = this.img.height;			

		$( this.north ).setStyle( { height: 0 } );
		$( this.east ).setStyle( { width: 0, height: 0 } );
		$( this.south ).setStyle( { height: 0 } );
		$( this.west ).setStyle( { width: 0, height: 0 } );
		
		// resize the container to fit the image
		$( this.imgWrap ).setStyle( { 'width': this.imgW + 'px', 'height': this.imgH + 'px' } );
		
		// hide the select area
		$( this.selArea ).hide();
						
		// setup the starting position of the select area
		var startCoords = { x1: 0, y1: 0, x2: 0, y2: 0 },
		    validCoordsSet = false;
		
		// display the select area 
		if( this.options.onloadCoords !== null ) {
			// if we've being given some coordinates to 
			startCoords = this.cloneCoords( this.options.onloadCoords );
			validCoordsSet = true;
		} else if( this.options.ratioDim.x > 0 && this.options.ratioDim.y > 0 ) {
			// if there is a ratio limit applied and the then set it to initial ratio
			startCoords.x1 = Math.ceil( ( this.imgW - this.options.ratioDim.x ) / 2 );
			startCoords.y1 = Math.ceil( ( this.imgH - this.options.ratioDim.y ) / 2 );
			startCoords.x2 = startCoords.x1 + this.options.ratioDim.x;
			startCoords.y2 = startCoords.y1 + this.options.ratioDim.y;
			validCoordsSet = true;
		}
		
		this.setAreaCoords( startCoords, false, false, 1 );
		
		if( this.options.displayOnInit && validCoordsSet ) {
			this.selArea.show();
			this.drawArea();
			this.endCrop();
		}
		
		this.attached = true;
	},
	
	/**
	 * Removes the cropper
	 * 
	 * @access public
	 * @return void
	 */
	remove: function() {
		if( this.attached ) {
			this.attached = false;
			
			// remove the elements we inserted
			this.imgWrap.parentNode.insertBefore( this.img, this.imgWrap );
			this.imgWrap.parentNode.removeChild( this.imgWrap );
			
			// remove the event observers
			Event.stopObserving( this.dragArea, 'mousedown', this.startDragBind );
			Event.stopObserving( document, 'mousemove', this.onDragBind );		
			Event.stopObserving( document, 'mouseup', this.endCropBind );
			this.registerHandles( false );
			if( this.options.captureKeys ) {
				Event.stopObserving( document, 'keypress', this.keysBind );
			}
		}
	},
	
	/**
	 * Resets the cropper, can be used either after being removed or any time you wish
	 * 
	 * @access public
	 * @return void
	 */
	reset: function() {
		if( !this.attached ) {
			this.onLoad();
		} else {
			this.setParams();
		}
		this.endCrop();
	},
	
	/**
	 * Handles the key functionality, currently just using arrow keys to move, if the user
	 * presses shift then the area will move by 10 pixels
	 */
	handleKeys: function( e ) {
		var dir = { x: 0, y: 0 }; // direction to move it in & the amount in pixels
		if( !this.dragging ) {
			
			// catch the arrow keys
			switch( e.keyCode ) {
				case( 37 ) : // left
					dir.x = -1;
					break;
				case( 38 ) : // up
					dir.y = -1;
					break;
				case( 39 ) : // right
					dir.x = 1;
					break;
				case( 40 ) : // down
					dir.y = 1;
					break;
			}
			
			if( dir.x !== 0 || dir.y !== 0 ) {
				// if shift is pressed then move by 10 pixels
				if( e.shiftKey ) {
					dir.x *= 10;
					dir.y *= 10;
				}
				
				this.moveArea( [ this.areaCoords.x1 + dir.x, this.areaCoords.y1 + dir.y ] );
				this.endCrop();
				Event.stop( e ); 
			}
		}
	},
	
	/**
	 * Calculates the width from the areaCoords
	 * 
	 * @access private
	 * @return int
	 */
	calcW: function() {
		return (this.areaCoords.x2 - this.areaCoords.x1);
	},
	
	/**
	 * Calculates the height from the areaCoords
	 * 
	 * @access private
	 * @return int
	 */
	calcH: function() {
		return (this.areaCoords.y2 - this.areaCoords.y1);
	},
	
	/**
	 * Moves the select area to the supplied point (assumes the point is x1 & y1 of the select area)
	 * 
	 * @access public
	 * @param array Point for x1 & y1 to move select area to
	 * @return void
	 */
	moveArea: function( point ) {
		// dump( 'moveArea        : ' + point[0] + ',' + point[1] + ',' + ( point[0] + ( this.areaCoords.x2 - this.areaCoords.x1 ) ) + ',' + ( point[1] + ( this.areaCoords.y2 - this.areaCoords.y1 ) ) + '\n' );
		this.setAreaCoords( 
			{
				x1: point[0], 
				y1: point[1],
				x2: point[0] + this.calcW(),
				y2: point[1] + this.calcH()
			},
			true,
			false
		);
		this.drawArea();
	},

	/**
	 * Clones a co-ordinates object, stops problems with handling them by reference
	 * 
	 * @access private
	 * @param obj Coordinate object x1, y1, x2, y2
	 * @return obj Coordinate object x1, y1, x2, y2
	 */
	cloneCoords: function( coords ) {
		return { x1: coords.x1, y1: coords.y1, x2: coords.x2, y2: coords.y2 };
	},

	/**
	 * Sets the select coords to those provided but ensures they don't go
	 * outside the bounding box
	 * 
	 * @access private
	 * @param obj Coordinates x1, y1, x2, y2
	 * @param boolean Whether this is a move
	 * @param boolean Whether to apply squaring
	 * @param obj Direction of mouse along both axis x, y ( -1 = negative, 1 = positive ) only required when moving etc.
	 * @param string The current resize handle || null
	 * @return void
	 */
	setAreaCoords: function( coords, moving, square, direction, resizeHandle ) {
		// dump( 'setAreaCoords (in) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 );
		if( moving ) {
			// if moving
			var targW = coords.x2 - coords.x1,
			    targH = coords.y2 - coords.y1;
			
			// ensure we're within the bounds
			if( coords.x1 < 0 ) {
				coords.x1 = 0;
				coords.x2 = targW;
			}
			if( coords.y1 < 0 ) {
				coords.y1 = 0;
				coords.y2 = targH;
			}
			if( coords.x2 > this.imgW ) {
				coords.x2 = this.imgW;
				coords.x1 = this.imgW - targW;
			}
			if( coords.y2 > this.imgH ) {
				coords.y2 = this.imgH;
				coords.y1 = this.imgH - targH;
			}			
		} else {
			// ensure we're within the bounds
			if( coords.x1 < 0 ) { coords.x1 = 0; }
			if( coords.y1 < 0 ) { coords.y1 = 0; }
			if( coords.x2 > this.imgW ) { coords.x2 = this.imgW; }
			if( coords.y2 > this.imgH ) { coords.y2 = this.imgH; }
			
			// This is passed as null in onload
			if( direction !== null ) {
								
				// apply the ratio or squaring where appropriate
				if( this.ratioX > 0 ) {
					this.applyRatio( coords, { x: this.ratioX, y: this.ratioY }, direction, resizeHandle );
				} else if( square ) {
					this.applyRatio( coords, { x: 1, y: 1 }, direction, resizeHandle );
				}
										
				var mins = [ this.options.minWidth, this.options.minHeight ], // minimum dimensions [x,y]			
				    maxs = [ this.options.maxWidth, this.options.maxHeight ]; // maximum dimensions [x,y]
		
				// apply dimensions where appropriate
				if( mins[0] > 0 || mins[1] > 0 || maxs[0] > 0 || maxs[1] > 0) {
				
					var coordsTransX = { a1: coords.x1, a2: coords.x2 },
					    coordsTransY = { a1: coords.y1, a2: coords.y2 },
					    boundsX      = { min: 0, max: this.imgW },
					    boundsY      = { min: 0, max: this.imgH };

					// handle squaring properly on single axis minimum dimensions
					if( (mins[0] !== 0 || mins[1] !== 0) && square ) {
						if( mins[0] > 0 ) {
							mins[1] = mins[0];
						} else if( mins[1] > 0 ) {
							mins[0] = mins[1];
						}
					}
					
					if( (maxs[0] !== 0 || maxs[0] !== 0) && square ) {
						// if we have a max x value & it is less than the max y value then we set the y max to the max x (so we don't go over the minimum maximum of one of the axes - if that makes sense)
						if( maxs[0] > 0 && maxs[0] <= maxs[1] ) {
							maxs[1] = maxs[0];
						} else if( maxs[1] > 0 && maxs[1] <= maxs[0] ) {
							maxs[0] = maxs[1];
						}
					}
					
					if( mins[0] > 0 ) { this.applyDimRestriction( coordsTransX, mins[0], direction.x, boundsX, 'min' ); }
					if( mins[1] > 1 ) { this.applyDimRestriction( coordsTransY, mins[1], direction.y, boundsY, 'min' ); }
					
					if( maxs[0] > 0 ) { this.applyDimRestriction( coordsTransX, maxs[0], direction.x, boundsX, 'max' ); }
					if( maxs[1] > 1 ) { this.applyDimRestriction( coordsTransY, maxs[1], direction.y, boundsY, 'max' ); }
					
					coords = { x1: coordsTransX.a1, y1: coordsTransY.a1, x2: coordsTransX.a2, y2: coordsTransY.a2 };
				}
				
			}
		}
		
		// dump( 'setAreaCoords (out) : ' + coords.x1 + ',' + coords.y1 + ',' + coords.x2 + ',' + coords.y2 + '\n' );
		this.areaCoords = coords;
	},
	
	/**
	* Applies the supplied dimension restriction to the supplied coordinates along a single axis
	 * 
	 * @access private
	 * @param obj Single axis coordinates, a1, a2 (e.g. for the x axis a1 = x1 & a2 = x2)
	 * @param int The restriction value
	 * @param int The direction ( -1 = negative, 1 = positive )
	 * @param obj The bounds of the image ( for this axis )
	 * @param string The dimension restriction type ( 'min' | 'max' )
	 * @return void
	 */
	applyDimRestriction: function( coords, val, direction, bounds, type ) {
		var check;
		if( type == 'min' ) { check = ( ( coords.a2 - coords.a1 ) < val ); }
		else { check = ( ( coords.a2 - coords.a1 ) > val ); }
		if( check ) {
			if( direction == 1 ) { coords.a2 = coords.a1 + val; }
			else { coords.a1 = coords.a2 - val; }
			
			// make sure we're still in the bounds (not too pretty for the user, but needed)
			if( coords.a1 < bounds.min ) {
				coords.a1 = bounds.min;
				coords.a2 = val;
			} else if( coords.a2 > bounds.max ) {
				coords.a1 = bounds.max - val;
				coords.a2 = bounds.max;
			}
		}
	},
		
	/**
	 * Applies the supplied ratio to the supplied coordinates
	 * 
	 * @access private
	 * @param obj Coordinates, x1, y1, x2, y2
	 * @param obj Ratio, x, y
	 * @param obj Direction of mouse, x & y : -1 == negative 1 == positive
	 * @param string The current resize handle || null
	 * @return void
	 */
	applyRatio : function( coords, ratio, direction, resizeHandle ) {
		// dump( 'direction.y : ' + direction.y + '\n');
		var newCoords;
		if( resizeHandle == 'N' || resizeHandle == 'S' ) {
			// dump( 'north south \n');
			// if moving on either the lone north & south handles apply the ratio on the y axis
			newCoords = this.applyRatioToAxis( 
				{ a1: coords.y1, b1: coords.x1, a2: coords.y2, b2: coords.x2 },
				{ a: ratio.y, b: ratio.x },
				{ a: direction.y, b: direction.x },
				{ min: 0, max: this.imgW }
			);
			coords.x1 = newCoords.b1;
			coords.y1 = newCoords.a1;
			coords.x2 = newCoords.b2;
			coords.y2 = newCoords.a2;
		} else {
			// otherwise deal with it as if we're applying the ratio on the x axis
			newCoords = this.applyRatioToAxis( 
				{ a1: coords.x1, b1: coords.y1, a2: coords.x2, b2: coords.y2 },
				{ a: ratio.x, b: ratio.y },
				{ a: direction.x, b: direction.y },
				{ min: 0, max: this.imgH }
			);
			coords.x1 = newCoords.a1;
			coords.y1 = newCoords.b1;
			coords.x2 = newCoords.a2;
			coords.y2 = newCoords.b2;
		}
		
	},
	
	/**
	 * Applies the provided ratio to the provided coordinates based on provided direction & bounds,
	 * use to encapsulate functionality to make it easy to apply to either axis. This is probably
	 * quite hard to visualise so see the x axis example within applyRatio()
	 * 
	 * Example in parameter details & comments is for requesting applying ratio to x axis.
	 * 
	 * @access private
	 * @param obj Coords object (a1, b1, a2, b2) where a = x & b = y in example
	 * @param obj Ratio object (a, b) where a = x & b = y in example
	 * @param obj Direction object (a, b) where a = x & b = y in example
	 * @param obj Bounds (min, max)
	 * @return obj Coords object (a1, b1, a2, b2) where a = x & b = y in example
	 */
	applyRatioToAxis: function( coords, ratio, direction, bounds ) {
		var newCoords = Object.extend( coords, {} ),
				calcDimA = newCoords.a2 - newCoords.a1, // calculate dimension a (e.g. width)
				targDimB = Math.floor( calcDimA * ratio.b / ratio.a ), // the target dimension b (e.g. height)
				targB = null, // to hold target b (e.g. y value)
				targDimA = null, // to hold target dimension a (e.g. width)
				calcDimB = null; // to hold calculated dimension b (e.g. height)
		
		// dump( 'newCoords[0]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');
				
		if( direction.b == 1 ) { // if travelling in a positive direction
			// make sure we're not going out of bounds
			targB = newCoords.b1 + targDimB;
			if( targB > bounds.max ) {
				targB = bounds.max;
				calcDimB = targB - newCoords.b1; // calcuate dimension b (e.g. height)
			}
			
			newCoords.b2 = targB;
		} else { // if travelling in a negative direction
			// make sure we're not going out of bounds
			targB = newCoords.b2 - targDimB;
			if( targB < bounds.min ) {
				targB = bounds.min;
				calcDimB = targB + newCoords.b2; // calcuate dimension b (e.g. height)
			}
			newCoords.b1 = targB;
		}
		
		// dump( 'newCoords[1]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');
			
		// apply the calculated dimensions
		if( calcDimB !== null ) {
			targDimA = Math.floor( calcDimB * ratio.a / ratio.b );
			
			if( direction.a == 1 ) { newCoords.a2 = newCoords.a1 + targDimA; }
			else { newCoords.a1 = newCoords.a1 = newCoords.a2 - targDimA; }
		}
		
		// dump( 'newCoords[2]: ' + newCoords.a1 + ',' + newCoords.b1 + ','+ newCoords.a2 + ',' + newCoords.b2 + '\n');
			
		return newCoords;
	},
	
	/**
	 * Draws the select area
	 * 
	 * @access private
	 * @return void
	 */
	drawArea: function( ) {	
		/*
			NOTE: I'm not using the Element.setStyle() shortcut as they make it 
			quite sluggish on Mac based browsers
		*/
		// dump( 'drawArea        : ' + this.areaCoords.x1 + ',' + this.areaCoords.y1 + ',' + this.areaCoords.x2 + ',' + this.areaCoords.y2 + '\n' );
		var areaWidth     = this.calcW(),
		    areaHeight    = this.calcH();
		
		/*
			Calculate all the style strings before we use them, allows reuse & produces quicker
			rendering (especially noticable in Mac based browsers)
		*/
		var px = 'px',
		    params = [
			this.areaCoords.x1 + px, // the left of the selArea
			this.areaCoords.y1 + px, // the top of the selArea
			areaWidth + px,          // width of the selArea
			areaHeight + px,         // height of the selArea
			this.areaCoords.x2 + px, // bottom of the selArea
			this.areaCoords.y2 + px, // right of the selArea
			(this.img.width - this.areaCoords.x2) + px, // right edge of selArea
			(this.img.height - this.areaCoords.y2) + px // bottom edge of selArea
		];
				
		// do the select area
		var areaStyle    = this.selArea.style;
		areaStyle.left   = params[0];
		areaStyle.top    = params[1];
		areaStyle.width  = params[2];
		areaStyle.height = params[3];

		// position the north, east, south & west handles
		var horizHandlePos = Math.ceil( (areaWidth - 6) / 2 ) + px,
		    vertHandlePos = Math.ceil( (areaHeight - 6) / 2 ) + px;
		
		this.handleN.style.left = horizHandlePos;
		this.handleE.style.top  = vertHandlePos;
		this.handleS.style.left = horizHandlePos;
		this.handleW.style.top  = vertHandlePos;
		
		// draw the four overlays
		this.north.style.height = params[1];
		
		var eastStyle     = this.east.style;
		eastStyle.top     = params[1];
		eastStyle.height  = params[3];
		eastStyle.left    = params[4];
		eastStyle.width   = params[6];

		var southStyle    = this.south.style;
		southStyle.top    = params[5];
		southStyle.height = params[7];

		var westStyle     = this.west.style;
		westStyle.top     = params[1];
		westStyle.height  = params[3];
		westStyle.width   = params[0];

		// call the draw method on sub classes
		this.subDrawArea();
		
		this.forceReRender();
	},
	
	/**
	 * Force the re-rendering of the selArea element which fixes rendering issues in Safari 
	 * & IE PC, especially evident when re-sizing perfectly vertical using any of the south handles
	 * 
	 * @access private
	 * @return void
	 */
	forceReRender: function() {
		if( this.isIE || this.isWebKit) {
			var n = document.createTextNode(' ');
			var d,el,fixEL,i;
		
			if( this.isIE ) { fixEl = this.selArea; }
			else if( this.isWebKit ) {
				fixEl = document.getElementsByClassName( 'imgCrop_marqueeSouth', this.imgWrap )[0];
				/* 
					we have to be a bit more forceful for Safari, otherwise the the marquee &
					the south handles still don't move
				*/ 
				d = new Element( 'div' );
				d.style.visibility = 'hidden';
				
				var classList = ['SE','S','SW'];
				for( i = 0; i < classList.length; i++ ) {
					el = document.getElementsByClassName( 'imgCrop_handle' + classList[i], this.selArea )[0];
					if( el.childNodes.length ) { el.removeChild( el.childNodes[0] ); }
					el.appendChild(d);
				}
			}
			fixEl.appendChild(n);
			fixEl.removeChild(n);
		}
	},
	
	/**
	 * Starts the resize
	 * 
	 * @access private
	 * @param obj Event
	 * @return void
	 */
	startResize: function( e ) {
		this.startCoords = this.cloneCoords( this.areaCoords );
		
		this.resizing = true;
		this.resizeHandle = Event.element( e ).classNames().toString().replace(/([^N|NE|E|SE|S|SW|W|NW])+/, '');
		// dump( 'this.resizeHandle : ' + this.resizeHandle + '\n' );
		Event.stop( e );
	},
	
	/**
	 * Starts the drag
	 * 
	 * @access private
	 * @param obj Event
	 * @return void
	 */
	startDrag: function( e ) {
		this.selArea.show();
		this.clickCoords = this.getCurPos( e );

		this.setAreaCoords( { x1: this.clickCoords.x, y1: this.clickCoords.y, x2: this.clickCoords.x, y2: this.clickCoords.y }, false, false, null );

		this.dragging = true;
		this.onDrag( e ); // incase the user just clicks once after already making a selection
		Event.stop( e );
	},
	
	/**
	 * Gets the current cursor position relative to the image
	 * 
	 * @access private
	 * @param obj Event
	 * @return obj x,y pixels of the cursor
	 */
	getCurPos: function( e ) {
		// get the offsets for the wrapper within the document
		// get the offsets for the wrapper within the document
		var el = this.imgWrap, wrapOffsets = Element.cumulativeOffset( el );
		// remove any scrolling that is applied to the wrapper (this may be buggy) - don't count the scroll on the body as that won't affect us
		while( el.nodeName != 'BODY' ) {
			wrapOffsets[1] -= el.scrollTop  || 0;
			wrapOffsets[0] -= el.scrollLeft || 0;
			el = el.parentNode;
		}
		return { 
			x: Event.pointerX(e) - wrapOffsets[0],
			y: Event.pointerY(e) - wrapOffsets[1]
		};
	},
	                               
	/**                            
	 * Performs the drag for both   resize & inital draw dragging
	 *
	 * @access private             
	 * @param obj Event
	 * @return void
	 */
	onDrag: function( e ) {
		if( this.dragging || this.resizing ) {
		
			var resizeHandle = null,
					curPos = this.getCurPos( e ),
					newCoords = this.cloneCoords( this.areaCoords ),
					direction = { x: 1, y: 1 };
			
			if( this.dragging ) {
				if( curPos.x < this.clickCoords.x ) { direction.x = -1; }
				if( curPos.y < this.clickCoords.y ) { direction.y = -1; }
				
				this.transformCoords( curPos.x, this.clickCoords.x, newCoords, 'x' );
				this.transformCoords( curPos.y, this.clickCoords.y, newCoords, 'y' );
			} else if( this.resizing ) {
				resizeHandle = this.resizeHandle;
				// do x movements first
				if( resizeHandle.match(/E/) ) {
					// if we're moving an east handle
					this.transformCoords( curPos.x, this.startCoords.x1, newCoords, 'x' );
					if( curPos.x < this.startCoords.x1 ) { direction.x = -1; }
				} else if( resizeHandle.match(/W/) ) {
					// if we're moving an west handle
					this.transformCoords( curPos.x, this.startCoords.x2, newCoords, 'x' );
					if( curPos.x < this.startCoords.x2 ) { direction.x = -1; }
				}
									
				// do y movements second
				if( resizeHandle.match(/N/) ) {
					// if we're moving an north handle	
					this.transformCoords( curPos.y, this.startCoords.y2, newCoords, 'y' );
					if( curPos.y < this.startCoords.y2 ) { direction.y = -1; }
				} else if( resizeHandle.match(/S/) ) {
					// if we're moving an south handle
					this.transformCoords( curPos.y, this.startCoords.y1, newCoords, 'y' );
					if( curPos.y < this.startCoords.y1 ) { direction.y = -1; }
				}
				
			}
		
			this.setAreaCoords( newCoords, false, e.shiftKey, direction, resizeHandle );
			this.drawArea();
			Event.stop( e ); // stop the default event (selecting images & text) in Safari & IE PC
		}
	},
	
	/**
	 * Applies the appropriate transform to supplied co-ordinates, on the
	 * defined axis, depending on the relationship of the supplied values
	 * 
	 * @access private
	 * @param int Current value of pointer
	 * @param int Base value to compare current pointer val to
	 * @param obj Coordinates to apply transformation on x1, x2, y1, y2
	 * @param string Axis to apply transformation on 'x' || 'y'
	 * @return void
	 */
	transformCoords : function( curVal, baseVal, coords, axis ) {
		var newVals = [ curVal, baseVal ];
		if( curVal > baseVal ) { newVals.reverse(); }
		coords[ axis + '1' ] = newVals[0];
		coords[ axis + '2' ] = newVals[1];
	},
	
	/**
	 * Ends the crop & passes the values of the select area on to the appropriate 
	 * callback function on completion of a crop
	 * 
	 * @access private
	 * @return void
	 */
	endCrop : function() {
		this.dragging = false;
		this.resizing = false;
		
		this.options.onEndCrop(
			this.areaCoords,
			{
				width: this.calcW(), 
				height: this.calcH() 
			}
		);
	},
	
	/**
	 * Abstract method called on the end of initialization
	 * 
	 * @access private
	 * @abstract
	 * @return void
	 */
	subInitialize: function() {},
	
	/**
	 * Abstract method called on the end of drawArea()
	 * 
	 * @access private
	 * @abstract
	 * @return void
	 */
	subDrawArea: function() {}
});




/**
 *	Extend the Cropper.Img class to allow for presentation of a preview image of the resulting crop,
 *	the option for displayOnInit is always overridden to true when displaying a preview image
 * 
 *	Usage:
 *		@param obj Image element to attach to
 *		@param obj Optional options:
 *			- see Cropper.Img for base options
 *			- previewWrap obj HTML element that will be used as a container for the preview image
 */
Cropper.ImgWithPreview = Class.create(Cropper.Img, {
	
	/**
	 * Implements the abstract method from Cropper.Img to initialize preview image settings.
	 * Will only attach a preview image is the previewWrap element is defined and the minWidth
	 * & minHeight options are set.
	 * 
	 * @see Croper.Img.subInitialize
	 */
	subInitialize: function() {
		/**
		* Whether or not we've attached a preview image
		* @var boolean
		*/
		this.hasPreviewImg = false;
		if( typeof(this.options.previewWrap) != 'undefined' && this.options.minWidth > 0 && this.options.minHeight > 0 ) {
			/**
			 * The preview image wrapper element
			 * @var obj HTML element
			 */
			this.previewWrap = $( this.options.previewWrap );
			/**
			 * The preview image element
			 * @var obj HTML IMG element
			 */
			this.previewImg = this.img.cloneNode( false );
			// set the ID of the preview image to be unique
			this.previewImg.id = 'imgCrop_' + this.previewImg.id;
			
			// set the displayOnInit option to true so we display the select area at the same time as the thumbnail
			this.options.displayOnInit = true;

			this.hasPreviewImg = true;
			
			this.previewWrap.addClassName( 'imgCrop_previewWrap' );
			
			this.previewWrap.setStyle({ 
				width: this.options.minWidth + 'px',
				height: this.options.minHeight + 'px'
			});
			
			this.previewWrap.appendChild( this.previewImg );
		}
	},
	
	/**
	 * Implements the abstract method from Cropper.Img to draw the preview image
	 * 
	 * @see Croper.Img.subDrawArea
	 */
	subDrawArea: function() {
		if( this.hasPreviewImg ) {
			// get the ratio of the select area to the src image
			var calcWidth = this.calcW(),
			    calcHeight = this.calcH();
			// ratios for the dimensions of the preview image
			var dimRatio = { 
				x: this.imgW / calcWidth, 
				y: this.imgH / calcHeight 
			}; 
			//ratios for the positions within the preview
			var posRatio = { 
				x: calcWidth / this.options.minWidth, 
				y: calcHeight / this.options.minHeight 
			};
			
			// setting the positions in an obj before apply styles for rendering speed increase
			var calcPos = {
				w: Math.ceil( this.options.minWidth * dimRatio.x ) + 'px',
				h: Math.ceil( this.options.minHeight * dimRatio.y ) + 'px',
				x: '-' + Math.ceil( this.areaCoords.x1 / posRatio.x )  + 'px',
				y: '-' + Math.ceil( this.areaCoords.y1 / posRatio.y ) + 'px'
			};
			
			var previewStyle = this.previewImg.style;
			previewStyle.width = calcPos.w;
			previewStyle.height= calcPos.h;
			previewStyle.left = calcPos.x;
			previewStyle.top = calcPos.y;
		}
	}
	
});

/* /assets/ctx/0.8/js/LazierLoad.js */;
/*****************************************************************
 *
 * lazierLoad 0.4 - by Bramus! - http://www.bram.us/
 * inspired upon http://www.appelsiini.net/projects/lazyload/
 *
 * v 0.4 - 2008.02.28 - added ability to automatically autoLoad or not
 *                    - backdrop from 0.2 where one could set options through a new instantiation, enabling one to have per page options
 * v 0.3 - 2008.02.26 - added options: minWidth, minHeight, imgTypes
 *                    - moved all options to global Object
 *                    - Works with latest Prototype (1.6.0.2)
 * v 0.2 - 2007.09.12 - added options: treshold, replaceImage, loadingImage
 * v 0.1 - 2007.09.11 - initial release
 *
 * Licensed under the Creative Commons Attribution 2.5 License - http://creativecommons.org/licenses/by/2.5/
 *
 *****************************************************************/
 
 
  /**
  * CONFIG - CHANGE THESE IF YOU LIKE
  * -------------------------------------------------------------
  */
		
 	// Should lazierLoad hook itself to the page? - default : true
		var lazierLoadAutoHook	= true;		

	// lazierLoad default options
		var lazierLoadDefaultOptions = {
			
			treshold		: 100,							// Offset from bottom to start preloading
			extensions		: ['gif','png','jpg','jpeg'],	// Array of extensions to lazyLoad
			
			// replaceImage	: "/img/blank.gif",					// Placeholder image to show instead of the image (best leave unchanged to this blank.gif!)
			loadingImage	: "/img/spinner.gif",				// Loading indicator
			
			minWidth		: 100,							// Minimum width of an image to lazyLoad
			minHeight		: 100							// Minimum height of an image to lazyLoad
		}
		
		
 /**
  * NO NEED TO CHANGE ANYTHING BENEATH THIS LINE
  * -------------------------------------------------------------
  */
 
 
	/**
	 * JS_BRAMUS Object
	 * -------------------------------------------------------------
	 */
	 
		if (!JS_BRAMUS) { var JS_BRAMUS = new Object(); }
		
		
	/**
	 * lazierLoad Class
	 * -------------------------------------------------------------
	 */

		JS_BRAMUS.lazierLoad 				= Class.create();
		JS_BRAMUS.lazierLoad.prototype 		= {
			
			initialize			: function(options) {				
				// find all images and lazyLoad 'm				
				$$('img.lazyload').each(function(image) {
					new JS_BRAMUS.lazierLoadImage(image, options);  
				});
			}		
			
		}
		
		
	/**
	 * lazierLoadImage Class
	 * -------------------------------------------------------------
	 */


		JS_BRAMUS.lazierLoadImage 				= Class.create();
		JS_BRAMUS.lazierLoadImage.prototype 	= {
			
			options				: null,			// options
			
			element				: null,			// the img element
			loading				: false,		// loading
			loaded				: false,		// loaded
			position			: null,			// element's position
			viewportHeight		: 0,			// height of the viewport
			lazyScroller		: null,			// cached bounds function - see http://www.prototypejs.org/api/event/stopObserving
		
			initialize			: function(image, options) {
						
				// set the options
				this.options				= Object.clone(lazierLoadDefaultOptions);
				Object.extend(this.options, options || {});
			
				// calculate position of image
				this.element				= image;
				this.position				= Position.page(this.element);
				this.viewportHeight			= document.viewport.getHeight();
				
				// image "above the fold" already
				if (this.position[1] < (this.viewportHeight + this.options.treshold)) {
								
					this.loading	= true;
					this.loaded		= true;
					
					this.element.src = this.element.readAttribute("_src");
				
				// image not "above the fold"
				} else {
					
					// get original source element (for further reference), the filename and the extension.
					this.element.origSrc 	= this.element.readAttribute("_src");
					this.element.fileName 	= this.element.origSrc.substring(this.element.origSrc.lastIndexOf('/')+1,this.element.origSrc.length);
					this.element.fileType 	= this.element.fileName.substring(this.element.fileName.lastIndexOf('.')+1, this.element.fileName.length);
					
					// extension not in array; no need to lazyload
					if (this.options.extensions.indexOf(this.element.fileType) == -1) {					
						return;	
					}
					
					// image not large enough
					if ((this.element.width <= parseInt(this.options.minWidth)) && (this.element.height <= parseInt(this.options.minHeight))) {						
						return;	
					}
					
					// set blank and loading image
					// this.element.src 		= this.options.replaceImage;
					this.element.setStyle({ backgroundImage: 'url(' + this.options.loadingImage + ')', backgroundPosition: '50% 50%', backgroundRepeat: 'no-repeat' });
			
					// observe the page scroll event	
					this.lazyScroller 		= this.lazyScroll.bindAsEventListener(this);
					Event.observe(window, 'scroll', this.lazyScroller.bind(this), false);	
				}
			},
			
			lazyScroll			: function() {
									
				// image not loaded and not loading
				if ((this.loaded == false) && (this.loading != true)) {
				
					// image "above the fold" ?
					if ((document.viewport.getScrollOffsets()[1] + document.viewport.getHeight() + parseInt(this.options.treshold)) > this.position[1]) {
	
						this.loading	= true;
						
						// load in the new image
						var newImage 	= null;
						newImage 		= new Image();
						newImage.src 	= this.element.origSrc;
	
						// image is in cache (IE6 & IE7 ... Firefox can handle the onload well even file was in cache);
						if (newImage.complete) {
								this.element.src 	= newImage.src;
								this.loaded			= true;
								
						// image not in cache
						} else {
							newImage.onload = function() {
								this.element.src 	= newImage.src;
								this.loaded			= true;
							}.bind(this);
						}
	
						// stop the observer
						Event.stopObserving(window, 'scroll', this.lazyScroller);
					}
				}
				
			}
		}
		
		
	/**
	 * Hook lazierLoad to the dom:loaded event
	 * -------------------------------------------------------------
	 */
	 
	 	if (lazierLoadAutoHook == true) {
			function initLazierLoad() { myLL = new JS_BRAMUS.lazierLoad(); }
			Event.observe(document, 'dom:loaded', initLazierLoad, false);
		}
		

/* /assets/ctx/0.8/js/ProtoTabs.js */;
/*  Prototabs
 *  (c) 2007 James Starmer
 *
 *  Prototabs is freely distributable under the terms of an MIT-style license.
 *  For details, see the web site: http://www.jamesstarmer.com/prototabs
 *
/*--------------------------------------------------------------------------*/

var ProtoTabs = Class.create();
ProtoTabs.prototype = {
	
	initialize: function(element, options) {
		this.options = Object.extend({
			defaultPanel: '',
			ajaxUrls: 			{},
			ajaxLoadingText: 	'Loading...'	
		}, options || {});
		
		this.currentTab = '';
		
		this.element = $(element);
		this.listElements = $A(this.element.getElementsByTagName('LI'));

		//loop over each list element
		for(i = 0; i < this.listElements.length; i++) {	
			
			//get the tabs
			tabLI = this.listElements[i];
			var itemLinks = tabLI.getElementsByTagName('A');
			tabLI.itemId = itemLinks[0].href.split("#")[1];
			tabLI.linkedPanel = $(tabLI.itemId);
			tabLI.linkedPanel.style.clear = "both";		//firefox hack

			//check for the intially active tab
			if((this.options.defaultPanel != '') && (this.options.defaultPanel == tabLI.itemId)){
				this.openPanel(tabLI);
			}else{
				$($(tabLI).linkedPanel).hide();
			}
			
			// watch for clicked
			$(itemLinks[0]).observe('click', function(event){
					element = Event.findElement(event, 'LI');
					this.openPanel(element);					
					Event.stop(event); // like return false;
			}.bind(this));
		}
		
	},
	
	openPanel: function(tab){
		tab = $(tab); // ie hack
		
		if(this.currentTab != ''){
			this.currentTab.linkedPanel.hide();
			this.currentTab.removeClassName('selected');
		}
		
		//set the currently open panel to the new panel
		this.currentTab = tab;
		
		tab.linkedPanel.show();
		tab.addClassName('selected');
		var url = this.options.ajaxUrls[tab.itemId];
		
		// if there is an ajax url defined update the panel with ajax
		if(url != undefined){
			tab.linkedPanel.update(this.options.ajaxLoadingText);
			new Ajax.Request(url,{
				onComplete: function(transport) {
					tab.linkedPanel.update(transport.responseText);
				}
			});
		}
		
	}
};
/* /assets/ctx/0.8/js/CookieJar.js */;
/**
 * Javascript code to store data as JSON strings in cookies. 
 * It uses prototype.js 1.5.1 (http://www.prototypejs.org)
 * 
 * Author : Lalit Patel
 * Website: http://www.lalit.org/lab/jsoncookies
 * License: Apache Software License 2
 *          http://www.apache.org/licenses/LICENSE-2.0
 * Version: 0.5
 * Updated: Jan 26, 2009 
 * 
 * Chnage Log:
 *   v 0.5
 *   -  Changed License from CC to Apache 2
 *   v 0.4
 *   -  Removed a extra comma in options (was breaking in IE and Opera). (Thanks Jason)
 *   -  Removed the parameter name from the initialize function
 *   -  Changed the way expires date was being calculated. (Thanks David)
 *   v 0.3
 *   -  Removed dependancy on json.js (http://www.json.org/json.js)
 *   -  empty() function only deletes the cookies set by CookieJar
 */

var CookieJar = Class.create();

CookieJar.prototype = {

	/**
	 * Append before all cookie names to differntiate them.
	 */
	appendString: "__CJ_",

	/**
	 * Initializes the cookie jar with the options.
	 */
	initialize: function(options) {
		this.options = {
			expires: 3600,		// seconds (1 hr)
			path: '',			// cookie path
			domain: '',			// cookie domain
			secure: ''			// secure ?
		};
		Object.extend(this.options, options || {});

		if (this.options.expires != '') {
			var date = new Date();
			date = new Date(date.getTime() + (this.options.expires * 1000));
			this.options.expires = '; expires=' + date.toGMTString();
		}
		if (this.options.path != '') {
			this.options.path = '; path=' + escape(this.options.path);
		}
		if (this.options.domain != '') {
			this.options.domain = '; domain=' + escape(this.options.domain);
		}
		if (this.options.secure == 'secure') {
			this.options.secure = '; secure';
		} else {
			this.options.secure = '';
		}
	},

	/**
	 * Adds a name values pair.
	 */
	put: function(name, value) {
		name = this.appendString + name;
		cookie = this.options;
		var type = typeof value;
		switch(type) {
		  case 'undefined':
		  case 'function' :
		  case 'unknown'  : return false;
		  case 'boolean'  : 
		  case 'string'   : 
		  case 'number'   : value = String(value.toString());
		}
		var cookie_str = name + "=" + escape(Object.toJSON(value));
		try {
			document.cookie = cookie_str + cookie.expires + cookie.path + cookie.domain + cookie.secure;
		} catch (e) {
			return false;
		}
		return true;
	},

	/**
	 * Removes a particular cookie (name value pair) form the Cookie Jar.
	 */
	remove: function(name) {
		name = this.appendString + name;
		cookie = this.options;
		try {
			var date = new Date();
			date.setTime(date.getTime() - (3600 * 1000));
			var expires = '; expires=' + date.toGMTString();
			document.cookie = name + "=" + expires + cookie.path + cookie.domain + cookie.secure;
		} catch (e) {
			return false;
		}
		return true;
	},

	/**
	 * Return a particular cookie by name;
	 */
	get: function(name) {
		name = this.appendString + name;
		var cookies = document.cookie.match(name + '=(.*?)(;|$)');
		if (cookies) {
			return (unescape(cookies[1])).evalJSON();
		} else {
			return null;
		}
	},

	/**
	 * Empties the Cookie Jar. Deletes all the cookies.
	 */
	empty: function() {
		keys = this.getKeys();
		size = keys.size();
		for(i=0; i<size; i++) {
			this.remove(keys[i]);
		}
	},

	/**
	 * Returns all cookies as a single object
	 */
	getPack: function() {
		pack = {};
		keys = this.getKeys();

		size = keys.size();
		for(i=0; i<size; i++) {
			pack[keys[i]] = this.get(keys[i]);
		}
		return pack;
	},

	/**
	 * Returns all keys.
	 */
	getKeys: function() {
		keys = $A();
		keyRe= /[^=; ]+(?=\=)/g;
		str  = document.cookie;
		CJRe = new RegExp("^" + this.appendString);
		while((match = keyRe.exec(str)) != undefined) {
			if (CJRe.test(match[0].strip())) {
				keys.push(match[0].strip().gsub("^" + this.appendString,""));
			}
		}
		return keys;
	}
};
/* /assets/ctx/0.8/js/packet.js */;
var foror = {
	utils: {}
};

var dago = {
	slideshow: {}
};
/* /assets/ctx/0.8/js/foror/Utils.js */;
foror.utils.scaleToWidth = function(img, newWidth) {
	var oldHeight = img.height;
	var oldWidth = img.width;
	
	img.height = oldHeight * (newWidth / oldWidth);
	img.width = newWidth;
};

foror.utils.centering = function(container, el, panelHeight) {
	panelHeight = panelHeight ? panelHeight : 0;
	
	var marginTop = (container.getHeight() -  el.getHeight()) / 2 - panelHeight;
	var marginLeft = (container.getWidth() - el.getWidth()) / 2;
	
	if (marginTop < 0) marginTop = 0;
	if (marginLeft < 0) marginLeft = 0;
	
	el.setStyle({marginTop: marginTop + "px", marginLeft: marginLeft + "px"});
};

foror.utils.getWindowWidth = function() {
	var width = 0;

	if (typeof(window.innerWidth) == 'number') {
		// Non-IE
		width = window.innerWidth;
	} else if (document.documentElement && document.documentElement.clientWidth) {
		// IE 6+ in 'standards compliant mode'
		width = document.documentElement.clientWidth;
	} else if (document.body && document.body.clientWidth) {
		// IE 4 compatible
		width = document.body.clientWidth;
	}
  
	return width;	
};

foror.utils.getWindowHeight = function() {
		var height = 0;
	      
  		if (typeof(window.innerHeight) == 'number') {
  			// Non-IE
  			height = window.innerHeight;
  		} else if (document.documentElement && document.documentElement.clientHeight) {
  			// IE 6+ in 'standards compliant mode'
  			height = document.documentElement.clientHeight;
  		} else if (document.body && document.body.clientHeight) {
  			// IE 4 compatible
  			height = document.body.clientHeight;
  		}
      
  		return height;	
};

foror.utils.imageMime = function(src) {
	var mime = src.substring(5, src.indexOf(";"));
	
	if (mime.startsWith("http")) {
		if (src.endsWith("png")) {
			mime = "image/png";
		} else if (src.endsWith("jpg") || src.endsWith("jpeg")) {
			mime = "image/jpeg";
		} else if (src.endsWith("gif")) {
			mime = "image/gif";
		} else {
			mime = undefined;
		}		
	}
	
	return mime;
};

foror.utils.encodeURL = function(input) {
	if (input == null)
		return this.ENCODED_NULL;

	if (input == "")
		return this.ENCODED_BLANK;
	
	var length = input.length;
	var output = "";
	
	 for (var i = 0; i < length; i++) {
		 var ch = input.charAt(i);
		 
		 if (ch == '$') {
			 output += "$$";
			 continue;
		 }
		 
		 if (ch == '/') {
			 output += "$002f";
			 continue;
		 }
		 
		 output += ch;
	 }
	
	return encodeURIComponent(output);
};

foror.utils.encodeURL.ENCODED_NULL = "$N";
foror.utils.encodeURL.ENCODED_BLANK = "$B";

foror.utils.aroundScreenRatio = function(customWidth, customHeight) {
	var rawRatio = customWidth && customHeight ? customWidth / customHeight 
			: screen.width / screen.height;
	
	if (rawRatio > (16 / 9 + 4 / 3) / 2) {
		return foror.utils.RATIO_169;
	} else {
		return foror.utils.RATIO_43;
	}
};

foror.utils.RATIO_169 = "169";
foror.utils.RATIO_43 = "43";

foror.utils.isAroundRatio169 = function(customWidth, customHeight) {
	return foror.utils.aroundScreenRatio(customWidth, customHeight) == foror.utils.RATIO_169; 
};

foror.utils.isAroundRatio43 = function(customWidth, customHeight) {
	return foror.utils.aroundScreenRatio(customWidth, customHeight) == foror.utils.RATIO_43; 
};

foror.utils.CookieJar = new CookieJar({expires:31536000, path: '/'});

foror.utils.parseBoolean = function(value) {
	var isTrue = (value === 'true');
	return isTrue ? true : false;
};

foror.utils.isGoogleChrome = function(version) {
	return navigator.userAgent.search("Chrome/" + version) > 0;
};

/* /assets/ctx/0.8/js/foror/Submenu.js */;
foror.Submenu = function() {};

foror.Submenu.prototype.bind = function(parentClass, menuClass) {
	var submenuTimeoutId;
	var submenuDelay = 0.3;
	var menu = new Array($$("." + parentClass)[0], $$("." + menuClass)[0]);
	var submenu = menu.last();
	
	if (submenu == undefined) {
		return;
	}
	
	submenu.hide();
	
	menu.each(function(el) {
		el.observe("mouseenter", function() {
			if (submenuTimeoutId != null) {
				clearTimeout(submenuTimeoutId);
				submenuTimeoutId = null;
			}
			
			submenu.show();
		});
		
		el.observe("mouseleave", function() {
			if (submenuTimeoutId == null) {
				submenuTimeoutId = submenu.hide.bind(submenu).delay(submenuDelay);
			}
		});
	});
};
/* /assets/ctx/0.8/js/dago/ImageEditor.js */;
dago.ImageEditor = function(saveLink, cropCoords169, originalWidth) {
	this.initRes();
	this.bindEvents();
	
	this.ratio = foror.utils.RATIO_169;
	this.SAVE_LINK = saveLink;
	this.originalWidth = originalWidth;
	
	if (cropCoords169) {
		this.cropperCoords = cropCoords169;
	}
	
	this.initCropper(16, 9, this.scaleCoords(this.originalWidth, this.EDITOR_WIDTH));
};

dago.ImageEditor.prototype.EDITOR_WIDTH = 710;
dago.ImageEditor.prototype.SAVE_LINK;

dago.ImageEditor.prototype.cropper;
dago.ImageEditor.prototype.cropperCoords;

dago.ImageEditor.prototype.ratio;
dago.ImageEditor.prototype.updateCount = 0;

dago.ImageEditor.prototype.originalWidth;

dago.ImageEditor.prototype.res = {};

dago.ImageEditor.prototype.initRes = function() {
	this.res.img    = $("img");
	this.res.update = $("update-coords");
	this.res.submit = $("submit");
};

dago.ImageEditor.prototype.bindEvents = function() {
	this.res.update.observe('click', this.saveCoords.bind(this));		
};

dago.ImageEditor.prototype.saveCoords = function() {
	this.res.update.disable();
	
	var coords = Object.toJSON(this.scaleCoords(this.EDITOR_WIDTH, this.originalWidth));
	var link = this.SAVE_LINK.replace("-context-", this.ratio + "/" + coords);
	
	Tapestry.ajaxRequest(link, function(transport) {
		this.cropperCoords = transport.responseJSON;
		
		if (this.ratio == foror.utils.RATIO_169) {
			this.initCropper(4, 3, this.scaleCoords(this.originalWidth, this.EDITOR_WIDTH));
			this.ratio = foror.utils.RATIO_43;
		} else if (this.ratio == foror.utils.RATIO_43) {
			this.initCropper(16, 9, this.scaleCoords(this.originalWidth, this.EDITOR_WIDTH));
			this.ratio = foror.utils.RATIO_169;
		}
		
		this.res.update.enable();
		
		if (++this.updateCount == 2) {
			this.res.submit.enable();
		}
	}.bind(this));
};

dago.ImageEditor.prototype.initCropper = function(ratioX, ratioY, coords) {
	if (this.cropper != null) {
		this.cropper.remove();
	}
	
	this.cropper = new Cropper.Img(this.res.img.identify(), {
			onEndCrop: this.onEndCrop.bind(this),
			displayOnInit: true,
			onloadCoords: coords ? coords : { x1: 0, y1: 0, x2: 160 * (ratioX/ratioY), y2: 160 },
			ratioDim: { x: ratioX, y: ratioY }	
		}
	);	
};

dago.ImageEditor.prototype.onEndCrop = function(coords, dimensions) {
	this.cropperCoords = coords;
};

dago.ImageEditor.prototype.scaleCoords = function(fromWidth, toWidth) {
	if (!this.cropperCoords || this.originalWidth <= this.EDITOR_WIDTH) {
		return this.cropperCoords;
	}
	
	var coords = Object.clone(this.cropperCoords);
	var k = toWidth / fromWidth;
	
	coords.x1 = coords.x1 * k;
	coords.x2 = coords.x2 * k;
	coords.y1 = coords.y1 * k;
	coords.y2 = coords.y2 * k;
	
	return coords;
};

/* /assets/ctx/0.8/js/dago/ImageFullscreener.js */;
dago.ImageFullscreener = function() {
	this.initRes();
	
	this.width = this.res.img.width + 32;
	this.height = this.res.img.height;
	
	this.windowWidth = $$("body")[0].getWidth();
	
	this.resizeToWindow(true);
	this.bindEvents();
};

dago.ImageFullscreener.prototype.DEFAULT_STYLE = "notscaled-img";
dago.ImageFullscreener.prototype.FULLSCREEN_STYLE = "notscaled-img-full";

/**
 * Original image width and height
 */
dago.ImageFullscreener.prototype.width;
dago.ImageFullscreener.prototype.height;

dago.ImageFullscreener.prototype.windowWidth;

dago.ImageFullscreener.prototype.res = {};

dago.ImageFullscreener.prototype.initRes = function() {
	this.res.img = $$("." + this.DEFAULT_STYLE).first();
};

dago.ImageFullscreener.prototype.resizeToWindow = function(force) {
	if (this.width > this.windowWidth || force) {
		foror.utils.scaleToWidth(this.res.img, this.windowWidth);
		
		this.res.img.removeClassName(this.DEFAULT_STYLE);
		this.res.img.addClassName(this.FULLSCREEN_STYLE);
	}
};	

dago.ImageFullscreener.prototype.bindEvents = function() {
	this.res.img.observe('click', this.resizing.bind(this));		
};

dago.ImageFullscreener.prototype.resizing = function() {
	if (this.res.img.width == this.windowWidth) {
		this.res.img.width = this.width;
		this.res.img.height = this.height;
		
		if (this.res.img.width >= this.windowWidth) {
			this.res.img.removeClassName(this.DEFAULT_STYLE);
			this.res.img.addClassName(this.FULLSCREEN_STYLE);
		} else {
			this.res.img.removeClassName(this.FULLSCREEN_STYLE);
			this.res.img.addClassName(this.DEFAULT_STYLE);
		}
	} else {
		this.resizeToWindow(true);
	}
};
/* /assets/ctx/0.8/js/dago/ImageTooltip.js */;
dago.ImageTooltip = function(elements, fadeByTimeout) {
	$$("body")[0].insert(this.tpl.evaluate({}));

	this.fadeByTimeout = fadeByTimeout;
	this.images = elements;
	
	this.initRes();
	this.bindEvents();
	this.updateImageLinks();
};

dago.ImageTooltip.prototype.images = null;

dago.ImageTooltip.prototype.fadeByTimeout = false;

dago.ImageTooltip.prototype.fadeTimeout = null;
dago.ImageTooltip.prototype.hideTimeout = null;

dago.ImageTooltip.prototype.tpl = new Template(
	'<div id="tooltip" class="notice" style="display:none;"></div>'
);

dago.ImageTooltip.prototype.menuTpl = new Template(
	'<ul class="resetul">' 
	+ '<li><a href="/desktop-wallpaper/#{id}">#{get_wallpaper}</a></li>'
	+ '<li id="setopenbig"><span class="link">#{set_open_big}</span></li>'
	+ '<li id="unsetopenbig"><span class="link">#{unset_open_big}</span></li>'
	+ '<li id="openbig"><a href="/#{type}/big/#{id}">#{open_big}</a></li>'
	+ '<li id="openabout"><a href="/#{type}/#{id}">#{open_about}</a></li>' +
	'</ul>'
);

dago.ImageTooltip.prototype.res = {};

dago.ImageTooltip.prototype.initRes = function() {
	this.res.tooltip = $('tooltip');
};

dago.ImageTooltip.prototype.i18n_en = {
	get_wallpaper: "Get Desktop Wallpaper",
	set_open_big: "Set open big by default",
	unset_open_big: "Unset open big",
	open_big: "Open Big Image",
	open_about: "About Image"
};

dago.ImageTooltip.prototype.bindEvents = function() {
	this.images.each(function(el) {		
		el.observe("mouseover", this.overImage.bind(this));
		el.observe("mouseout", this.outImage.bind(this));
	}.bind(this));
	
	this.res.tooltip.observe("mouseover", this.overTooltip.bind(this));
};

dago.ImageTooltip.prototype.overImage = function(e) {
	clearTimeout(this.hideTimeout);
	
	var offset = e.element().cumulativeOffset();
	var link = e.element().up().readAttribute("href");
	
	if (!link)
		link = document.location.href;	
	
	var id = link.substr(link.lastIndexOf("/") + 1);
	
	this.buildMenu(id, e.element());
	
	this.res.tooltip.setStyle({top:offset.top + 15 + "px", left:offset.left + 15 + "px"});
	this.res.tooltip.show();
	
	if (this.fadeByTimeout) {
		if (this.fadeTimeout) 
			clearTimeout(this.fadeTimeout);
		
		this.fadeTimeout = setTimeout(function() {
			this.res.tooltip.fade();
		}.bind(this), 3000);
	}
};

dago.ImageTooltip.prototype.buildMenu = function(id, img) {
	this.res.tooltip.update(this.menuTpl.evaluate(
			Object.extend({id:id, type:this.imageType(img)}, this.i18n_en)));	
	
	var setOpenBig = function(img, e) {
		$("unsetopenbig").show();
		$("setopenbig").hide();
		
		if (e) {
			this.setOpenBigByDefault(true);
			this.updateImageLinks();
			this.showImageTooltipLink(img);
		}
	};
	
	var unsetOpenBig = function(img, e) {
		$("unsetopenbig").hide();
		$("setopenbig").show();
		
		if (e) {
			this.setOpenBigByDefault(false);
			this.updateImageLinks();
			this.showImageTooltipLink(img);
		}
	};
	
	if (this.isOpenBigByDefault()) {
		setOpenBig.bind(this)();
	} else {
		unsetOpenBig.bind(this)();
	}
	
	this.showImageTooltipLink(img);
	
	$("unsetopenbig").observe("click", unsetOpenBig.bind(this, img));
	$("setopenbig").observe("click", setOpenBig.bind(this, img));
};

dago.ImageTooltip.prototype.outImage = function(e) {
	this.hideTimeout = setTimeout(function() {
		this.res.tooltip.hide();
	}.bind(this), 250);
};

dago.ImageTooltip.prototype.overTooltip = function(e) {
	clearTimeout(this.hideTimeout);
	clearTimeout(this.fadeTimeout);
};

dago.ImageTooltip.prototype.imageType = function(img) {
	return img.hasClassName("photo") ? "photo" : "picture";
};

dago.ImageTooltip.prototype.showImageTooltipLink = function(img) {
	if (img.up().readAttribute("href")) {
		if (img.hasClassName("scaled-img")) {
			$("openbig").show();
			$("openabout").hide();
		} else if (img.up().readAttribute("href").indexOf("/big/") > 0) {
			$("openbig").hide();
			$("openabout").show();
		} else {
			$("openbig").show();
			$("openabout").hide();
		}
	} else {
		$("openbig").hide();
		$("openabout").show();
	}
};

dago.ImageTooltip.prototype.updateImageLinks = function() {
	if (this.isOpenBigByDefault()) {
		this.images.each(function(el) {		
			var anchor = el.up();
			
			if (anchor.readAttribute("href")) {
				var imgLink = anchor.readAttribute("href");
				var id = imgLink.substr(imgLink.lastIndexOf("/") + 1);
				
				anchor.writeAttribute("href", "/"+ this.imageType(el) +"/big/" + id);
			}
		}.bind(this));
	} else {
		this.images.each(function(el) {	
			if (el.hasClassName("scaled-img")) return;
			
			var anchor = el.up();
			
			if (anchor.readAttribute("href")) {
				anchor.writeAttribute("href", 
						anchor.readAttribute("href").replace("big/", ""));
			}
		});
	}
};

dago.ImageTooltip.prototype.setOpenBigByDefault = function(value) {
	foror.utils.CookieJar.put("dago.ImageTooltip.openbig", value);
};

dago.ImageTooltip.prototype.isOpenBigByDefault = function() {
	return foror.utils.parseBoolean(foror.utils.CookieJar.get("dago.ImageTooltip.openbig"));
};

/* /assets/ctx/0.8/js/dago/Wallpaper.js */;
dago.Wallpaper = function(area, serverImgSrc, coordsRatio169, coordsRatio43) {
	this.initRes();

	this.showDnDSupportNotice();
		
	this.area = area;
	this.areaHeight = this.area.getHeight();
	this.areaWidth = this.area.getWidth();
	
	this.res.resolution.value = this.resolution.x + "x" + this.resolution.y;
	
	this.bindEvents();
	
	if (serverImgSrc) {
		this.loadServerImg(serverImgSrc);
	}
	
	this.coordsRatio169 = coordsRatio169;
	this.coordsRatio43 = coordsRatio43;
};

dago.Wallpaper.prototype.area = null;
dago.Wallpaper.prototype.areaHeight = null;
dago.Wallpaper.prototype.areaWidth = null;

dago.Wallpaper.prototype.cropper = null;
dago.Wallpaper.prototype.cropperCoords = null;
dago.Wallpaper.prototype.coordsRatio169 = null;
dago.Wallpaper.prototype.coordsRatio43 = null;

dago.Wallpaper.prototype.originalImgWidth = null;
dago.Wallpaper.prototype.originalImgHeight = null;
dago.Wallpaper.prototype.resolution = {x:screen.width, y:screen.height};

dago.Wallpaper.prototype.serverImgSrc = null;
dago.Wallpaper.prototype.extImgDuplicate = null;
dago.Wallpaper.prototype.imgDuplicateLoaded = false;
dago.Wallpaper.prototype.intervalId = null;

dago.Wallpaper.prototype.res = {};

dago.Wallpaper.prototype.initRes = function() {
	this.res.preloader   = $("preloader");
	this.res.intro       = $("intro");
	this.res.img         = $("img");
	this.res.result      = $("result");
	this.res.resultImg   = $$("#result img")[0];
	this.res.restore	 = $("restore");
	this.res.generate    = $("generate");
	this.res.reset       = $("reset");
	this.res.resolution  = $("resolution");
	this.res.canvas      = $("canvas");
	this.res.form        = $("wallpaperForm");
	this.res.submit      = $("submit");
	this.res.cropWrap    = null;
	this.res.dndSupport  = $("dnd-support");
	
	this.res.ajaxCropUrl     = "/wallpaper:crop";
	this.res.ajaxGetRelative = "/wallpaper:getRelative";
};

dago.Wallpaper.prototype.i18n_en = {
	ajax_crop_error: "Could not crop the image on the server. Perhaps the server is overloaded, try a bit later.",
	dnd_img_only: "Unable to process the downloaded image, make sure that the image format is JPG, PNG or GIF",
	error_ext_img_download: "Failed to upload the image to the server from your link. Perhaps the server is overloaded, try refreshing the page after 5-10 seconds.",
	dnd_not_support: "Sorry, your browser does not support the drag & drop for files and images from your folders. Use the form below.",
	dnd_support_but: "Your browser supports drag & drop, but because of <a href='http://code.google.com/p/chromium/issues/detail?id=44820'>Issue 44820</a> with data URI this feature not stable. At this moment it is better to use the form below.",
	dnd_support: "You browser support drag & drop for files and images. Drag the image from any folder in this area to make desktop wallpaper.",
	only_online: "Sorry, the upload on the server only online"
};

dago.Wallpaper.prototype.bindEvents = function() {
	if (this.isDnDFileSupport()) {
		this.area.observe("drop", this.drop.bind(this));
		this.area.observe("dragover", this.dragOver.bind(this));
		this.area.observe("dragleave", this.dragLeave.bind(this));
	}
	
	this.res.restore.observe("click", this.restore.bind(this));
	this.res.generate.observe("click", this.generate.bind(this));
	this.res.reset.observe("click", this.reset.bind(this));
	
	new Form.Element.Observer(
		this.res.resolution.identify(),
		2,
		this.resolutionChanged.bind(this)
	);
	
	this.res.form.observe("submit", this.submit.bind(this));
};

dago.Wallpaper.prototype.isIssue44820 = function() {
	return foror.utils.isGoogleChrome("5") 
				|| foror.utils.isGoogleChrome("6")
				|| foror.utils.isGoogleChrome("7")
				|| foror.utils.isGoogleChrome("8")
				|| foror.utils.isGoogleChrome("9")
				|| foror.utils.isGoogleChrome("10")
				|| foror.utils.isGoogleChrome("11")
				|| foror.utils.isGoogleChrome("12")
				|| foror.utils.isGoogleChrome("13")
				|| foror.utils.isGoogleChrome("14");
};

dago.Wallpaper.prototype.showDnDSupportNotice = function() {
	if (this.isIssue44820()) {
		this.res.dndSupport.addClassName("error");
		this.res.dndSupport.update(this.i18n_en.dnd_support_but);		
	} else if (this.isDnDFileSupport()) {
		this.res.dndSupport.addClassName("success");
		this.res.dndSupport.update(this.i18n_en.dnd_support);
	} else {
		this.res.dndSupport.addClassName("error");
		this.res.dndSupport.update(this.i18n_en.dnd_not_support);
	}	
};

dago.Wallpaper.prototype.loadServerImg = function(serverImgSrc) {
	this.serverImgSrc = serverImgSrc;
	var img = new Image();
	
	img.onload = function(img) {
		if (this.serverImgSrc.startsWith("http")) {
			this.extImgDuplicate = new Image();
			var url = this.res.ajaxGetRelative + "/" + foror.utils.encodeURL(this.serverImgSrc);
			
			Tapestry.ajaxRequest(url, function(transport) {
				var json = transport.responseJSON;
				
				if (json.relativePath) {
					this.serverImgSrc = json.relativePath;
					
					this.extImgDuplicate.onerror = function() {
						alert(this.i18n_en.error_ext_img_download);
						this.reset(null, false);
					}.bind(this);
					
					this.extImgDuplicate.onload = function() {
						this.imgDuplicateLoaded = true;
					}.bind(this);
					
					this.extImgDuplicate.src = this.serverImgSrc;
				} else {
					alert(this.i18n_en.error_ext_img_download);
					this.reset(null, false);
				}
			}.bind(this));
		}
		
		this.showImage(img);
	}.bind(this, img);
	
	img.src = serverImgSrc;
	
	this.showPreloader();	
};

dago.Wallpaper.prototype.drop = function(e) {
	e.stopPropagation();
    e.preventDefault();
    
	var file = e.dataTransfer.files[0];
	
	if (file.type.match('image.*')) {
		this.serverImgSrc = null;
		this.coordsRatio169 = null;
		this.coordsRatio43 = null;
		
		this.reset(null, true);
		this.loadImageDnD(file);
	} else {
		alert(this.i18n_en.dnd_img_only);
	}
};

dago.Wallpaper.prototype.dragLeave = function(e) {
	e.stopPropagation();
    e.preventDefault();
    this.area.setStyle({background:"#ddd"});
};

dago.Wallpaper.prototype.dragOver = function(e) {
	e.stopPropagation();
    e.preventDefault();
    this.area.setStyle({background:"#E7FAD5"});
};

dago.Wallpaper.prototype.isDnDFileSupport = function() {
	return window.File && window.FileReader && window.FileList;
};

dago.Wallpaper.prototype.isCanvasSupport = function() {
	if (this.isIssue44820() && this.serverImgSrc != null) {
		return false;
	}
	
	return this.res.canvas.toDataURL;
};

dago.Wallpaper.prototype.loadImageDnD = function(file) {
	var reader = new FileReader();
	
	reader.onload = function(e) {
		var img = new Image();
		img.onload = this.showImage.bind(this, img);
		img.src = e.target.result;
		
		this.showPreloader();
	}.bind(this);
		
	reader.readAsDataURL(file);
};

dago.Wallpaper.prototype.showPreloader = function() {
	this.res.intro.hide();
	
	if (this.res.cropWrap) {
		this.res.img.hide();
		this.res.cropWrap.hide();
	}
	
	this.area.setStyle({background:"black", height:this.areaHeight + "px"});
	
	foror.utils.centering(this.area, this.res.preloader);
	this.res.preloader.show();
};

dago.Wallpaper.prototype.swapPreloaderImg = function() {
	this.res.preloader.hide();
	this.res.img.show();
	this.res.cropWrap.show();
	this.res.generate.enable();
};

dago.Wallpaper.prototype.showImage = function(img) {	
	this.res.img.src = img.src;
	
	this.res.img.width = img.width;
	this.res.img.height = img.height;	
	this.originalImgWidth = img.width;
	this.originalImgHeight = img.height;
	
	foror.utils.scaleToWidth(this.res.img, this.areaWidth);
	this.area.setStyle({lineHeight:0, height:this.res.img.height + "px"});
	
	this.res.preloader.hide();
	this.res.img.style.visibility = "visible"; // IE hack
	this.res.img.show();
	this.res.generate.enable();
	this.res.reset.enable();
	
	this.initCropper();
};

dago.Wallpaper.prototype.initCropper = function() {
	this.cropper = new Cropper.Img(this.res.img.identify(), {
		onEndCrop: this.onEndCrop.bind(this),
		displayOnInit: true,
		onloadCoords: this.buildCropCoords(),
		ratioDim: { x: this.resolution.x, y: this.resolution.y }	
	});
	
	this.res.cropWrap = $$(".imgCrop_wrap")[0];
};

dago.Wallpaper.prototype.buildCropCoords = function() {
	if (this.coordsRatio169 == null) {
		var	x2 = this.areaWidth;
		var	y2 = x2 / (this.resolution.x/this.resolution.y);
		
		return { x1: 0, y1: 0, x2: x2, y2: y2};		
	}
	
	if (this.cropperCoords) {
		return this.cropperCoords;
	}
	
	var ratio = this.areaWidth / this.originalImgWidth;
	var coords;
	
	if (this.coordsRatio169 
			&& foror.utils.isAroundRatio169(this.resolution.x, this.resolution.y)) {
		coords = Object.clone(this.coordsRatio169);
	} else {
		coords = Object.clone(this.coordsRatio43);
	}
	
	coords.x1 *= ratio;
	coords.x2 *= ratio;
	coords.y1 *= ratio;
	coords.y2 *= ratio;
	
	return coords;
};

dago.Wallpaper.prototype.onEndCrop = function(coords, dimensions) {
	this.cropperCoords = Object.clone(coords);
	var ratio = this.originalImgWidth / this.areaWidth;
	
	this.cropperCoords.x1 *= ratio;
	this.cropperCoords.x2 *= ratio;
	this.cropperCoords.y1 *= ratio;
	this.cropperCoords.y2 *= ratio;
};

dago.Wallpaper.prototype.restore = function(e) {
	this.res.result.hide();
	this.swapPreloaderImg();
	this.area.setStyle({height:this.res.img.height + "px"});
	this.area.show();
	
	this.res.restore.hide();
	this.res.generate.show();
	
	this.extImgDuplicate = null;
	this.imgDuplicateLoaded = false;
};

dago.Wallpaper.prototype.reset = function(e, fromDrop) {
	if (this.res.restore.visible()) {
		this.res.restore.hide();
		this.res.generate.show();
		this.area.show();
	}
	
	this.res.generate.disable();
	this.res.reset.disable();
	this.res.submit.enable();
	
	this.res.preloader.hide();
	this.res.img.hide();
	this.res.result.hide();
	
	if (this.cropper) {
		this.cropperCoords = null;
		this.res.cropWrap = null;
		this.cropper.remove();
		this.cropper = null;
	}
	
	this.area.setStyle({background:"#ddd", lineHeight:1.5+"em", 
		height:this.areaHeight + "px"});
	
	if (!fromDrop) {
		this.res.intro.show();
	}
	
	this.extImgDuplicate = null;
	this.imgDuplicateLoaded = false;
};

dago.Wallpaper.prototype.generate = function(e) {
	var src;
	this.res.generate.disable();
	
	this.showPreloader();
	
	if (this.extImgDuplicate) {
		this.intervalId = setInterval(function(intervalId) {
			if (this.imgDuplicateLoaded) {
				clearInterval(this.intervalId);
				
				this.res.img.onload = function() {
					if (this.isCanvasSupport()) {
						this.showResult(this.canvasCropping());
					} else {
						this.serverCropping();
					}
				}.bind(this);
				
				this.res.img.src = this.extImgDuplicate.src;
			}
		}.bind(this), 500);
	} else {
		if (this.isCanvasSupport()) {
			this.showResult(this.canvasCropping());
		} else {
			this.serverCropping();
		}
	}
};

dago.Wallpaper.prototype.showResult = function(src) {
	this.res.resultImg.onload = function() {
		var imgWidth = this.res.resultImg.width;
		var docWidth = document.viewport.getWidth();
		
		this.area.hide();
		this.res.generate.hide();
		this.res.restore.show();
		
		var left = 0;
		
		if (imgWidth > docWidth) {
			left = (docWidth - this.areaWidth) / 2;
		}
		
		this.res.resultImg.setStyle({marginLeft:"-" + left + "px"});
		this.res.result.show();
	}.bind(this);
	
	this.res.resultImg.src = src;
	
	// Chrome hack
	if (this.res.resultImg.complete && !this.res.result.visible()) {
		this.res.resultImg.onload();
	}
};

dago.Wallpaper.prototype.canvasCropping = function() {
	var ctx = this.res.canvas.getContext('2d');
	
	this.res.canvas.width = this.resolution.x;
	this.res.canvas.height = this.resolution.y;	
	
	ctx.drawImage(this.res.img, 
			this.cropperCoords.x1, 
			this.cropperCoords.y1, 
			this.cropperCoords.x2 - this.cropperCoords.x1, 
			this.cropperCoords.y2 - this.cropperCoords.y1,
			0,
			0,
			this.resolution.x,
			this.resolution.y);
	
	return this.res.canvas.toDataURL(foror.utils.imageMime(this.res.img.src));	
};

dago.Wallpaper.prototype.serverCropping = function() {
	var url = this.res.ajaxCropUrl 
		+ "/" + foror.utils.encodeURL(this.serverImgSrc) 
		+ "/" + foror.utils.encodeURL(Object.toJSON(this.cropperCoords)) 
		+ "/" + this.res.resolution.value; 
	
	Tapestry.ajaxRequest(url, function(transport) {
		var json = transport.responseJSON;
		
		if (json.path) {
			this.showResult(json.path);
		} else {
			alert(this.i18n_en.ajax_crop_error);
			this.swapPreloaderImg();
		}
	}.bind(this));
};

dago.Wallpaper.prototype.resolutionChanged = function() {
	var result = this.res.resolution.value.split("x");
	
	if (result.length > 1) {
		this.resolution.x = parseInt(result[0]);
		this.resolution.y = parseInt(result[1]);
		
		if (isNaN(this.resolution.x) 
				|| isNaN(this.resolution.y)
				|| this.resolution.x <= 0 
				|| this.resolution.y <= 0) {
			this.res.generate.disable();
		} else if (this.cropper) {
			this.res.generate.enable();
		}
		
		if (this.cropper) {
			this.cropperCoords = null;
			this.cropper.remove();
			this.initCropper();
		}
	} else {
		this.res.generate.disable();
	}
};

dago.Wallpaper.prototype.submit = function(e) {
	if (navigator.onLine != undefined && !navigator.onLine) {
		alert(this.i18n_en.only_online);
		return false;
	}
	
	this.res.submit.disable();
	this.showPreloader();
	
	return true;
};
/* /assets/ctx/0.8/js/dago/ImageBanner.js */;
dago.ImageBanner = function() {
	var image = $$(".picture").first();
	
	if (image == null) {
		image = $$(".photo").first();
	}
	
	if ($("big-img-left-banner") != null) {
		$("close").observe("click", function() {
			$("big-img-left-banner").hide();
		});		
		
		if (image.getHeight() - 390 <= 0) {
			$("big-img-left-banner").setStyle({
				top:"-30px"
			});
			
			return;
		}		
		
		window.onscroll = function(image) {		
			var panelOffset = 360;
			var offset = document.viewport.getScrollOffsets().top;
			var nextOffset = 360 + offset;
			
			if (this.timeout == null && panelOffset - offset < 0 && image.getHeight() - nextOffset > 200) {
				this.timeout = setTimeout(function() {
					$("big-img-left-banner").setStyle({
						top: (80 + document.viewport.getScrollOffsets().top) + "px"
					});
					
					this.timeout = null;
				}.bind(this), 50);
			}
		}.bind(this, image);
	}
};
/* /assets/ctx/0.8/js/dago/slideshow/Settings.js */;
dago.slideshow.Settings = function(player) {
	this.player = player;
};

dago.slideshow.Settings.loaded = false;
dago.slideshow.Settings.value;

dago.slideshow.Settings.prototype.MIN_DELAY = 2;
dago.slideshow.Settings.prototype.MIN_BUFFER = 1;
dago.slideshow.Settings.prototype.MAX_BUFFER = 10;

dago.slideshow.Settings.prototype.LOAD_FRAME_LINK = "/player:loadFrameSettings";
dago.slideshow.Settings.prototype.LOAD_LINK = "/player:loadSettings";

dago.slideshow.Settings.prototype.load = function() {
	var link = this.player.frameMode ? this.LOAD_FRAME_LINK : this.LOAD_LINK;
	
	if (this.player.galleryId) link += "?t:ac=" + this.player.galleryId;
	
	Tapestry.ajaxRequest(link, function(transport) {
		this.value = transport.responseJSON;
		
		if (!this.player.frameMode) {
			this.player.settingPanel.reset();
		}
		
		this.loaded = true;
		this.player.showImage();
	}.bind(this));
};
/* /assets/ctx/0.8/js/dago/slideshow/BottomPanel.js */;
dago.slideshow.BottomPanel = function(player) {
	this.player = player;
	
	this.initRes();
	this.bindEvents();
	
	new PeriodicalExecuter(function(pe) {
		if (this.player.settings.value.panelAutoHide) this.tryHide();
	}.bind(this), this.player.MIN_PERIOD);
};

dago.slideshow.BottomPanel.prototype.SHOW_TIMEOUT = 6000;
dago.slideshow.BottomPanel.prototype.EFFECT_DURATION = 0.5;

dago.slideshow.BottomPanel.prototype.player;
	
dago.slideshow.BottomPanel.prototype.mouseMove = 0;
dago.slideshow.BottomPanel.prototype.mouseOver = false;
dago.slideshow.BottomPanel.prototype.showTime = new Date().getTime();
dago.slideshow.BottomPanel.prototype.lock = false;

dago.slideshow.BottomPanel.prototype.res = {};

dago.slideshow.BottomPanel.prototype.initRes = function() {
	this.res.panel  = $("fs-player-panel");
	this.res.info   = $$("#fs-player-info .img-info").first();
	this.res.fav    = $$("#fs-player-favorite .vote").first();
	this.res.rating = $$("#fs-player-rating .vote").first();
};
	
dago.slideshow.BottomPanel.prototype.bindEvents = function() {
    this.player.res.area.observe('mousemove', function() {
    	this.mouseMove++;
    	
    	if (this.mouseMove > 2) {
    		this.show();
    		this.mouseMove = 0;
    	}
    }.bind(this));
	
	this.res.panel.observe('mouseenter', function() {
		this.mouseOver = true;
	}.bind(this));
	
	this.res.panel.observe('mouseleave', function() {
		this.mouseOver = false;
	}.bind(this));
};

dago.slideshow.BottomPanel.prototype.tryHide = function() {
	if (new Date().getTime() - this.showTime < this.SHOW_TIMEOUT) return;
	
	if (!this.lock && !this.mouseOver && this.res.panel.visible()) {	
		this.lock = true;
		this.player.res.area.style.cursor = "none";
		
		new Effect.BlindUp(this.res.panel, { 
			afterFinish: function() {
				this.res.panel.hide();
				this.lock = false;
			}.bind(this),
			
			duration: this.EFFECT_DURATION
		});
	}	
};

dago.slideshow.BottomPanel.prototype.show = function() {
	this.showTime = new Date().getTime();
	
	if (!this.lock && !this.res.panel.visible()) {	
		this.lock = true;
		this.player.res.area.style.cursor = "auto";
		
		//if (!this.player.frameMode && !this.res.info.visible()) {
		//	this.player.notice.show();
		//}
		
		new Effect.BlindDown(this.res.panel, { 
			afterFinish: function() {
				this.res.panel.show();
				this.lock = false;
			}.bind(this),
		
			duration: this.EFFECT_DURATION
		});
	}
};
	
dago.slideshow.BottomPanel.prototype.hideImageInfo = function() {
	if (!this.player.frameMode) {
		this.res.fav.hide();
		this.res.rating.hide();
		this.res.info.hide();
	}
};

dago.slideshow.BottomPanel.prototype.showImageInfo = function() {
	if (!this.player.frameMode) {
		this.res.fav.show();
		this.res.rating.show();
		this.res.info.show();
	}
};
/* /assets/ctx/0.8/js/dago/slideshow/Buttons.js */;
dago.slideshow.Buttons = function(player) {
	this.player = player;
	
	this.initRes();
	this.bindEvents();
};

dago.slideshow.Buttons.prototype.player;
dago.slideshow.Buttons.prototype.lastClickTime;

dago.slideshow.Buttons.prototype.res = {};

dago.slideshow.Buttons.prototype.initRes = function() {
	this.res.settings     = $('fs-player-settings');
	this.res.play         = $('fs-player-play');
	this.res.pause        = $('fs-player-pause');
	this.res.fullscreen   = $('fs-player-fullscreen');
	this.res.nofullscreen = $('fs-player-nofullscreen');
	this.res.rev          = $('fs-player-rev');
	this.res.fwd          = $('fs-player-fwd');
};

dago.slideshow.Buttons.prototype.bindEvents = function() {	
	document.observe('keydown', this.onkeydown.bind(this));
	
	this.res.settings.observe('click', function() {
    	if (this.isDisabled(this.res.settings)) return;
    	
    	this.disable(this.res.settings);
    	this.pause(true);
    	
    	this.player.centering(this.player.settingPanel.res.panel);
    	this.player.settingPanel.res.panel.show();
    }.bind(this));
    
	this.res.pause.observe('click', function() {
    	if (!this.isDisabled(this.res.pause)) this.pause();
    }.bind(this));
    
	this.res.play.observe('click', function() {
    	if (!this.isDisabled(this.res.play)) this.play();
    }.bind(this));
    
    this.res.rev.observe('click', function() {
    	if (!this.isDisabled(this.res.rev)) this.player.revImage();
    }.bind(this));
    
    this.res.fwd.observe('click', function() {
    	if (!this.isDisabled(this.res.fwd)) this.player.fwdImage();
    }.bind(this));
    
    this.res.fullscreen.observe('click', function() {
    	if (this.isDisabled(this.res.fullscreen)) return;
    	
    	this.disable(this.res.fullscreen);
    	this.pause(true);
    	this.player.hidePreloader();
    	
    	this.player.notice.res.messages.show();
    	this.player.notice.res.nosupport.show();
    	
    	this.player.centering(this.player.notice.res.nosupport);
    }.bind(this));
    
    this.res.nofullscreen.observe('click', function() {
    	if (this.isDisabled(this.res.nofullscreen)) return;
    	
    	this.disable(this.res.nofullscreen);
    	this.pause(true);
    	this.player.hidePreloader();
    	
    	this.player.notice.res.messages.show();
    	this.player.notice.res.nosupport.show();
    	
    	this.player.centering(this.player.notice.res.nosupport);
    }.bind(this));
};

dago.slideshow.Buttons.prototype.onkeydown = function(e) {
	switch (e.keyCode) {
	case 37:
    	if (!this.isDisabled(this.res.rev)) this.player.revImage();
		break;
		
	case 39:
    	if (!this.isDisabled(this.res.fwd)) this.player.fwdImage();
    	break;
    	
	case 32:
		if (this.res.pause.visible()) {
	    	if (!this.isDisabled(this.res.pause)) this.pause();
		} else {
	    	if (!this.isDisabled(this.res.play)) this.play();
		}
		
		break;
		
	case 122:
		this.player.notice.closeMessage();
	
		if (this.res.fullscreen.visible()) {
			this.res.fullscreen.hide();
			this.res.nofullscreen.show();
		} else {
			this.res.nofullscreen.hide();
			this.res.fullscreen.show();
		}
		
		break;
		
	default:
		break;
	}	
};

dago.slideshow.Buttons.prototype.enableRev = function() {
	if (this.player.manualShow && this.player.manualIndex == 0) {
		return;
	} else if (!this.player.manualShow && this.player.autoIndex <= 1) {
		return;
	} else if (this.player.advScreenLock) {
		return;
	}
	
	this.enable(this.res.rev);
};

dago.slideshow.Buttons.prototype.play = function() {
	if (this.player.advScreenLock) return;
	
	this.player.stopFastWaiter();
	this.player.stopLowWaiter();
	
	if (this.player.manualShow && this.player.settings.value.playerLoop) {
		this.player.autoIndex = this.player.manualIndex + 1;
	}
	
	this.player.manualShow = false;
	
	this.res.play.hide();
	this.res.pause.show();
	
	this.player.notice.hide();
	this.player.showImage();	
	this.enableRev();
};

dago.slideshow.Buttons.prototype.pause = function(disableSwapButtons) {
	if (this.player.advScreenLock) return;
	if (this.player.timer) this.player.pause();

	this.player.stopFastWaiter();
	this.player.stopLowWaiter();
	
	this.player.notice.queue.push(this.player.i18n_en.pause);
	this.player.notice.show();
	
	if (!disableSwapButtons) {
		this.res.pause.hide();
		this.res.play.show();
	}
};

dago.slideshow.Buttons.prototype.isDisabled = function(button) {
	if (new Date().getTime() - this.lastClickTime <= this.player.MIN_PERIOD * 1000) {
		return;
	} else {
		this.lastClickTime = new Date().getTime();
	}
	
	if (this.player.settingPanel.res.panel.visible()
			|| this.player.notice.res.nosupport.visible()) {
		return true;
	}
	
	return button.readAttribute("disabled") != null;
};

dago.slideshow.Buttons.prototype.disable = function(button) {
	if (this.player.frameMode) return;
	button.writeAttribute("disabled", true);
	
	button.setStyle({opacity:"0.3", cursor:"auto", 
		"-ms-filter":'"progid:DXImageTransform.Microsoft.Alpha(opacity=60)"'});
};

dago.slideshow.Buttons.prototype.enable = function(button) {
	if (this.player.frameMode) return;
	button.writeAttribute("disabled", false);
	
	button.setStyle({opacity:"1", cursor:"pointer",
		"-ms-filter":'"progid:DXImageTransform.Microsoft.Alpha(opacity=100)"'});
};
/* /assets/ctx/0.8/js/dago/slideshow/Notice.js */;
dago.slideshow.Notice = function(player) {
	this.player = player;
	
	this.initRes();
	this.bindEvents();
	
	new PeriodicalExecuter(this.show.bind(this), this.REFRESH_TIME);
};

dago.slideshow.Notice.prototype.REFRESH_TIME = 2;

dago.slideshow.Notice.prototype.player;

dago.slideshow.Notice.prototype.lock = false;
dago.slideshow.Notice.prototype.queue = new Array();

dago.slideshow.Notice.prototype.res = {};

dago.slideshow.Notice.prototype.initRes = function() {
	if (!this.player.frameMode) {
		this.res.close = $('fs-player-close-message');
		this.res.messages = $('fs-player-messages');
		this.res.nosupport = $('nosupport-fs-message');
	}
	
	this.res.info = $("fs-player-info");
	this.res.notice = $$("#fs-player-info > h2.message").first();
	
	if (!this.player.frameMode) {
		this.res.imginfo = $$(".img-info")[0];
	}
};

dago.slideshow.Notice.prototype.bindEvents = function() {
	if (!this.player.frameMode) {
		this.res.close.observe("click", this.closeMessage.bind(this));
	}
};
	
dago.slideshow.Notice.prototype.closeMessage = function() {
	if (!this.res.messages.visible()) return;
	
	this.res.messages.hide();
	this.res.nosupport.hide();
	
	this.player.buttons.enable(this.player.buttons.res.fullscreen);
	this.player.buttons.enable(this.player.buttons.res.nofullscreen);
		
	if (this.player.buttons.res.pause.visible()) {
		this.player.buttons.play();
	} else {
		this.hide();
		if (!this.player.playerImg()) this.player.showImage("FWD");
	}
}; 

dago.slideshow.Notice.prototype.show = function() {
	if (this.lock || !this.check()) return;
	this.lock = true;
	
	this.res.notice.style.left = 0;
	
	if (!this.player.frameMode) {
		this.player.bottomPanel.hideImageInfo();
	}
	
	this.player.bottomPanel.show();
	
	new Effect.Parallel([
		new Effect.Move(this.res.notice, { sync: true, x: -50, y: 0, mode: 'relative', duration: 1 }), 
		new Effect.Appear(this.res.notice, { sync: true, duration: 0.5 })
	], {
		afterFinish: function() {
			this.lock = false;
		}.bind(this)
	});
};

dago.slideshow.Notice.prototype.check = function() {
	if ((!this.player.frameMode && this.res.imginfo.visible()) 
			|| this.queue.length == 0) {
		return false;
	}
	
	if (this.res.notice == null) {
		this.res.info.insert({top:'<h2 style="display:none" class="message"></h2>'});
		this.res.notice = $$("#fs-player-info > h2.message").first();
	}
	
	var m = this.queue.pop(); 
	this.queue.clear();
	
	if (this.res.notice.innerHTML == m) return false;
	this.res.notice.innerHTML = m;
	
	return true;
};
	
dago.slideshow.Notice.prototype.hide = function() {
	if (this.res.notice == null) return;
	
	this.res.notice.hide();
	this.res.notice.innerHTML = "";
};
/* /assets/ctx/0.8/js/dago/slideshow/Player.js */;
dago.slideshow.Player = function(galleryId, frameMode, options) {
	this.initRes(); if (this.res.area == null) return;
	
	this.frameMode = frameMode;
	this.galleryId = galleryId;
	this.options = options;
	
	if (!this.frameMode) {
		this.topPanel = new dago.slideshow.TopPanel(this);
		this.buttons = new dago.slideshow.Buttons(this);
		this.settingPanel = new dago.slideshow.SettingPanel(this);
		
		this.vote = new dago.slideshow.Vote(this, 
				function() { return this.store[this.autoIndex - 1].id; }.bind(this), 
				function() { return this.store[this.autoIndex - 1].favorite; }.bind(this));
	}
	
	this.settings = new dago.slideshow.Settings(this);
	this.bottomPanel = new dago.slideshow.BottomPanel(this);
	this.notice = new dago.slideshow.Notice(this);	
	
	this.resizeArea();
	this.showPreloader();
	this.reset();
	this.startCheckErrorLoad();
	this.bindEvents();
};

dago.slideshow.Player.prototype.MIN_PERIOD = 0.3;
dago.slideshow.Player.prototype.DEFAULT_WAITING = 10;

dago.slideshow.Player.prototype.BANNER_IMG_PERIOD = 15;
dago.slideshow.Player.prototype.BANNER_TIME = 60;

dago.slideshow.Player.prototype.IMG_REFRESH_DURATION = 1;
dago.slideshow.Player.prototype.IMG_REFRESH_FPS = 15;

dago.slideshow.Player.prototype.FULLSCREEN_LINK = "/slideshow-player/fullscreen";

dago.slideshow.Player.prototype.settingPanel;
dago.slideshow.Player.prototype.vote;
dago.slideshow.Player.prototype.buttons;
dago.slideshow.Player.prototype.notice;
dago.slideshow.Player.prototype.topPanel;
dago.slideshow.Player.prototype.bottomPanel;
dago.slideshow.Player.prototype.settings;

dago.slideshow.Player.prototype.galleryId;
dago.slideshow.Player.prototype.frameMode;
dago.slideshow.Player.prototype.options;

dago.slideshow.Player.prototype.timer;

dago.slideshow.Player.prototype.bufferChecker;

dago.slideshow.Player.prototype.store = new Array();
dago.slideshow.Player.prototype.loadStore = new Array();
dago.slideshow.Player.prototype.autoIndex;
dago.slideshow.Player.prototype.manualIndex;
dago.slideshow.Player.prototype.manualShow = false;

dago.slideshow.Player.prototype.forLoadCount;
dago.slideshow.Player.prototype.loadedCount;
dago.slideshow.Player.prototype.noneForLoad;
dago.slideshow.Player.prototype.stopLoad;

dago.slideshow.Player.prototype.lastLoad;

dago.slideshow.Player.prototype.readyNextImg;

dago.slideshow.Player.prototype.fastWaiter;
dago.slideshow.Player.prototype.lowWaiter;
dago.slideshow.Player.prototype.waitLock;

dago.slideshow.Player.prototype.screenLock;

dago.slideshow.Player.prototype.advLock;
dago.slideshow.Player.prototype.advScreenLock;
dago.slideshow.Player.prototype.advBody;
dago.slideshow.Player.prototype.advEnabled;

dago.slideshow.Player.prototype.freeAccount;

dago.slideshow.Player.prototype.revFwdLock;

dago.slideshow.Player.prototype.i18n_en = {
	fullscreen: "Press F11 to the fullscreen mode",
	none_for_load: "No more images to load",
	please_wait: "Image is loading",
	adv: "Free account advertising",
	pause: "Slideshow is paused"
};

dago.slideshow.Player.prototype.res = {};

dago.slideshow.Player.prototype.initRes = function() {
	this.res.area         = $("fs-area");
	this.res.preloader    = $("fs-preloader");
	this.res.imgcontainer = $("fs-player-img");
	this.res.imgtitle     = $("img-title");
	this.res.artistname   = $("artist-name");
	this.res.adv          = $("fs-adv");
};

dago.slideshow.Player.prototype.bindEvents = function() {
	if (!this.frameMode) {
		window.onresize = this.onresize.bind(this);
	}
	
	if (this.frameMode) { 
	    document.observe('keydown', function(e) { 
	    	if (e.keyCode == 122) document.location.href = this.FULLSCREEN_LINK;
	    }.bind(this));
	}
};

dago.slideshow.Player.prototype.onresize = function() {
	this.resizeArea();
	
	if (this.res.preloader.visible()) {
		this.centering(this.res.preloader);
	} else {
		this.adaptImage(this.playerImg());
	}
	
	if (!this.frameMode && this.notice.res.nosupport.visible()) {
		this.centering(this.res.nosupport);
	}
	
	if (!this.frameMode && this.settingPanel.res.panel.visible()) {
		this.centering(this.settingPanel.res.panel);
	}
};

dago.slideshow.Player.prototype.startCheckErrorLoad = function() {
	new PeriodicalExecuter(function() {
		for (var i = 0; i < this.loadStore.length; i++) {
			var endWaitTime = this.loadStore[i].downloaded 
					+ this.DEFAULT_WAITING * 1000 * 3;
			
			if (new Date().getTime() > endWaitTime) {
				this.removeFromLoadStore(i);
				break;
			}
		}
	}.bind(this), this.DEFAULT_WAITING);
};

dago.slideshow.Player.prototype.startCheckBuffer = function() {
	if (this.bufferChecker) this.bufferChecker.stop();
	
	this.bufferChecker = new PeriodicalExecuter(function(pe) {
		if (this.advLock) return;
		
		var afterLastLoad = new Date().getTime() - this.lastLoad;
		
		if (afterLastLoad < this.settings.MIN_DELAY * 1000 / 2) return;	
		if (this.noneForLoad && afterLastLoad < this.DEFAULT_WAITING * 1000) return;
		if (this.noneForLoad && this.frameMode) {pe.stop(); return;}
		
		if (this.freeAccount 
				&& this.advEnabled
				&& (this.store.length + this.forLoadCount - (this.autoIndex - 1) >= this.BANNER_IMG_PERIOD)) {
			return;
		}
		
		if (this.settings.value.buffer - this.loadedCount - this.forLoadCount > 0) {		
			this.lastLoad = new Date().getTime();
			this.loadNewImages();
		}	
	}.bind(this), this.settings.MIN_DELAY / 2 + 0.1);
};

dago.slideshow.Player.prototype.reset = function() {
	this.stopLoad = true;
	
	if (this.timer) this.pause();
	
	this.stopImageLoad();
	
	if (!this.settings.loaded) {
		this.settings.load();
	}
	
	this.store.clear();
	this.loadStore.clear();
	
	this.autoIndex = 0;
	this.manualShow = false;
	
	this.forLoadCount = 0;
	this.loadedCount = 0;
	this.noneForLoad = false;
	
	this.stopLoad = false;
	this.waitLock = false;
	
	this.notice.queue.clear();
	
	if (!this.frameMode) {
	    this.buttons.enable(this.buttons.res.play);
	    this.buttons.enable(this.buttons.res.pause);
	    this.buttons.disable(this.buttons.res.rev);
	    this.buttons.disable(this.buttons.res.fwd);
	    
	    this.buttons.enable(this.buttons.res.settings);
	    this.buttons.enable(this.buttons.res.fullscreen);
	    this.buttons.enable(this.buttons.res.nofullscreen);
	}
    
    this.startCheckBuffer();
};

dago.slideshow.Player.prototype.stopImageLoad = function() {
	this.loadStore.each(function(imgExt) {
		imgExt.data.src = null;
		imgExt.data.onload = null;
	});
};

dago.slideshow.Player.prototype.showImage = function(type) {
	if (!this.frameMode && this.vote.lock && type == "TIMER") {
		this.readyNextImg = true;
		return;
	}
	
	if (this.stopLoad || this.waitLock) {
		if (type == "FWD") this.revFwdLock = false;
		return;
	}
	
	if (this.advLock) {
		if (this.timer) this.pause();
		this.showAdv();
		if (type == "FWD") this.revFwdLock = false;
		return;
	}
	
	if (type != "TIMER" && this.timer) {
		this.pause();
	}
	
	if (this.loadedCount > 0 
			|| (this.settings.value.playerLoop && this.noneForLoad && this.store.length > 0) 
			|| (this.manualShow && this.autoIndex < this.store.length)) {
		if (this.settings.value.playerLoop && this.autoIndex >= this.store.length && this.noneForLoad) {
			this.autoIndex = 0;
		} else if (this.autoIndex >= this.store.length) {
			this.autoIndex = this.store.length - 1;
			if (this.manualShow) this.manualIndex = this.autoIndex;
			
			if (this.playerImg() != null) {
				if (type == "FWD") this.revFwdLock = false;
				return;
			}
		}
		
		var loadedImg = this.store[this.autoIndex++];
		
		if (this.settings.value.playerLoop && this.autoIndex >= this.store.length && this.noneForLoad) {
			if (!this.frameMode) this.buttons.disable(this.buttons.res.fwd);
		} else {
			if (!this.frameMode) this.buttons.enable(this.buttons.res.fwd);
		}
		
		if (this.settings.value.playerLoop && this.autoIndex == 1 && this.noneForLoad) {
			if (!this.frameMode) this.buttons.disable(this.buttons.res.rev);
		} else {
			if (!this.frameMode) this.buttons.enableRev();
		}
		
		this.updateScreen(loadedImg);
		
		if (type != "TIMER" && type != "FWD" && this.settings.value.delay > 0) {
			this.play();
		}
	} else {
		if (!this.frameMode) {
			this.buttons.disable(this.buttons.res.fwd);
			
			if (this.store.length > 0) {
				this.buttons.enable(this.buttons.res.rev);
			}
		}
		
		this.waitAnyLoaded(type);
		if (type == "FWD") this.revFwdLock = false;
	}
};

dago.slideshow.Player.prototype.fwdImage = function() {
	if (this.revFwdLock) return;
	this.revFwdLock = true;
	
	if (this.advLock) {
		if (this.timer) this.pause();
		this.showAdv();
		this.revFwdLock = false;
		return;
	}
	
	if (this.store.length == 0) {
		this.revFwdLock = false;
		return;
	}
	
	if (!this.manualShow) {
		this.buttons.pause();
		
		this.manualShow = true;
		this.manualIndex = this.autoIndex;
	} else {
		++this.manualIndex;
	}
	
	if (this.manualIndex >= this.store.length - 1) {
		this.buttons.disable(this.buttons.res.fwd);
		
		if (this.settings.value.playerLoop && this.noneForLoad) {
			if (this.manualIndex < this.store.length) {
				this.updateScreen(this.store[this.manualIndex]);
			} else {
				this.manualIndex = this.store.length - 1;
				this.revFwdLock = false;
			}
		} else {
			this.autoIndex = this.manualIndex;
			this.showImage("FWD");
		}
	} else {
		this.updateScreen(this.store[this.manualIndex]);
	}
	
	this.buttons.enableRev();
};

dago.slideshow.Player.prototype.revImage = function() {
	if (this.revFwdLock) return;
	this.revFwdLock = true;
	
	if (this.advScreenLock || this.store.length == 0) {
		this.revFwdLock = false;
		return;
	}
	
	this.stopFastWaiter();
	this.stopLowWaiter();
	
	if (!this.manualShow) {
		this.buttons.pause();
		this.manualShow = true;
		
		if (this.autoIndex == 1 && this.store.length > this.autoIndex) {
			this.buttons.disable(this.buttons.res.rev);
			this.revFwdLock = false;
			return;
		}
		
		if (this.res.preloader.visible() || this.playerImg() == null) {
			this.manualIndex = this.autoIndex - 1;
		} else if (this.autoIndex > 1) {
			this.manualIndex = this.autoIndex - 2;
		}
	} else {
		--this.manualIndex;
	}
	
	if (this.manualIndex == 0) {
		this.buttons.disable(this.buttons.res.rev);
	}
	
	if (this.manualIndex < 0) {
		this.revFwdLock = false;
		this.manualIndex = 0;
		return;
	}
	
	if (this.manualIndex >= this.store.length) this.manualIndex = this.store.length - 1;
	
	var loadedImg = this.store[this.manualIndex];
	this.updateScreen(loadedImg);
	
	this.buttons.enable(this.buttons.res.fwd);
};

dago.slideshow.Player.prototype.waitAnyLoaded = function(type) {	
	this.waitLock = true;
	this.showPreloader();
	
	if (this.timer) this.pause();
		
	if (this.noneForLoad) {
		this.notice.queue.push(this.i18n_en.none_for_load);
		
		if (this.lowWaiter) {
			return;
		}
		
		this.lowWaiter = new PeriodicalExecuter(function(type, pe) {
			this.stopLowWaiter();
			this.showImage(type);
		}.bind(this, type), this.DEFAULT_WAITING);
	} else {
		this.notice.queue.push(this.i18n_en.please_wait);
		
		if (this.fastWaiter) {
			return;
		}
		
		this.fastWaiter = new PeriodicalExecuter(function(type, pe) {
			if (this.loadedCount > 0 || this.noneForLoad || this.advLock) {
				this.stopFastWaiter();
				type = type == "TIMER" ? undefined : type;
				this.showImage(type);
			}
		}.bind(this, type), this.MIN_PERIOD);
	}
};

dago.slideshow.Player.prototype.stopFastWaiter = function() {
	if (this.fastWaiter) {
		this.fastWaiter.stop();
		this.fastWaiter = null;
		this.hidePreloader();
		this.notice.hide();
		this.waitLock = false;
	}
};

dago.slideshow.Player.prototype.stopLowWaiter = function() {
	if (this.lowWaiter) {
		this.lowWaiter.stop();
		this.lowWaiter = null;
		this.hidePreloader();
		this.waitLock = false;
	}
};

dago.slideshow.Player.prototype.approveView = function(loadedImg) {
	if (this.settings.value.showViewed || this.settings.value.myFavorites) return;
	
	var link = "/player:approveView/" + loadedImg.id;
	
	if (this.galleryId) {
		link += "?t:ac=" + this.galleryId;
	}
	
	Tapestry.ajaxRequest(link, function(){});
};

dago.slideshow.Player.prototype.updateScreen = function(loadedImg) {
	if (!this.tryUpdateScreen(loadedImg)) {
		new PeriodicalExecuter(function(loadedImg, pe) {
			this.tryUpdateScreen(loadedImg, pe);
		}.bind(this, loadedImg), this.MIN_PERIOD);
	}
};

dago.slideshow.Player.prototype.tryUpdateScreen = function(loadedImg, pe) {
	if (this.screenLock) return false;
	this.screenLock = true;
	if (pe) pe.stop();
	
	if (!this.frameMode) {
		this.topPanel.res.wallpaperLinks.each(function(el) {
			if (el.href.endsWith('wallpaper')) {
				el.href += "/" + loadedImg.id; 
			} else {
				el.href = el.href.substring(0, el.href.lastIndexOf("/") + 1) + loadedImg.id;
			}
		}.bind(loadedImg));
	}
	
	var newImg;
	var parallel = false;
	
	if (this.res.preloader.visible()) {
		setTimeout(this.hidePreloader.bind(this), this.IMG_REFRESH_DURATION * 1000 / 5);
	}
	
	if (this.playerImg() == null) {
		this.res.imgcontainer.insert(loadedImg.html);
		newImg = this.playerImg();
	} else {
		this.res.imgcontainer.insert(loadedImg.html);
		newImg = this.playerImg2();
		parallel = true;
	}
	
	if (!loadedImg.showed) {
		loadedImg.showed = true;
		if (this.loadedCount > 0) this.loadedCount--;
		
		if (!this.frameMode) {
			this.approveView(loadedImg);
		} else {
			this.notice.queue.push(this.i18n_en.fullscreen);
			this.notice.show();
		}
	}
	
	this.adaptImage(newImg);
	
	if (parallel) {
		if (this.settings.value.imageSmoothFade && !this.manualShow) {
			new Effect.Parallel([
			    new Effect.Fade(this.playerImg(), {sync: true}),
				new Effect.Appear(newImg, {sync: true})
			], {
				duration: this.IMG_REFRESH_DURATION,
				fps: this.IMG_REFRESH_FPS,
				
				afterFinish: function() {
					this.playerImg().remove();
					this.screenLock = false;
				}.bind(this)
			});				
		} else {
			this.playerImg().remove();
			newImg.show();
			this.screenLock = false;
		}
	} else {
		if (this.settings.value.imageSmoothFade && !this.manualShow) {
			new Effect.Appear(newImg, {
		    	duration: this.IMG_REFRESH_DURATION, 
		    	fps: this.IMG_REFRESH_FPS,
		    	
				afterFinish: function() {
					this.screenLock = false;
				}.bind(this)
			});
		} else {
			newImg.show();
			this.screenLock = false;
		}
	}
	
	if (!this.frameMode) {
		if (this.settings.value.imageSmoothFade && !this.manualShow) {
			setTimeout(this.updateImageInfo.bind(this, loadedImg), this.MIN_PERIOD);
		} else {
			this.updateImageInfo(loadedImg);
		}
	}
	
	this.revFwdLock = false;
	
	return true;
};

dago.slideshow.Player.prototype.fadeFirstImage = function(afterFinish) {	
	if (this.settings.value.imageSmoothFade) {
		this.playerImg().fade({
			duration: this.IMG_REFRESH_DURATION, 
			fps: this.IMG_REFRESH_FPS,
			
			afterFinish: function(afterFinish) {		
				if (this.playerImg()) this.playerImg().remove();
				if (afterFinish) afterFinish();
			}.bind(this, afterFinish)
		});
	} else {
		if (this.playerImg()) this.playerImg().remove();
		if (afterFinish) afterFinish();
	}
};

dago.slideshow.Player.prototype.updateImageInfo = function(loadedImg) {
	this.res.imgtitle.href = loadedImg.type == "photo" ? "/photo/" : "/picture/";
	this.res.imgtitle.href += loadedImg.id;
	this.res.imgtitle.innerHTML = loadedImg.title;
	
	this.res.artistname.href = "/artist/" + loadedImg.artist_id;
	this.res.artistname.innerHTML = loadedImg.artist_name;
	
	this.vote.res.votecount.innerHTML = loadedImg.rating;
	
	if (loadedImg.vote) {
		this.vote.res.voteup.src = this.vote.VOTE_UP_IMG;
		this.vote.res.votedown.src = this.vote.VOTE_DOWN_IMG;
	} else {
		this.vote.res.voteup.src = this.vote.VOTE_UP_OFF_IMG;
		this.vote.res.votedown.src = this.vote.VOTE_DOWN_OFF_IMG;		
	}
	
	if (loadedImg.favorite) {
		this.vote.res.favorite.src = this.vote.FAV_ON_IMG;
	} else {
		this.vote.res.favorite.src = this.vote.FAV_OFF_IMG;
	}
	
	this.notice.hide();
	this.bottomPanel.showImageInfo();
};

dago.slideshow.Player.prototype.loadNewImages = function() {
	var totalForLoad = this.settings.value.buffer - this.loadedCount - this.forLoadCount;
	var link;
	
	if (this.frameMode) {
		link = "/player:frameImages/" + this.store.length + "/";
	} else {
		link = "/player:nextImages/";
	}
	
	link += this.getWidth() + "/" + this.getRatio() + "/" + totalForLoad;
	
	if (this.galleryId) {
		link += "?t:ac=" + this.galleryId;
	}
	
	this.forLoadCount += totalForLoad;
	
	Tapestry.ajaxRequest(link, function(totalForLoad, transport) {
		var data = transport.responseJSON;
		
		if (data.noop) {
			this.forLoadCount -= totalForLoad;
			return;
		}
		
		this.advEnabled = data.advEnabled;
		this.freeAccount = data.freeAccount;
		
		if (data.adv != undefined) {
			this.forLoadCount -= totalForLoad;
			
			this.advLock = true;
			this.advBody = data.adv;
			this.buttons.enable(this.buttons.res.fwd);
			
			return;
		}
		
		if (data.images.length == 0) {
			this.forLoadCount -= totalForLoad;
			this.noneForLoad = true;
			return;
		} else {
			this.noneForLoad = false;
		}
		
		this.forLoadCount -= totalForLoad - data.images.length;
		this.load(data.images, 0);
	}.bind(this, totalForLoad));
};

dago.slideshow.Player.prototype.load = function(images, currentIndex) {
	if (this.stopLoad) return;
	
	var imgExt = this.createImageExt(images[currentIndex]);
	this.loadStore.push(imgExt);

	if (imgExt.data.complete && imgExt.data.height > 0) {
		this.completeLoad(imgExt, images, currentIndex);
	} else {
		imgExt.data.onload = this.completeLoad.bind(this, imgExt, images, currentIndex);
	}
	
	return true;	
};

dago.slideshow.Player.prototype.completeLoad = function(imgExt, images, currentIndex) {
	if (this.stopLoad) return;
	
	this.store.push(imgExt);
	this.loadedCount++;
	
	for (var i = 0; i < this.loadStore.length; i++) {
		if (this.loadStore[i].id == imgExt.id) {
			this.removeFromLoadStore(i);
			break;
		}
	}
	
	if (!this.frameMode && !this.advScreenLock) this.buttons.enable(this.buttons.res.fwd);
	
	if (++currentIndex < images.length) {
		this.load(images, currentIndex);
	}
};

dago.slideshow.Player.prototype.removeFromLoadStore = function(i) {
	this.loadStore.splice(i, 1);
	this.forLoadCount--;
	
	if (this.forLoadCount < 0) {
		this.forLoadCount = 0;
	}
};

dago.slideshow.Player.prototype.showAdv = function() {
	if (this.advScreenLock) return;
	this.advScreenLock = true;
	
	if (!this.tryShowAdv()) {
		new PeriodicalExecuter(function(pe) {
			this.tryShowAdv(pe);
		}.bind(this), this.MIN_PERIOD);
	}
	
	new PeriodicalExecuter(function(pe) {
		pe.stop();
		
		this.res.adv.fade({
			duration: 1, 
			
			afterFinish: function() {
				this.res.adv.hide();
				this.res.adv.innerHTML = "";
				
			    this.buttons.enable(this.buttons.res.play);
			    this.buttons.enable(this.buttons.res.pause);
			    this.buttons.enable(this.buttons.res.settings);
			    this.buttons.enable(this.buttons.res.fullscreen);
			    this.buttons.enable(this.buttons.res.nofullscreen);
			    
				this.advLock = false;
				this.advScreenLock = false;
				this.screenLock = false;
			    
			    if (this.buttons.res.pause.visible()) {
			    	this.buttons.play();
			    } else {
			    	this.notice.hide();
			    	this.buttons.enable(this.buttons.res.fwd);
			    	this.fwdImage();
			    }
			}.bind(this)
		});		
	}.bind(this), this.BANNER_TIME);
};

dago.slideshow.Player.prototype.tryShowAdv = function(pe) {
	if (this.screenLock) return false;
	this.screenLock = true;
	if (pe) pe.stop();
	
	var showFunc = function() {
		if (this.buttons.res.pause.visible()) {
			if (this.timer) this.pause();
			this.stopFastWaiter();
			this.stopLowWaiter();
		}
		
		this.bottomPanel.hideImageInfo();
		this.notice.queue.push(this.i18n_en.adv);
		this.notice.show();
		this.res.adv.innerHTML = this.advBody;
		
		
	    this.buttons.enable(this.buttons.res.play);
	    this.buttons.enable(this.buttons.res.pause);
	    this.buttons.enable(this.buttons.res.settings);
	    this.buttons.enable(this.buttons.res.fullscreen);
	    this.buttons.enable(this.buttons.res.nofullscreen);
		
	    this.buttons.disable(this.buttons.res.play);
	    this.buttons.disable(this.buttons.res.pause);
	    this.buttons.disable(this.buttons.res.rev);
	    this.buttons.disable(this.buttons.res.fwd);
	    this.buttons.disable(this.buttons.res.settings);
	    this.buttons.disable(this.buttons.res.fullscreen);
	    this.buttons.disable(this.buttons.res.nofullscreen);
		
	    this.res.adv.appear({
			duration: 2,
			
			afterFinish: function() {			
	    		this.res.adv.show();
			}.bind(this)
		});
	}.bind(this);
	
	if (this.playerImg() == null) {
		this.hidePreloader();
		this.showFunc();
	} else {
		this.fadeFirstImage(showFunc);	
	}
	
	return true;
};

dago.slideshow.Player.prototype.play = function() {
	this.timer = new PeriodicalExecuter(function(pe) {
		this.showImage("TIMER");
	}.bind(this), this.settings.value.delay);	
};

dago.slideshow.Player.prototype.pause = function() {
	this.timer.stop();
	this.timer = null;
};

dago.slideshow.Player.prototype.resizeArea = function() {
	this.res.area.setStyle({ 
		width: this.getWidth() + "px", 
		height: this.getHeight() + "px" 
	});
};

dago.slideshow.Player.prototype.adaptImage = function(img) {
	if (img == null) return;
	
	var newWidth = this.getWidth();
	var newHeight = this.getHeight();
	
	var r = this.getRatio() == foror.utils.RATIO_169 ? 16 / 9 : 4 / 3;
	var imgWidth = newHeight * r;
	var imgHeight = imgWidth / r;
	
	if (imgHeight < newHeight) {
		var percent = (newHeight - imgHeight) / (imgHeight / 100);
		imgWidth = imgWidth + (imgWidth / 100) * percent;
		imgHeight = newHeight;
	}
	
	if (imgWidth < newWidth) {
		var percent = (newWidth - imgWidth) / (imgWidth / 100);
		imgHeight = imgHeight + (imgHeight / 100) * percent;
		imgWidth = newWidth;
	}
	
	var offsetLeft = (Math.abs(newWidth - imgWidth) / 2);
	var offsetTop = (Math.abs(newHeight - imgHeight) / 2);
	
	img.setStyle({
		"left":"-" + offsetLeft + "px", 
		"top":"-" + offsetTop + "px",
		"width": imgWidth,
		"height": imgHeight
	});
	
	img.width = imgWidth;
	img.height = imgHeight;
	
	this.res.imgcontainer.setStyle({ 
		width: this.getWidth() + "px", 
		height: this.getHeight() + "px" 
	});
};

dago.slideshow.Player.prototype.createImageExt = function(json) {
	var img = new Image();
	img.src = json.src;
	
	var html;
	
	if (this.frameMode) {
		html = '<img src="'+img.src+'" style="display:none" title="' + json.title + " by " + json.artist_name + '"/>';
	} else {
		html = '<img src="'+img.src+'" style="display:none"/>';
	}
	
	return {
		data : img,
		html : html,
		id   : json.id,
		title : json.title.length > 34 ? json.title.substring(0, 31) + "..." : json.title,
		artist_name : json.artist_name,
		artist_id : json.artist_id,
		rating : json.rating,
		vote: json.vote,
		favorite : json.favorite,
		downloaded: new Date().getTime(),
		src: json.src,
		showed: false,
		type: json.type
	};
};

dago.slideshow.Player.prototype.showPreloader = function() {
	if (this.advLock) return;
	
	if (!this.tryShowPreloader()) {
		new PeriodicalExecuter(function(pe) {
			this.tryShowPreloader(pe);
		}.bind(this), this.MIN_PERIOD);		
	}
};

dago.slideshow.Player.prototype.tryShowPreloader = function(pe) {
	if (this.screenLock) return false;
	this.screenLock = true;
	if (pe) pe.stop();
	
	if (this.res.preloader.visible()) {
		this.screenLock = false;
		return true;
	}
	
	var showFunc = function() {
		this.bottomPanel.hideImageInfo();
		this.centering(this.res.preloader);
		this.res.preloader.show();
		
		this.screenLock = false;
	}.bind(this);
	
	if (this.playerImg() == null) {
		showFunc();
	} else {
		this.fadeFirstImage(showFunc);
	}
	
	return true;
};

dago.slideshow.Player.prototype.hidePreloader = function() {
	if (this.res.preloader.visible()) {
		this.res.preloader.hide();	
	}
};

dago.slideshow.Player.prototype.playerImg = function() {
	if ($("fs-player-img").select("img").length > 0) {
		return $("fs-player-img").select("img")[0];
	}
	
	return null;
};

dago.slideshow.Player.prototype.playerImg2 = function() {
	if ($("fs-player-img").select("img").length > 1) {
		return $("fs-player-img").select("img")[1];
	}
	
	return null;
};

dago.slideshow.Player.prototype.find = function(imgid) {
	for (var i = 0; i < this.store.length; i++) {
		if (this.store[i].id == imgid) return this.store[i];
	}
	
	return null;
};

dago.slideshow.Player.prototype.getWidth = function() {
	if (this.frameMode)
		return this.options.width;
	
	return foror.utils.getWindowWidth();
};

dago.slideshow.Player.prototype.getHeight = function() {
	if (this.frameMode)
		return this.options.height;
	
	return foror.utils.getWindowHeight();
};

dago.slideshow.Player.prototype.getRatio = function() {
	if (this.frameMode)
		return foror.utils.aroundScreenRatio(this.options.width, this.options.height);
	
	return foror.utils.aroundScreenRatio();
};

dago.slideshow.Player.prototype.centering = function(el) {
	foror.utils.centering(this.res.area, el, this.frameMode ? 0 : 80);
};

/* /assets/ctx/0.8/js/dago/slideshow/SettingPanel.js */;
dago.slideshow.SettingPanel = function(player) {
	this.player = player;
	
	this.initRes();
	this.bindEvents();
	
	new ProtoTabs('settings-tabs', { defaultPanel: 'image-filter' });
};

dago.slideshow.SettingPanel.prototype.UPDATE_LINK = "/player:updateSettings/";
dago.slideshow.SettingPanel.prototype.FIELD_POSTFIX = "-settings-field";

dago.slideshow.SettingPanel.prototype.player;
dago.slideshow.SettingPanel.prototype.needPlayerReset;

dago.slideshow.SettingPanel.prototype.res = {};

dago.slideshow.SettingPanel.prototype.initRes = function() {
	this.res.panel      = $('fs-player-settings-panel');
	this.res.form       = $$(".settings-body").first();
	this.res.save       = $('settings-save');
	this.res.cancel     = $('settings-cancel');
	this.res.myfav      = $('myFavorites-settings-field');
	this.res.topfav     = $("topFavorites-settings-field");
	this.res.showviewed = $('showViewed-settings-field');
	this.res.buffer     = $('buffer-settings-field');
	this.res.delay      = $('delay-settings-field');
	this.res.toprated   = $("topRated-settings-field");
};

dago.slideshow.SettingPanel.prototype.bindEvents = function() {	
	this.res.save.observe('click', this.save.bind(this));
    this.res.cancel.observe('click', this.cancel.bind(this));
    
    this.res.myfav.observe('change', function() {
    	if (this.res.myfav.checked) {
    		this.res.topfav.checked = false;
    		this.res.toprated.checked = false;
    		this.res.showviewed.checked = false;
    	}
    }.bind(this));
    
    this.res.showviewed.observe('change', function() {
    	if (this.res.showviewed.checked) this.res.myfav.checked = false;
    }.bind(this));
    
    this.res.buffer.observe('change', function() {
    	if (this.res.buffer.value < this.player.settings.MIN_BUFFER) 
    		this.res.buffer.value = this.player.settings.MIN_BUFFER;
    	
    	if (this.res.buffer.value > this.player.settings.MAX_BUFFER)
    		this.res.buffer.value = this.player.settings.MAX_BUFFER;
    }.bind(this));
    
    this.res.delay.observe('change', function() {
    	if (this.res.delay.value < this.player.settings.MIN_DELAY)
    		this.res.delay.value.value = this.player.settings.MIN_DELAY;
    }.bind(this));
};

dago.slideshow.SettingPanel.prototype.reset = function() {
	for (field in this.player.settings.value) {
		this.writeField($(field + this.FIELD_POSTFIX), this.player.settings.value[field]);
	}
};

dago.slideshow.SettingPanel.prototype.save = function() {
	this.needPlayerReset = false;
	this.check();
	
	var link = this.UPDATE_LINK + this.needPlayerReset + "/" + Object.toJSON(this.player.settings.value);
	
	Tapestry.ajaxRequest(link, function() {
		if (this.player.galleryId) this.player.galleryId = null;
		if (this.needPlayerReset) this.player.reset();
		
		if (this.player.buttons.res.pause.visible()) {
			this.player.buttons.play();
		} else if (this.player.playerImg() == null) {
			this.player.notice.hide();
			this.player.showImage("FWD");
		}
	}.bind(this));
	
	this.reset();
	
	this.res.panel.hide();
	this.player.buttons.enable(this.player.buttons.res.settings);
	
	if (this.needPlayerReset) {
		if (this.player.buttons.res.pause.visible()) {
			this.player.showPreloader();
		} else {
			if (this.player.playerImg() != null) {
				this.player.bottomPanel.hideImageInfo();
				this.player.playerImg().remove();
			}
		}
	}
};

dago.slideshow.SettingPanel.prototype.cancel = function() {
	this.reset();
	this.res.panel.hide();
	this.player.buttons.enable(this.player.buttons.res.settings);
	
	if (this.player.buttons.res.pause.visible()) {
		this.player.buttons.play();
	} else if (this.player.playerImg() == null) {
		this.player.notice.hide();
		this.player.showImage("FWD");
	}	
};

dago.slideshow.SettingPanel.prototype.writeField = function(el, value) {
	if (el.type && el.type == "checkbox") {
		el.checked = value;
	} else if (el.tagName == "textarea") {
		el.innerHTML = value;
	} else if (el.type && el.type == "radio") {
		this.res.form.getInputs('radio', el.name).find(function(re) {
			if (re.value == value) re.checked = true;
		});		
	} else {
		el.value = value;
	}
};

dago.slideshow.SettingPanel.prototype.readField = function(el) {
	if (el.type && el.type == "checkbox") {
		return el.checked;
	} else if (el.tagName == "textarea") {
		return el.innerHTML;
	} else if (el.type && el.type == "radio") {
		var checkedRadio = this.res.form.getInputs('radio', el.name).find(function(re) {
			return re.checked;
		});
		
		return (checkedRadio) ? checkedRadio.value : null;
	} else {
		return el.value;
	}	
};

dago.slideshow.SettingPanel.prototype.check = function() {
	for (field in this.player.settings.value) {		
		var newValue = this.readField($(field + this.FIELD_POSTFIX));
		
		if (this.player.settings.value[field] != newValue) {
			switch (field) {
			case "tags":
			case "showPictures":
			case "showPhoto":
			case "topFavorites":
			case "topRated":
			case "myFavorites":
			case "showViewed":
				this.needPlayerReset = true;
				break;

			default:
				break;
			}
		}
		
		this.player.settings.value[field] = newValue;
	}	
	
	if (this.player.settings.value["myFavorites"]) {
		this.player.settings.value["topFavorites"] = false;
		this.player.settings.value["topRated"] = false;
		this.player.settings.value["showViewed"] = false;
	}
	
	if (this.player.settings.value["showViewed"]) {
		this.player.settings.value["myFavorites"] = false;
	}
	
	if (this.player.settings.value["delay"] < this.player.settings.MIN_DELAY) {
		this.player.settings.value["delay"] = this.player.settings.MIN_DELAY;
	}
	
	if (this.player.settings.value["buffer"] < this.player.settings.MIN_BUFFER) {
		this.player.settings.value["buffer"] = this.player.settings.MIN_BUFFER;
	}
	
	if (this.player.settings.value["buffer"] > this.player.settings.MAX_BUFFER) {
		this.player.settings.value["buffer"] = this.player.settings.MAX_BUFFER;
	}
};

/* /assets/ctx/0.8/js/dago/slideshow/TopPanel.js */;
dago.slideshow.TopPanel = function(player) {
	this.player = player;
	
	this.initRes();
	this.bindEvents();
	this.callByTimeout(this.show);
	
	setTimeout(function() {		
		if (this.timeoutId) {
			this.callByTimeout(this.hide);
		}
	}.bind(this), 3000);
};

dago.slideshow.TopPanel.prototype.EFFECT_DURATION = 0.5;

dago.slideshow.TopPanel.prototype.player;

dago.slideshow.TopPanel.prototype.timeoutId;
dago.slideshow.TopPanel.prototype.effectLock;

dago.slideshow.TopPanel.prototype.res = {};

dago.slideshow.TopPanel.prototype.initRes = function() {
	this.res.panel = $("fs-player-top-panel");
	this.res.wallpaperLinks = $$("#wallpaper-links a");
};

dago.slideshow.TopPanel.prototype.bindEvents = function() {
	this.res.panel.observe("mouseleave", this.callByTimeout.bind(this, this.hide));
	this.res.panel.observe("mouseenter", this.clearTimeout.bind(this));
	
	this.player.res.area.observe("mousemove", function(e) {
		if (e.pointerY() < 60 && !this.effectLock && !this.res.panel.visible()) {
			this.show();
		}
	}.bind(this));
};

dago.slideshow.TopPanel.prototype.show = function() {	
	this.effectLock = true;
	
	this.res.panel.slideDown({
		duration: this.EFFECT_DURATION,
		
		afterFinish: function() {
			this.effectLock = false;
		}.bind(this)
	});
};

dago.slideshow.TopPanel.prototype.hide = function() {
	if (!this.res.panel.visible()) return;
	
	this.effectLock = true;
	
	this.res.panel.slideUp({
		duration: this.EFFECT_DURATION,
	
		afterFinish: function() {
			this.effectLock = false;
		}.bind(this)
	});
};

dago.slideshow.TopPanel.prototype.callByTimeout = function(func) {
	this.timeoutId = setTimeout(function(func) {
		func.bind(this)();
	}.bind(this, func), 700);
};

dago.slideshow.TopPanel.prototype.clearTimeout = function() {
	if (this.timeoutId) {
		clearTimeout(this.timeoutId);
		this.timeoutId = null;
	}
};
/* /assets/ctx/0.8/js/dago/slideshow/Vote.js */;
dago.slideshow.Vote = function(player, imgid, favorited) {
	this.player = player;
	
	this.imgid = this.player ? imgid 
			: function(imgid) {return imgid;}.bind(this, imgid);
	
	this.favorited = this.player ? favorited 
			: function(favorited) {return favorited;}.bind(this, favorited);
	
	this.initRes();
	this.bindEvents();	
};

dago.slideshow.Vote.prototype.FAVORITE_LINK = "/player:favorite/";
dago.slideshow.Vote.prototype.REMOVE_FAVORITE_LINK = "/player:removefavorite/";

dago.slideshow.Vote.prototype.FAV_ON_IMG = "/img/favorite-on.png";
dago.slideshow.Vote.prototype.FAV_OFF_IMG = "/img/favorite-off.png";

dago.slideshow.Vote.prototype.VOTE_UP_OFF_IMG = "/img/vote-arrow-up-off.png";
dago.slideshow.Vote.prototype.VOTE_DOWN_OFF_IMG = "/img/vote-arrow-down-off.png";
dago.slideshow.Vote.prototype.VOTE_UP_IMG = "/img/vote-arrow-up.png";
dago.slideshow.Vote.prototype.VOTE_DOWN_IMG = "/img/vote-arrow-down.png";

dago.slideshow.Vote.prototype.player;
dago.slideshow.Vote.prototype.imgid;
dago.slideshow.Vote.prototype.favorited;
dago.slideshow.Vote.prototype.lock;
dago.slideshow.Vote.prototype.readyNextImg;

dago.slideshow.Vote.prototype.res = {};

dago.slideshow.Vote.prototype.initRes = function() {	
	this.res.favorite  = $('vote-favorite');
	this.res.voteup    = $('vote-up');
	this.res.votedown  = $('vote-down');
	this.res.votecount = $("vote-count-post");
	this.res.favcount  = $("favorite-count");
};

dago.slideshow.Vote.prototype.bindEvents = function() {
	this.res.favorite.observe('click', this.favorite.bind(this));
	this.res.favorite.observe('mouseenter', this.updateFavorite.bind(this, false));
	this.res.favorite.observe('mouseleave', this.updateFavorite.bind(this, true));	
	
	this.res.voteup.observe('click', this.vote.bind(this, "voteup"));
	this.res.votedown.observe('click', this.vote.bind(this, "votedown"));
};

dago.slideshow.Vote.prototype.favorite = function() {	
	if (this.favorited()) {
		Tapestry.ajaxRequest(this.REMOVE_FAVORITE_LINK + this.imgid(), function() {});
	} else {
		Tapestry.ajaxRequest(this.FAVORITE_LINK + this.imgid(), function() {});
	}
	
	this.updateFavorite(false);
	
	if (this.player) this.timeoutAndShowNext();	
};

dago.slideshow.Vote.prototype.vote = function(action) {
	if (this.res.voteup.src.search(this.VOTE_UP_IMG) != -1) {
		this.updateVote(action);
		
		if (this.player) this.timeoutAndShowNext();
	}	
};

dago.slideshow.Vote.prototype.updateVote = function(action) {
	Tapestry.ajaxRequest("/player:" + action + "/" + this.imgid(), function() {});
	
	this.res.voteup.src = this.VOTE_UP_OFF_IMG;
	this.res.votedown.src = this.VOTE_DOWN_OFF_IMG;
	
	if (this.res.votecount.innerHTML.search("K") == -1) {
		var value = this.res.votecount.innerHTML;
		this.res.votecount.innerHTML = action == "voteup" ? ++value : --value;
		
		if (this.player) {
			imgExt = this.player.find(this.imgid());
			imgExt.rating = value;
			imgExt.vote = false;
		}
	}	
};

dago.slideshow.Vote.prototype.updateFavorite = function(invert, e) {
	var favorited = this.favorited();
	favorited = invert ? !favorited : favorited;
	
	if (favorited) {
		this.res.favorite.src = this.FAV_OFF_IMG;
		
		if (this.player) {
			if (e == null) this.player.find(this.imgid()).favorite = false;
		} else {
			if (e == null) {
				var value = this.res.favcount.innerHTML;
				this.res.favcount.innerHTML = --value;
				
				this.favorited = function(favorited) {return favorited;}.bind(this, false);
			}
		}
	} else {
		this.res.favorite.src = this.FAV_ON_IMG;
		
		if (this.player) { 
			if (e == null) this.player.find(this.imgid()).favorite = true;
		} else {
			if (e == null) {
				var value = this.res.favcount.innerHTML;
				this.res.favcount.innerHTML = ++value;
				
				this.favorited = function(favorited) {return favorited;}.bind(this, true);
			}
		}
	}
};

dago.slideshow.Vote.prototype.timeoutAndShowNext = function() {
	if (this.player.manualShow || this.lock) return;
	
	this.lock = true;
	
	setTimeout(function() {
		if (this.player.readyNextImg) {
			if (this.player.timer) this.player.pause();
			this.lock = false;
			
			this.player.showImage();
		} else {
			this.lock = false;
		}
		
		this.player.readyNextImg = false;
	}.bind(this), 3000);
};
;/**/
Tapestry.markScriptLibrariesLoaded(["/assets/ctx/0.8/js/scriptaculous/prototype.js","/assets/ctx/0.8/js/scriptaculous/scriptaculous.js","/assets/ctx/0.8/js/scriptaculous/effects.js","/assets/tapestry/5.1.0.8-SNAPSHOT/tapestry.js","/assets/blackbird/5.1.0.8-SNAPSHOT/blackbird.js","/assets/tapestry/5.1.0.8-SNAPSHOT/tapestry-messages.js","/assets/ctx/0.8/js/scriptaculous/dragdrop.js","/assets/ctx/0.8/js/Cropper.js","/assets/ctx/0.8/js/LazierLoad.js","/assets/ctx/0.8/js/ProtoTabs.js","/assets/ctx/0.8/js/CookieJar.js","/assets/ctx/0.8/js/packet.js","/assets/ctx/0.8/js/foror/Utils.js","/assets/ctx/0.8/js/foror/Submenu.js","/assets/ctx/0.8/js/dago/ImageEditor.js","/assets/ctx/0.8/js/dago/ImageFullscreener.js","/assets/ctx/0.8/js/dago/ImageTooltip.js","/assets/ctx/0.8/js/dago/Wallpaper.js","/assets/ctx/0.8/js/dago/ImageBanner.js","/assets/ctx/0.8/js/dago/slideshow/Settings.js","/assets/ctx/0.8/js/dago/slideshow/BottomPanel.js","/assets/ctx/0.8/js/dago/slideshow/Buttons.js","/assets/ctx/0.8/js/dago/slideshow/Notice.js","/assets/ctx/0.8/js/dago/slideshow/Player.js","/assets/ctx/0.8/js/dago/slideshow/SettingPanel.js","/assets/ctx/0.8/js/dago/slideshow/TopPanel.js","/assets/ctx/0.8/js/dago/slideshow/Vote.js"]);

