/*
Copyright (c) 2005 JSON.org

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 Software shall be used for Good, not Evil.

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.
*/

/*
    The global object JSON contains two methods.

    JSON.stringify(value) takes a JavaScript value and produces a JSON text.
    The value must not be cyclical.

    JSON.parse(text) takes a JSON text and produces a JavaScript value. It will
    return false if there is an error.
*/
var JSON = function () {
    var m = {
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        },
        s = {
            'boolean': function (x) {
                return String(x);
            },
            number: function (x) {
                return isFinite(x) ? String(x) : 'null';
            },
            string: function (x) {
                if (/["\\\x00-\x1f]/.test(x)) {
                    x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
                        var c = m[b];
                        if (c) {
                            return c;
                        }
                        c = b.charCodeAt();
                        return '\\u00' +
                            Math.floor(c / 16).toString(16) +
                            (c % 16).toString(16);
                    });
                }
                return '"' + x + '"';
            },
            object: function (x) {
                if (x) {
                    var a = [], b, f, i, l, v;
                    if (x instanceof Array) {
                        a[0] = '[';
                        l = x.length;
                        for (i = 0; i < l; i += 1) {
                            v = x[i];
                            f = s[typeof v];
                            if (f) {
                                v = f(v);
                                if (typeof v == 'string') {
                                    if (b) {
                                        a[a.length] = ',';
                                    }
                                    a[a.length] = v;
                                    b = true;
                                }
                            }
                        }
                        a[a.length] = ']';
                    } else if (x instanceof Object) {
                        a[0] = '{';
                        for (i in x) {
                            v = x[i];
                            f = s[typeof v];
                            if (f) {
                                v = f(v);
                                if (typeof v == 'string') {
                                    if (b) {
                                        a[a.length] = ',';
                                    }
                                    a.push(s.string(i), ':', v);
                                    b = true;
                                }
                            }
                        }
                        a[a.length] = '}';
                    } else {
                        return;
                    }
                    return a.join('');
                }
                return 'null';
            }
        };
    return {
        copyright: '(c)2005 JSON.org',
        license: 'http://www.JSON.org/license.html',
/*
    Stringify a JavaScript value, producing a JSON text.
*/
        stringify: function (v) {
            var f = s[typeof v];
            if (f) {
                v = f(v);
                if (typeof v == 'string') {
                    return v;
                }
            }
            return null;
        },
/*
    Parse a JSON text, producing a JavaScript value.
    It returns false if there is a syntax error.
*/
        parse: function (text) {
            try {
                return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
                        text.replace(/"(\\.|[^"\\])*"/g, ''))) &&
                    eval('(' + text + ')');
            } catch (e) {
                return false;
            }
        }
    };
}();

/*  Prototype JavaScript framework, version 1.6.1
 *  (c) 2005-2009 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://www.prototypejs.org/
 *
 *--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.6.1',

  Browser: (function(){
    var ua = navigator.userAgent;
    var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
    return {
      IE:             !!window.attachEvent && !isOpera,
      Opera:          isOpera,
      WebKit:         ua.indexOf('AppleWebKit/') > -1,
      Gecko:          ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1,
      MobileSafari:   /Apple.*Mobile.*Safari/.test(ua)
    }
  })(),

  BrowserFeatures: {
    XPath: !!document.evaluate,
    SelectorsAPI: !!document.querySelector,
    ElementExtensions: (function() {
      var constructor = window.Element || window.HTMLElement;
      return !!(constructor && constructor.prototype);
    })(),
    SpecificElementExtensions: (function() {
      if (typeof window.HTMLDivElement !== 'undefined')
        return true;

      var div = document.createElement('div');
      var form = document.createElement('form');
      var isSupported = false;

      if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) {
        isSupported = true;
      }

      div = form = null;

      return isSupported;
    })()
  },

  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,

  emptyFunction: function() { },
  K: function(x) { return x }
};

if (Prototype.Browser.MobileSafari)
  Prototype.BrowserFeatures.SpecificElementExtensions = false;


var Abstract = { };


var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) { }
    }

    return returnValue;
  }
};

/* Based on Alex Arnell's inheritance implementation. */

var Class = (function() {
  function subclass() {};
  function create() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

    function klass() {
      this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    klass.prototype.constructor = klass;
    return klass;
  }

  function addMethods(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = Object.keys(source);

    if (!Object.keys({ toString: true }).length) {
      if (source.toString != Object.prototype.toString)
        properties.push("toString");
      if (source.valueOf != Object.prototype.valueOf)
        properties.push("valueOf");
    }

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() == "$super") {
        var method = value;
        value = (function(m) {
          return function() { return ancestor[m].apply(this, arguments); };
        })(property).wrap(method);

        value.valueOf = method.valueOf.bind(method);
        value.toString = method.toString.bind(method);
      }
      this.prototype[property] = value;
    }

    return this;
  }

  return {
    create: create,
    Methods: {
      addMethods: addMethods
    }
  };
})();
(function() {

  var _toString = Object.prototype.toString;

  function extend(destination, source) {
    for (var property in source)
      destination[property] = source[property];
    return destination;
  }

  function inspect(object) {
    try {
      if (isUndefined(object)) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : String(object);
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  }

  function toJSON(object) {
    var type = typeof object;
    switch (type) {
      case 'undefined':
      case 'function':
      case 'unknown': return;
      case 'boolean': return object.toString();
    }

    if (object === null) return 'null';
    if (object.toJSON) return object.toJSON();
    if (isElement(object)) return;

    var results = [];
    for (var property in object) {
      var value = toJSON(object[property]);
      if (!isUndefined(value))
        results.push(property.toJSON() + ': ' + value);
    }

    return '{' + results.join(', ') + '}';
  }

  function toQueryString(object) {
    return $H(object).toQueryString();
  }

  function toHTML(object) {
    return object && object.toHTML ? object.toHTML() : String.interpret(object);
  }

  function keys(object) {
    var results = [];
    for (var property in object)
      results.push(property);
    return results;
  }

  function values(object) {
    var results = [];
    for (var property in object)
      results.push(object[property]);
    return results;
  }

  function clone(object) {
    return extend({ }, object);
  }

  function isElement(object) {
    return !!(object && object.nodeType == 1);
  }

  function isArray(object) {
    return _toString.call(object) == "[object Array]";
  }


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

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

  function isString(object) {
    return _toString.call(object) == "[object String]";
  }

  function isNumber(object) {
    return _toString.call(object) == "[object Number]";
  }

  function isUndefined(object) {
    return typeof object === "undefined";
  }

  extend(Object, {
    extend:        extend,
    inspect:       inspect,
    toJSON:        toJSON,
    toQueryString: toQueryString,
    toHTML:        toHTML,
    keys:          keys,
    values:        values,
    clone:         clone,
    isElement:     isElement,
    isArray:       isArray,
    isHash:        isHash,
    isFunction:    isFunction,
    isString:      isString,
    isNumber:      isNumber,
    isUndefined:   isUndefined
  });
})();
Object.extend(Function.prototype, (function() {
  var slice = Array.prototype.slice;

  function update(array, args) {
    var arrayLength = array.length, length = args.length;
    while (length--) array[arrayLength + length] = args[length];
    return array;
  }

  function merge(array, args) {
    array = slice.call(array, 0);
    return update(array, args);
  }

  function argumentNames() {
    var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
      .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
      .replace(/\s+/g, '').split(',');
    return names.length == 1 && !names[0] ? [] : names;
  }

  function bind(context) {
    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
    var __method = this, args = slice.call(arguments, 1);
    return function() {
      var a = merge(args, arguments);
      return __method.apply(context, a);
    }
  }

  function bindAsEventListener(context) {
    var __method = this, args = slice.call(arguments, 1);
    return function(event) {
      var a = update([event || window.event], args);
      return __method.apply(context, a);
    }
  }

  function curry() {
    if (!arguments.length) return this;
    var __method = this, args = slice.call(arguments, 0);
    return function() {
      var a = merge(args, arguments);
      return __method.apply(this, a);
    }
  }

  function delay(timeout) {
    var __method = this, args = slice.call(arguments, 1);
    timeout = timeout * 1000
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  }

  function defer() {
    var args = update([0.01], arguments);
    return this.delay.apply(this, args);
  }

  function wrap(wrapper) {
    var __method = this;
    return function() {
      var a = update([__method.bind(this)], arguments);
      return wrapper.apply(this, a);
    }
  }

  function methodize() {
    if (this._methodized) return this._methodized;
    var __method = this;
    return this._methodized = function() {
      var a = update([this], arguments);
      return __method.apply(null, a);
    };
  }

  return {
    argumentNames:       argumentNames,
    bind:                bind,
    bindAsEventListener: bindAsEventListener,
    curry:               curry,
    delay:               delay,
    defer:               defer,
    wrap:                wrap,
    methodize:           methodize
  }
})());


Date.prototype.toJSON = function() {
  return '"' + this.getUTCFullYear() + '-' +
    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
    this.getUTCDate().toPaddedString(2) + 'T' +
    this.getUTCHours().toPaddedString(2) + ':' +
    this.getUTCMinutes().toPaddedString(2) + ':' +
    this.getUTCSeconds().toPaddedString(2) + 'Z"';
};


RegExp.prototype.match = RegExp.prototype.test;

RegExp.escape = function(str) {
  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};
var PeriodicalExecuter = Class.create({
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

  registerCallback: function() {
    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  execute: function() {
    this.callback(this);
  },

  stop: function() {
    if (!this.timer) return;
    clearInterval(this.timer);
    this.timer = null;
  },

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.execute();
        this.currentlyExecuting = false;
      } catch(e) {
        this.currentlyExecuting = false;
        throw e;
      }
    }
  }
});
Object.extend(String, {
  interpret: function(value) {
    return value == null ? '' : String(value);
  },
  specialChar: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '\\': '\\\\'
  }
});

Object.extend(String.prototype, (function() {

  function prepareReplacement(replacement) {
    if (Object.isFunction(replacement)) return replacement;
    var template = new Template(replacement);
    return function(match) { return template.evaluate(match) };
  }

  function gsub(pattern, replacement) {
    var result = '', source = this, match;
    replacement = prepareReplacement(replacement);

    if (Object.isString(pattern))
      pattern = RegExp.escape(pattern);

    if (!(pattern.length || pattern.source)) {
      replacement = replacement('');
      return replacement + source.split('').join(replacement) + replacement;
    }

    while (source.length > 0) {
      if (match = source.match(pattern)) {
        result += source.slice(0, match.index);
        result += String.interpret(replacement(match));
        source  = source.slice(match.index + match[0].length);
      } else {
        result += source, source = '';
      }
    }
    return result;
  }

  function sub(pattern, replacement, count) {
    replacement = prepareReplacement(replacement);
    count = Object.isUndefined(count) ? 1 : count;

    return this.gsub(pattern, function(match) {
      if (--count < 0) return match[0];
      return replacement(match);
    });
  }

  function scan(pattern, iterator) {
    this.gsub(pattern, iterator);
    return String(this);
  }

  function truncate(length, truncation) {
    length = length || 30;
    truncation = Object.isUndefined(truncation) ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : String(this);
  }

  function strip() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  }

  function stripTags() {
    return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, '');
  }

  function stripScripts() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  }

  function extractScripts() {
    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
    return (this.match(matchAll) || []).map(function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  }

  function evalScripts() {
    return this.extractScripts().map(function(script) { return eval(script) });
  }

  function escapeHTML() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }

  function unescapeHTML() {
    return this.stripTags().replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&');
  }


  function toQueryParams(separator) {
    var match = this.strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return { };

    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var key = decodeURIComponent(pair.shift());
        var value = pair.length > 1 ? pair.join('=') : pair[0];
        if (value != undefined) value = decodeURIComponent(value);

        if (key in hash) {
          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
          hash[key].push(value);
        }
        else hash[key] = value;
      }
      return hash;
    });
  }

  function toArray() {
    return this.split('');
  }

  function succ() {
    return this.slice(0, this.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
  }

  function times(count) {
    return count < 1 ? '' : new Array(count + 1).join(this);
  }

  function camelize() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  }

  function capitalize() {
    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
  }

  function underscore() {
    return this.replace(/::/g, '/')
               .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
               .replace(/([a-z\d])([A-Z])/g, '$1_$2')
               .replace(/-/g, '_')
               .toLowerCase();
  }

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

  function inspect(useDoubleQuotes) {
    var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) {
      if (character in String.specialChar) {
        return String.specialChar[character];
      }
      return '\\u00' + character.charCodeAt().toPaddedString(2, 16);
    });
    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  }

  function toJSON() {
    return this.inspect(true);
  }

  function unfilterJSON(filter) {
    return this.replace(filter || Prototype.JSONFilter, '$1');
  }

  function isJSON() {
    var str = this;
    if (str.blank()) return false;
    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
  }

  function evalJSON(sanitize) {
    var json = this.unfilterJSON();
    try {
      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
    } catch (e) { }
    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
  }

  function include(pattern) {
    return this.indexOf(pattern) > -1;
  }

  function startsWith(pattern) {
    return this.indexOf(pattern) === 0;
  }

  function endsWith(pattern) {
    var d = this.length - pattern.length;
    return d >= 0 && this.lastIndexOf(pattern) === d;
  }

  function empty() {
    return this == '';
  }

  function blank() {
    return /^\s*$/.test(this);
  }

  function interpolate(object, pattern) {
    return new Template(this, pattern).evaluate(object);
  }

  return {
    gsub:           gsub,
    sub:            sub,
    scan:           scan,
    truncate:       truncate,
    strip:          String.prototype.trim ? String.prototype.trim : strip,
    stripTags:      stripTags,
    stripScripts:   stripScripts,
    extractScripts: extractScripts,
    evalScripts:    evalScripts,
    escapeHTML:     escapeHTML,
    unescapeHTML:   unescapeHTML,
    toQueryParams:  toQueryParams,
    parseQuery:     toQueryParams,
    toArray:        toArray,
    succ:           succ,
    times:          times,
    camelize:       camelize,
    capitalize:     capitalize,
    underscore:     underscore,
    dasherize:      dasherize,
    inspect:        inspect,
    toJSON:         toJSON,
    unfilterJSON:   unfilterJSON,
    isJSON:         isJSON,
    evalJSON:       evalJSON,
    include:        include,
    startsWith:     startsWith,
    endsWith:       endsWith,
    empty:          empty,
    blank:          blank,
    interpolate:    interpolate
  };
})());

var Template = Class.create({
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    if (object && Object.isFunction(object.toTemplateReplacements))
      object = object.toTemplateReplacements();

    return this.template.gsub(this.pattern, function(match) {
      if (object == null) return (match[1] + '');

      var before = match[1] || '';
      if (before == '\\') return match[2];

      var ctx = object, expr = match[3];
      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
      match = pattern.exec(expr);
      if (match == null) return before;

      while (match != null) {
        var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : match[1];
        ctx = ctx[comp];
        if (null == ctx || '' == match[3]) break;
        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
        match = pattern.exec(expr);
      }

      return before + String.interpret(ctx);
    });
  }
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

var $break = { };

var Enumerable = (function() {
  function each(iterator, context) {
    var index = 0;
    try {
      this._each(function(value) {
        iterator.call(context, value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  }

  function eachSlice(number, iterator, context) {
    var index = -number, slices = [], array = this.toArray();
    if (number < 1) return array;
    while ((index += number) < array.length)
      slices.push(array.slice(index, index+number));
    return slices.collect(iterator, context);
  }

  function all(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = true;
    this.each(function(value, index) {
      result = result && !!iterator.call(context, value, index);
      if (!result) throw $break;
    });
    return result;
  }

  function any(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = false;
    this.each(function(value, index) {
      if (result = !!iterator.call(context, value, index))
        throw $break;
    });
    return result;
  }

  function collect(iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];
    this.each(function(value, index) {
      results.push(iterator.call(context, value, index));
    });
    return results;
  }

  function detect(iterator, context) {
    var result;
    this.each(function(value, index) {
      if (iterator.call(context, value, index)) {
        result = value;
        throw $break;
      }
    });
    return result;
  }

  function findAll(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  }

  function grep(filter, iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];

    if (Object.isString(filter))
      filter = new RegExp(RegExp.escape(filter));

    this.each(function(value, index) {
      if (filter.match(value))
        results.push(iterator.call(context, value, index));
    });
    return results;
  }

  function include(object) {
    if (Object.isFunction(this.indexOf))
      if (this.indexOf(object) != -1) return true;

    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  }

  function inGroupsOf(number, fillWith) {
    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
    return this.eachSlice(number, function(slice) {
      while(slice.length < number) slice.push(fillWith);
      return slice;
    });
  }

  function inject(memo, iterator, context) {
    this.each(function(value, index) {
      memo = iterator.call(context, memo, value, index);
    });
    return memo;
  }

  function invoke(method) {
    var args = $A(arguments).slice(1);
    return this.map(function(value) {
      return value[method].apply(value, args);
    });
  }

  function max(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value >= result)
        result = value;
    });
    return result;
  }

  function min(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value < result)
        result = value;
    });
    return result;
  }

  function partition(iterator, context) {
    iterator = iterator || Prototype.K;
    var trues = [], falses = [];
    this.each(function(value, index) {
      (iterator.call(context, value, index) ?
        trues : falses).push(value);
    });
    return [trues, falses];
  }

  function pluck(property) {
    var results = [];
    this.each(function(value) {
      results.push(value[property]);
    });
    return results;
  }

  function reject(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (!iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  }

  function sortBy(iterator, context) {
    return this.map(function(value, index) {
      return {
        value: value,
        criteria: iterator.call(context, value, index)
      };
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }).pluck('value');
  }

  function toArray() {
    return this.map();
  }

  function zip() {
    var iterator = Prototype.K, args = $A(arguments);
    if (Object.isFunction(args.last()))
      iterator = args.pop();

    var collections = [this].concat(args).map($A);
    return this.map(function(value, index) {
      return iterator(collections.pluck(index));
    });
  }

  function size() {
    return this.toArray().length;
  }

  function inspect() {
    return '#<Enumerable:' + this.toArray().inspect() + '>';
  }









  return {
    each:       each,
    eachSlice:  eachSlice,
    all:        all,
    every:      all,
    any:        any,
    some:       any,
    collect:    collect,
    map:        collect,
    detect:     detect,
    findAll:    findAll,
    select:     findAll,
    filter:     findAll,
    grep:       grep,
    include:    include,
    member:     include,
    inGroupsOf: inGroupsOf,
    inject:     inject,
    invoke:     invoke,
    max:        max,
    min:        min,
    partition:  partition,
    pluck:      pluck,
    reject:     reject,
    sortBy:     sortBy,
    toArray:    toArray,
    entries:    toArray,
    zip:        zip,
    size:       size,
    inspect:    inspect,
    find:       detect
  };
})();
function $A(iterable) {
  if (!iterable) return [];
  if ('toArray' in Object(iterable)) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

function $w(string) {
  if (!Object.isString(string)) return [];
  string = string.strip();
  return string ? string.split(/\s+/) : [];
}

Array.from = $A;


(function() {
  var arrayProto = Array.prototype,
      slice = arrayProto.slice,
      _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available

  function each(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  }
  if (!_each) _each = each;

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

  function first() {
    return this[0];
  }

  function last() {
    return this[this.length - 1];
  }

  function compact() {
    return this.select(function(value) {
      return value != null;
    });
  }

  function flatten() {
    return this.inject([], function(array, value) {
      if (Object.isArray(value))
        return array.concat(value.flatten());
      array.push(value);
      return array;
    });
  }

  function without() {
    var values = slice.call(arguments, 0);
    return this.select(function(value) {
      return !values.include(value);
    });
  }

  function reverse(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  }

  function uniq(sorted) {
    return this.inject([], function(array, value, index) {
      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
        array.push(value);
      return array;
    });
  }

  function intersect(array) {
    return this.uniq().findAll(function(item) {
      return array.detect(function(value) { return item === value });
    });
  }


  function clone() {
    return slice.call(this, 0);
  }

  function size() {
    return this.length;
  }

  function inspect() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  }

  function toJSON() {
    var results = [];
    this.each(function(object) {
      var value = Object.toJSON(object);
      if (!Object.isUndefined(value)) results.push(value);
    });
    return '[' + results.join(', ') + ']';
  }

  function indexOf(item, i) {
    i || (i = 0);
    var length = this.length;
    if (i < 0) i = length + i;
    for (; i < length; i++)
      if (this[i] === item) return i;
    return -1;
  }

  function lastIndexOf(item, i) {
    i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
    var n = this.slice(0, i).reverse().indexOf(item);
    return (n < 0) ? n : i - n - 1;
  }

  function concat() {
    var array = slice.call(this, 0), item;
    for (var i = 0, length = arguments.length; i < length; i++) {
      item = arguments[i];
      if (Object.isArray(item) && !('callee' in item)) {
        for (var j = 0, arrayLength = item.length; j < arrayLength; j++)
          array.push(item[j]);
      } else {
        array.push(item);
      }
    }
    return array;
  }

  Object.extend(arrayProto, Enumerable);

  if (!arrayProto._reverse)
    arrayProto._reverse = arrayProto.reverse;

  Object.extend(arrayProto, {
    _each:     _each,
    clear:     clear,
    first:     first,
    last:      last,
    compact:   compact,
    flatten:   flatten,
    without:   without,
    reverse:   reverse,
    uniq:      uniq,
    intersect: intersect,
    clone:     clone,
    toArray:   clone,
    size:      size,
    inspect:   inspect,
    toJSON:    toJSON
  });

  var CONCAT_ARGUMENTS_BUGGY = (function() {
    return [].concat(arguments)[0][0] !== 1;
  })(1,2)

  if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat;

  if (!arrayProto.indexOf) arrayProto.indexOf = indexOf;
  if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf;
})();
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {
  function initialize(object) {
    this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
  }

  function _each(iterator) {
    for (var key in this._object) {
      var value = this._object[key], pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  }

  function set(key, value) {
    return this._object[key] = value;
  }

  function get(key) {
    if (this._object[key] !== Object.prototype[key])
      return this._object[key];
  }

  function unset(key) {
    var value = this._object[key];
    delete this._object[key];
    return value;
  }

  function toObject() {
    return Object.clone(this._object);
  }

  function keys() {
    return this.pluck('key');
  }

  function values() {
    return this.pluck('value');
  }

  function index(value) {
    var match = this.detect(function(pair) {
      return pair.value === value;
    });
    return match && match.key;
  }

  function merge(object) {
    return this.clone().update(object);
  }

  function update(object) {
    return new Hash(object).inject(this, function(result, pair) {
      result.set(pair.key, pair.value);
      return result;
    });
  }

  function toQueryPair(key, value) {
    if (Object.isUndefined(value)) return key;
    return key + '=' + encodeURIComponent(String.interpret(value));
  }

  function toQueryString() {
    return this.inject([], function(results, pair) {
      var key = encodeURIComponent(pair.key), values = pair.value;

      if (values && typeof values == 'object') {
        if (Object.isArray(values))
          return results.concat(values.map(toQueryPair.curry(key)));
      } else results.push(toQueryPair(key, values));
      return results;
    }).join('&');
  }

  function inspect() {
    return '#<Hash:{' + this.map(function(pair) {
      return pair.map(Object.inspect).join(': ');
    }).join(', ') + '}>';
  }

  function toJSON() {
    return Object.toJSON(this.toObject());
  }

  function clone() {
    return new Hash(this);
  }

  return {
    initialize:             initialize,
    _each:                  _each,
    set:                    set,
    get:                    get,
    unset:                  unset,
    toObject:               toObject,
    toTemplateReplacements: toObject,
    keys:                   keys,
    values:                 values,
    index:                  index,
    merge:                  merge,
    update:                 update,
    toQueryString:          toQueryString,
    inspect:                inspect,
    toJSON:                 toJSON,
    clone:                  clone
  };
})());

Hash.from = $H;
Object.extend(Number.prototype, (function() {
  function toColorPart() {
    return this.toPaddedString(2, 16);
  }

  function succ() {
    return this + 1;
  }

  function times(iterator, context) {
    $R(0, this, true).each(iterator, context);
    return this;
  }

  function toPaddedString(length, radix) {
    var string = this.toString(radix || 10);
    return '0'.times(length - string.length) + string;
  }

  function toJSON() {
    return isFinite(this) ? this.toString() : 'null';
  }

  function abs() {
    return Math.abs(this);
  }

  function round() {
    return Math.round(this);
  }

  function ceil() {
    return Math.ceil(this);
  }

  function floor() {
    return Math.floor(this);
  }

  return {
    toColorPart:    toColorPart,
    succ:           succ,
    times:          times,
    toPaddedString: toPaddedString,
    toJSON:         toJSON,
    abs:            abs,
    round:          round,
    ceil:           ceil,
    floor:          floor
  };
})());

function $R(start, end, exclusive) {
  return new ObjectRange(start, end, exclusive);
}

var ObjectRange = Class.create(Enumerable, (function() {
  function initialize(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  }

  function _each(iterator) {
    var value = this.start;
    while (this.include(value)) {
      iterator(value);
      value = value.succ();
    }
  }

  function include(value) {
    if (value < this.start)
      return false;
    if (this.exclusive)
      return value < this.end;
    return value <= this.end;
  }

  return {
    initialize: initialize,
    _each:      _each,
    include:    include
  };
})());



var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (Object.isFunction(responder[callback])) {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) { }
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate:   function() { Ajax.activeRequestCount++ },
  onComplete: function() { Ajax.activeRequestCount-- }
});
Ajax.Base = Class.create({
  initialize: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   '',
      evalJSON:     true,
      evalJS:       true
    };
    Object.extend(this.options, options || { });

    this.options.method = this.options.method.toLowerCase();

    if (Object.isString(this.options.parameters))
      this.options.parameters = this.options.parameters.toQueryParams();
    else if (Object.isHash(this.options.parameters))
      this.options.parameters = this.options.parameters.toObject();
  }
});
Ajax.Request = Class.create(Ajax.Base, {
  _complete: false,

  initialize: function($super, url, options) {
    $super(options);
    this.transport = Ajax.getTransport();
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = Object.clone(this.options.parameters);

    if (!['get', 'post'].include(this.method)) {
      params['_method'] = this.method;
      this.method = 'post';
    }

    this.parameters = params;

    if (params = Object.toQueryString(params)) {
      if (this.method == 'get')
        this.url += (this.url.include('?') ? '&' : '?') + params;
      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
        params += '&_=';
    }

    try {
      var response = new Ajax.Response(this);
      if (this.options.onCreate) this.options.onCreate(response);
      Ajax.Responders.dispatch('onCreate', this, response);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
      this.transport.send(this.body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'X-Prototype-Version': Prototype.Version,
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    };

    if (this.method == 'post') {
      headers['Content-type'] = this.options.contentType +
        (this.options.encoding ? '; charset=' + this.options.encoding : '');

      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (Object.isFunction(extras.push))
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    var status = this.getStatus();
    return !status || (status >= 200 && status < 300);
  },

  getStatus: function() {
    try {
      return this.transport.status || 0;
    } catch (e) { return 0 }
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && this.isSameOrigin() && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  isSameOrigin: function() {
    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
      protocol: location.protocol,
      domain: document.domain,
      port: location.port ? ':' + location.port : ''
    }));
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name) || null;
    } catch (e) { return null; }
  },

  evalResponse: function() {
    try {
      return eval((this.transport.responseText || '').unfilterJSON());
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];








Ajax.Response = Class.create({
  initialize: function(request){
    this.request = request;
    var transport  = this.transport  = request.transport,
        readyState = this.readyState = transport.readyState;

    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
      this.status       = this.getStatus();
      this.statusText   = this.getStatusText();
      this.responseText = String.interpret(transport.responseText);
      this.headerJSON   = this._getHeaderJSON();
    }

    if(readyState == 4) {
      var xml = transport.responseXML;
      this.responseXML  = Object.isUndefined(xml) ? null : xml;
      this.responseJSON = this._getResponseJSON();
    }
  },

  status:      0,

  statusText: '',

  getStatus: Ajax.Request.prototype.getStatus,

  getStatusText: function() {
    try {
      return this.transport.statusText || '';
    } catch (e) { return '' }
  },

  getHeader: Ajax.Request.prototype.getHeader,

  getAllHeaders: function() {
    try {
      return this.getAllResponseHeaders();
    } catch (e) { return null }
  },

  getResponseHeader: function(name) {
    return this.transport.getResponseHeader(name);
  },

  getAllResponseHeaders: function() {
    return this.transport.getAllResponseHeaders();
  },

  _getHeaderJSON: function() {
    var json = this.getHeader('X-JSON');
    if (!json) return null;
    json = decodeURIComponent(escape(json));
    try {
      return json.evalJSON(this.request.options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  },

  _getResponseJSON: function() {
    var options = this.request.options;
    if (!options.evalJSON || (options.evalJSON != 'force' &&
      !(this.getHeader('Content-type') || '').include('application/json')) ||
        this.responseText.blank())
          return null;
    try {
      return this.responseText.evalJSON(options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  }
});

Ajax.Updater = Class.create(Ajax.Request, {
  initialize: function($super, container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    };

    options = Object.clone(options);
    var onComplete = options.onComplete;
    options.onComplete = (function(response, json) {
      this.updateContent(response.responseText);
      if (Object.isFunction(onComplete)) onComplete(response, json);
    }).bind(this);

    $super(url, options);
  },

  updateContent: function(responseText) {
    var receiver = this.container[this.success() ? 'success' : 'failure'],
        options = this.options;

    if (!options.evalScripts) responseText = responseText.stripScripts();

    if (receiver = $(receiver)) {
      if (options.insertion) {
        if (Object.isString(options.insertion)) {
          var insertion = { }; insertion[options.insertion] = responseText;
          receiver.insert(insertion);
        }
        else options.insertion(receiver, responseText);
      }
      else receiver.update(responseText);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
  initialize: function($super, container, url, options) {
    $super(options);
    this.onComplete = this.options.onComplete;

    this.frequency = (this.options.frequency || 2);
    this.decay = (this.options.decay || 1);

    this.updater = { };
    this.container = container;
    this.url = url;

    this.start();
  },

  start: function() {
    this.options.onComplete = this.updateComplete.bind(this);
    this.onTimerEvent();
  },

  stop: function() {
    this.updater.options.onComplete = undefined;
    clearTimeout(this.timer);
    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
  },

  updateComplete: function(response) {
    if (this.options.decay) {
      this.decay = (response.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = response.responseText;
    }
    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});



function $(element) {
  if (arguments.length > 1) {
    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
      elements.push($(arguments[i]));
    return elements;
  }
  if (Object.isString(element))
    element = document.getElementById(element);
  return Element.extend(element);
}

if (Prototype.BrowserFeatures.XPath) {
  document._getElementsByXPath = function(expression, parentElement) {
    var results = [];
    var query = document.evaluate(expression, $(parentElement) || document,
      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, length = query.snapshotLength; i < length; i++)
      results.push(Element.extend(query.snapshotItem(i)));
    return results;
  };
}

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

if (!window.Node) var Node = { };

if (!Node.ELEMENT_NODE) {
  Object.extend(Node, {
    ELEMENT_NODE: 1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE: 3,
    CDATA_SECTION_NODE: 4,
    ENTITY_REFERENCE_NODE: 5,
    ENTITY_NODE: 6,
    PROCESSING_INSTRUCTION_NODE: 7,
    COMMENT_NODE: 8,
    DOCUMENT_NODE: 9,
    DOCUMENT_TYPE_NODE: 10,
    DOCUMENT_FRAGMENT_NODE: 11,
    NOTATION_NODE: 12
  });
}


(function(global) {

  var SETATTRIBUTE_IGNORES_NAME = (function(){
    var elForm = document.createElement("form");
    var elInput = document.createElement("input");
    var root = document.documentElement;
    elInput.setAttribute("name", "test");
    elForm.appendChild(elInput);
    root.appendChild(elForm);
    var isBuggy = elForm.elements
      ? (typeof elForm.elements.test == "undefined")
      : null;
    root.removeChild(elForm);
    elForm = elInput = null;
    return isBuggy;
  })();

  var element = global.Element;
  global.Element = function(tagName, attributes) {
    attributes = attributes || { };
    tagName = tagName.toLowerCase();
    var cache = Element.cache;
    if (SETATTRIBUTE_IGNORES_NAME && attributes.name) {
      tagName = '<' + tagName + ' name="' + attributes.name + '">';
      delete attributes.name;
      return Element.writeAttribute(document.createElement(tagName), attributes);
    }
    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
  };
  Object.extend(global.Element, element || { });
  if (element) global.Element.prototype = element.prototype;
})(this);

Element.cache = { };
Element.idCounter = 1;

Element.Methods = {
  visible: function(element) {
    return $(element).style.display != 'none';
  },

  toggle: function(element) {
    element = $(element);
    Element[Element.visible(element) ? 'hide' : 'show'](element);
    return element;
  },


  hide: function(element) {
    element = $(element);
    element.style.display = 'none';
    return element;
  },

  show: function(element) {
    element = $(element);
    element.style.display = '';
    return element;
  },

  remove: function(element) {
    element = $(element);
    element.parentNode.removeChild(element);
    return element;
  },

  update: (function(){

    var SELECT_ELEMENT_INNERHTML_BUGGY = (function(){
      var el = document.createElement("select"),
          isBuggy = true;
      el.innerHTML = "<option value=\"test\">test</option>";
      if (el.options && el.options[0]) {
        isBuggy = el.options[0].nodeName.toUpperCase() !== "OPTION";
      }
      el = null;
      return isBuggy;
    })();

    var TABLE_ELEMENT_INNERHTML_BUGGY = (function(){
      try {
        var el = document.createElement("table");
        if (el && el.tBodies) {
          el.innerHTML = "<tbody><tr><td>test</td></tr></tbody>";
          var isBuggy = typeof el.tBodies[0] == "undefined";
          el = null;
          return isBuggy;
        }
      } catch (e) {
        return true;
      }
    })();

    var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () {
      var s = document.createElement("script"),
          isBuggy = false;
      try {
        s.appendChild(document.createTextNode(""));
        isBuggy = !s.firstChild ||
          s.firstChild && s.firstChild.nodeType !== 3;
      } catch (e) {
        isBuggy = true;
      }
      s = null;
      return isBuggy;
    })();

    function update(element, content) {
      element = $(element);

      if (content && content.toElement)
        content = content.toElement();

      if (Object.isElement(content))
        return element.update().insert(content);

      content = Object.toHTML(content);

      var tagName = element.tagName.toUpperCase();

      if (tagName === 'SCRIPT' && SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING) {
        element.text = content;
        return element;
      }

      if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) {
        if (tagName in Element._insertionTranslations.tags) {
          while (element.firstChild) {
            element.removeChild(element.firstChild);
          }
          Element._getContentFromAnonymousElement(tagName, content.stripScripts())
            .each(function(node) {
              element.appendChild(node)
            });
        }
        else {
          element.innerHTML = content.stripScripts();
        }
      }
      else {
        element.innerHTML = content.stripScripts();
      }

      content.evalScripts.bind(content).defer();
      return element;
    }

    return update;
  })(),

  replace: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    else if (!Object.isElement(content)) {
      content = Object.toHTML(content);
      var range = element.ownerDocument.createRange();
      range.selectNode(element);
      content.evalScripts.bind(content).defer();
      content = range.createContextualFragment(content.stripScripts());
    }
    element.parentNode.replaceChild(content, element);
    return element;
  },

  insert: function(element, insertions) {
    element = $(element);

    if (Object.isString(insertions) || Object.isNumber(insertions) ||
        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
          insertions = {bottom:insertions};

    var content, insert, tagName, childNodes;

    for (var position in insertions) {
      content  = insertions[position];
      position = position.toLowerCase();
      insert = Element._insertionTranslations[position];

      if (content && content.toElement) content = content.toElement();
      if (Object.isElement(content)) {
        insert(element, content);
        continue;
      }

      content = Object.toHTML(content);

      tagName = ((position == 'before' || position == 'after')
        ? element.parentNode : element).tagName.toUpperCase();

      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

      if (position == 'top' || position == 'after') childNodes.reverse();
      childNodes.each(insert.curry(element));

      content.evalScripts.bind(content).defer();
    }

    return element;
  },

  wrap: function(element, wrapper, attributes) {
    element = $(element);
    if (Object.isElement(wrapper))
      $(wrapper).writeAttribute(attributes || { });
    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
    else wrapper = new Element('div', wrapper);
    if (element.parentNode)
      element.parentNode.replaceChild(wrapper, element);
    wrapper.appendChild(element);
    return wrapper;
  },

  inspect: function(element) {
    element = $(element);
    var result = '<' + element.tagName.toLowerCase();
    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
      var property = pair.first(), attribute = pair.last();
      var value = (element[property] || '').toString();
      if (value) result += ' ' + attribute + '=' + value.inspect(true);
    });
    return result + '>';
  },

  recursivelyCollect: function(element, property) {
    element = $(element);
    var elements = [];
    while (element = element[property])
      if (element.nodeType == 1)
        elements.push(Element.extend(element));
    return elements;
  },

  ancestors: function(element) {
    return Element.recursivelyCollect(element, 'parentNode');
  },

  descendants: function(element) {
    return Element.select(element, "*");
  },

  firstDescendant: function(element) {
    element = $(element).firstChild;
    while (element && element.nodeType != 1) element = element.nextSibling;
    return $(element);
  },

  immediateDescendants: function(element) {
    if (!(element = $(element).firstChild)) return [];
    while (element && element.nodeType != 1) element = element.nextSibling;
    if (element) return [element].concat($(element).nextSiblings());
    return [];
  },

  previousSiblings: function(element) {
    return Element.recursivelyCollect(element, 'previousSibling');
  },

  nextSiblings: function(element) {
    return Element.recursivelyCollect(element, 'nextSibling');
  },

  siblings: function(element) {
    element = $(element);
    return Element.previousSiblings(element).reverse()
      .concat(Element.nextSiblings(element));
  },

  match: function(element, selector) {
    if (Object.isString(selector))
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(element.parentNode);
    var ancestors = Element.ancestors(element);
    return Object.isNumber(expression) ? ancestors[expression] :
      Selector.findElement(ancestors, expression, index);
  },

  down: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return Element.firstDescendant(element);
    return Object.isNumber(expression) ? Element.descendants(element)[expression] :
      Element.select(element, expression)[index || 0];
  },

  previous: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
    var previousSiblings = Element.previousSiblings(element);
    return Object.isNumber(expression) ? previousSiblings[expression] :
      Selector.findElement(previousSiblings, expression, index);
  },

  next: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
    var nextSiblings = Element.nextSiblings(element);
    return Object.isNumber(expression) ? nextSiblings[expression] :
      Selector.findElement(nextSiblings, expression, index);
  },


  select: function(element) {
    var args = Array.prototype.slice.call(arguments, 1);
    return Selector.findChildElements(element, args);
  },

  adjacent: function(element) {
    var args = Array.prototype.slice.call(arguments, 1);
    return Selector.findChildElements(element.parentNode, args).without(element);
  },

  identify: function(element) {
    element = $(element);
    var id = Element.readAttribute(element, 'id');
    if (id) return id;
    do { id = 'anonymous_element_' + Element.idCounter++ } while ($(id));
    Element.writeAttribute(element, 'id', id);
    return id;
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (Prototype.Browser.IE) {
      var t = Element._attributeTranslations.read;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name]) name = t.names[name];
      if (name.include(':')) {
        return (!element.attributes || !element.attributes[name]) ? null :
         element.attributes[name].value;
      }
    }
    return element.getAttribute(name);
  },

  writeAttribute: function(element, name, value) {
    element = $(element);
    var attributes = { }, t = Element._attributeTranslations.write;

    if (typeof name == 'object') attributes = name;
    else attributes[name] = Object.isUndefined(value) ? true : value;

    for (var attr in attributes) {
      name = t.names[attr] || attr;
      value = attributes[attr];
      if (t.values[attr]) name = t.values[attr](element, value);
      if (value === false || value === null)
        element.removeAttribute(name);
      else if (value === true)
        element.setAttribute(name, name);
      else element.setAttribute(name, value);
    }
    return element;
  },

  getHeight: function(element) {
    return Element.getDimensions(element).height;
  },

  getWidth: function(element) {
    return Element.getDimensions(element).width;
  },

  classNames: function(element) {
    return new Element.ClassNames(element);
  },

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    var elementClassName = element.className;
    return (elementClassName.length > 0 && (elementClassName == className ||
      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    if (!Element.hasClassName(element, className))
      element.className += (element.className ? ' ' : '') + className;
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    element.className = element.className.replace(
      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
    return element;
  },

  toggleClassName: function(element, className) {
    if (!(element = $(element))) return;
    return Element[Element.hasClassName(element, className) ?
      'removeClassName' : 'addClassName'](element, className);
  },

  cleanWhitespace: function(element) {
    element = $(element);
    var node = element.firstChild;
    while (node) {
      var nextNode = node.nextSibling;
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        element.removeChild(node);
      node = nextNode;
    }
    return element;
  },

  empty: function(element) {
    return $(element).innerHTML.blank();
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);

    if (element.compareDocumentPosition)
      return (element.compareDocumentPosition(ancestor) & 8) === 8;

    if (ancestor.contains)
      return ancestor.contains(element) && ancestor !== element;

    while (element = element.parentNode)
      if (element == ancestor) return true;

    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = Element.cumulativeOffset(element);
    window.scrollTo(pos[0], pos[1]);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    style = style == 'float' ? 'cssFloat' : style.camelize();
    var value = element.style[style];
    if (!value || value == 'auto') {
      var css = document.defaultView.getComputedStyle(element, null);
      value = css ? css[style] : null;
    }
    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
    return value == 'auto' ? null : value;
  },

  getOpacity: function(element) {
    return $(element).getStyle('opacity');
  },

  setStyle: function(element, styles) {
    element = $(element);
    var elementStyle = element.style, match;
    if (Object.isString(styles)) {
      element.style.cssText += ';' + styles;
      return styles.include('opacity') ?
        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
    }
    for (var property in styles)
      if (property == 'opacity') element.setOpacity(styles[property]);
      else
        elementStyle[(property == 'float' || property == 'cssFloat') ?
          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
            property] = styles[property];

    return element;
  },

  setOpacity: function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    var display = Element.getStyle(element, 'display');
    if (display != 'none' && display != null) // Safari bug
      return {width: element.offsetWidth, height: element.offsetHeight};

    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    if (originalPosition != 'fixed') // Switching fixed to absolute causes issues in Safari
      els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};
  },

  makePositioned: function(element) {
    element = $(element);
    var pos = Element.getStyle(element, 'position');
    if (pos == 'static' || !pos) {
      element._madePositioned = true;
      element.style.position = 'relative';
      if (Prototype.Browser.Opera) {
        element.style.top = 0;
        element.style.left = 0;
      }
    }
    return element;
  },

  undoPositioned: function(element) {
    element = $(element);
    if (element._madePositioned) {
      element._madePositioned = undefined;
      element.style.position =
        element.style.top =
        element.style.left =
        element.style.bottom =
        element.style.right = '';
    }
    return element;
  },

  makeClipping: function(element) {
    element = $(element);
    if (element._overflow) return element;
    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
    if (element._overflow !== 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

  undoClipping: function(element) {
    element = $(element);
    if (!element._overflow) return element;
    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
    element._overflow = null;
    return element;
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        if (element.tagName.toUpperCase() == 'BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p !== 'static') break;
      }
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  absolutize: function(element) {
    element = $(element);
    if (Element.getStyle(element, 'position') == 'absolute') return element;

    var offsets = Element.positionedOffset(element);
    var top     = offsets[1];
    var left    = offsets[0];
    var width   = element.clientWidth;
    var height  = element.clientHeight;

    element._originalLeft   = left - parseFloat(element.style.left  || 0);
    element._originalTop    = top  - parseFloat(element.style.top || 0);
    element._originalWidth  = element.style.width;
    element._originalHeight = element.style.height;

    element.style.position = 'absolute';
    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.width  = width + 'px';
    element.style.height = height + 'px';
    return element;
  },

  relativize: function(element) {
    element = $(element);
    if (Element.getStyle(element, 'position') == 'relative') return element;

    element.style.position = 'relative';
    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.height = element._originalHeight;
    element.style.width  = element._originalWidth;
    return element;
  },

  cumulativeScrollOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  getOffsetParent: function(element) {
    if (element.offsetParent) return $(element.offsetParent);
    if (element == document.body) return $(element);

    while ((element = element.parentNode) && element != document.body)
      if (Element.getStyle(element, 'position') != 'static')
        return $(element);

    return $(document.body);
  },

  viewportOffset: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      if (element.offsetParent == document.body &&
        Element.getStyle(element, 'position') == 'absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return Element._returnOffset(valueL, valueT);
  },

  clonePosition: function(element, source) {
    var options = Object.extend({
      setLeft:    true,
      setTop:     true,
      setWidth:   true,
      setHeight:  true,
      offsetTop:  0,
      offsetLeft: 0
    }, arguments[2] || { });

    source = $(source);
    var p = Element.viewportOffset(source);

    element = $(element);
    var delta = [0, 0];
    var parent = null;
    if (Element.getStyle(element, 'position') == 'absolute') {
      parent = Element.getOffsetParent(element);
      delta = Element.viewportOffset(parent);
    }

    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
    return element;
  }
};

Object.extend(Element.Methods, {
  getElementsBySelector: Element.Methods.select,

  childElements: Element.Methods.immediateDescendants
});

Element._attributeTranslations = {
  write: {
    names: {
      className: 'class',
      htmlFor:   'for'
    },
    values: { }
  }
};

if (Prototype.Browser.Opera) {
  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
    function(proceed, element, style) {
      switch (style) {
        case 'left': case 'top': case 'right': case 'bottom':
          if (proceed(element, 'position') === 'static') return null;
        case 'height': case 'width':
          if (!Element.visible(element)) return null;

          var dim = parseInt(proceed(element, style), 10);

          if (dim !== element['offset' + style.capitalize()])
            return dim + 'px';

          var properties;
          if (style === 'height') {
            properties = ['border-top-width', 'padding-top',
             'padding-bottom', 'border-bottom-width'];
          }
          else {
            properties = ['border-left-width', 'padding-left',
             'padding-right', 'border-right-width'];
          }
          return properties.inject(dim, function(memo, property) {
            var val = proceed(element, property);
            return val === null ? memo : memo - parseInt(val, 10);
          }) + 'px';
        default: return proceed(element, style);
      }
    }
  );

  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
    function(proceed, element, attribute) {
      if (attribute === 'title') return element.title;
      return proceed(element, attribute);
    }
  );
}

else if (Prototype.Browser.IE) {
  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
    function(proceed, element) {
      element = $(element);
      try { element.offsetParent }
      catch(e) { return $(document.body) }
      var position = element.getStyle('position');
      if (position !== 'static') return proceed(element);
      element.setStyle({ position: 'relative' });
      var value = proceed(element);
      element.setStyle({ position: position });
      return value;
    }
  );

  $w('positionedOffset viewportOffset').each(function(method) {
    Element.Methods[method] = Element.Methods[method].wrap(
      function(proceed, element) {
        element = $(element);
        try { element.offsetParent }
        catch(e) { return Element._returnOffset(0,0) }
        var position = element.getStyle('position');
        if (position !== 'static') return proceed(element);
        var offsetParent = element.getOffsetParent();
        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
          offsetParent.setStyle({ zoom: 1 });
        element.setStyle({ position: 'relative' });
        var value = proceed(element);
        element.setStyle({ position: position });
        return value;
      }
    );
  });

  Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
    function(proceed, element) {
      try { element.offsetParent }
      catch(e) { return Element._returnOffset(0,0) }
      return proceed(element);
    }
  );

  Element.Methods.getStyle = function(element, style) {
    element = $(element);
    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
    var value = element.style[style];
    if (!value && element.currentStyle) value = element.currentStyle[style];

    if (style == 'opacity') {
      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if (value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }

    if (value == 'auto') {
      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
        return element['offset' + style.capitalize()] + 'px';
      return null;
    }
    return value;
  };

  Element.Methods.setOpacity = function(element, value) {
    function stripAlpha(filter){
      return filter.replace(/alpha\([^\)]*\)/gi,'');
    }
    element = $(element);
    var currentStyle = element.currentStyle;
    if ((currentStyle && !currentStyle.hasLayout) ||
      (!currentStyle && element.style.zoom == 'normal'))
        element.style.zoom = 1;

    var filter = element.getStyle('filter'), style = element.style;
    if (value == 1 || value === '') {
      (filter = stripAlpha(filter)) ?
        style.filter = filter : style.removeAttribute('filter');
      return element;
    } else if (value < 0.00001) value = 0;
    style.filter = stripAlpha(filter) +
      'alpha(opacity=' + (value * 100) + ')';
    return element;
  };

  Element._attributeTranslations = (function(){

    var classProp = 'className';
    var forProp = 'for';

    var el = document.createElement('div');

    el.setAttribute(classProp, 'x');

    if (el.className !== 'x') {
      el.setAttribute('class', 'x');
      if (el.className === 'x') {
        classProp = 'class';
      }
    }
    el = null;

    el = document.createElement('label');
    el.setAttribute(forProp, 'x');
    if (el.htmlFor !== 'x') {
      el.setAttribute('htmlFor', 'x');
      if (el.htmlFor === 'x') {
        forProp = 'htmlFor';
      }
    }
    el = null;

    return {
      read: {
        names: {
          'class':      classProp,
          'className':  classProp,
          'for':        forProp,
          'htmlFor':    forProp
        },
        values: {
          _getAttr: function(element, attribute) {
            return element.getAttribute(attribute);
          },
          _getAttr2: function(element, attribute) {
            return element.getAttribute(attribute, 2);
          },
          _getAttrNode: function(element, attribute) {
            var node = element.getAttributeNode(attribute);
            return node ? node.value : "";
          },
          _getEv: (function(){

            var el = document.createElement('div');
            el.onclick = Prototype.emptyFunction;
            var value = el.getAttribute('onclick');
            var f;

            if (String(value).indexOf('{') > -1) {
              f = function(element, attribute) {
                attribute = element.getAttribute(attribute);
                if (!attribute) return null;
                attribute = attribute.toString();
                attribute = attribute.split('{')[1];
                attribute = attribute.split('}')[0];
                return attribute.strip();
              };
            }
            else if (value === '') {
              f = function(element, attribute) {
                attribute = element.getAttribute(attribute);
                if (!attribute) return null;
                return attribute.strip();
              };
            }
            el = null;
            return f;
          })(),
          _flag: function(element, attribute) {
            return $(element).hasAttribute(attribute) ? attribute : null;
          },
          style: function(element) {
            return element.style.cssText.toLowerCase();
          },
          title: function(element) {
            return element.title;
          }
        }
      }
    }
  })();

  Element._attributeTranslations.write = {
    names: Object.extend({
      cellpadding: 'cellPadding',
      cellspacing: 'cellSpacing'
    }, Element._attributeTranslations.read.names),
    values: {
      checked: function(element, value) {
        element.checked = !!value;
      },

      style: function(element, value) {
        element.style.cssText = value ? value : '';
      }
    }
  };

  Element._attributeTranslations.has = {};

  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
      'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
  });

  (function(v) {
    Object.extend(v, {
      href:        v._getAttr2,
      src:         v._getAttr2,
      type:        v._getAttr,
      action:      v._getAttrNode,
      disabled:    v._flag,
      checked:     v._flag,
      readonly:    v._flag,
      multiple:    v._flag,
      onload:      v._getEv,
      onunload:    v._getEv,
      onclick:     v._getEv,
      ondblclick:  v._getEv,
      onmousedown: v._getEv,
      onmouseup:   v._getEv,
      onmouseover: v._getEv,
      onmousemove: v._getEv,
      onmouseout:  v._getEv,
      onfocus:     v._getEv,
      onblur:      v._getEv,
      onkeypress:  v._getEv,
      onkeydown:   v._getEv,
      onkeyup:     v._getEv,
      onsubmit:    v._getEv,
      onreset:     v._getEv,
      onselect:    v._getEv,
      onchange:    v._getEv
    });
  })(Element._attributeTranslations.read.values);

  if (Prototype.BrowserFeatures.ElementExtensions) {
    (function() {
      function _descendants(element) {
        var nodes = element.getElementsByTagName('*'), results = [];
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName !== "!") // Filter out comment nodes.
            results.push(node);
        return results;
      }

      Element.Methods.down = function(element, expression, index) {
        element = $(element);
        if (arguments.length == 1) return element.firstDescendant();
        return Object.isNumber(expression) ? _descendants(element)[expression] :
          Element.select(element, expression)[index || 0];
      }
    })();
  }

}

else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1) ? 0.999999 :
      (value === '') ? '' : (value < 0.00001) ? 0 : value;
    return element;
  };
}

else if (Prototype.Browser.WebKit) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;

    if (value == 1)
      if(element.tagName.toUpperCase() == 'IMG' && element.width) {
        element.width++; element.width--;
      } else try {
        var n = document.createTextNode(' ');
        element.appendChild(n);
        element.removeChild(n);
      } catch (e) { }

    return element;
  };

  Element.Methods.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return Element._returnOffset(valueL, valueT);
  };
}

if ('outerHTML' in document.documentElement) {
  Element.Methods.replace = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) {
      element.parentNode.replaceChild(content, element);
      return element;
    }

    content = Object.toHTML(content);
    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

    if (Element._insertionTranslations.tags[tagName]) {
      var nextSibling = element.next();
      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
      parent.removeChild(element);
      if (nextSibling)
        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
      else
        fragments.each(function(node) { parent.appendChild(node) });
    }
    else element.outerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

Element._returnOffset = function(l, t) {
  var result = [l, t];
  result.left = l;
  result.top = t;
  return result;
};

Element._getContentFromAnonymousElement = function(tagName, html) {
  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
  if (t) {
    div.innerHTML = t[0] + html + t[1];
    t[2].times(function() { div = div.firstChild });
  } else div.innerHTML = html;
  return $A(div.childNodes);
};

Element._insertionTranslations = {
  before: function(element, node) {
    element.parentNode.insertBefore(node, element);
  },
  top: function(element, node) {
    element.insertBefore(node, element.firstChild);
  },
  bottom: function(element, node) {
    element.appendChild(node);
  },
  after: function(element, node) {
    element.parentNode.insertBefore(node, element.nextSibling);
  },
  tags: {
    TABLE:  ['<table>',                '</table>',                   1],
    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
    SELECT: ['<select>',               '</select>',                  1]
  }
};

(function() {
  var tags = Element._insertionTranslations.tags;
  Object.extend(tags, {
    THEAD: tags.TBODY,
    TFOOT: tags.TBODY,
    TH:    tags.TD
  });
})();

Element.Methods.Simulated = {
  hasAttribute: function(element, attribute) {
    attribute = Element._attributeTranslations.has[attribute] || attribute;
    var node = $(element).getAttributeNode(attribute);
    return !!(node && node.specified);
  }
};

Element.Methods.ByTag = { };

Object.extend(Element, Element.Methods);

(function(div) {

  if (!Prototype.BrowserFeatures.ElementExtensions && div['__proto__']) {
    window.HTMLElement = { };
    window.HTMLElement.prototype = div['__proto__'];
    Prototype.BrowserFeatures.ElementExtensions = true;
  }

  div = null;

})(document.createElement('div'))

Element.extend = (function() {

  function checkDeficiency(tagName) {
    if (typeof window.Element != 'undefined') {
      var proto = window.Element.prototype;
      if (proto) {
        var id = '_' + (Math.random()+'').slice(2);
        var el = document.createElement(tagName);
        proto[id] = 'x';
        var isBuggy = (el[id] !== 'x');
        delete proto[id];
        el = null;
        return isBuggy;
      }
    }
    return false;
  }

  function extendElementWith(element, methods) {
    for (var property in methods) {
      var value = methods[property];
      if (Object.isFunction(value) && !(property in element))
        element[property] = value.methodize();
    }
  }

  var HTMLOBJECTELEMENT_PROTOTYPE_BUGGY = checkDeficiency('object');

  if (Prototype.BrowserFeatures.SpecificElementExtensions) {
    if (HTMLOBJECTELEMENT_PROTOTYPE_BUGGY) {
      return function(element) {
        if (element && typeof element._extendedByPrototype == 'undefined') {
          var t = element.tagName;
          if (t && (/^(?:object|applet|embed)$/i.test(t))) {
            extendElementWith(element, Element.Methods);
            extendElementWith(element, Element.Methods.Simulated);
            extendElementWith(element, Element.Methods.ByTag[t.toUpperCase()]);
          }
        }
        return element;
      }
    }
    return Prototype.K;
  }

  var Methods = { }, ByTag = Element.Methods.ByTag;

  var extend = Object.extend(function(element) {
    if (!element || typeof element._extendedByPrototype != 'undefined' ||
        element.nodeType != 1 || element == window) return element;

    var methods = Object.clone(Methods),
        tagName = element.tagName.toUpperCase();

    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

    extendElementWith(element, methods);

    element._extendedByPrototype = Prototype.emptyFunction;
    return element;

  }, {
    refresh: function() {
      if (!Prototype.BrowserFeatures.ElementExtensions) {
        Object.extend(Methods, Element.Methods);
        Object.extend(Methods, Element.Methods.Simulated);
      }
    }
  });

  extend.refresh();
  return extend;
})();

Element.hasAttribute = function(element, attribute) {
  if (element.hasAttribute) return element.hasAttribute(attribute);
  return Element.Methods.Simulated.hasAttribute(element, attribute);
};

Element.addMethods = function(methods) {
  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

  if (!methods) {
    Object.extend(Form, Form.Methods);
    Object.extend(Form.Element, Form.Element.Methods);
    Object.extend(Element.Methods.ByTag, {
      "FORM":     Object.clone(Form.Methods),
      "INPUT":    Object.clone(Form.Element.Methods),
      "SELECT":   Object.clone(Form.Element.Methods),
      "TEXTAREA": Object.clone(Form.Element.Methods)
    });
  }

  if (arguments.length == 2) {
    var tagName = methods;
    methods = arguments[1];
  }

  if (!tagName) Object.extend(Element.Methods, methods || { });
  else {
    if (Object.isArray(tagName)) tagName.each(extend);
    else extend(tagName);
  }

  function extend(tagName) {
    tagName = tagName.toUpperCase();
    if (!Element.Methods.ByTag[tagName])
      Element.Methods.ByTag[tagName] = { };
    Object.extend(Element.Methods.ByTag[tagName], methods);
  }

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    for (var property in methods) {
      var value = methods[property];
      if (!Object.isFunction(value)) continue;
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = value.methodize();
    }
  }

  function findDOMClass(tagName) {
    var klass;
    var trans = {
      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
      "FrameSet", "IFRAME": "IFrame"
    };
    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName.capitalize() + 'Element';
    if (window[klass]) return window[klass];

    var element = document.createElement(tagName);
    var proto = element['__proto__'] || element.constructor.prototype;
    element = null;
    return proto;
  }

  var elementPrototype = window.HTMLElement ? HTMLElement.prototype :
   Element.prototype;

  if (F.ElementExtensions) {
    copy(Element.Methods, elementPrototype);
    copy(Element.Methods.Simulated, elementPrototype, true);
  }

  if (F.SpecificElementExtensions) {
    for (var tag in Element.Methods.ByTag) {
      var klass = findDOMClass(tag);
      if (Object.isUndefined(klass)) continue;
      copy(T[tag], klass.prototype);
    }
  }

  Object.extend(Element, Element.Methods);
  delete Element.ByTag;

  if (Element.extend.refresh) Element.extend.refresh();
  Element.cache = { };
};


document.viewport = {

  getDimensions: function() {
    return { width: this.getWidth(), height: this.getHeight() };
  },

  getScrollOffsets: function() {
    return Element._returnOffset(
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop  || document.body.scrollTop);
  }
};

(function(viewport) {
  var B = Prototype.Browser, doc = document, element, property = {};

  function getRootElement() {
    if (B.WebKit && !doc.evaluate)
      return document;

    if (B.Opera && window.parseFloat(window.opera.version()) < 9.5)
      return document.body;

    return document.documentElement;
  }

  function define(D) {
    if (!element) element = getRootElement();

    property[D] = 'client' + D;

    viewport['get' + D] = function() { return element[property[D]] };
    return viewport['get' + D]();
  }

  viewport.getWidth  = define.curry('Width');

  viewport.getHeight = define.curry('Height');
})(document.viewport);


Element.Storage = {
  UID: 1
};

Element.addMethods({
  getStorage: function(element) {
    if (!(element = $(element))) return;

    var uid;
    if (element === window) {
      uid = 0;
    } else {
      if (typeof element._prototypeUID === "undefined")
        element._prototypeUID = [Element.Storage.UID++];
      uid = element._prototypeUID[0];
    }

    if (!Element.Storage[uid])
      Element.Storage[uid] = $H();

    return Element.Storage[uid];
  },

  store: function(element, key, value) {
    if (!(element = $(element))) return;

    if (arguments.length === 2) {
      Element.getStorage(element).update(key);
    } else {
      Element.getStorage(element).set(key, value);
    }

    return element;
  },

  retrieve: function(element, key, defaultValue) {
    if (!(element = $(element))) return;
    var hash = Element.getStorage(element), value = hash.get(key);

    if (Object.isUndefined(value)) {
      hash.set(key, defaultValue);
      value = defaultValue;
    }

    return value;
  },

  clone: function(element, deep) {
    if (!(element = $(element))) return;
    var clone = element.cloneNode(deep);
    clone._prototypeUID = void 0;
    if (deep) {
      var descendants = Element.select(clone, '*'),
          i = descendants.length;
      while (i--) {
        descendants[i]._prototypeUID = void 0;
      }
    }
    return Element.extend(clone);
  }
});
/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
 * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
 * license.  Please see http://www.yui-ext.com/ for more information. */

var Selector = Class.create({
  initialize: function(expression) {
    this.expression = expression.strip();

    if (this.shouldUseSelectorsAPI()) {
      this.mode = 'selectorsAPI';
    } else if (this.shouldUseXPath()) {
      this.mode = 'xpath';
      this.compileXPathMatcher();
    } else {
      this.mode = "normal";
      this.compileMatcher();
    }

  },

  shouldUseXPath: (function() {

    var IS_DESCENDANT_SELECTOR_BUGGY = (function(){
      var isBuggy = false;
      if (document.evaluate && window.XPathResult) {
        var el = document.createElement('div');
        el.innerHTML = '<ul><li></li></ul><div><ul><li></li></ul></div>';

        var xpath = ".//*[local-name()='ul' or local-name()='UL']" +
          "//*[local-name()='li' or local-name()='LI']";

        var result = document.evaluate(xpath, el, null,
          XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

        isBuggy = (result.snapshotLength !== 2);
        el = null;
      }
      return isBuggy;
    })();

    return function() {
      if (!Prototype.BrowserFeatures.XPath) return false;

      var e = this.expression;

      if (Prototype.Browser.WebKit &&
       (e.include("-of-type") || e.include(":empty")))
        return false;

      if ((/(\[[\w-]*?:|:checked)/).test(e))
        return false;

      if (IS_DESCENDANT_SELECTOR_BUGGY) return false;

      return true;
    }

  })(),

  shouldUseSelectorsAPI: function() {
    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;

    if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false;

    if (!Selector._div) Selector._div = new Element('div');

    try {
      Selector._div.querySelector(this.expression);
    } catch(e) {
      return false;
    }

    return true;
  },

  compileMatcher: function() {
    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
        c = Selector.criteria, le, p, m, len = ps.length, name;

    if (Selector._cache[e]) {
      this.matcher = Selector._cache[e];
      return;
    }

    this.matcher = ["this.matcher = function(root) {",
                    "var r = root, h = Selector.handlers, c = false, n;"];

    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i = 0; i<len; i++) {
        p = ps[i].re;
        name = ps[i].name;
        if (m = e.match(p)) {
          this.matcher.push(Object.isFunction(c[name]) ? c[name](m) :
            new Template(c[name]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.matcher.push("return h.unique(n);\n}");
    eval(this.matcher.join('\n'));
    Selector._cache[this.expression] = this.matcher;
  },

  compileXPathMatcher: function() {
    var e = this.expression, ps = Selector.patterns,
        x = Selector.xpath, le, m, len = ps.length, name;

    if (Selector._cache[e]) {
      this.xpath = Selector._cache[e]; return;
    }

    this.matcher = ['.//*'];
    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i = 0; i<len; i++) {
        name = ps[i].name;
        if (m = e.match(ps[i].re)) {
          this.matcher.push(Object.isFunction(x[name]) ? x[name](m) :
            new Template(x[name]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.xpath = this.matcher.join('');
    Selector._cache[this.expression] = this.xpath;
  },

  findElements: function(root) {
    root = root || document;
    var e = this.expression, results;

    switch (this.mode) {
      case 'selectorsAPI':
        if (root !== document) {
          var oldId = root.id, id = $(root).identify();
          id = id.replace(/([\.:])/g, "\\$1");
          e = "#" + id + " " + e;
        }

        results = $A(root.querySelectorAll(e)).map(Element.extend);
        root.id = oldId;

        return results;
      case 'xpath':
        return document._getElementsByXPath(this.xpath, root);
      default:
       return this.matcher(root);
    }
  },

  match: function(element) {
    this.tokens = [];

    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
    var le, p, m, len = ps.length, name;

    while (e && le !== e && (/\S/).test(e)) {
      le = e;
      for (var i = 0; i<len; i++) {
        p = ps[i].re;
        name = ps[i].name;
        if (m = e.match(p)) {
          if (as[name]) {
            this.tokens.push([name, Object.clone(m)]);
            e = e.replace(m[0], '');
          } else {
            return this.findElements(document).include(element);
          }
        }
      }
    }

    var match = true, name, matches;
    for (var i = 0, token; token = this.tokens[i]; i++) {
      name = token[0], matches = token[1];
      if (!Selector.assertions[name](element, matches)) {
        match = false; break;
      }
    }

    return match;
  },

  toString: function() {
    return this.expression;
  },

  inspect: function() {
    return "#<Selector:" + this.expression.inspect() + ">";
  }
});

if (Prototype.BrowserFeatures.SelectorsAPI &&
 document.compatMode === 'BackCompat') {
  Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){
    var div = document.createElement('div'),
     span = document.createElement('span');

    div.id = "prototype_test_id";
    span.className = 'Test';
    div.appendChild(span);
    var isIgnored = (div.querySelector('#prototype_test_id .test') !== null);
    div = span = null;
    return isIgnored;
  })();
}

Object.extend(Selector, {
  _cache: { },

  xpath: {
    descendant:   "//*",
    child:        "/*",
    adjacent:     "/following-sibling::*[1]",
    laterSibling: '/following-sibling::*',
    tagName:      function(m) {
      if (m[1] == '*') return '';
      return "[local-name()='" + m[1].toLowerCase() +
             "' or local-name()='" + m[1].toUpperCase() + "']";
    },
    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
    id:           "[@id='#{1}']",
    attrPresence: function(m) {
      m[1] = m[1].toLowerCase();
      return new Template("[@#{1}]").evaluate(m);
    },
    attr: function(m) {
      m[1] = m[1].toLowerCase();
      m[3] = m[5] || m[6];
      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
    },
    pseudo: function(m) {
      var h = Selector.xpath.pseudos[m[1]];
      if (!h) return '';
      if (Object.isFunction(h)) return h(m);
      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
    },
    operators: {
      '=':  "[@#{1}='#{3}']",
      '!=': "[@#{1}!='#{3}']",
      '^=': "[starts-with(@#{1}, '#{3}')]",
      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
      '*=': "[contains(@#{1}, '#{3}')]",
      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
    },
    pseudos: {
      'first-child': '[not(preceding-sibling::*)]',
      'last-child':  '[not(following-sibling::*)]',
      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
      'empty':       "[count(*) = 0 and (count(text()) = 0)]",
      'checked':     "[@checked]",
      'disabled':    "[(@disabled) and (@type!='hidden')]",
      'enabled':     "[not(@disabled) and (@type!='hidden')]",
      'not': function(m) {
        var e = m[6], p = Selector.patterns,
            x = Selector.xpath, le, v, len = p.length, name;

        var exclusion = [];
        while (e && le != e && (/\S/).test(e)) {
          le = e;
          for (var i = 0; i<len; i++) {
            name = p[i].name
            if (m = e.match(p[i].re)) {
              v = Object.isFunction(x[name]) ? x[name](m) : new Template(x[name]).evaluate(m);
              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
              e = e.replace(m[0], '');
              break;
            }
          }
        }
        return "[not(" + exclusion.join(" and ") + ")]";
      },
      'nth-child':      function(m) {
        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
      },
      'nth-last-child': function(m) {
        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
      },
      'nth-of-type':    function(m) {
        return Selector.xpath.pseudos.nth("position() ", m);
      },
      'nth-last-of-type': function(m) {
        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
      },
      'first-of-type':  function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
      },
      'last-of-type':   function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
      },
      'only-of-type':   function(m) {
        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
      },
      nth: function(fragment, m) {
        var mm, formula = m[6], predicate;
        if (formula == 'even') formula = '2n+0';
        if (formula == 'odd')  formula = '2n+1';
        if (mm = formula.match(/^(\d+)$/)) // digit only
          return '[' + fragment + "= " + mm[1] + ']';
        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
          if (mm[1] == "-") mm[1] = -1;
          var a = mm[1] ? Number(mm[1]) : 1;
          var b = mm[2] ? Number(mm[2]) : 0;
          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
          "((#{fragment} - #{b}) div #{a} >= 0)]";
          return new Template(predicate).evaluate({
            fragment: fragment, a: a, b: b });
        }
      }
    }
  },

  criteria: {
    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
    attr: function(m) {
      m[3] = (m[5] || m[6]);
      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
    },
    pseudo: function(m) {
      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
    },
    descendant:   'c = "descendant";',
    child:        'c = "child";',
    adjacent:     'c = "adjacent";',
    laterSibling: 'c = "laterSibling";'
  },

  patterns: [
    { name: 'laterSibling', re: /^\s*~\s*/ },
    { name: 'child',        re: /^\s*>\s*/ },
    { name: 'adjacent',     re: /^\s*\+\s*/ },
    { name: 'descendant',   re: /^\s/ },

    { name: 'tagName',      re: /^\s*(\*|[\w\-]+)(\b|$)?/ },
    { name: 'id',           re: /^#([\w\-\*]+)(\b|$)/ },
    { name: 'className',    re: /^\.([\w\-\*]+)(\b|$)/ },
    { name: 'pseudo',       re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ },
    { name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ },
    { name: 'attr',         re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ }
  ],

  assertions: {
    tagName: function(element, matches) {
      return matches[1].toUpperCase() == element.tagName.toUpperCase();
    },

    className: function(element, matches) {
      return Element.hasClassName(element, matches[1]);
    },

    id: function(element, matches) {
      return element.id === matches[1];
    },

    attrPresence: function(element, matches) {
      return Element.hasAttribute(element, matches[1]);
    },

    attr: function(element, matches) {
      var nodeValue = Element.readAttribute(element, matches[1]);
      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
    }
  },

  handlers: {
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        a.push(node);
      return a;
    },

    mark: function(nodes) {
      var _true = Prototype.emptyFunction;
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = _true;
      return nodes;
    },

    unmark: (function(){

      var PROPERTIES_ATTRIBUTES_MAP = (function(){
        var el = document.createElement('div'),
            isBuggy = false,
            propName = '_countedByPrototype',
            value = 'x'
        el[propName] = value;
        isBuggy = (el.getAttribute(propName) === value);
        el = null;
        return isBuggy;
      })();

      return PROPERTIES_ATTRIBUTES_MAP ?
        function(nodes) {
          for (var i = 0, node; node = nodes[i]; i++)
            node.removeAttribute('_countedByPrototype');
          return nodes;
        } :
        function(nodes) {
          for (var i = 0, node; node = nodes[i]; i++)
            node._countedByPrototype = void 0;
          return nodes;
        }
    })(),

    index: function(parentNode, reverse, ofType) {
      parentNode._countedByPrototype = Prototype.emptyFunction;
      if (reverse) {
        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
          var node = nodes[i];
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
        }
      } else {
        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
      }
    },

    unique: function(nodes) {
      if (nodes.length == 0) return nodes;
      var results = [], n;
      for (var i = 0, l = nodes.length; i < l; i++)
        if (typeof (n = nodes[i])._countedByPrototype == 'undefined') {
          n._countedByPrototype = Prototype.emptyFunction;
          results.push(Element.extend(n));
        }
      return Selector.handlers.unmark(results);
    },

    descendant: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, node.getElementsByTagName('*'));
      return results;
    },

    child: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        for (var j = 0, child; child = node.childNodes[j]; j++)
          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
      }
      return results;
    },

    adjacent: function(nodes) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        var next = this.nextElementSibling(node);
        if (next) results.push(next);
      }
      return results;
    },

    laterSibling: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, Element.nextSiblings(node));
      return results;
    },

    nextElementSibling: function(node) {
      while (node = node.nextSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    previousElementSibling: function(node) {
      while (node = node.previousSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    tagName: function(nodes, root, tagName, combinator) {
      var uTagName = tagName.toUpperCase();
      var results = [], h = Selector.handlers;
      if (nodes) {
        if (combinator) {
          if (combinator == "descendant") {
            for (var i = 0, node; node = nodes[i]; i++)
              h.concat(results, node.getElementsByTagName(tagName));
            return results;
          } else nodes = this[combinator](nodes);
          if (tagName == "*") return nodes;
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName.toUpperCase() === uTagName) results.push(node);
        return results;
      } else return root.getElementsByTagName(tagName);
    },

    id: function(nodes, root, id, combinator) {
      var targetNode = $(id), h = Selector.handlers;

      if (root == document) {
        if (!targetNode) return [];
        if (!nodes) return [targetNode];
      } else {
        if (!root.sourceIndex || root.sourceIndex < 1) {
          var nodes = root.getElementsByTagName('*');
          for (var j = 0, node; node = nodes[j]; j++) {
            if (node.id === id) return [node];
          }
        }
      }

      if (nodes) {
        if (combinator) {
          if (combinator == 'child') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (targetNode.parentNode == node) return [targetNode];
          } else if (combinator == 'descendant') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Element.descendantOf(targetNode, node)) return [targetNode];
          } else if (combinator == 'adjacent') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Selector.handlers.previousElementSibling(targetNode) == node)
                return [targetNode];
          } else nodes = h[combinator](nodes);
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node == targetNode) return [targetNode];
        return [];
      }
      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
    },

    className: function(nodes, root, className, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      return Selector.handlers.byClassName(nodes, root, className);
    },

    byClassName: function(nodes, root, className) {
      if (!nodes) nodes = Selector.handlers.descendant([root]);
      var needle = ' ' + className + ' ';
      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
        nodeClassName = node.className;
        if (nodeClassName.length == 0) continue;
        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
          results.push(node);
      }
      return results;
    },

    attrPresence: function(nodes, root, attr, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var results = [];
      for (var i = 0, node; node = nodes[i]; i++)
        if (Element.hasAttribute(node, attr)) results.push(node);
      return results;
    },

    attr: function(nodes, root, attr, value, operator, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var handler = Selector.operators[operator], results = [];
      for (var i = 0, node; node = nodes[i]; i++) {
        var nodeValue = Element.readAttribute(node, attr);
        if (nodeValue === null) continue;
        if (handler(nodeValue, value)) results.push(node);
      }
      return results;
    },

    pseudo: function(nodes, name, value, root, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      if (!nodes) nodes = root.getElementsByTagName("*");
      return Selector.pseudos[name](nodes, value, root);
    }
  },

  pseudos: {
    'first-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.previousElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'last-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.nextElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'only-child': function(nodes, value, root) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
          results.push(node);
      return results;
    },
    'nth-child':        function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root);
    },
    'nth-last-child':   function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true);
    },
    'nth-of-type':      function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, false, true);
    },
    'nth-last-of-type': function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true, true);
    },
    'first-of-type':    function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, false, true);
    },
    'last-of-type':     function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, true, true);
    },
    'only-of-type':     function(nodes, formula, root) {
      var p = Selector.pseudos;
      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
    },

    getIndices: function(a, b, total) {
      if (a == 0) return b > 0 ? [b] : [];
      return $R(1, total).inject([], function(memo, i) {
        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
        return memo;
      });
    },

    nth: function(nodes, formula, root, reverse, ofType) {
      if (nodes.length == 0) return [];
      if (formula == 'even') formula = '2n+0';
      if (formula == 'odd')  formula = '2n+1';
      var h = Selector.handlers, results = [], indexed = [], m;
      h.mark(nodes);
      for (var i = 0, node; node = nodes[i]; i++) {
        if (!node.parentNode._countedByPrototype) {
          h.index(node.parentNode, reverse, ofType);
          indexed.push(node.parentNode);
        }
      }
      if (formula.match(/^\d+$/)) { // just a number
        formula = Number(formula);
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.nodeIndex == formula) results.push(node);
      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
        if (m[1] == "-") m[1] = -1;
        var a = m[1] ? Number(m[1]) : 1;
        var b = m[2] ? Number(m[2]) : 0;
        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
          for (var j = 0; j < l; j++)
            if (node.nodeIndex == indices[j]) results.push(node);
        }
      }
      h.unmark(nodes);
      h.unmark(indexed);
      return results;
    },

    'empty': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (node.tagName == '!' || node.firstChild) continue;
        results.push(node);
      }
      return results;
    },

    'not': function(nodes, selector, root) {
      var h = Selector.handlers, selectorType, m;
      var exclusions = new Selector(selector).findElements(root);
      h.mark(exclusions);
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node._countedByPrototype) results.push(node);
      h.unmark(exclusions);
      return results;
    },

    'enabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node.disabled && (!node.type || node.type !== 'hidden'))
          results.push(node);
      return results;
    },

    'disabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.disabled) results.push(node);
      return results;
    },

    'checked': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.checked) results.push(node);
      return results;
    }
  },

  operators: {
    '=':  function(nv, v) { return nv == v; },
    '!=': function(nv, v) { return nv != v; },
    '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
    '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
     '-').include('-' + (v || "").toUpperCase() + '-'); }
  },

  split: function(expression) {
    var expressions = [];
    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
      expressions.push(m[1].strip());
    });
    return expressions;
  },

  matchElements: function(elements, expression) {
    var matches = $$(expression), h = Selector.handlers;
    h.mark(matches);
    for (var i = 0, results = [], element; element = elements[i]; i++)
      if (element._countedByPrototype) results.push(element);
    h.unmark(matches);
    return results;
  },

  findElement: function(elements, expression, index) {
    if (Object.isNumber(expression)) {
      index = expression; expression = false;
    }
    return Selector.matchElements(elements, expression || '*')[index || 0];
  },

  findChildElements: function(element, expressions) {
    expressions = Selector.split(expressions.join(','));
    var results = [], h = Selector.handlers;
    for (var i = 0, l = expressions.length, selector; i < l; i++) {
      selector = new Selector(expressions[i].strip());
      h.concat(results, selector.findElements(element));
    }
    return (l > 1) ? h.unique(results) : results;
  }
});

if (Prototype.Browser.IE) {
  Object.extend(Selector.handlers, {
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        if (node.tagName !== "!") a.push(node);
      return a;
    }
  });
}

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}

var Form = {
  reset: function(form) {
    form = $(form);
    form.reset();
    return form;
  },

  serializeElements: function(elements, options) {
    if (typeof options != 'object') options = { hash: !!options };
    else if (Object.isUndefined(options.hash)) options.hash = true;
    var key, value, submitted = false, submit = options.submit;

    var data = elements.inject({ }, function(result, element) {
      if (!element.disabled && element.name) {
        key = element.name; value = $(element).getValue();
        if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted = true)))) {
          if (key in result) {
            if (!Object.isArray(result[key])) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return options.hash ? data : Object.toQueryString(data);
  }
};

Form.Methods = {
  serialize: function(form, options) {
    return Form.serializeElements(Form.getElements(form), options);
  },

  getElements: function(form) {
    var elements = $(form).getElementsByTagName('*'),
        element,
        arr = [ ],
        serializers = Form.Element.Serializers;
    for (var i = 0; element = elements[i]; i++) {
      arr.push(element);
    }
    return arr.inject([], function(elements, child) {
      if (serializers[child.tagName.toLowerCase()])
        elements.push(Element.extend(child));
      return elements;
    })
  },

  getInputs: function(form, typeName, name) {
    form = $(form);
    var inputs = form.getElementsByTagName('input');

    if (!typeName && !name) return $A(inputs).map(Element.extend);

    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
      var input = inputs[i];
      if ((typeName && input.type != typeName) || (name && input.name != name))
        continue;
      matchingInputs.push(Element.extend(input));
    }

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('disable');
    return form;
  },

  enable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('enable');
    return form;
  },

  findFirstElement: function(form) {
    var elements = $(form).getElements().findAll(function(element) {
      return 'hidden' != element.type && !element.disabled;
    });
    var firstByIndex = elements.findAll(function(element) {
      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
    }).sortBy(function(element) { return element.tabIndex }).first();

    return firstByIndex ? firstByIndex : elements.find(function(element) {
      return /^(?:input|select|textarea)$/i.test(element.tagName);
    });
  },

  focusFirstElement: function(form) {
    form = $(form);
    form.findFirstElement().activate();
    return form;
  },

  request: function(form, options) {
    form = $(form), options = Object.clone(options || { });

    var params = options.parameters, action = form.readAttribute('action') || '';
    if (action.blank()) action = window.location.href;
    options.parameters = form.serialize(true);

    if (params) {
      if (Object.isString(params)) params = params.toQueryParams();
      Object.extend(options.parameters, params);
    }

    if (form.hasAttribute('method') && !options.method)
      options.method = form.method;

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

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


Form.Element = {
  focus: function(element) {
    $(element).focus();
    return element;
  },

  select: function(element) {
    $(element).select();
    return element;
  }
};

Form.Element.Methods = {

  serialize: function(element) {
    element = $(element);
    if (!element.disabled && element.name) {
      var value = element.getValue();
      if (value != undefined) {
        var pair = { };
        pair[element.name] = value;
        return Object.toQueryString(pair);
      }
    }
    return '';
  },

  getValue: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    return Form.Element.Serializers[method](element);
  },

  setValue: function(element, value) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    Form.Element.Serializers[method](element, value);
    return element;
  },

  clear: function(element) {
    $(element).value = '';
    return element;
  },

  present: function(element) {
    return $(element).value != '';
  },

  activate: function(element) {
    element = $(element);
    try {
      element.focus();
      if (element.select && (element.tagName.toLowerCase() != 'input' ||
          !(/^(?:button|reset|submit)$/i.test(element.type))))
        element.select();
    } catch (e) { }
    return element;
  },

  disable: function(element) {
    element = $(element);
    element.disabled = true;
    return element;
  },

  enable: function(element) {
    element = $(element);
    element.disabled = false;
    return element;
  }
};

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

var Field = Form.Element;

var $F = Form.Element.Methods.getValue;

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

Form.Element.Serializers = {
  input: function(element, value) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        return Form.Element.Serializers.inputSelector(element, value);
      default:
        return Form.Element.Serializers.textarea(element, value);
    }
  },

  inputSelector: function(element, value) {
    if (Object.isUndefined(value)) return element.checked ? element.value : null;
    else element.checked = !!value;
  },

  textarea: function(element, value) {
    if (Object.isUndefined(value)) return element.value;
    else element.value = value;
  },

  select: function(element, value) {
    if (Object.isUndefined(value))
      return this[element.type == 'select-one' ?
        'selectOne' : 'selectMany'](element);
    else {
      var opt, currentValue, single = !Object.isArray(value);
      for (var i = 0, length = element.length; i < length; i++) {
        opt = element.options[i];
        currentValue = this.optionValue(opt);
        if (single) {
          if (currentValue == value) {
            opt.selected = true;
            return;
          }
        }
        else opt.selected = value.include(currentValue);
      }
    }
  },

  selectOne: function(element) {
    var index = element.selectedIndex;
    return index >= 0 ? this.optionValue(element.options[index]) : null;
  },

  selectMany: function(element) {
    var values, length = element.length;
    if (!length) return null;

    for (var i = 0, values = []; i < length; i++) {
      var opt = element.options[i];
      if (opt.selected) values.push(this.optionValue(opt));
    }
    return values;
  },

  optionValue: function(opt) {
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
};

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


Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
  initialize: function($super, element, frequency, callback) {
    $super(callback, frequency);
    this.element   = $(element);
    this.lastValue = this.getValue();
  },

  execute: function() {
    var value = this.getValue();
    if (Object.isString(this.lastValue) && Object.isString(value) ?
        this.lastValue != value : String(this.lastValue) != String(value)) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
});

Form.Element.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});

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

Abstract.EventObserver = Class.create({
  initialize: function(element, callback) {
    this.element  = $(element);
    this.callback = callback;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback, this);
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          Event.observe(element, 'click', this.onElementEvent.bind(this));
          break;
        default:
          Event.observe(element, 'change', this.onElementEvent.bind(this));
          break;
      }
    }
  }
});

Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
(function() {

  var Event = {
    KEY_BACKSPACE: 8,
    KEY_TAB:       9,
    KEY_RETURN:   13,
    KEY_ESC:      27,
    KEY_LEFT:     37,
    KEY_UP:       38,
    KEY_RIGHT:    39,
    KEY_DOWN:     40,
    KEY_DELETE:   46,
    KEY_HOME:     36,
    KEY_END:      35,
    KEY_PAGEUP:   33,
    KEY_PAGEDOWN: 34,
    KEY_INSERT:   45,

    cache: {}
  };

  var docEl = document.documentElement;
  var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl
    && 'onmouseleave' in docEl;

  var _isButton;
  if (Prototype.Browser.IE) {
    var buttonMap = { 0: 1, 1: 4, 2: 2 };
    _isButton = function(event, code) {
      return event.button === buttonMap[code];
    };
  } else if (Prototype.Browser.WebKit) {
    _isButton = function(event, code) {
      switch (code) {
        case 0: return event.which == 1 && !event.metaKey;
        case 1: return event.which == 1 && event.metaKey;
        default: return false;
      }
    };
  } else {
    _isButton = function(event, code) {
      return event.which ? (event.which === code + 1) : (event.button === code);
    };
  }

  function isLeftClick(event)   { return _isButton(event, 0) }

  function isMiddleClick(event) { return _isButton(event, 1) }

  function isRightClick(event)  { return _isButton(event, 2) }

  function element(event) {
    event = Event.extend(event);

    var node = event.target, type = event.type,
     currentTarget = event.currentTarget;

    if (currentTarget && currentTarget.tagName) {
      if (type === 'load' || type === 'error' ||
        (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
          && currentTarget.type === 'radio'))
            node = currentTarget;
    }

    if (node.nodeType == Node.TEXT_NODE)
      node = node.parentNode;

    return Element.extend(node);
  }

  function findElement(event, expression) {
    var element = Event.element(event);
    if (!expression) return element;
    var elements = [element].concat(element.ancestors());
    return Selector.findElement(elements, expression, 0);
  }

  function pointer(event) {
    return { x: pointerX(event), y: pointerY(event) };
  }

  function pointerX(event) {
    var docElement = document.documentElement,
     body = document.body || { scrollLeft: 0 };

    return event.pageX || (event.clientX +
      (docElement.scrollLeft || body.scrollLeft) -
      (docElement.clientLeft || 0));
  }

  function pointerY(event) {
    var docElement = document.documentElement,
     body = document.body || { scrollTop: 0 };

    return  event.pageY || (event.clientY +
       (docElement.scrollTop || body.scrollTop) -
       (docElement.clientTop || 0));
  }


  function stop(event) {
    Event.extend(event);
    event.preventDefault();
    event.stopPropagation();

    event.stopped = true;
  }

  Event.Methods = {
    isLeftClick: isLeftClick,
    isMiddleClick: isMiddleClick,
    isRightClick: isRightClick,

    element: element,
    findElement: findElement,

    pointer: pointer,
    pointerX: pointerX,
    pointerY: pointerY,

    stop: stop
  };


  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
    m[name] = Event.Methods[name].methodize();
    return m;
  });

  if (Prototype.Browser.IE) {
    function _relatedTarget(event) {
      var element;
      switch (event.type) {
        case 'mouseover': element = event.fromElement; break;
        case 'mouseout':  element = event.toElement;   break;
        default: return null;
      }
      return Element.extend(element);
    }

    Object.extend(methods, {
      stopPropagation: function() { this.cancelBubble = true },
      preventDefault:  function() { this.returnValue = false },
      inspect: function() { return '[object Event]' }
    });

    Event.extend = function(event, element) {
      if (!event) return false;
      if (event._extendedByPrototype) return event;

      event._extendedByPrototype = Prototype.emptyFunction;
      var pointer = Event.pointer(event);

      Object.extend(event, {
        target: event.srcElement || element,
        relatedTarget: _relatedTarget(event),
        pageX:  pointer.x,
        pageY:  pointer.y
      });

      return Object.extend(event, methods);
    };
  } else {
    Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__;
    Object.extend(Event.prototype, methods);
    Event.extend = Prototype.K;
  }

  function _createResponder(element, eventName, handler) {
    var registry = Element.retrieve(element, 'prototype_event_registry');

    if (Object.isUndefined(registry)) {
      CACHE.push(element);
      registry = Element.retrieve(element, 'prototype_event_registry', $H());
    }

    var respondersForEvent = registry.get(eventName);
    if (Object.isUndefined(respondersForEvent)) {
      respondersForEvent = [];
      registry.set(eventName, respondersForEvent);
    }

    if (respondersForEvent.pluck('handler').include(handler)) return false;

    var responder;
    if (eventName.include(":")) {
      responder = function(event) {
        if (Object.isUndefined(event.eventName))
          return false;

        if (event.eventName !== eventName)
          return false;

        Event.extend(event, element);
        handler.call(element, event);
      };
    } else {
      if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED &&
       (eventName === "mouseenter" || eventName === "mouseleave")) {
        if (eventName === "mouseenter" || eventName === "mouseleave") {
          responder = function(event) {
            Event.extend(event, element);

            var parent = event.relatedTarget;
            while (parent && parent !== element) {
              try { parent = parent.parentNode; }
              catch(e) { parent = element; }
            }

            if (parent === element) return;

            handler.call(element, event);
          };
        }
      } else {
        responder = function(event) {
          Event.extend(event, element);
          handler.call(element, event);
        };
      }
    }

    responder.handler = handler;
    respondersForEvent.push(responder);
    return responder;
  }

  function _destroyCache() {
    for (var i = 0, length = CACHE.length; i < length; i++) {
      Event.stopObserving(CACHE[i]);
      CACHE[i] = null;
    }
  }

  var CACHE = [];

  if (Prototype.Browser.IE)
    window.attachEvent('onunload', _destroyCache);

  if (Prototype.Browser.WebKit)
    window.addEventListener('unload', Prototype.emptyFunction, false);


  var _getDOMEventName = Prototype.K;

  if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) {
    _getDOMEventName = function(eventName) {
      var translations = { mouseenter: "mouseover", mouseleave: "mouseout" };
      return eventName in translations ? translations[eventName] : eventName;
    };
  }

  function observe(element, eventName, handler) {
    element = $(element);

    var responder = _createResponder(element, eventName, handler);

    if (!responder) return element;

    if (eventName.include(':')) {
      if (element.addEventListener)
        element.addEventListener("dataavailable", responder, false);
      else {
        element.attachEvent("ondataavailable", responder);
        element.attachEvent("onfilterchange", responder);
      }
    } else {
      var actualEventName = _getDOMEventName(eventName);

      if (element.addEventListener)
        element.addEventListener(actualEventName, responder, false);
      else
        element.attachEvent("on" + actualEventName, responder);
    }

    return element;
  }

  function stopObserving(element, eventName, handler) {
    element = $(element);

    var registry = Element.retrieve(element, 'prototype_event_registry');

    if (Object.isUndefined(registry)) return element;

    if (eventName && !handler) {
      var responders = registry.get(eventName);

      if (Object.isUndefined(responders)) return element;

      responders.each( function(r) {
        Element.stopObserving(element, eventName, r.handler);
      });
      return element;
    } else if (!eventName) {
      registry.each( function(pair) {
        var eventName = pair.key, responders = pair.value;

        responders.each( function(r) {
          Element.stopObserving(element, eventName, r.handler);
        });
      });
      return element;
    }

    var responders = registry.get(eventName);

    if (!responders) return;

    var responder = responders.find( function(r) { return r.handler === handler; });
    if (!responder) return element;

    var actualEventName = _getDOMEventName(eventName);

    if (eventName.include(':')) {
      if (element.removeEventListener)
        element.removeEventListener("dataavailable", responder, false);
      else {
        element.detachEvent("ondataavailable", responder);
        element.detachEvent("onfilterchange",  responder);
      }
    } else {
      if (element.removeEventListener)
        element.removeEventListener(actualEventName, responder, false);
      else
        element.detachEvent('on' + actualEventName, responder);
    }

    registry.set(eventName, responders.without(responder));

    return element;
  }

  function fire(element, eventName, memo, bubble) {
    element = $(element);

    if (Object.isUndefined(bubble))
      bubble = true;

    if (element == document && document.createEvent && !element.dispatchEvent)
      element = document.documentElement;

    var event;
    if (document.createEvent) {
      event = document.createEvent('HTMLEvents');
      event.initEvent('dataavailable', true, true);
    } else {
      event = document.createEventObject();
      event.eventType = bubble ? 'ondataavailable' : 'onfilterchange';
    }

    event.eventName = eventName;
    event.memo = memo || { };

    if (document.createEvent)
      element.dispatchEvent(event);
    else
      element.fireEvent(event.eventType, event);

    return Event.extend(event);
  }


  Object.extend(Event, Event.Methods);

  Object.extend(Event, {
    fire:          fire,
    observe:       observe,
    stopObserving: stopObserving
  });

  Element.addMethods({
    fire:          fire,

    observe:       observe,

    stopObserving: stopObserving
  });

  Object.extend(document, {
    fire:          fire.methodize(),

    observe:       observe.methodize(),

    stopObserving: stopObserving.methodize(),

    loaded:        false
  });

  if (window.Event) Object.extend(window.Event, Event);
  else window.Event = Event;
})();

(function() {
  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
     Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */

  var timer;

  function fireContentLoadedEvent() {
    if (document.loaded) return;
    if (timer) window.clearTimeout(timer);
    document.loaded = true;
    document.fire('dom:loaded');
  }

  function checkReadyState() {
    if (document.readyState === 'complete') {
      document.stopObserving('readystatechange', checkReadyState);
      fireContentLoadedEvent();
    }
  }

  function pollDoScroll() {
    try { document.documentElement.doScroll('left'); }
    catch(e) {
      timer = pollDoScroll.defer();
      return;
    }
    fireContentLoadedEvent();
  }

  if (document.addEventListener) {
    document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false);
  } else {
    document.observe('readystatechange', checkReadyState);
    if (window == top)
      timer = pollDoScroll.defer();
  }

  Event.observe(window, 'load', fireContentLoadedEvent);
})();

Element.addMethods();

/*------------------------------- DEPRECATED -------------------------------*/

Hash.toQueryString = Object.toQueryString;

var Toggle = { display: Element.toggle };

Element.Methods.childOf = Element.Methods.descendantOf;

var Insertion = {
  Before: function(element, content) {
    return Element.insert(element, {before:content});
  },

  Top: function(element, content) {
    return Element.insert(element, {top:content});
  },

  Bottom: function(element, content) {
    return Element.insert(element, {bottom:content});
  },

  After: function(element, content) {
    return Element.insert(element, {after:content});
  }
};

var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

var Position = {
  includeScrollOffsets: false,

  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = Element.cumulativeOffset(element);

    return (y >= this.offset[1] &&
            y <  this.offset[1] + element.offsetHeight &&
            x >= this.offset[0] &&
            x <  this.offset[0] + element.offsetWidth);
  },

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = Element.cumulativeScrollOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = Element.cumulativeOffset(element);

    return (this.ycomp >= this.offset[1] &&
            this.ycomp <  this.offset[1] + element.offsetHeight &&
            this.xcomp >= this.offset[0] &&
            this.xcomp <  this.offset[0] + element.offsetWidth);
  },

  overlap: function(mode, element) {
    if (!mode) return 0;
    if (mode == 'vertical')
      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
        element.offsetHeight;
    if (mode == 'horizontal')
      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
        element.offsetWidth;
  },


  cumulativeOffset: Element.Methods.cumulativeOffset,

  positionedOffset: Element.Methods.positionedOffset,

  absolutize: function(element) {
    Position.prepare();
    return Element.absolutize(element);
  },

  relativize: function(element) {
    Position.prepare();
    return Element.relativize(element);
  },

  realOffset: Element.Methods.cumulativeScrollOffset,

  offsetParent: Element.Methods.getOffsetParent,

  page: Element.Methods.viewportOffset,

  clone: function(source, target, options) {
    options = options || { };
    return Element.clonePosition(target, source, options);
  }
};

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

if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
  function iter(name) {
    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
  }

  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
  function(element, className) {
    className = className.toString().strip();
    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
  } : function(element, className) {
    className = className.toString().strip();
    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
    if (!classNames && !className) return elements;

    var nodes = $(element).getElementsByTagName('*');
    className = ' ' + className + ' ';

    for (var i = 0, child, cn; child = nodes[i]; i++) {
      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
          (classNames && classNames.all(function(name) {
            return !name.toString().blank() && cn.include(' ' + name + ' ');
          }))))
        elements.push(Element.extend(child));
    }
    return elements;
  };

  return function(className, parentElement) {
    return $(parentElement || document.body).getElementsByClassName(className);
  };
}(Element.Methods);

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

Element.ClassNames = Class.create();
Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = $(element);
  },

  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },

  set: function(className) {
    this.element.className = className;
  },

  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set($A(this).concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set($A(this).without(classNameToRemove).join(' '));
  },

  toString: function() {
    return $A(this).join(' ');
  }
};

Object.extend(Element.ClassNames.prototype, Enumerable);

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


/**
 * @author Ryan Johnson <http://saucytiger.com/>
 * @copyright 2008 PersonalGrid Corporation <http://personalgrid.com/>
 * @package LivePipe UI
 * @license MIT
 * @url http://livepipe.net/controls/hotkey/
 * @attribution http://www.quirksmode.org/js/cookies.html
 */

//if(typeof(Prototype) == "undefined")
//	throw "Cookie requires Prototype to be loaded."
//if(typeof(Object.Event) == "undefined")
//	throw "Cookie requires Object.Event to be loaded.";
var Cookie = {
	set: function(name,value,seconds){
		if(seconds){
			var d = new Date();
			d.setTime(d.getTime() + (seconds * 1000));
			var expiry = '; expires=' + d.toGMTString();
		}else
			var expiry = '';
//		Cookie.notify('set',name,value);
		document.cookie = name + "=" + encodeURIComponent(value) + expiry + "; path=/";
	},
	get: function(name){
//		Cookie.notify('get',name);
		var nameEQ = name + "=";
		var ca = document.cookie.split(';');
		for(var i = 0; i < ca.length; i++){
			var c = ca[i];
			while(c.charAt(0) == ' ')
				c = c.substring(1,c.length);
			if(c.indexOf(nameEQ) == 0) {
				var beforeCleanup = c.substring(nameEQ.length,c.length);
        return decodeURIComponent(beforeCleanup.replace(/\+/g, " "));
      }
    }
		return null;
	},
	unset: function(name){
//		Cookie.notify('unset',name);
		Cookie.set(name,'',-1);
	}
};
//Object.Event.extend(Cookie);

var BackButtonProtection = {
  _location: window.location,

  pageId : undefined,
  
  reloadIfModified: function() {
    var validPages = JSON.parse(Cookie.get('unmodified_pages'));
    if(BackButtonProtection.pageId && validPages && validPages.indexOf(BackButtonProtection.pageId) == -1) {
      BackButtonProtection._location.reload();
    }
  },

  noCache: function() {
    var validPages = JSON.parse(Cookie.get('unmodified_pages'));
    if(validPages) {
      var index = validPages.indexOf(BackButtonProtection.pageId);
      if(index != -1) {
        validPages.splice(index, 1);
        Cookie.set('unmodified_pages', JSON.stringify(validPages));
      }
    }
  }
};
try {
  BackButtonProtection.pageId = pageId;
} catch (e) {}
BackButtonProtection.reloadIfModified();

// 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);

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

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

// Copyright (c) 2005-2008 Marty Haught, Thomas Fuchs
//
// 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 (!Control) var Control = { };

// options:
//  axis: 'vertical', or 'horizontal' (default)
//
// callbacks:
//  onChange(value)
//  onSlide(value)
Control.Slider = Class.create({
  initialize: function(handle, track, options) {
    var slider = this;

    if (Object.isArray(handle)) {
      this.handles = handle.collect( function(e) { return $(e) });
    } else {
      this.handles = [$(handle)];
    }

    this.track   = $(track);
    this.options = options || { };

    this.axis      = this.options.axis || 'horizontal';
    this.increment = this.options.increment || 1;
    this.step      = parseInt(this.options.step || '1');
    this.range     = this.options.range || $R(0,1);

    this.value     = 0; // assure backwards compat
    this.values    = this.handles.map( function() { return 0 });
    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
    this.options.startSpan = $(this.options.startSpan || null);
    this.options.endSpan   = $(this.options.endSpan || null);

    this.restricted = this.options.restricted || false;

    this.maximum   = this.options.maximum || this.range.end;
    this.minimum   = this.options.minimum || this.range.start;

    // Will be used to align the handle onto the track, if necessary
    this.alignX = parseInt(this.options.alignX || '0');
    this.alignY = parseInt(this.options.alignY || '0');

    this.trackLength = this.maximumOffset() - this.minimumOffset();

    this.handleLength = this.isVertical() ?
      (this.handles[0].offsetHeight != 0 ?
        this.handles[0].offsetHeight : this.handles[0].style.height.replace(/px$/,"")) :
      (this.handles[0].offsetWidth != 0 ? this.handles[0].offsetWidth :
        this.handles[0].style.width.replace(/px$/,""));

    this.active   = false;
    this.dragging = false;
    this.disabled = false;

    if (this.options.disabled) this.setDisabled();

    // Allowed values array
    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
    if (this.allowedValues) {
      this.minimum = this.allowedValues.min();
      this.maximum = this.allowedValues.max();
    }

    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
    this.eventMouseMove = this.update.bindAsEventListener(this);

    // Initialize handles in reverse (make sure first handle is active)
    this.handles.each( function(h,i) {
      i = slider.handles.length-1-i;
      slider.setValue(parseFloat(
        (Object.isArray(slider.options.sliderValue) ?
          slider.options.sliderValue[i] : slider.options.sliderValue) ||
         slider.range.start), i);
      h.makePositioned().observe("mousedown", slider.eventMouseDown);
    });

    this.track.observe("mousedown", this.eventMouseDown);
    document.observe("mouseup", this.eventMouseUp);
    document.observe("mousemove", this.eventMouseMove);

    this.initialized = true;
  },
  dispose: function() {
    var slider = this;
    Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
    Event.stopObserving(document, "mouseup", this.eventMouseUp);
    Event.stopObserving(document, "mousemove", this.eventMouseMove);
    this.handles.each( function(h) {
      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
    });
  },
  setDisabled: function(){
    this.disabled = true;
  },
  setEnabled: function(){
    this.disabled = false;
  },
  getNearestValue: function(value){
    if (this.allowedValues){
      if (value >= this.allowedValues.max()) return(this.allowedValues.max());
      if (value <= this.allowedValues.min()) return(this.allowedValues.min());

      var offset = Math.abs(this.allowedValues[0] - value);
      var newValue = this.allowedValues[0];
      this.allowedValues.each( function(v) {
        var currentOffset = Math.abs(v - value);
        if (currentOffset <= offset){
          newValue = v;
          offset = currentOffset;
        }
      });
      return newValue;
    }
    if (value > this.range.end) return this.range.end;
    if (value < this.range.start) return this.range.start;
    return value;
  },
  setValue: function(sliderValue, handleIdx){
    if (!this.active) {
      this.activeHandleIdx = handleIdx || 0;
      this.activeHandle    = this.handles[this.activeHandleIdx];
      this.updateStyles();
    }
    handleIdx = handleIdx || this.activeHandleIdx || 0;
    if (this.initialized && this.restricted) {
      if ((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
        sliderValue = this.values[handleIdx-1];
      if ((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
        sliderValue = this.values[handleIdx+1];
    }
    sliderValue = this.getNearestValue(sliderValue);
    this.values[handleIdx] = sliderValue;
    this.value = this.values[0]; // assure backwards compat

    this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] =
      this.translateToPx(sliderValue);

    this.drawSpans();
    if (!this.dragging || !this.event) this.updateFinished();
  },
  setValueBy: function(delta, handleIdx) {
    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta,
      handleIdx || this.activeHandleIdx || 0);
  },
  translateToPx: function(value) {
    return Math.round(
      ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) *
      (value - this.range.start)) + "px";
  },
  translateToValue: function(offset) {
    return ((offset/(this.trackLength-this.handleLength) *
      (this.range.end-this.range.start)) + this.range.start);
  },
  getRange: function(range) {
    var v = this.values.sortBy(Prototype.K);
    range = range || 0;
    return $R(v[range],v[range+1]);
  },
  minimumOffset: function(){
    return(this.isVertical() ? this.alignY : this.alignX);
  },
  maximumOffset: function(){
    return(this.isVertical() ?
      (this.track.offsetHeight != 0 ? this.track.offsetHeight :
        this.track.style.height.replace(/px$/,"")) - this.alignY :
      (this.track.offsetWidth != 0 ? this.track.offsetWidth :
        this.track.style.width.replace(/px$/,"")) - this.alignX);
  },
  isVertical:  function(){
    return (this.axis == 'vertical');
  },
  drawSpans: function() {
    var slider = this;
    if (this.spans)
      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
    if (this.options.startSpan)
      this.setSpan(this.options.startSpan,
        $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
    if (this.options.endSpan)
      this.setSpan(this.options.endSpan,
        $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
  },
  setSpan: function(span, range) {
    if (this.isVertical()) {
      span.style.top = this.translateToPx(range.start);
      span.style.height = this.translateToPx(range.end - range.start + this.range.start);
    } else {
      span.style.left = this.translateToPx(range.start);
      span.style.width = this.translateToPx(range.end - range.start + this.range.start);
    }
  },
  updateStyles: function() {
    this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
    Element.addClassName(this.activeHandle, 'selected');
  },
  startDrag: function(event) {
    if (Event.isLeftClick(event)) {
      if (!this.disabled){
        this.active = true;

        var handle = Event.element(event);
        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
        var track = handle;
        if (track==this.track) {
          var offsets  = Position.cumulativeOffset(this.track);
          this.event = event;
          this.setValue(this.translateToValue(
           (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
          ));
          var offsets  = Position.cumulativeOffset(this.activeHandle);
          this.offsetX = (pointer[0] - offsets[0]);
          this.offsetY = (pointer[1] - offsets[1]);
        } else {
          // find the handle (prevents issues with Safari)
          while((this.handles.indexOf(handle) == -1) && handle.parentNode)
            handle = handle.parentNode;

          if (this.handles.indexOf(handle)!=-1) {
            this.activeHandle    = handle;
            this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
            this.updateStyles();

            var offsets  = Position.cumulativeOffset(this.activeHandle);
            this.offsetX = (pointer[0] - offsets[0]);
            this.offsetY = (pointer[1] - offsets[1]);
          }
        }
      }
      Event.stop(event);
    }
  },
  update: function(event) {
   if (this.active) {
      if (!this.dragging) this.dragging = true;
      this.draw(event);
      if (Prototype.Browser.WebKit) window.scrollBy(0,0);
      Event.stop(event);
   }
  },
  draw: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var offsets = Position.cumulativeOffset(this.track);
    pointer[0] -= this.offsetX + offsets[0];
    pointer[1] -= this.offsetY + offsets[1];
    this.event = event;
    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
    if (this.initialized && this.options.onSlide)
      this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
  },
  endDrag: function(event) {
    if (this.active && this.dragging) {
      this.finishDrag(event, true);
      Event.stop(event);
    }
    this.active = false;
    this.dragging = false;
  },
  finishDrag: function(event, success) {
    this.active = false;
    this.dragging = false;
    this.updateFinished();
  },
  updateFinished: function() {
    if (this.initialized && this.options.onChange)
      this.options.onChange(this.values.length>1 ? this.values : this.value, this);
    this.event = null;
  }
});

// script.aculo.us controls.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 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//           (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
// Contributors:
//  Richard Livsey
//  Rahul Bhargava
//  Rob Wills
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

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

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

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

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

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

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

    this.observer = null;

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

    Element.hide(this.update);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/**
*
*  AJAX IFRAME METHOD (AIM)
*  http://www.webtoolkit.info/
*
**/

AIM = {

	frame : function(c) {

		var n = 'f' + Math.floor(Math.random() * 99999);
		var d = document.createElement('DIV');
		d.innerHTML = '<iframe style="display:none" src="about:blank" id="'+n+'" name="'+n+'" onload="AIM.loaded(\''+n+'\')"></iframe>';
		document.body.appendChild(d);

		var i = document.getElementById(n);
		if (c && typeof(c.onComplete) == 'function') {
			i.onComplete = c.onComplete;
		}

		return n;
	},

	form : function(f, name) {
		f.setAttribute('target', name);
	},

	submit : function(f, c) {
		AIM.form(f, AIM.frame(c));
		if (c && typeof(c.onStart) == 'function') {
			return c.onStart();
		} else {
			return true;
		}
	},

	loaded : function(id) {
		var i = document.getElementById(id);
		if (i.contentDocument) {
			var d = i.contentDocument;
		} else if (i.contentWindow) {
			var d = i.contentWindow.document;
		} else {
			var d = window.frames[id].document;
		}
		if (d.location.href == "about:blank") {
			return;
		}

		if (typeof(i.onComplete) == 'function') {
      i.onComplete(AIM.getResults(d.body));
		}
	},

  getResults: function(body) {
    var text = '';
    for(var i = 0, j = body.childNodes.length; i < j; i++) {
      if(body.childNodes[i].nodeValue){
        text = text.concat(body.childNodes[i].nodeValue.slice(1,-1));
      }  
    }
    return text;
  }
}

/*
*    script.aculo.us resizable.js, Mon Aug 20th 2007
*
*    Orginal: http://script.aculo.us/
*
*    Scriptaculous extension "Vasil Popovski" => vas_popovski@hotmail.com
*    Extension based on Scriptaculous dragdrop.js
*
*  This extenssion is freely distributable under the terms of an MIT-style license.
*/

var Resizables = {
  resizers: [],
  observers: [],

  register: function(resizable) {
    if(this.resizers.length == 0) {
      this.eventMouseUp   = this.endResize.bindAsEventListener(this);
      this.eventMouseMove = this.updateResize.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.resizers.push(resizable);
  },

  unregister: function(resizable) {
    this.resizers = this.resizers.reject(function(r){return r==resizable});
    if(this.resizers.length == 0) {
      Event.stopObserving(document, "mouseup", this.eventMouseUp);
      Event.stopObserving(document, "mousemove", this.eventMouseMove);
      Event.stopObserving(document, "keypress", this.eventKeypress);
    }
  },

  activate: function(resizable) {
    if(resizable.options.delay) {
      this._timeout = setTimeout(function() {
        Resizables._timeout = null;
        window.focus();
        Resizables.activeResizable = resizable;
      }.bind(this), resizable.options.delay);
    } else {
      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
      this.activeResizable = resizable;
    }
  },
  deactivate: function() {
    this.activeResizable = null;
  },
  updateResize: function(event) {
    if(!this.activeResizable) return;
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
     if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
    this._lastPointer = pointer;
    this.activeResizable.updateResize(event, pointer);
  },
  endResize: function(event) {
    if(this._timeout) {
      clearTimeout(this._timeout);
      this._timeout = null;
    }
    if(!this.activeResizable) return;
    this._lastPointer = null;
    this.activeResizable.endResize(event);
    this.activeResizable = null;
  },

  keyPress: function(event) {
    if(this.activeResizable)
      this.activeResizable.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, resizable, event) {  // 'onStart', 'onEnd', 'onDrag'
    if(this[eventName+'Count'] > 0)
      this.observers.each( function(o) {
        if(o[eventName]) o[eventName](eventName, resizable, event);
      });
    if(resizable.options[eventName]) resizable.options[eventName](resizable, event);
  },

  _cacheObserverCallbacks: function() {
    ['onStart','onEnd','onResize'].each( function(eventName) {
      Resizables[eventName+'Count'] = Resizables.observers.select(
        function(o) { return o[eventName]; }
      ).length;
    });
  }
}

var Resizable = Class.create();
Resizable._resizing    = {};

Resizable.prototype = {
  initialize: function(element) {
    var defaults = {
      handle: false,

      endeffect: function(element) {
        var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
          queue: {scope:'_resizable', position:'end'},
          afterFinish: function(){
            Resizable._resizing[element] = false
          }
        });
      },
      zindex: 1000,
      revert: false,
      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
      delay: 0
    };

    if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
      Object.extend(defaults, {
        starteffect: function(element) {
          element._opacity = Element.getOpacity(element);
          Resizable._resizing[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 && (typeof options.handle == 'string'))
      this.handle = this.element.down('.'+options.handle, 0);

    if(!this.handle) this.handle = $(options.handle);
    if(!this.handle) this.handle = this.element;

    Element.makePositioned(this.element); // fix IE
    this.delta    = this.currentDelta();
    this.options  = options;
    this.resizing = false;

    this.eventMouseDown = this.initResize.bindAsEventListener(this);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);

    Resizables.register(this);
  },
   reverteffect: function(element, horizontal, vertical) {
       var horiz = this._edim[0] - horizontal;
        var vert = this._edim[1] - vertical;
        new Effect.ReSize(element, {direction:'vert', amount:vert});
        new Effect.ReSize(element, {direction:'horizontal', amount:horiz});
    },

  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    Resizables.unregister(this);
  },

  currentDelta: function() {
    return([
      parseInt(Element.getStyle(this.element,'left') || '0'),
      parseInt(Element.getStyle(this.element,'top') || '0')]);
  },
  initResize: function(event) {
    if(typeof Resizable._resizing[this.element] != 'undefined' &&
      Resizable._resizing[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)];
          this._initialX = pointer[0];
          this._initialY = pointer[1];
          var dim = Element.getDimensions(this.element);

          this._edim = [dim.width,dim.height - 25];
          this._min = [1,1];
          this._max = [0,0];
      var pos = Position.cumulativeOffset(this.element);
      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });

        if(this.options.bind == true){
            this._parentDim = Element.getDimensions(this.element.parentNode);
            var cop = Position.cumulativeOffset(this.element.parentNode);
            var coe = Position.cumulativeOffset(this.element);
                    this.elementOffset = [coe[0]-cop[0], coe[1]-cop[1]];
        }
        if(this.options.min){
            if(this.options.min instanceof Array){
                this._min = this._min.map(function(v,i){return (this.options.min[i] > 0 ? this.options.min[i] : 1);}.bind(this));
            }
            else
                this._min = this._min.map(function(v,i){return (this.options.min > 0 ? this.options.min : 1);}.bind(this));
        }
        if(this.options.max){
            if(this.options.max instanceof Array){
                this._max = this._max.map(function(v,i){ return (this.options.max[i] >= this._min[i]) ? this.options.max[i] : 0; }.bind(this));
            }
            else
                this._max = this._max.map(function(v,i){ return (this.options.max >= this._min[i]) ? this.options.max : 0; }.bind(this));
        }

      Resizables.activate(this);
      Event.stop(event);
    }
  },

  startResize: function(event) {
    this.resizing = true;
    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) {
      // If element has margin-left/right/top/bottom set all sort of problems occurs regarding the elements starting position
      // and final position especially in IE (more problems when also snap and ghosting are initiated together)
      // (similar happens in Draggable - might need fixing there as well??)
      // next few lines SOLVE the problem (works for every combination: position:absolute+offset || relative+margins etc.)
      this._clone = this.element.cloneNode(true);
      this.element.parentNode.insertBefore(this._clone, this.element);
      var style = this._clone.style;
      Position.absolutize(this._clone);
      // partial IE margin fix (if another element is below it in IE it looses its position i.e. offset on bottom is reset)
      if(navigator.appName.indexOf('Microsoft') != -1 && parseInt(Element.getStyle(this.element, 'margin-top')) > 0){
         this.element.style.top = style.marginTop;
      }
      //
      style.margin = '0px';
    }
    Resizables.notify('onStart', this, event);

    if(this.options.starteffect) this.options.starteffect(this.element);
  },
  updateResize: function(event, pointer) {
   if(!this.resizing) this.startResize(event);

    Resizables.notify('onResize', this, event);
    this.draw(pointer);
    if(this.options.change) this.options.change(this);

    Event.stop(event);
  },

  finishResize: function(event, success) {
    this.resizing = false;
    if(this.options.ghosting) {
      if(navigator.appName.indexOf('Microsoft') != -1 && parseInt(Element.getStyle(this.element, 'margin-top')) > 0)
          this.element.style.top = this._clone.style.marginTop;
      Element.remove(this._clone);
      this._clone = null;
    }
    Resizables.notify('onEnd', this, event);
    var revert = this.options.revert;
    if(revert && typeof revert == 'function') revert = revert(this.element);

    if(revert && this.reverteffect) {
        var dim = Element.getDimensions(this.element);
        this.reverteffect(this.element, dim.width, dim.height);//d[1]-this.delta[1], d[0]-this.delta[0]
    }
    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if(this.options.endeffect)
      this.options.endeffect(this.element);

    Resizables.deactivate(this);
  },

  keyPress: function(event) {
    if(event.keyCode!=Event.KEY_ESC) return;
    this.finishResize(event, false);
    Event.stop(event);
  },

  endResize: function(event) {
    if(!this.resizing) return;
    this.finishResize(event, true);
    Event.stop(event);
  },

  draw: function(point) {
      var pos = Position.cumulativeOffset(this.element);
    var 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));

    var l_width = p[0] + this._edim[0] - d[0];
    var l_height = p[1] + this._edim[1] - d[1];

    p[0] = (l_width > this._min[0]) ? l_width : this._min[0];
    p[1] = (l_height > this._min[1]) ? l_height : this._min[1];

    if(this.options.snap) {
        if(typeof this.options.snap == 'function') {
            p = this.options.snap(p[0],p[1],this);
          }
          else {
          if(this.options.snap instanceof Array) {
            p = p.map( function(v, i) {
            // IF Javascript alert activated in IE throws error if one of the snap values is 0 : [20,0]
            // or if this map functions returns 0 for i-th element
            // Same happens in Draggable (needs to be patched??)
            var dim = Math.round(v/this.options.snap[i])*this.options.snap[i];
            return (this.options.snap[i] > 0) ? ((dim > this._min[i]) ? dim : this._min[i]) : this._edim[i];
            }.bind(this))
          }
          else {
            p = p.map( function(v,i) {
            var dim = Math.round(v/this.options.snap)*this.options.snap-d[i];
            return (this.options.snap > 0) ? ((dim > this._min[i]) ? dim : this._min[i]) : this._edim[i] }.bind(this))
          }
        }
    }

    if(this.options.bind){
        if(this._parentDim.width <= p[0]+this.elementOffset[0])
            p[0] = this._parentDim.width - this.elementOffset[0] - 2;
        if(this._parentDim.height <= p[1]+this.elementOffset[1])
            p[1] = this._parentDim.height - this.elementOffset[1] - 2;
    }

    if(this.options.min){
        p[0] = p[0] > this._min[0] ? p[0] : this._min[0];
        p[1] = p[1] > this._min[1] ? p[1] : this._min[1];
    }
    if(this.options.max){
        p[0] = p[0] < this._max[0] ? p[0] : (this._max[0] > 0 ? this._max[0] : p[0]);
        p[1] = p[1] < this._max[1] ? p[1] : (this._max[1] > 0 ? this._max[1] : p[1]);
    }

    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
        style.width = p[0]+"px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
         style.height = p[1]+"px";
    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  }
}

// script.aculo.us EffectResize.js

// Copyright(c) 2007 - Frost Innovation AS, http://ajaxwidgets.com
//
// EffectResize.js is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

/* Helper Effect for resizing elements...
 */
Effect.ReSize = Class.create();
Object.extend(Object.extend(Effect.ReSize.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = element;
    if(!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({ amount: 100, direction: 'vert', toSize:null }, arguments[1] || {});
    if( options.direction == 'vert' )
      this.originalSize = options.originalSize || parseInt(this.element.style.height);
    else
      this.originalSize = options.originalSize || parseInt(this.element.style.width);

    if( options.toSize != null )
      options.amount = options.toSize - this.originalSize;

    this.start(options);
  },
  setup: function() {
    // Prevent executing on elements not in the layout flow
    if(this.element.getStyle('display')=='none') { this.cancel(); return; }
  },
  update: function(position) {
    if( this.options.direction == 'vert' ){
      this.element.setStyle({height: this.originalSize+(this.options.amount*position)+'px'});
    } else {
      this.element.setStyle({width: this.originalSize+(this.options.amount*position)+'px'});
    }
  },
  finish: function(){
    if( this.options.direction == 'vert' ){
      this.element.setStyle({height: this.originalSize+this.options.amount+'px'});
    } else {
      this.element.setStyle({width: this.originalSize+this.options.amount+'px'});
    }
  }
});



var Pivotal = {};
var Socialitis = {};
var LikeMe = {};
LikeMe.Command = {};
LikeMe.Mixin = {};
LikeMe.Model = {};
LikeMe.Page = {};
LikeMe.Service = {};
LikeMe.View = {};
LikeMe.View.Partial = {};


/*
ModalBox - The pop-up window thingie with AJAX, based on prototype and script.aculo.us.

Copyright Andrey Okonetchnikov (andrej.okonetschnikow@gmail.com), 2006-2007
All rights reserved.
 
VERSION 1.6.0
Last Modified: 12/13/2007
*/

if (!window.Modalbox)
	var Modalbox = new Object();

Modalbox.Methods = {
	overrideAlert: false, // Override standard browser alert message with ModalBox
	focusableElements: new Array,
	currFocused: 0,
	initialized: false,
	active: true,
	options: {
		title: null, // Title of the ModalBox window
		overlayClose: true, // Close modal box by clicking on overlay
		width: 500, // Default width in px
		height: 90, // Default height in px
		overlayOpacity: .65, // Default overlay opacity
		overlayDuration: .25, // Default overlay fade in/out duration in seconds
		slideDownDuration: .25, // Default Modalbox appear slide down effect in seconds
		slideUpDuration: .25, // Default Modalbox hiding slide up effect in seconds
		resizeDuration: .25, // Default resize duration seconds
		inactiveFade: true, // Fades MB window on inactive state
		transitions: true, // Toggles transition effects. Transitions are enabled by default
		loadingString: "Please wait. Loading...", // Default loading string message
		closeString: "Close window", // Default title attribute for close window link
		closeValue: "&times;", // Default string for close link in the header
		params: {},
		method: 'get', // Default Ajax request method
		autoFocusing: false, // Toggles auto-focusing for form elements. Disable for long text pages.
		aspnet: false, // Should be use then using with ASP.NET costrols. Then true Modalbox window will be injected into the first form element.
    disableSpinner: false
  },
	_options: new Object,
	
	setOptions: function(options) {
		Object.extend(this.options, options || {});
	},
	
	_init: function(options) {
		// Setting up original options with default options
		Object.extend(this._options, this.options);
		this.setOptions(options);
    this.doneAppearing = false;

		//Create the overlay
		this.MBoverlay = new Element("div", { id: "modalbox_overlay", 'class': 'modalbox-overlay', opacity: "0" });
		
		//Create DOm for the window
		this.MBwindow = new Element("div", {id: "modalbox_window", 'class': 'modalbox-window', style: "display: none"}).update(
			this.MBframe = new Element("div", {id: "modalbox_frame"}).update(
				this.MBheader = new Element("div", {id: "modalbox_header"}).update(
					this.MBcaption = new Element("div", {id: "modalbox_caption"})
				)
			)
		);
		this.MBclose = new Element("a", {id: "modalbox_close", title: this.options.closeString, href: "#"}).update("<span>" + this.options.closeValue + "</span>");
		this.MBheader.insert({'bottom':this.MBclose});
		
		this.MBcontent = new Element("div", {id: "modalbox_content"})
    if(!this.options.disableSpinner) {
      this.MBcontent.update(this.MBloading = new Element("div", {id: "modalbox_loading"}).update(this.options.loadingString));
    }
    this.MBframe.insert({'bottom':this.MBcontent});
		
		// Inserting into DOM. If parameter set and form element have been found will inject into it. Otherwise will inject into body as topmost element.
		// Be sure to set padding and marging to null via CSS for both body and (in case of asp.net) form elements. 
		var injectToEl = this.options.aspnet ? $(document.body).down('form') : $(document.body);
		injectToEl.insert({'top':this.MBwindow});
		injectToEl.insert({'top':this.MBoverlay});
		
		// Initial scrolling position of the window. To be used for remove scrolling effect during ModalBox appearing
		this.initScrollX = window.pageXOffset || document.body.scrollLeft || document.documentElement.scrollLeft;
		this.initScrollY = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
		
		//Adding event observers
		this.hideObserver = this._hide.bindAsEventListener(this);
		this.kbdObserver = this._kbdHandler.bindAsEventListener(this);
		this._initObservers();

		this.initialized = true; // Mark as initialized
	},
	
	show: function(content, options) {
		if(!this.initialized) this._init(options); // Check for is already initialized
		
		this.content = content;
		this.setOptions(options);
    $$('.hide-for-modal').each(function(item) {
      item.style.visibility = "hidden";
    });

    $(this.MBcaption).update(this.options.title);

		if(this.MBwindow.style.display == "none") { // First modal box appearing
			this._appear();
			this.event("onShow"); // Passing onShow callback
		}
		else { // If MB already on the screen, update it
			this._update();
			this.event("onUpdate"); // Passing onUpdate callback
		}
	},
	
	hide: function(options) { // External hide method to use from external HTML and JS
		if(this.initialized) {
			// Reading for options/callbacks except if event given as a pararmeter
			if(options && typeof options.element != 'function') Object.extend(this.options, options); 
			// Passing beforeHide callback
			this.event("beforeHide");
			if(this.options.transitions)
				Effect.SlideUp(this.MBwindow, { duration: this.options.slideUpDuration, transition: Effect.Transitions.sinoidal, afterFinish: this._deinit.bind(this) } );
			else {
				$(this.MBwindow).hide();
				this._deinit();
			}
		} else throw("Modalbox is not initialized.");
	},
	
	_hide: function(event) { // Internal hide method to use with overlay and close link
		event.stop(); // Stop event propaganation for link elements
		/* Then clicked on overlay we'll check the option and in case of overlayClose == false we'll break hiding execution [Fix for #139] */
		if(event.element().id == 'modalbox_overlay' && !this.options.overlayClose) return false;
    this.hide();
  },
	
	alert: function(message){
		var html = '<div class="modalbox_alert"><p>' + message + '</p><input type="button" onclick="Modalbox.hide()" value="OK" /></div>';
		Modalbox.show(html, {title: 'Alert: ' + document.title, width: 300});
	},
		
	_appear: function() { // First appearing of MB
		if(Prototype.Browser.IE && !navigator.appVersion.match(/\b7.0\b/)) { // Preparing IE 6 for showing modalbox
			window.scrollTo(0,0);
			this._prepareIE("100%", "hidden"); 
		}
		this._setWidth();
    this.options.height = null;
    this._setPosition();
		if(this.options.transitions) {
			$(this.MBoverlay).setStyle({opacity: 0});
			new Effect.Fade(this.MBoverlay, {
					from: 0, 
					to: this.options.overlayOpacity, 
					duration: this.options.overlayDuration, 
					afterFinish: function() {
						new Effect.SlideDown(this.MBwindow, {
							duration: this.options.slideDownDuration, 
							transition: Effect.Transitions.sinoidal,
							afterFinish: function(){
								this._setPosition();
								this.loadContent();
                this.doneAppearing = true;
                if(this.resizeToContentOptions) { this.actuallyResizeToContent(this.resizeToContentOptions); }
              }.bind(this)
						});
					}.bind(this)
			});
		} else {
			$(this.MBoverlay).setStyle({opacity: this.options.overlayOpacity});
			$(this.MBwindow).show();
			this._setPosition(); 
			this.loadContent();
      this.doneAppearing = true;      
    }
		this._setWidthAndPosition = this._setWidthAndPosition.bindAsEventListener(this);
		Event.observe(window, "resize", this._setWidthAndPosition);
	},

	setHeight: function(newHeight) {
		var currentHeight = $(this.MBwindow).getHeight();

		if ( newHeight != currentHeight )
			this.resize(0, newHeight - currentHeight );
	},

	resize: function(byWidth, byHeight, options) { // Change size of MB without loading content
		var wHeight = $(this.MBwindow).getHeight();
		var wWidth = $(this.MBwindow).getWidth();
		var hHeight = $(this.MBheader).getHeight();
		var cHeight = $(this.MBcontent).getHeight();
		var newHeight = ((wHeight - hHeight + byHeight) < cHeight) ? (cHeight + hHeight - wHeight) : byHeight;
		if(options) this.setOptions(options); // Passing callbacks
		if(this.options.transitions) {
			new Effect.ScaleBy(this.MBwindow, byWidth, newHeight, {
					duration: this.options.resizeDuration,
				  	afterFinish: function() { 
						this.event("_afterResize"); // Passing internal callback
						this.event("afterResize"); // Passing callback
					}.bind(this)
				});
		} else {
			this.MBwindow.setStyle({width: wWidth + byWidth + "px", height: wHeight + newHeight + "px"});
      this.options.height = wHeight + newHeight;
      setTimeout(function() {
				this.event("_afterResize"); // Passing internal callback
				this.event("afterResize"); // Passing callback
			}.bind(this), 1);
			
		}
    this.focusableElements = this._findFocusableElements();
    this.currFocused = null;
  },
	
	resizeToContent: function(options){
		
		// Resizes the modalbox window to the actual content height.
		// This might be useful to resize modalbox after some content modifications which were changed ccontent height.
    if(!options) { options = {}; }
    if(!this.doneAppearing) {
      //queue up the fact that we want to resize in the future
      this.resizeToContentOptions = options;
    } else {
      this.actuallyResizeToContent(options);
    }

	},

  actuallyResizeToContent: function(options) {
    var byHeight = this.options.height - this.MBwindow.offsetHeight;
    if(byHeight != 0) {
      if(options) this.setOptions(options); // Passing callbacks
      Modalbox.resize(0, byHeight);
    }
    this.resizeToContentOptions = undefined;
  },

  resizeToInclude: function(element, options){
		
		// Resizes the modalbox window to the camulative height of element. Calculations are using CSS properties for margins and border.
		// This method might be useful to resize modalbox before including or updating content.
		
		var el = $(element);
		var elHeight = el.getHeight() + parseInt(el.getStyle('margin-top')) + parseInt(el.getStyle('margin-bottom')) + parseInt(el.getStyle('border-top-width')) + parseInt(el.getStyle('border-bottom-width'));
		if(elHeight > 0) {
			if(options) this.setOptions(options); // Passing callbacks
			Modalbox.resize(0, elHeight);
		}
	},
	
	_update: function() { // Updating MB in case of wizards
		$(this.MBcontent).update("");
		this.MBcontent.appendChild(this.MBloading);
		$(this.MBloading).update(this.options.loadingString);
		this.currentDims = [this.MBwindow.offsetWidth, this.MBwindow.offsetHeight];
		Modalbox.resize((this.options.width - this.currentDims[0]), (this.options.height - this.currentDims[1]), {_afterResize: this._loadAfterResize.bind(this) });
	},
	
	loadContent: function () {
		if(this.event("beforeLoad") != false) { // If callback passed false, skip loading of the content
			if(typeof this.content == 'string') {
				var htmlRegExp = new RegExp(/<\/?[^>]+>/gi);
				if(htmlRegExp.test(this.content)) { // Plain HTML given as a parameter
					this._insertContent(this.content.stripScripts());
					this._putContent(function(){
						this.content.extractScripts().map(function(script) {
							return eval(script.replace("<!--", "").replace("// -->", ""));
						}.bind(window));
					}.bind(this));
				} else // URL given as a parameter. We'll request it via Ajax
					new Ajax.Request( this.content, { method: this.options.method.toLowerCase(), parameters: this.options.params,
						onSuccess: function(transport) {
							var response = new String(transport.responseText);
							this._insertContent(transport.responseText.stripScripts());
							this._putContent(function(){
								response.extractScripts().map(function(script) {
									return eval(script.replace("<!--", "").replace("// -->", ""));
								}.bind(window));
							});
						}.bind(this),
						onException: function(instance, exception){
							Modalbox.hide();
							throw('Modalbox Loading Error: ' + exception);
						}
					});

			} else if (typeof this.content == 'object') {// HTML Object is given
				this._insertContent(this.content);
				this._putContent(undefined, true);
			} else {
				Modalbox.hide();
				throw('Modalbox Parameters Error: Please specify correct URL or HTML element (plain HTML or object)');
			}
		}
	},
	
	_insertContent: function(content){
		$(this.MBcontent).hide().update("");
		if(typeof content == 'string') {
			setTimeout(function() { // Hack to disable content flickering in Firefox
				this.MBcontent.update(content);
			}.bind(this), 1);
		} else if (typeof content == 'object') { // HTML Object is given
			// var _htmlObj = content.cloneNode(true); // If node already a part of DOM we'll clone it
			// If clonable element has ID attribute defined, modifying it to prevent duplicates
			// if(content.id) content.id = "MB_" + content.id;
			/* Add prefix for IDs on all elements inside the DOM node */
			// $(content).select('*[id]').each(function(el){ el.id = "MB_" + el.id; });
			this.MBcontent.appendChild(content);
			this.MBcontent.down().show(); // Toggle visibility for hidden nodes
			if(Prototype.Browser.IE) // Toggling back visibility for hidden selects in IE
				$$("#modalbox_content select").invoke('setStyle', {'visibility': ''});
		}
	},

  _actuallyPutContent: function(callback) {
    this.MBcontent.show().makePositioned();
    this.focusableElements = this._findFocusableElements();
    this._setFocus(); // Setting focus on first 'focusable' element in content (input, select, textarea, link or button)
    setTimeout(function(){ // MSIE fix
      if(callback != undefined)
        callback(); // Executing internal JS from loaded content
      this.event("afterLoad"); // Passing callback
    }.bind(this),1);
  },

  _putContent: function(callback, fromGeneratedContent){
		// Prepare and resize modal box for content
//		if(this.options.height == this._options.height) {
			setTimeout(function() { // MSIE sometimes doesn't display content correctly
        if(fromGeneratedContent) {
          this._actuallyPutContent(callback);
        }
        else {
          Modalbox.resize(0, $(this.MBcontent).getHeight() - $(this.MBwindow).getHeight() + $(this.MBheader).getHeight(), {
            afterResize: function(){
              this._actuallyPutContent(callback);  
            }.bind(this)
          });
        }
      }.bind(this), 1);
//		} else { // Height is defined. Creating a scrollable window
//			this._setWidth();
//			this.MBcontent.setStyle({overflow: 'auto', height: $(this.MBwindow).getHeight() - $(this.MBheader).getHeight() - 13 + 'px'});
//			this.MBcontent.show();
//			this.focusableElements = this._findFocusableElements();
//			this._setFocus(); // Setting focus on first 'focusable' element in content (input, select, textarea, link or button)
//			setTimeout(function(){ // MSIE fix
//				if(callback != undefined)
//					callback(); // Executing internal JS from loaded content
//				this.event("afterLoad"); // Passing callback
//			}.bind(this),1);
//		}
	},
	
	activate: function(options){
		this.setOptions(options);
		this.active = true;
		$(this.MBclose).observe("click", this.hideObserver);
		if(this.options.overlayClose)
			$(this.MBoverlay).observe("click", this.hideObserver);
		$(this.MBclose).show();
		if(this.options.transitions && this.options.inactiveFade)
			new Effect.Appear(this.MBwindow, {duration: this.options.slideUpDuration});
	},
	
	deactivate: function(options) {
		this.setOptions(options);
		this.active = false;
		$(this.MBclose).stopObserving("click", this.hideObserver);
		if(this.options.overlayClose)
			$(this.MBoverlay).stopObserving("click", this.hideObserver);
		$(this.MBclose).hide();
		if(this.options.transitions && this.options.inactiveFade)
			new Effect.Fade(this.MBwindow, {duration: this.options.slideUpDuration, to: .75});
	},
	
	_initObservers: function(){
		$(this.MBclose).observe("click", this.hideObserver);
		if(this.options.overlayClose)
			$(this.MBoverlay).observe("click", this.hideObserver);
		if(Prototype.Browser.IE)
			Event.observe(document, "keydown", this.kbdObserver);
		else
			Event.observe(document, "keypress", this.kbdObserver);
	},
	
	_removeObservers: function(){
		$(this.MBclose).stopObserving("click", this.hideObserver);
		if(this.options.overlayClose)
			$(this.MBoverlay).stopObserving("click", this.hideObserver);
		if(Prototype.Browser.IE)
			Event.stopObserving(document, "keydown", this.kbdObserver);
		else
			Event.stopObserving(document, "keypress", this.kbdObserver);
	},
	
	_loadAfterResize: function() {
		this._setWidth();
		this._setPosition();
		this.loadContent();
	},
	
	_setFocus: function() { 
		/* Setting focus to the first 'focusable' element which is one with tabindex = 1 or the first in the form loaded. */
		if(this.focusableElements.length > 0 && this.options.autoFocusing == true) {
			var firstEl = this.focusableElements.find(function (el){
				return el.tabIndex == 1;
			}) || this.focusableElements.first();
			this.currFocused = this.focusableElements.toArray().indexOf(firstEl);
			firstEl.focus(); // Focus on first focusable element except close button
		} else if($(this.MBclose).visible() && this.options.autoFocusing == true)
			$(this.MBclose).focus(); // If no focusable elements exist focus on close button
	},
	
	_findFocusableElements: function(){ // Collect form elements or links from MB content
		this.MBcontent.select('input:not([type~=hidden]), select, textarea, button, a[href]').invoke('addClassName', 'modalbox_focusable');
		return this.MBcontent.select('.modalbox_focusable');
	},
	
	_kbdHandler: function(event) {
		var node = event.element();
		switch(event.keyCode) {
			case Event.KEY_TAB:
				event.stop();
				
				/* Switching currFocused to the element which was focused by mouse instead of TAB-key. Fix for #134 */ 
				if(node != this.focusableElements[this.currFocused])
					this.currFocused = this.focusableElements.toArray().indexOf(node);
				
				if(!event.shiftKey) { //Focusing in direct order
					if(this.currFocused == this.focusableElements.length - 1) {
						this.focusableElements.first().focus();
						this.currFocused = 0;
					} else {
						this.currFocused++;
						this.focusableElements[this.currFocused].focus();
					}
				} else { // Shift key is pressed. Focusing in reverse order
					if(this.currFocused == 0) {
						this.focusableElements.last().focus();
						this.currFocused = this.focusableElements.length - 1;
					} else {
						this.currFocused--;
						this.focusableElements[this.currFocused].focus();
					}
				}
				break;			
			case Event.KEY_ESC:
				if(this.active) this._hide(event);
				break;
			case 32:
				this._preventScroll(event);
				break;
			case 0: // For Gecko browsers compatibility
				if(event.which == 32) this._preventScroll(event);
				break;
			case Event.KEY_UP:
			case Event.KEY_DOWN:
			case Event.KEY_PAGEDOWN:
			case Event.KEY_PAGEUP:
			case Event.KEY_HOME:
			case Event.KEY_END:
				// Safari operates in slightly different way. This realization is still buggy in Safari.
				if(Prototype.Browser.WebKit && !["textarea", "select"].include(node.tagName.toLowerCase()))
					event.stop();
				else if( (node.tagName.toLowerCase() == "input" && ["submit", "button"].include(node.type)) || (node.tagName.toLowerCase() == "a") )
					event.stop();
				break;
		}
	},
	
	_preventScroll: function(event) { // Disabling scrolling by "space" key
		if(!["input", "textarea", "select", "button"].include(event.element().tagName.toLowerCase())) 
			event.stop();
	},
	
	_deinit: function()
	{	
		this._removeObservers();
		Event.stopObserving(window, "resize", this._setWidthAndPosition );
		if(this.options.transitions) {
			Effect.toggle(this.MBoverlay, 'appear', {duration: this.options.overlayDuration, afterFinish: this._removeElements.bind(this) });
		} else {
			this.MBoverlay.hide();
			this._removeElements();
		}
		$(this.MBcontent).setStyle({overflow: '', height: ''});
  },
	
	_removeElements: function () {
    $$('.hide-for-modal').each(function(item) {
      item.style.visibility = "visible";
    });
    
    $(this.MBoverlay).remove();
		$(this.MBwindow).remove();
		if(Prototype.Browser.IE && !navigator.appVersion.match(/\b7.0\b/)) {
			this._prepareIE("", ""); // If set to auto MSIE will show horizontal scrolling
			window.scrollTo(this.initScrollX, this.initScrollY);
		}
		
		/* Replacing prefixes 'modalbox_' in IDs for the original content */
		if(typeof this.content == 'object') {
			if(this.content.id && this.content.id.match(/modalbox_/)) {
				this.content.id = this.content.id.replace(/modalbox_/, "");
			}
			this.content.select('*[id]').each(function(el){ el.id = el.id.replace(/modalbox_/, ""); });
		}
		/* Initialized will be set to false */
		this.initialized = false;
		this.event("afterHide"); // Passing afterHide callback
		this.setOptions(this._options); //Settings options object into intial state
	},
	
	_setWidth: function () { //Set size
		$(this.MBwindow).setStyle({width: this.options.width + "px"});
    if(this.options.height) {
      $(this.MBwindow).setStyle({height: this.options.height + "px"});
    }
  },
	
	_setPosition: function () {
		$(this.MBwindow).setStyle({left: Math.round((Element.getWidth(document.body) - Element.getWidth(this.MBwindow)) / 2 ) + "px"});
	},
	
	_setWidthAndPosition: function () {
		$(this.MBwindow).setStyle({width: this.options.width + "px"});
		this._setPosition();
	},
	
	_getScrollTop: function () { //From: http://www.quirksmode.org/js/doctypes.html
		var theTop;
		if (document.documentElement && document.documentElement.scrollTop)
			theTop = document.documentElement.scrollTop;
		else if (document.body)
			theTop = document.body.scrollTop;
		return theTop;
	},
	_prepareIE: function(height, overflow){
		$$('html, body').invoke('setStyle', {width: height, height: height, overflow: overflow}); // IE requires width and height set to 100% and overflow hidden
		$$("select").invoke('setStyle', {'visibility': overflow}); // Toggle visibility for all selects in the common document
	},
	event: function(eventName) {
		if(this.options[eventName]) {
			var returnValue = this.options[eventName](); // Executing callback
			this.options[eventName] = null; // Removing callback after execution
			if(returnValue != undefined) 
				return returnValue;
			else 
				return true;
		}
		return true;
	}
};

Object.extend(Modalbox, Modalbox.Methods);

if(Modalbox.overrideAlert) window.alert = Modalbox.alert;

Effect.ScaleBy = Class.create();
Object.extend(Object.extend(Effect.ScaleBy.prototype, Effect.Base.prototype), {
  initialize: function(element, byWidth, byHeight, options) {
    this.element = $(element)
    var options = Object.extend({
	  scaleFromTop: true,
      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
      scaleByWidth: byWidth,
	  scaleByHeight: byHeight
    }, arguments[3] || {});
    this.start(options);
  },
  setup: function() {
    this.elementPositioning = this.element.getStyle('position');
      
    this.originalTop  = this.element.offsetTop;
    this.originalLeft = this.element.offsetLeft;
	
    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];
	  
	this.deltaY = this.options.scaleByHeight;
	this.deltaX = this.options.scaleByWidth;
  },
  update: function(position) {
    var currentHeight = this.dims[0] + (this.deltaY * position);
	var currentWidth = this.dims[1] + (this.deltaX * position);
	
	currentHeight = (currentHeight > 0) ? currentHeight : 0;
	currentWidth = (currentWidth > 0) ? currentWidth : 0;
	
    this.setDimensions(currentHeight, currentWidth);
  },

  setDimensions: function(height, width) {
    var d = {};
    d.width = width + 'px';
    d.height = height + 'px';
    
	var topd  = Math.round((height - this.dims[0])/2);
	var leftd = Math.round((width  - this.dims[1])/2);
	if(this.elementPositioning == 'absolute' || this.elementPositioning == 'fixed') {
		if(!this.options.scaleFromTop) d.top = this.originalTop-topd + 'px';
		d.left = this.originalLeft-leftd + 'px';
	} else {
		if(!this.options.scaleFromTop) d.top = -topd + 'px';
		d.left = -leftd + 'px';
	}
    this.element.setStyle(d);
  }
});


var Routing = {
	get_path: function (segments, options, overrides) {
		var extras = null;

		for (var property in overrides) {
			if (options[property] != null) {
				options[property] = overrides[property];
			}
			else {
				extras = extras ? extras : {};
				extras[property] = overrides[property]
			}
		}

		for (var prop in options) {
			segments = segments.replace(":"+prop, encodeURIComponent(options[prop]));
		}

    if (options["format"] && options["format"] != "") {
      segments = segments + "." + options["format"]
    }

		var query="";
		if (extras) {
			query += "?"
      query += StringUtils.toParamString(extras);
		}

		var path = segments;
    while (path.charAt(path.length - 1) == "/") {
			path = path.substring(0, path.length - 1);
		}
		return path + query;
	},

	get_url: function (segments, options, overrides) {
		return Routing.host + Routing.get_path(segments, options, overrides);
	},

    browse_state_city_path: function (overrides) {
    var options = {
      category: '',
      state: '',
      city: '',
      action: 'show',
      controller: 'browse'
    };
    return Routing.get_path('/browse/:category/:state/:city/', options, overrides);
  },

    user_friends_path: function (overrides) {
    var options = {
      user_id: '',
      format: '',
      action: 'index',
      controller: 'user/friends'
    };
    return Routing.get_path('/users/:user_id/friends', options, overrides);
  },

    place_to_dos_path: function (overrides) {
    var options = {
      place_id: '',
      format: '',
      action: 'show',
      controller: 'places/to_dos'
    };
    return Routing.get_path('/places/:place_id/to_dos', options, overrides);
  },

    share_message_path: function (overrides) {
    var options = {
      format: '',
      action: 'show',
      controller: 'share_messages'
    };
    return Routing.get_path('/share_message', options, overrides);
  },

    new_user_path: function (overrides) {
    var options = {
      format: '',
      action: 'new',
      controller: 'users'
    };
    return Routing.get_path('/users/new', options, overrides);
  },

    user_to_do_group_place_path: function (overrides) {
    var options = {
      user_id: '',
      to_do_group_id: '',
      id: '',
      format: '',
      action: 'show',
      controller: 'user/to_do_group_places'
    };
    return Routing.get_path('/users/:user_id/to_dos/:to_do_group_id/places/:id', options, overrides);
  },

    place_reviews_path: function (overrides) {
    var options = {
      place_id: '',
      format: '',
      action: 'index',
      controller: 'places/reviews'
    };
    return Routing.get_path('/places/:place_id/reviews', options, overrides);
  },

    new_facebook_login_path: function (overrides) {
    var options = {
      format: '',
      action: 'new',
      controller: 'facebook/login'
    };
    return Routing.get_path('/facebook/login/new', options, overrides);
  },

    admin_ratable_tag_path: function (overrides) {
    var options = {
      ratable_id: '',
      id: '',
      format: '',
      action: 'show',
      controller: 'admin/ratables/tags'
    };
    return Routing.get_path('/admin/ratables/:ratable_id/tags/:id', options, overrides);
  },

    place_tag_path: function (overrides) {
    var options = {
      place_id: '',
      id: '',
      format: '',
      action: 'show',
      controller: 'places/tags'
    };
    return Routing.get_path('/places/:place_id/tags/:id', options, overrides);
  },

    whats_new_path: function (overrides) {
    var options = {
      format: '',
      action: 'show',
      controller: 'news'
    };
    return Routing.get_path('/whats_new', options, overrides);
  },

    recommendation_collections_path: function (overrides) {
    var options = {
      format: '',
      action: 'show',
      controller: 'recommendation_collections'
    };
    return Routing.get_path('/recommendation_collections', options, overrides);
  },

    user_recommendation_group_path: function (overrides) {
    var options = {
      user_id: '',
      id: '',
      format: '',
      action: 'show',
      controller: 'user/recommendation_groups'
    };
    return Routing.get_path('/users/:user_id/recommendation_groups/:id', options, overrides);
  },

    user_recommendation_group_place_path: function (overrides) {
    var options = {
      user_id: '',
      recommendation_group_id: '',
      id: '',
      format: '',
      action: 'show',
      controller: 'user/recommendation_group_places'
    };
    return Routing.get_path('/users/:user_id/recommendation_groups/:recommendation_group_id/places/:id', options, overrides);
  },

    browse_cities_path: function (overrides) {
    var options = {
      category: '',
      state: '',
      action: 'cities',
      controller: 'browse'
    };
    return Routing.get_path('/browse/:category/:state/', options, overrides);
  },

    place_community_path: function (overrides) {
    var options = {
      place_id: '',
      format: '',
      action: 'show',
      controller: 'places/communities'
    };
    return Routing.get_path('/places/:place_id/community', options, overrides);
  },

    user_guidebook_path: function (overrides) {
    var options = {
      user_id: '',
      format: '',
      action: 'show',
      controller: 'guidebook'
    };
    return Routing.get_path('/users/:user_id/recommendations', options, overrides);
  },

    user_to_do_groups_path: function (overrides) {
    var options = {
      user_id: '',
      format: '',
      action: 'index',
      controller: 'user/to_do_groups'
    };
    return Routing.get_path('/users/:user_id/to_dos', options, overrides);
  },

    city_state_autocompletions_path: function (overrides) {
    var options = {
      format: '',
      action: 'index',
      controller: 'city_state_autocompletions'
    };
    return Routing.get_path('/city_state_autocompletions', options, overrides);
  },

    new_facebook_connect_path: function (overrides) {
    var options = {
      format: '',
      action: 'new',
      controller: 'facebook/connect'
    };
    return Routing.get_path('/facebook/connect/new', options, overrides);
  },

    place_note_path: function (overrides) {
    var options = {
      place_id: '',
      format: '',
      action: 'show',
      controller: 'places/notes'
    };
    return Routing.get_path('/places/:place_id/note', options, overrides);
  },

    new_place_path: function (overrides) {
    var options = {
      format: '',
      action: 'new',
      controller: 'places'
    };
    return Routing.get_path('/places/new', options, overrides);
  },

    people_path: function (overrides) {
    var options = {
      format: '',
      action: 'show',
      controller: 'people'
    };
    return Routing.get_path('/people', options, overrides);
  },

    user_to_do_group_places_path: function (overrides) {
    var options = {
      user_id: '',
      to_do_group_id: '',
      format: '',
      action: 'index',
      controller: 'user/to_do_group_places'
    };
    return Routing.get_path('/users/:user_id/to_dos/:to_do_group_id/places', options, overrides);
  },

    reorder_user_friends_path: function (overrides) {
    var options = {
      user_id: '',
      format: '',
      action: 'reorder',
      controller: 'user/friends'
    };
    return Routing.get_path('/users/:user_id/friends/reorder', options, overrides);
  },

    user_hidden_flaggings_path: function (overrides) {
    var options = {
      user_id: '',
      format: '',
      action: 'show',
      controller: 'user/hidden_flaggings'
    };
    return Routing.get_path('/users/:user_id/hidden_items', options, overrides);
  },

    browse_states_path: function (overrides) {
    var options = {
      category: '',
      action: 'states',
      controller: 'browse'
    };
    return Routing.get_path('/browse/:category/', options, overrides);
  },

    reorder_user_to_do_group_path: function (overrides) {
    var options = {
      user_id: '',
      id: '',
      format: '',
      action: 'reorder',
      controller: 'user/to_do_groups'
    };
    return Routing.get_path('/users/:user_id/to_dos/:id/reorder', options, overrides);
  },

    user_recommendation_group_places_path: function (overrides) {
    var options = {
      user_id: '',
      recommendation_group_id: '',
      format: '',
      action: 'index',
      controller: 'user/recommendation_group_places'
    };
    return Routing.get_path('/users/:user_id/recommendation_groups/:recommendation_group_id/places', options, overrides);
  },

    place_recommendations_path: function (overrides) {
    var options = {
      place_id: '',
      format: '',
      action: 'show',
      controller: 'places/recommendations'
    };
    return Routing.get_path('/places/:place_id/recommendations', options, overrides);
  },

    find_people_path: function (overrides) {
    var options = {
      action: 'show',
      controller: 'find_people'
    };
    return Routing.get_path('/find/people/', options, overrides);
  },

    place_photos_path: function (overrides) {
    var options = {
      place_id: '',
      format: '',
      action: 'index',
      controller: 'places/photos'
    };
    return Routing.get_path('/places/:place_id/photos', options, overrides);
  },

    popular_places_path: function (overrides) {
    var options = {
      format: '',
      action: 'index',
      controller: 'popular_places'
    };
    return Routing.get_path('/popular_places', options, overrides);
  },

    reorder_user_recommendation_group_path: function (overrides) {
    var options = {
      user_id: '',
      id: '',
      format: '',
      action: 'reorder',
      controller: 'user/recommendation_groups'
    };
    return Routing.get_path('/users/:user_id/recommendation_groups/:id/reorder', options, overrides);
  },

    login_path: function (overrides) {
    var options = {
      format: '',
      action: 'show',
      controller: 'login'
    };
    return Routing.get_path('/login', options, overrides);
  },

    discover_near_path: function (overrides) {
    var options = {
      category_plural_name: '',
      near: '',
      action: 'show',
      controller: 'discover'
    };
    return Routing.get_path('/discover/:category_plural_name/near/:near/', options, overrides);
  },

    place_tags_path: function (overrides) {
    var options = {
      place_id: '',
      format: '',
      action: 'index',
      controller: 'places/tags'
    };
    return Routing.get_path('/places/:place_id/tags', options, overrides);
  },

    user_follow_path: function (overrides) {
    var options = {
      user_id: '',
      id: '',
      format: '',
      action: 'show',
      controller: 'user/follow'
    };
    return Routing.get_path('/users/:user_id/follow/:id', options, overrides);
  }
};


/**
 * Version: 1.0 Alpha-1 
 * Build Date: 13-Nov-2007
 * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved.
 * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. 
 * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/
 */
Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}};
Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
return-1;};Date.getDayNumberFromName=function(name){var n=Date.CultureInfo.dayNames,m=Date.CultureInfo.abbreviatedDayNames,o=Date.CultureInfo.shortestDayNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
return-1;};Date.isLeapYear=function(year){return(((year%4===0)&&(year%100!==0))||(year%400===0));};Date.getDaysInMonth=function(year,month){return[31,(Date.isLeapYear(year)?29:28),31,30,31,30,31,31,30,31,30,31][month];};Date.getTimezoneOffset=function(s,dst){return(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST[s.toUpperCase()]:Date.CultureInfo.abbreviatedTimeZoneStandard[s.toUpperCase()];};Date.getTimezoneAbbreviation=function(offset,dst){var n=(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST:Date.CultureInfo.abbreviatedTimeZoneStandard,p;for(p in n){if(n[p]===offset){return p;}}
return null;};Date.prototype.clone=function(){return new Date(this.getTime());};Date.prototype.compareTo=function(date){if(isNaN(this)){throw new Error(this);}
if(date instanceof Date&&!isNaN(date)){return(this>date)?1:(this<date)?-1:0;}else{throw new TypeError(date);}};Date.prototype.equals=function(date){return(this.compareTo(date)===0);};Date.prototype.between=function(start,end){var t=this.getTime();return t>=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);}
if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);}
if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);}
if(x.hour||x.hours){this.addHours(x.hour||x.hours);}
if(x.month||x.months){this.addMonths(x.month||x.months);}
if(x.year||x.years){this.addYears(x.year||x.years);}
if(x.day||x.days){this.addDays(x.day||x.days);}
return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(value<min||value>max){throw new RangeError(value+" is not a valid value for "+name+".");}
return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;}
if(!x.second&&x.second!==0){x.second=-1;}
if(!x.minute&&x.minute!==0){x.minute=-1;}
if(!x.hour&&x.hour!==0){x.hour=-1;}
if(!x.day&&x.day!==0){x.day=-1;}
if(!x.month&&x.month!==0){x.month=-1;}
if(!x.year&&x.year!==0){x.year=-1;}
if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());}
if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());}
if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());}
if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());}
if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());}
if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());}
if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());}
if(x.timezone){this.setTimezone(x.timezone);}
if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);}
return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;}
var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}}
return w;};Date.prototype.isDST=function(){console.log('isDST');return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();};
Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i<dx.length;i++){$D[dx[i]]=$D[dx[i].substring(0,3)]=df(i);}
var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
return this.moveToMonth(n,this._orient);};};for(var j=0;j<mx.length;j++){$D[mx[j]]=$D[mx[j].substring(0,3)]=mf(j);}
var ef=function(j){return function(){if(j.substring(j.length-1)!="s"){j+="s";}
return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k<px.length;k++){de=px[k].toLowerCase();$D[de]=$D[de+"s"]=ef(px[k]);$N[de]=$N[de+"s"]=nf(de);}}());Date.prototype.toJSONString=function(){return this.toString("yyyy-MM-ddThh:mm:ssZ");};Date.prototype.toShortDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortDatePattern);};Date.prototype.toLongDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.longDatePattern);};Date.prototype.toShortTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortTimePattern);};Date.prototype.toLongTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.longTimePattern);};Date.prototype.getOrdinal=function(){switch(this.getDate()){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};
(function(){Date.Parsing={Exception:function(s){this.message="Parse error at '"+s.substring(0,10)+" ...'";}};var $P=Date.Parsing;var _=$P.Operators={rtoken:function(r){return function(s){var mx=s.match(r);if(mx){return([mx[0],s.substring(mx[0].length)]);}else{throw new $P.Exception(s);}};},token:function(s){return function(s){return _.rtoken(new RegExp("^\s*"+s+"\s*"))(s);};},stoken:function(s){return _.rtoken(new RegExp("^"+s));},until:function(p){return function(s){var qx=[],rx=null;while(s.length){try{rx=p.call(this,s);}catch(e){qx.push(rx[0]);s=rx[1];continue;}
break;}
return[qx,s];};},many:function(p){return function(s){var rx=[],r=null;while(s.length){try{r=p.call(this,s);}catch(e){return[rx,s];}
rx.push(r[0]);s=r[1];}
return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
throw new $P.Exception(s);};},ignore:function(p){return p?function(s){var r=null;r=p.call(this,s);return[null,r[1]];}:null;},product:function(){var px=arguments[0],qx=Array.prototype.slice.call(arguments,1),rx=[];for(var i=0;i<px.length;i++){rx.push(_.each(px[i],qx));}
return rx;},cache:function(rule){var cache={},r=null;return function(s){try{r=cache[s]=(cache[s]||rule.call(this,s));}catch(e){r=cache[s]=e;}
if(r instanceof $P.Exception){throw r;}else{return r;}};},any:function(){var px=arguments;return function(s){var r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){r=null;}
if(r){return r;}}
throw new $P.Exception(s);};},each:function(){var px=arguments;return function(s){var rx=[],r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
rx.push(r[0]);s=r[1];}
return[rx,s];};},all:function(){var px=arguments,_=_;return _.each(_.optional(px));},sequence:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;if(px.length==1){return px[0];}
return function(s){var r=null,q=null;var rx=[];for(var i=0;i<px.length;i++){try{r=px[i].call(this,s);}catch(e){break;}
rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
s=q[1];}
if(!r){throw new $P.Exception(s);}
if(q){throw new $P.Exception(q[1]);}
if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
return[rx,(r?r[1]:s)];};},between:function(d1,p,d2){d2=d2||d1;var _fn=_.each(_.ignore(d1),p,_.ignore(d2));return function(s){var rx=_fn.call(this,s);return[[rx[0][0],r[0][2]],rx[1]];};},list:function(p,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return(p instanceof Array?_.each(_.product(p.slice(0,-1),_.ignore(d)),p.slice(-1),_.ignore(c)):_.each(_.many(_.each(p,_.ignore(d))),px,_.ignore(c)));},set:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return function(s){var r=null,p=null,q=null,rx=null,best=[[],s],last=false;for(var i=0;i<px.length;i++){q=null;p=null;r=null;last=(px.length==1);try{r=px[i].call(this,s);}catch(e){continue;}
rx=[[r[0]],r[1]];if(r[1].length>0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
if(!last&&q[1].length===0){last=true;}
if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
if(rx[1].length<best[1].length){best=rx;}
if(best[1].length===0){break;}}
if(best[0].length===0){return best;}
if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
best[1]=q[1];}
return best;};},forward:function(gr,fname){return function(s){return gr[fname].call(this,s);};},replace:function(rule,repl){return function(s){var r=rule.call(this,s);return[repl,r[1]];};},process:function(rule,fn){return function(s){var r=rule.call(this,s);return[fn.call(this,r[0]),r[1]];};},min:function(min,rule){return function(s){var rx=rule.call(this,s);if(rx[0].length<min){throw new $P.Exception(s);}
return rx;};}};var _generator=function(op){return function(){var args=null,rx=[];if(arguments.length>1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
if(args){for(var i=0,px=args.shift();i<px.length;i++){args.unshift(px[i]);rx.push(op.apply(null,args));args.shift();return rx;}}else{return op.apply(null,arguments);}};};var gx="optional not ignore cache".split(/\s/);for(var i=0;i<gx.length;i++){_[gx[i]]=_generator(_[gx[i]]);}
var _vector=function(op){return function(){if(arguments[0]instanceof Array){return op.apply(null,arguments[0]);}else{return op.apply(null,arguments);}};};var vx="each any all".split(/\s/);for(var j=0;j<vx.length;j++){_[vx[j]]=_vector(_[vx[j]]);}}());(function(){var flattenAndCompact=function(ax){var rx=[];for(var i=0;i<ax.length;i++){if(ax[i]instanceof Array){rx=rx.concat(flattenAndCompact(ax[i]));}else{if(ax[i]){rx.push(ax[i]);}}}
return rx;};Date.Grammar={};Date.Translator={hour:function(s){return function(){this.hour=Number(s);};},minute:function(s){return function(){this.minute=Number(s);};},second:function(s){return function(){this.second=Number(s);};},meridian:function(s){return function(){this.meridian=s.slice(0,1).toLowerCase();};},timezone:function(s){return function(){var n=s.replace(/[^\d\+\-]/g,"");if(n.length){this.timezoneOffset=Number(n);}else{this.timezone=s.toLowerCase();}};},day:function(x){var s=x[0];return function(){this.day=Number(s.match(/\d+/)[0]);};},month:function(s){return function(){this.month=((s.length==3)?Date.getMonthNumberFromName(s):(Number(s)-1));};},year:function(s){return function(){var n=Number(s);this.year=((s.length>2)?n:(n+(((n+2000)<Date.CultureInfo.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];var now=new Date();this.year=now.getFullYear();this.month=now.getMonth();this.day=1;this.hour=0;this.minute=0;this.second=0;for(var i=0;i<x.length;i++){if(x[i]){x[i].call(this);}}
this.hour=(this.meridian=="p"&&this.hour<13)?this.hour+12:this.hour;if(this.day>Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
if(this.now){return new Date();}
var today=Date.today();var method=null;var expression=!!(this.days!=null||this.orient||this.operator);if(expression){var gap,mod,orient;orient=((this.orient=="past"||this.operator=="subtract")?-1:1);if(this.weekday){this.unit="day";gap=(Date.getDayNumberFromName(this.weekday)-today.getDay());mod=7;this.days=gap?((gap+(orient*mod))%mod):(orient*mod);}
if(this.month){this.unit="month";gap=(this.month-today.getMonth());mod=12;this.months=gap?((gap+(orient*mod))%mod):(orient*mod);this.month=null;}
if(!this.unit){this.unit="day";}
if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
if(this.unit=="week"){this.unit="day";this.value=this.value*7;}
this[this.unit+"s"]=this.value*orient;}
return today.add(this);}else{if(this.meridian&&this.hour){this.hour=(this.hour<13&&this.meridian=="p")?this.hour+12:this.hour;}
if(this.weekday&&!this.day){this.day=(today.addDays((Date.getDayNumberFromName(this.weekday)-today.getDay()))).getDate();}
if(this.month&&!this.day){this.day=1;}
return today.set(this);}}};var _=Date.Parsing.Operators,g=Date.Grammar,t=Date.Translator,_fn;g.datePartDelimiter=_.rtoken(/^([\s\-\.\,\/\x27]+)/);g.timePartDelimiter=_.stoken(":");g.whiteSpace=_.rtoken(/^\s*/);g.generalDelimiter=_.rtoken(/^(([\s\,]|at|on)+)/);var _C={};g.ctoken=function(keys){var fn=_C[keys];if(!fn){var c=Date.CultureInfo.regexPatterns;var kx=keys.split(/\s+/),px=[];for(var i=0;i<kx.length;i++){px.push(_.replace(_.rtoken(c[kx[i]]),kx[i]));}
fn=_C[keys]=_.any.apply(null,px);}
return fn;};g.ctoken2=function(key){return _.rtoken(Date.CultureInfo.regexPatterns[key]);};g.h=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2]|[1-9])/),t.hour));g.hh=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2])/),t.hour));g.H=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3]|[0-9])/),t.hour));g.HH=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3])/),t.hour));g.m=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.minute));g.mm=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.minute));g.s=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.second));g.ss=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.second));g.hms=_.cache(_.sequence([g.H,g.mm,g.ss],g.timePartDelimiter));g.t=_.cache(_.process(g.ctoken2("shortMeridian"),t.meridian));g.tt=_.cache(_.process(g.ctoken2("longMeridian"),t.meridian));g.z=_.cache(_.process(_.rtoken(/^(\+|\-)?\s*\d\d\d\d?/),t.timezone));g.zz=_.cache(_.process(_.rtoken(/^(\+|\-)\s*\d\d\d\d/),t.timezone));g.zzz=_.cache(_.process(g.ctoken2("timezone"),t.timezone));g.timeSuffix=_.each(_.ignore(g.whiteSpace),_.set([g.tt,g.zzz]));g.time=_.each(_.optional(_.ignore(_.stoken("T"))),g.hms,g.timeSuffix);g.d=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1]|\d)/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.dd=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1])/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.ddd=g.dddd=_.cache(_.process(g.ctoken("sun mon tue wed thu fri sat"),function(s){return function(){this.weekday=s;};}));g.M=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d|\d)/),t.month));g.MM=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d)/),t.month));g.MMM=g.MMMM=_.cache(_.process(g.ctoken("jan feb mar apr may jun jul aug sep oct nov dec"),t.month));g.y=_.cache(_.process(_.rtoken(/^(\d\d?)/),t.year));g.yy=_.cache(_.process(_.rtoken(/^(\d\d)/),t.year));g.yyy=_.cache(_.process(_.rtoken(/^(\d\d?\d?\d?)/),t.year));g.yyyy=_.cache(_.process(_.rtoken(/^(\d\d\d\d)/),t.year));_fn=function(){return _.each(_.any.apply(null,arguments),_.not(g.ctoken2("timeContext")));};g.day=_fn(g.d,g.dd);g.month=_fn(g.M,g.MMM);g.year=_fn(g.yyyy,g.yy);g.orientation=_.process(g.ctoken("past future"),function(s){return function(){this.orient=s;};});g.operator=_.process(g.ctoken("add subtract"),function(s){return function(){this.operator=s;};});g.rday=_.process(g.ctoken("yesterday tomorrow today now"),t.rday);g.unit=_.process(g.ctoken("minute hour day week month year"),function(s){return function(){this.unit=s;};});g.value=_.process(_.rtoken(/^\d\d?(st|nd|rd|th)?/),function(s){return function(){this.value=s.replace(/\D/g,"");};});g.expression=_.set([g.rday,g.operator,g.value,g.unit,g.orientation,g.ddd,g.MMM]);_fn=function(){return _.set(arguments,g.datePartDelimiter);};g.mdy=_fn(g.ddd,g.month,g.day,g.year);g.ymd=_fn(g.ddd,g.year,g.month,g.day);g.dmy=_fn(g.ddd,g.day,g.month,g.year);g.date=function(s){return((g[Date.CultureInfo.dateElementOrder]||g.mdy).call(this,s));};g.format=_.process(_.many(_.any(_.process(_.rtoken(/^(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?)/),function(fmt){if(g[fmt]){return g[fmt];}else{throw Date.Parsing.Exception(fmt);}}),_.process(_.rtoken(/^[^dMyhHmstz]+/),function(s){return _.ignore(_.stoken(s));}))),function(rules){return _.process(_.each.apply(null,rules),t.finishExact);});var _F={};var _get=function(f){return _F[f]=(_F[f]||g.format(f)[0]);};g.formats=function(fx){if(fx instanceof Array){var rx=[];for(var i=0;i<fx.length;i++){rx.push(_get(fx[i]));}
return _.any.apply(null,rx);}else{return _get(fx);}};g._formats=g.formats(["yyyy-MM-ddTHH:mm:ss","ddd, MMM dd, yyyy H:mm:ss tt","ddd MMM d yyyy HH:mm:ss zzz","d"]);g._start=_.process(_.set([g.date,g.time,g.expression],g.generalDelimiter,g.whiteSpace),t.finish);g.start=function(s){try{var r=g._formats.call({},s);if(r[1].length===0){return r;}}catch(e){}
return g._start.call({},s);};}());Date._parse=Date.parse;Date.parse=function(s){var r=null;if(!s){return null;}
try{r=Date.Grammar.start.call({},s);}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};Date.getParseFunction=function(fx){var fn=Date.Grammar.formats(fx);return function(s){var r=null;try{r=fn.call({},s);}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};};Date.parseExact=function(s,fx){return Date.getParseFunction(fx)(s);};


var OasAd = Class.create({

  url: 'http://oascentral.likeme.net/RealMedia/ads/',
  listpos : 'Top,Bottom,Middle,Left',
  query : '',
  target : '_top',

  initialize: function(sitepage) {
    this.sitepage = sitepage;
    if(this.sitepage) {
      this.version = this.getVersion();
      this.randomString = new String(Math.random()).substring (2, 11);

      if (this.version >= 11) {
        document.write('<script type="text/javascript" src="' + this.url + 'adstream_mjx.ads/' + this.sitepage + '/1' + this.randomString + '@' + this.listpos + '?' + this.query + '"><\/script>');
      }
    } else {
      this.drawAd = function() {};
    }
  },

  drawNormal: function(pos) {
    document.write('<a href="' + this.url + 'click_nx.ads/' + this.sitepage + '/1' + this.randomString + '@' + this.listpos + '!' + pos + '?' + this.query + '" target=' + this.target + '>');
    document.write('<img src="' + this.url + 'adstream_nx.ads/' + this.sitepage + '/1' + this.randomString + '@' + this.listpos + '!' + pos + '?' + this.query + '" border=0></a>');
  },

  drawAd: function(pos) {
    if (this.version >= 11) {
      OAS_RICH(pos);
    } else {
      this.drawNormal(pos);
    }
  },

  getVersion: function() {
    if((navigator.userAgent.indexOf('Mozilla/3') != -1) || (navigator.userAgent.indexOf('Mozilla/4.0 WebTV') != -1)) {
      return 10;
    } else {
      return 11;
    }
  }
});

Pivotal.ServerProxy = Class.create({
  initialize: function() {
  },

  sendCommand: function(command, callback) {
    var ajaxPayload = command.asAjaxPayload();
    var parameters = ajaxPayload.paramHash;
    if(!Object.isString(parameters)) {
      parameters = StringUtils.toParamString(parameters);
    }
    var method = ajaxPayload.method || "post";
    if(method.toLowerCase() != 'get') {
      BackButtonProtection.noCache();
    }
    var postBody = JSON.stringify(ajaxPayload.postBodyJson);

    var request =
      new Ajax.Request(ajaxPayload.url,
      {
        method: method,
        parameters: parameters,
        postBody: postBody,
        onComplete: function(response) {
          if (!(response && response.responseText && response.responseText.length > 0)) {
            try {
              this.currentAjaxRequest.responseIsFailure();
            } catch (e) {
              this.currentAjaxRequest.options.onFailure(response);
            }
          }

        }.bind(this),
        onSuccess: function(response) {
          var responseJson = JSON.parse(response.responseText);
          callback(true, responseJson);
        }.bind(this),
        onFailure: function(response) {
          callback(false, {status: "FAIL"});
        }.bind(this)
      });


    this.currentAjaxRequest = request;
  }
});


Pivotal.CommandQueue = Class.create({
  initialize: function(server) {
    this.commandWaitingForResponse = null;
    this.rollingBack = false;
    this.server = server;
    this.clear();
  },

  processHead: function() {
    if (this.contents.length == 0) {
      return;
    }
    var commandAtHead = this.contents[0];
//        console.debug("processing:" + commandAtHead.asAjaxPayload().url + ", contents length:" + this.contents.length)
    this.commandWaitingForResponse = commandAtHead;
    this.sendToServer(commandAtHead);
  },

  sendToServer: function(command) {
    if (command.beforeSendingToServer) {
      command.beforeSendingToServer();
    }
    this.server.sendCommand(command, this.reactToAjaxResponse.bind(this));
  },

  reactToAjaxResponse: function(success, responseJson) {
    var latestCommand = this.contents[0];
    if (latestCommand.afterServerResponse) {
      latestCommand.afterServerResponse();
    }
    if (success) {
      if (latestCommand.responseFromServer) {
        latestCommand.responseFromServer(responseJson);
      }
      this.dequeue();
    } else {
      this.rollback();
    }
  },

  enqueue: function(command) {
    //       console.debug("enqueuing: " + command.asAjaxPayload().url + ", contents length:" + this.contents.length)
    this.failIfRollingBack("enqueue");

    this.contents.push(command);
    if (command.executeLocally) {
      command.executeLocally();
    }

    if (!this.commandWaitingForResponse) {
      this.processHead(command);
    }
  },

  dequeue: function() {
    this.failIfRollingBack("dequeue");

    if (this.contents.length == 0) {
      return Pivotal.CommandQueue.END_OF_QUEUE;
    }

    var shifted = this.contents.shift();
    this.commandWaitingForResponse = null;
//        console.debug("dequeuing: " + shifted.asAjaxPayload().url + ", contents length:" + this.contents.length)
    this.processHead();
    return shifted;
  },

  rollback: function() {
    this.failIfRollingBack("another rollback");

    this.rollingBack = true;
    try {
      while (this.contents.length > 0) {
        var cmd = this.contents.pop();
        cmd.undo();
      }
    } catch (e) {
    } finally {
      this.clear();
      this.rollingBack = false;
    }
  },

  clear: function() {
    this.contents = [];
    this.commandWaitingForResponse = null;
  },

  failIfRollingBack: function(operation) {
    if (this.rollingBack) {
      var msg = "queue is rolling back, " + operation + " not allowed.";
      throw msg;
    }
  }

});
Pivotal.CommandQueue.END_OF_QUEUE = "End of Queue";

var DOMUTILS_ELEMENT_NODE = 1;
var DOMUTILS_ATTRIBUTE_NODE = 2;
var DOMUTILS_TEXT_NODE = 3;
var DomUtils = {};

DomUtils = {
  replaceElementInDocument: function(newElem) {
    if (!newElem.id) {
      throw("DomUtils.replaceElementInDocument: The specified element is missing an id.");
    }
    var origElem = $(newElem.id);
    var parent = origElem.parentNode;
    parent.replaceChild(newElem, origElem);
  },

  extractText: function (elem) {
    for (var i = 0; i < elem.childNodes.length; i++) {
      if (elem.childNodes[i].nodeType == DOMUTILS_TEXT_NODE) {
        return elem.childNodes[i].nodeValue;
      }
    }
    return "";
  },

  _collectChildNodesIgnoringText: function (elem) {
    var children = [];
    for (var i = 0; i < elem.childNodes.length; i++) {
      if (elem.childNodes[i].nodeType != DOMUTILS_TEXT_NODE) {
        children.push(elem.childNodes[i]);
      }
    }
    return children;
  },

  collectAndSortAttributes: function (elem) {
    var attributes = [];
    for (var i = 0; i < elem.attributes.length; i++) {
      attributes.push(elem.attributes.item(i));
    }
    attributes.sort(DomUtils._compareAttributeByName);
    return attributes;
  },

  _compareAttributeByName: function (attribA, attribB) {
    if (attribA.nodeName < attribB.nodeName) {
      return -1;
    }
    else if (attribA.nodeName > attribB.nodeName) {
      return 1;
    } else {
      return 0;
    }
  },

  createElementFromString: function(html) {
    var wrapperElem = document.createElement("div");
    wrapperElem.innerHTML = html;
    return wrapperElem.childNodes[0];
  },

  areElementsEqual: function(elemA, elemB) {
    if (DomUtils._areEitherNull(elemA, elemB)) {
      return false;
    }
    if (!(elemA.tagName == elemB.tagName)) {
      return false;
    }
    if (!DomUtils._areTrimmedTextsEqual(elemA, elemB)) {
      return false;
    }
    if (!DomUtils._areAttributesOfElementsEqual(elemA, elemB)) {
      return false;
    }
    return DomUtils._areArraysEqual(DomUtils._collectChildNodesIgnoringText(elemA), DomUtils._collectChildNodesIgnoringText(elemB));
  },

  _areEitherNull: function(a, b) {
    return a == null || b == null;
  },

  _areAttributesOfElementsEqual: function (elemA, elemB) {
    var attributesForA = DomUtils.collectAndSortAttributes(elemA);
    var attributesForB = DomUtils.collectAndSortAttributes(elemB);

    return DomUtils._areArraysEqual(attributesForA, attributesForB);
  },

  _areTrimmedTextsEqual: function (elemA, elemB) {
    var textForA = DomUtils.extractText(elemA);
    var textForB = DomUtils.extractText(elemB);
    return StringUtils.trim(textForA) == StringUtils.trim(textForB);
  },

  _areAttributesEqual: function (attribA, attribB) {
    if (DomUtils._areEitherNull(attribA, attribB)) {
      return false;
    }

    if (attribA.nodeName != attribB.nodeName) {
      return false;
    }
    return attribA.nodeValue == attribB.nodeValue;
  },

  _areArraysEqual:function(arrayA, arrayB) {
    if (DomUtils._areEitherNull(arrayA, arrayB)) {
      return false;
    }

    if (arrayA.length != arrayB.length) {
      return false;
    }

    for (var i = 0; i < arrayA.length; i++) {
      var childA = arrayA[i];
      var childB = arrayB[i];
      if (childA.nodeType != childB.nodeType) {
        return false;
      }
      if (childA.nodeType == DOMUTILS_ATTRIBUTE_NODE) {
        if (!DomUtils._areAttributesEqual(childA, childB)) {
          return false;
        }
      }
      else if (childA.nodeType == DOMUTILS_ELEMENT_NODE) {
        if (!DomUtils.areElementsEqual(childA, childB)) {
          return false;
        }
      }
      else if (childA.nodeType == DOMUTILS_TEXT_NODE) {
        //ignore text nodes as they are handled in the AreElementsEqual function
      }
      else {
        throw new Error("Unexpected node type: " + childA.nodeType);
      }
    }
    return true;
  },

  findMatchingChildren: function(element, selector) {
    var matches = $A([]);

    var childCount = element.childNodes.length;
    for (var i=0; i<childCount; i++) {
      var child = element.childNodes[i];
      if (selector(child)) {
        matches.push(child);
      } else {
        childMatches = DomUtils.findMatchingChildren(child, selector);
        matches.push(childMatches);
      }
    }

    return matches.flatten();
  },

  ELEMENT_NODE_TYPE: 1
}

function $e(tagName, optionsOrText, childrenOrText) {
  var element = document.createElement(tagName);
  if (optionsOrText) {
    if (optionsOrText.indexOf) {
      element.innerHTML = optionsOrText;
    } else if (optionsOrText.nodeType) {
      element.appendChild(optionsOrText);
    }
    else {
      for (var key in optionsOrText) {
        if(key == 'style') {
          for (var styleKey in optionsOrText.style) {
            element.style[styleKey] = optionsOrText.style[styleKey];
          }
        }
        else {
          element[key] = optionsOrText[key];
        }
      }
    }
  }

  if (childrenOrText) {
    if (childrenOrText instanceof Array) {
      $A(childrenOrText).each(function(child) {
        if (child.nodeType) {
          element.appendChild(child);
        }
        else {
          element.appendChild(textNode(child));
        }
      })
    } else {
      element.innerHTML = childrenOrText;
    }
  }

  if( Element && Element.extend ) return $(element);
  return element;
}

function hr(options, children) {
  return $e("hr", options, children)
}

function ul(options, children) {
  return $e("ul", options, children)
}

function li(options, children) {
  return $e("li", options, children)
}

function table(options, children) {
  var myTable = $e("table", options)
  myTable.appendChild(tBody({}, children))
  return myTable
}

function tBody(options, children) {
  return $e("tbody", options, children)
}

function tr(options, children) {
  return $e("tr", options, children)
}

function td(options, children) {
  return $e("td", options, children)
}

function span(options, children) {
  return $e("span", options, children)
}

function div(options, children) {
  return $e("div", options, children)
}

function h1(options, children) {
  return $e("h1", options, children);
}

function h2(options, children) {
  return $e("h2", options, children);
}

function h3(options, children) {
  return $e("h3", options, children);
}

function h4(options, children) {
  return $e("h4", options, children);
}

function h5(options, children) {
  return $e("h5", options, children);
}

function h6(options, children) {
  return $e("h6", options, children);
}

function p(options, children) {
  return $e("p", options, children);
}

function a(options, children) {
  return $e("a", options, children)
}

function input(options, children) {
  return $e("input", options, children)
}

function textarea(options, children) {
  return $e("textarea", options, children)
}

function img(options, children) {
  if(!(options && options.alt)) {
    options.alt = '';
  }

  var rolloverImageSrc = null
  if (options && options.rollover) {
    rolloverImageSrc = options.rollover
    delete options["rollover"]
  }

  var imgElement = $e("img", options, children)

  if (rolloverImageSrc) {
    var originalSrc = imgElement.src
    imgElement.onmouseover = function(){imgElement.src = rolloverImageSrc}
    imgElement.onmouseout = function(){imgElement.src = originalSrc}
  }

  return imgElement
}

function button(options, children) {
  return $e("button", options, children);
}

function label(options, children) {
  return $e("label", options, children);
}

function form(options, children) {
  if(options && options.enctype) {
    options.encoding = options.enctype;
  }
  return $e("form", options, children);
}

function em(options, children) {
  return $e("em", options, children);
}

function br(options) {
  return $e("br", options);
}

function textNode(text) {
  return document.createTextNode(text);
}

function select(options, children) {
  var selectElement = $e('select', options);
  if(children) {
    children.each(function(child, index) {
      selectElement.options[index] = child;
    });
  }
  if(options && options.selectedIndex) {
    selectElement.selectedIndex = options.selectedIndex;
  }
  return selectElement;
}

function option(options) {
  return $e('option', options);
}

function abutton(children) {
  return a({className: 'navbutton'}, [span({}, [children] )] );
}

var StringUtils = {};

StringUtils = {
    trim: function(text) {
        var stripped = text.replace(/^\s+/g, "");
        return stripped.replace(/\s+$/g, "");
    },

    formatNumberWithCommas: function(number) {
        var numString = number.toString();
        var re = /(-?\d+)(\d{3})/;
        while (re.test(numString)) {
            numString = numString.replace(re, "$1,$2")
        }
        return numString;
    },

    replace: function(text, textToReplace, replacement) {
        if (text) {
            return text.replace(textToReplace, replacement);
        }
        else {
            return null;
        }
    },

    _addToParamsArray: function(prefix, array, hash) {
      $H(hash).keys().each(function(key) {
        var keyToUse = prefix == "" ? key : prefix + "[" + key + "]";
        if (Object.isArray(hash[key])) {
          var arrayKey = keyToUse + '[]';
          hash[key].each(function(val){ array.push([arrayKey,val]); });
        } else if(Object.isString(hash[key]) || Object.isNumber(hash[key]) || hash[key] === true || !hash[key]) {
          array.push([keyToUse, hash[key]]);
        } else {
          this._addToParamsArray(keyToUse, array, hash[key]);
        }
      }.bind(this));
    },

    toParamString: function(hash) {
      var stringPairs = [];
      this._addToParamsArray("", stringPairs, hash);
      stringPairs.each(function(pair) {
        if(pair[1]) {
          pair[1] = encodeURIComponent(pair[1]);
        }
      });
      var pairsWithEquals = stringPairs.collect(function(pair) { return pair.join("="); });
      return pairsWithEquals.join("&");
    },

    _copyArray: function(arr) {
        var copy = []
        $A(arr).each(function (element) {
            copy.push(element)
        })

        return copy
    }
}

Socialitis.RatingSelector = Class.create();
Socialitis.RatingSelector.prototype = {
  initialize: function(name, options) {
    this.name = name;
    this.options = options;
    this.builder = new DomBuilder({binding: this});
    if (this.options.rating_for_user) {
      this.setRating(this.options.rating_for_user);
    } else {
      this.setRating(this.options.default_rating);
    }
  },

  setRating: function(rating) {
    this.rating = rating;
    if (this.options.hidden_field_id != null) {
      $(this.options.hidden_field_id).value = this.rating;
    }
  },

  getRating: function() {
    return this.rating;
  },

  getStars: function() {
    return this.stars;
  },

  render: function() {
    return this.builder.div(function() {
      this.stars = [
          new Socialitis.RatingSelectorStar(this, 0),
          new Socialitis.RatingSelectorStar(this, 1),
          new Socialitis.RatingSelectorStar(this, 2),
          new Socialitis.RatingSelectorStar(this, 3),
          new Socialitis.RatingSelectorStar(this, 4)
          ]
      for (var i = 0; i < this.stars.length; i++) {
        this.stars[i].render(this.builder);
      }
    });
  },

  setTentativeRatingFunction: function(position) {
    return function() {
      var star;
      var i;
      for (i = 0; i <= position; i++) {
        star = this.stars[i];
        star.turnOn();
      }
      for (i = position + 1; i < this.stars.length; i++) {
        star = this.stars[i];
        star.turnOff();
      }
    };
  },

  clearTentativeRating: function() {
    for (var i = 0; i < this.stars.length; i++) {
      var star = this.stars[i];
      star.turnRated();
    }
  },

  sendRatingFunction: function(rating) {
    return function() {
      if (this.options.ajax_submit_url != null) {
        LikeMe.Command.Rate.send(this.options.ajax_submit_url, rating);
      }
      this.setRating(rating);
    }
  }
};

Socialitis.RatingSelectorStar = Class.create();
Socialitis.RatingSelectorStar.prototype = {
  initialize: function(selector, position) {
    this.selector = selector;
    this.position = position;
  },

  render: function(builder) {
    this.star = builder.img();
    this.turnRated();
    if (this.selector.options.editable) {
      Event.observe(this.star, 'mouseover', this.selector.setTentativeRatingFunction(this.position).bindAsEventListener(this.selector));
      Event.observe(this.star, 'mouseout', this.selector.clearTentativeRating.bindAsEventListener(this.selector));
      Event.observe(this.star, 'click', this.selector.sendRatingFunction(this.correspondingRating()).bindAsEventListener(this.selector));
    }  
    return this.star;
  },

  selectRatedImage: function() {
    var rating = this.selector.getRating();

    if (this.correspondingRating() <= rating) {
      if (this.selector.options.rating_for_user) {
        return this.selector.options.user_rated_image;
      } else {
        return this.selector.options.default_rated_image;
      }
    } else if (this.correspondingRating() == Math.ceil(rating)) {
      return this.selector.options.half_rated_image;
    } else {
      return this.selector.options.off_image;
    }
  },

  turnOn: function() {
    this.star.src = '/images/' + this.selector.options.on_image;
  },

  turnOff: function() {
    this.star.src = '/images/' + this.selector.options.off_image;
  },

  turnRated: function() {
    this.star.src = '/images/' + this.selectRatedImage();
  },

  correspondingRating: function() {
    return this.position + 1;
  }

}


var DomBuilder = function(params) {
  var that = this;
  var objectParams = {
    parent: null,
    binding: this
  };
  objectParams = DomBuilder.extend(objectParams, params);
  this.parent = objectParams.parent;

  var currentElement;
  if(this.parent) currentElement = this.parent;
  var elements = [this.parent];
  this.binding = objectParams.binding;
  this.createElement = DomBuilder.createElement;
  this.createTextNode = DomBuilder.createTextNode;

  this.tag = function(tagName) {
    var arity = arguments.length;
    var element = null;
    if(arity == 1) {
      element = tagWithOneArgument(tagName);
    }
    else if(arity == 2) {
      element = tagWithTwoArguments(tagName, arguments[1]);
    }
    else if(arity == 3) {
      element = tagWithThreeArguments(tagName, arguments[1], arguments[2]);
    }
    else {
      throw "Invalid number of arguments";
    }
    return element;
  }

  this.appendText = function(text) {
    appendTextToElement(currentElement, text);
  }

  this.tagWithArrayArgs = function(tag, args) {
    if(!args) return this.tag(tag);

    var newArguments = [tag];
    for(var i=0; i < args.length; i++) {
      newArguments.push(args[i]);
    }
    return this.tag.apply(this, newArguments);
  }

  this.appendXml = function(xml) {
    currentElement.innerHTML += xml;
  }

  function tagWithOneArgument(tagName) {
    var element = that.createElement(tagName);
    appendChild(element);
    return element;
  };

  function tagWithTwoArguments(tagName, secondArgument) {
    var element = null;
    if(typeof secondArgument == 'function') {
      element = renderAttributesAndFunction(tagName, null, secondArgument);
    }
    else if(typeof secondArgument == 'string') {
      element = renderAttributesAndText(tagName, null, secondArgument);
    }
    else {
      element = that.createElement(tagName, secondArgument);
      appendChild(element);
    }
    return element;
  };

  function tagWithThreeArguments(tagName, attributes, thirdArgument) {
    var element = null;
    if(typeof thirdArgument == 'function') {
      element = renderAttributesAndFunction(tagName, attributes, thirdArgument);
    }
    else {
      element = renderAttributesAndText(tagName, attributes, thirdArgument);
    }
    return element;
  };

  function renderAttributesAndFunction(tagName, attributes, theFunction) {
    var element = that.createElement(tagName, attributes);
    pushElement(element);
    theFunction.call(that.binding, that);
    popElement();
    return element;
  }

  function renderAttributesAndText(tagName, attributes, text) {
    var element = that.createElement(tagName, attributes);
    pushElement(element);
    that.appendText((text) ? text.toString() : "");
    popElement();
    return element;
  }

  function appendTextToElement(element, text) {
    var node = that.createTextNode(text);
    element.appendChild(node);
  }

  function pushElement(element) {
    appendChild(element);
    elements.push(element);
    currentElement = element;
  }

  function appendChild(element) {
    if(currentElement) {
      try {
        currentElement.appendChild(element);
      }
      catch(e) {
        throw "Current element does not support appendChild";
      }
    }
  }

  function popElement(element) {
    element = elements.pop();
    var length = elements.length;
    if(length == 0) {
      currentElement = null;
    }
    else {
      currentElement = elements[elements.length - 1];
    }
    return element;
  }
}
DomBuilder.initialize = function(document) {
  var that = this;
  this.createElement = function(tagName, attributes) {
    var element = document.createElement(tagName);
    for(var key in attributes) {
      var value = attributes[key];
      try {
        setAttributeOnElement(element, key, value);
      }
      catch(e) {
        throw(
          "Error while trying to create an element with the attribute " +
          key + " and value " + value + ": " + e
        );
      }
    };
    return element;
  };

  this.createTextNode = function(text) {
    return document.createTextNode(text);
  };

  this.registerTag = function(tagName) {
    DomBuilder.prototype[tagName] = function() {
      return this.tagWithArrayArgs(tagName, arguments);
    };
  }

  this.extend = function(destination, source) {
    for(var property in source) {
      destination[property] = source[property];
    }
    return destination;
  }

  function setAttributeOnElement(element, attributeName, attributeValue) {
    if(attributeName == "class") {
      setCssClassOnElement(element, attributeValue);
    }
    else if(attributeName == "style") {
      setStylesOnElement(element, attributeValue);
    }
    else {
      element.setAttribute(attributeName, attributeValue);
    }
  }

  function setCssClassOnElement(element, className) {
    element.className = className;
  }

  function setStylesOnElement(element, stylesString) {
    var styles = stylesString.split(";");
    for(var i = 0; i < styles.length; i++) {
      var styleString = strip(styles[i]);
      if(styleString.length == 0) continue;

      var keyValue = styleString.split(':', 2);
      var styleKey = strip(keyValue[0]);
      var styleValue = strip(keyValue[1]);
      if (styleKey.length > 0 && styleValue.length > 0) {
        if(styleKey.toLowerCase() == "float") {
          element.style["cssFloat"] = element.style["styleFloat"] = styleValue;
        }
        else {
          element.style[styleKey] = styleValue;
        }
      }
    }
  }
  
  function strip(str) {
    return str.replace(/^\s+/, '').replace(/\s+$/, '');
  }

  var supportedTags = [
    'a', 'acronym', 'address', 'area', 'b', 'base', 'bdo', 'big', 'blockquote', 'body',
    'br', 'button', 'caption', 'cite', 'code', 'dd', 'del', 'div', 'dl', 'dt', 'em',
    'fieldset', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i',
    'img', 'iframe', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'map',
    'meta', 'noframes', 'noscript', 'ol', 'optgroup', 'option', 'p', 'param', 'pre',
    'samp', 'script', 'select', 'small', 'span', 'strong', 'style', 'sub', 'sup',
    'table', 'tbody', 'td', 'textarea', 'th', 'thead', 'title', 'tr', 'tt', 'ul', 'var'
  ];
  for(var i=0; i < supportedTags.length; i++) {
    var tag = supportedTags[i];
    this.registerTag(tag);
  }  
}
DomBuilder.initialize.call(DomBuilder, document);

/**
 * This psuedo class can be used to apply designerization to anchor and input
 * tags to provide a consistent button style using CSS.  Input tags are filtered
 * to types which match button, submit, and reset.
 * 
 * You can apply designerization to any element (that matches the criteria
 * outlined above) by calling the applyTo method statically and passing the
 * element.
 */
var AbleButton = {
  version: '0.8',
  options: {
    className: 'able-button'
  },

  /**
   * Constructor: Applies the designerization effect to all A and
   * INPUT{submit, reset, button} elements with a classname of 'button' by
   * default.
   */
  initialize: function(options) {
    Object.extend(this.options, options);

    // apply the tweaks to all the various elements when first possible
    document.observe("dom:loaded", function() {
      $$('.' + this.options.className).each(function(element) {
        this.applyTo(element);
      }.bind(this));
    }.bind(this));
  },
  
  resetDoubleClick: function(element) {
    element.submitted = false;
  },

  /**
   * Applies the designerization effect to any element, regardless of classname.
   * This can be used to apply the effect to any A, INPUT{submit, reset, button}
   * or BUTTON element.  If it's a submit or reset button, the corralating form
   * event will be attached to them automatically.
   * 
   * Returns the modified element, or an unmodified element if designerization
   * can't be applied because the element doesn't match the required criteria or
   * already has the designerization applied.
   * 
   * You can optionally pass in a className (when creating from js, you should
   * do this), and skipWrapping (which will not wrap it in a div -- clear
   * doesn't work if you don't wrap it)
   */
  applyTo: function(element, className, options) {
    options = options || {};
    if (element._able_button_applied || element.id == 'applied' || (element.tagName != 'A' && element.tagName != 'INPUT')) return element;
    // converts any input elements into an anchor, and add any event
    // observations -- restricting designerization to input and anchor tags
    var type = (element.type) ? element.type.toLowerCase() : 'button';
    if (element.tagName == "INPUT" && (type == "submit" || type == "reset" || type == "button")) {
      var anchor = $(a({
        href: '#',
        className: element.className,
        onclick: function() { return false }
        })).update(element.value);
      if(Element.readAttribute(element, 'id')) {
        anchor.id = element.id;
      }
      // adds event handlers for for submit and reset buttons
      if (type == 'submit' || type == 'reset') {
        var form = element.form;
        if (form && type == 'submit') {
          var hidden = new Element('i', {style: 'display:block;position:absolute;overflow:hidden' });
          hidden.addClassName('able-button-hidden');
          hidden.appendChild(new Element('input', {type: 'submit', name: element.name, value: element.value}));
          anchor.appendChild(hidden);
        }

        var elementOnclick = null;
        if (form) {
          elementOnclick = element.onclick;
          anchor.observe('click', function(event) {
            if (type == 'submit' && (options.allowDoubleClick || !anchor.submitted) ) {
              var originalReturn;
              if (elementOnclick) {
                originalReturn = elementOnclick();
              }

              if (originalReturn != false) {
                if (form.onsubmit) {
                  if (form.onsubmit() != false) {
                    form.submit();
                    anchor.submitted = true;
                  } else if (options.remoteForm) {
                    anchor.submitted = true;
                  }
                } else {
                  form.submit();
                  anchor.submitted = true;
                }
              }
            } else if (type == 'reset')  {
              form.reset();
            }
          });
        }
      } else if (element.onclick) {
        elementOnclick = element.onclick;
        anchor.observe('click', function(event) {
          elementOnclick();
        });
      }
      // replaces the input element with the anchor we've created
      if (element.parentNode) element.parentNode.replaceChild(anchor, element);
      element = anchor;
    }
    // builds the nodes that are needed for styling, and clones the existing
    // contents into them
    var container = new Element('span');
    container.appendChild(new Element('i'));
    container.appendChild(new Element('span'));
    var contentsContainer = new Element('div');
    container.appendChild(contentsContainer);
    while (element.firstChild) {
      contentsContainer.appendChild(element.firstChild);
    }
    element.appendChild(container);
    element.insertBefore(new Element('i'), container);
    element._able_button_applied = true;
    if (className) Element.addClassName(element, className);

    return element;
  }
}


var AbleCropper = Class.create({
  version: '0.8',
  croppers: {},
  options: {
    className: 'able-cropper',
    maxScaleFactor: 7,
    acceptsDroppingOf: 'croppable', // classname or false
    autoClickable: 'croppable' // classname or false
  }, 

  initialize: function(hookElement, options) {
    this.hookElement = hookElement;
		this.fitEntireImageCheckbox = Element.down(this.hookElement, '.fit-entire-image');
    this.fitEntireImageCheckbox.disabled = 'disabled';

    this.active = true;
    Object.extend(this.options, options);
    this._setup();
  },

  _setup: function() {
    // makes all elements with the "autoClickable" classname clickable
    if (this.options.autoClickable) {
      Element.select(this.hookElement, '.' + this.options.autoClickable).each(function(element) {
        new Event.observe(element, 'click', function() {  
          this._loadFromAutoElement(element);
        }.bind(this));
      }.bind(this));
    }

    // sets up each of the croppers that want it
    this.applyTo(Element.select(this.hookElement, 'div.' + this.options.className).first());
  },

  applyTo: function(element) {
    this.cropperElement = element;
    this.slider = this.cropperElement.down('.slider');
    this.sliderHandle = this.cropperElement.down('.slider div');
    this.cropper = this.cropperElement.down('.cropper');
    this.cropperImage = this.cropperElement.down('.cropper img');

    // gets all the dimensions for the elements we have so far
    this.cropperDimensions = this.cropper.getDimensions();
    this.sliderDimensions = this.slider.getDimensions();
    this.sliderHandleDimensions = this.sliderHandle.getDimensions();

    // sets some various styles
    this.cropper.setStyle({overflow: 'hidden', position: 'relative', cursor: 'move'});
    this.cropperImage.setStyle({visibility: 'hidden'});
    this.sliderHandle.setStyle({visibility: 'hidden'});

    // watches for when an image is loaded, and start the cropping for it
    this._setupOnLoadObserver();
  },

  _setupOnLoadObserver: function () {
    this.cropperImage.observe('load', function() {
      this._initCropping();
    }.bind(this));
  },
  
  load: function(image, top, left, width, height, showEntireImage, saveUrl) {
    if(this.isActive())
    {
      // gets the cropper and sets properties from params passed in
      this.saveUrl = saveUrl;
      this.image = {image: image, width: width, height: height, top: top, left: left, showEntireImage: showEntireImage.toString() == "1"};

      // loads the image desired into the cropper for cropping
      this.reset();
      this.cropperElement.addClassName('active');
      this.cropperImage.writeAttribute({src: image});
    }
  },

  reset: function() {
    if(this.isActive())
    {
			this._destroyDraggableImage();
      this._destroyDraggableSlider();
      this.fitEntireImageCheckbox.disabled = 'disabled';
      this.cropperElement.removeClassName('active');
      this.cropperElement.removeClassName('loaded');
      this.cropperImage.setStyle({top: '0px', left: '0px', width: null, height: null, visibility: 'hidden'});
      this.sliderHandle.setStyle({visibility: 'hidden'});
    }
  },

  ajaxParameters: function() {
    return {
      top: Math.round(this.image.top * this.imageRatio),
      left: Math.round(this.image.left * this.imageRatio),
      width: Math.round(this.image.width * this.imageRatio),
      height: Math.round(this.image.height * this.imageRatio),
      showEntireImage: this.image.showEntireImage
    };
  },

  crop: function() {
    if(this.isActive())
    {
      new Ajax.Request(this.saveUrl, {
        method: 'put',
        asynchronous: true,
        evalScripts: true,
        parameters: this.ajaxParameters()
      });
      this.deactivate();
    }
  },

  isActive: function() {
    return this.active;
  },

  deactivate: function() {
    this.active = false;
    this.cropperImage.setStyle({opacity: 0.5});
    this.makeNonCroppable();
  },

  activate: function() {
    this.active = true;
    this.cropperImage.setStyle({opacity: 1});
    this.makeCroppable();
  },
  
  isCroppable: function() {
    return !this.fitEntireImageCheckbox.checked;
  },

  setCroppable: function() {
    this.fitEntireImageCheckbox.checked = this.image.showEntireImage;
  },
  
  makeNonCroppable: function() {
    this.fitEntireImageCheckbox.disabled = '';
    this.cropperImage.setStyle({visibility: 'visible'});
    this.cropperElement.removeClassName('loaded');
    this.setCroppable();
    this._destroyDraggableImage();
    this._destroyDraggableSlider();
  },
  
  makeCroppable: function() {
    this.fitEntireImageCheckbox.disabled = '';
    this.cropperImage.setStyle({visibility: 'visible'});
    this.cropperElement.addClassName('loaded');
    this.setCroppable();
    this._scaleImage();
    this._setupDraggableImage();
    this._setupDraggableSlider();
  },
  
  showEntireImage: function() {
    this.makeNonCroppable();
    this.scaleToEntireImage();
  },
  
  scaleToEntireImage: function() {
    var aspectRatio = this.imageDimensions.width / this.imageDimensions.height;
    var top = 0, left = 0, width = 0, height = 0;
    if (aspectRatio > 1) { // landscape
      width = this.cropperDimensions.width;
      height = Math.round(this.cropperDimensions.width / aspectRatio);
      top = Math.round((width - height) / 2);
    } else {
      width = Math.round(this.cropperDimensions.height * aspectRatio);
      height = this.cropperDimensions.height;
      left = Math.round((height - width) / 2);
    }
    this.cropperImage.setStyle({top: top + 'px', left: left + 'px', width: width + 'px', height: height + 'px', position: 'relative'});
  },
  
  setEntireImage: function(entire_image) {
    if(entire_image) {
      this.image.showEntireImage = true;
      this.showEntireImage();
    } else {
      this.image.showEntireImage = false;
      this.makeCroppable();
    }
  },
  
  _loadFromAutoElement: function(element) { 
    var args = $w(element.readAttribute('longdesc'));
    var saveUrl = $w(element.readAttribute('rel'));
    if (!args || !saveUrl) {
      throw("Unable to handle draggable element because it doesn't contain the required attributes (longdesc and rel)");
    }
    
    if (args.length < 5) {
      throw("Wrong number of parameters.");
    }
    this.load(args[0], parseInt(args[1]), parseInt(args[2]), parseInt(args[3]), parseInt(args[4]), saveUrl);
  },
  
  _updateImage: function(top, left) {
    var normalize = function(coord) {
      return Math.round(Math.abs(coord) * (this.imageDimensions.height / this.currentImageDimensions.height));
    }.bind(this);
    Object.extend(this.image, {
      top: normalize(top),
      left: normalize(left)
    });
  },

  _initCropping: function() {
    this.imageDimensions = this.cropperImage.getDimensions();
    // used for images smaller than the cropper, to get the correct dimensions
    this.imageRatio = 1;
    // make sure any smaller images get scaled up
    if (this.imageDimensions.width < this.cropperDimensions.width || this.imageDimensions.height < this.cropperDimensions.height) {
      var imageAspect = this.imageDimensions.width / this.imageDimensions.height;
      var cropperAspect = this.cropperDimensions.width / this.cropperDimensions.height;
      var height = null;
      var width = null;

      if (cropperAspect >= 1 && imageAspect > 1) { // landscape
        height = this.cropperDimensions.height;
        width = height * imageAspect;
        this.imageRatio = this.imageDimensions.height / this.cropperDimensions.height;
      } else {
        width = this.cropperDimensions.width;
        height = width / imageAspect;
        this.imageRatio = this.imageDimensions.width / this.cropperDimensions.width;
      }
      this.image.width = Math.round(this.image.width / this.imageRatio);
      this.image.height = Math.round(this.image.height / this.imageRatio);
      this.image.top = Math.round(this.image.top / this.imageRatio);
      this.image.left = Math.round(this.image.left / this.imageRatio);
      this.imageDimensions = {width: width, height: height};
      this.cropperImage.setStyle({width: width + 'px', height: height + 'px'});
    }
    
    this.currentImageDimensions = this.imageDimensions;


    if (this.image.showEntireImage) {
      this.showEntireImage();
    } else {
      this._adjustImagePositionToScale();
      this.makeCroppable();
    }
  },
  
  _adjustImagePositionToScale: function () { 
    var top = this.image.top;
    var left = this.image.left;
    // sizes the image
    this._scaleImage();
    //denormalize top and left before setting.
    this.image.top = Math.round(parseInt(top) / (this.imageDimensions.height / this.currentImageDimensions.height));
    this.image.left = Math.round(parseInt(left) / (this.imageDimensions.height / this.currentImageDimensions.height));
    if (this.image.top >= 0 && this.image.left >= 0) this.cropperImage.setStyle({top: -this.image.top + 'px', left: -this.image.left + 'px'});
  },
  
  _scaleImage: function() {
    this.imageAspect = this.imageDimensions.width / this.imageDimensions.height;
    var cropperAspect = this.cropperDimensions.width / this.cropperDimensions.height;

    // scales the image based on aspect ratio
    var newImageHeight;
    var newImageWidth;

    if (cropperAspect >= 1 && this.imageAspect > 1) { // landscape
      if(this.image.height == 0) {
        this.image.height = this.imageDimensions.height;
        this.image.width = this.image.height * cropperAspect;
      }
      newImageHeight = Math.floor(this.imageDimensions.height / this.image.height * this.cropperDimensions.height);
      newImageWidth = Math.floor(newImageHeight * this.imageAspect);
    } else {
      if(this.image.width == 0) {
        this.image.width = this.imageDimensions.width;
        this.image.height = this.image.width * cropperAspect;
      }
      newImageWidth = Math.floor(this.imageDimensions.width / this.image.width * this.cropperDimensions.width);
      newImageHeight = Math.floor(newImageWidth / this.imageAspect);
    }
    
    var centerX = Math.abs(parseInt(this.cropperImage.style.left)) + (this.cropperDimensions.width / 2);
    var centerY = Math.abs(parseInt(this.cropperImage.style.top)) + (this.cropperDimensions.height / 2);
    var newCenterX = centerX * (newImageWidth / this.currentImageDimensions.width);
    var newCenterY = centerY * (newImageHeight / this.currentImageDimensions.height);
    var imageX = -(newCenterX - (this.cropperDimensions.width / 2));
    var imageY = -(newCenterY - (this.cropperDimensions.height / 2));
    
    // constrains the image to the bounding box
    var top = Math.round(imageY > 0 ? 0 : imageY + newImageHeight < this.cropperDimensions.height ? imageY + (this.cropperDimensions.height - (imageY + newImageHeight)) : imageY);
    var left = Math.round(imageX > 0 ? 0 : imageX + newImageWidth < this.cropperDimensions.width ? imageX + (this.cropperDimensions.width - (imageX + newImageWidth)) : imageX);
    this._updateImage(top, left);
    // resets the current dimensions to the new ones
    this.currentImageDimensions = {width: newImageWidth, height: newImageHeight};
    this.cropperImage.setStyle({top: top + 'px', left: left + 'px', width: newImageWidth + 'px', height: newImageHeight + 'px'});
  },
  
  _setupDraggableImage: function() {
    // makes the image draggable, and constrains it to the bounding box
    this.imageDraggable = new Draggable(this.cropperImage, {
      snap: function(x, y) { return this._dragImage(x, y); }.bind(this)
    });
  },
  
  _destroyDraggableImage: function() {
    if(this.imageDraggable) {
      this.imageDraggable.destroy();
      this.imageDraggable = undefined;
    }
  },

  scaleFactor: function() {
    if (this.imageAspect > 1) { // landscape
      return this.imageDimensions.height / this.image.height;
    } else {
      return this.imageDimensions.width / this.image.width;
    }
  },

  setSizeFromScaleFactor: function(scaleFactor) {
    if (this.imageAspect > 1) { // landscape
      this.image.height = this.image.width = Math.round(this.imageDimensions.height / scaleFactor);
    } else {
      this.image.height = this.image.width = Math.round(this.imageDimensions.width / scaleFactor);
    }
  },

  _destroyDraggableSlider: function() {
    if(this.sliderSlider) {
      this.sliderSlider.dispose();
      this.sliderHandle.setStyle({visibility: 'hidden'});
      this.sliderSlider = undefined;
    }
  },

  _setupDraggableSlider: function() {
    this.sliderSlider = new Control.Slider(this.sliderHandle, this.slider, {
      axis: 'horizontal',
      onSlide: this._dragSliderHandle.bind(this),
      sliderValue: this.scaleFactor(),
      range: $R(1, this.options.maxScaleFactor)
    });
    this.sliderHandle.setStyle({visibility: 'visible'});
  },

  _dragSliderHandle: function(x) {
    this.setSizeFromScaleFactor( x );
    this._scaleImage();
  },

  _dragImage: function(x, y) {
    var top = y > 0 ? 0 : y + this.currentImageDimensions.height <= this.cropperDimensions.height ? this.cropperDimensions.height - this.currentImageDimensions.height : y
    var left = x > 0 ? 0 : x + this.currentImageDimensions.width <= this.cropperDimensions.width ? this.cropperDimensions.width - this.currentImageDimensions.width : x;
    this._updateImage(top, left);
    return [left, top];
  }
});


/**
 * This class is intendted to be used once on a page, directly after the html
 * of the tooltip.  One tooltip is shared throughout the page, and moves around
 * and changes its contents when various hoverables are moused over.  Hoverables
 * are defined by giving them a classname of "hoverable" by default.
 * 
 * HTML Structure:
 * <div id="able_tooltip" class="able-tooltip">
 *   <table border="0" cellpadding="0" cellspacing="0">
 *   <tr><td class="top-left"></td><td class="top"><img src="/images/tooltips/able-tooltip-top-tail.png" class="tail"/></td><td class="top-right"></td></tr>
 *   <tr><td class="left"><div></div></td><td class="content" valign="top"><div class="content"></div></td><td class="right"><div></div></td></tr>
 *   <tr><td class="bottom-left"></td><td class="bottom"></td><td class="bottom-right"></td></tr>
 *   </table>
 * </div>
 * <script type="text/javascript">
 *   window.tooltip = new AbleTooltip({className: 'hoverable', tooltip: 'able_tooltip'});
 * </script>
 */
var AbleTooltip = Class.create({
  version: '0.8',
  options: {
    tooltip: 'able_tooltip',
    className: 'hoverable',
    durations: {hide: 0.4, show: 0.2, move: 0.25},
    hideDelay: 0.25,
    maxWidth: 350
  },
  tooltipElement: null,

  initialize: function(options) {
    Object.extend(this.options, options);

    // removes the animations for IE by setting the durations to 0
    if (Prototype.Browser.IE || (navigator.userAgent.indexOf('Firefox/2.0') != -1 
                                && navigator.userAgent.indexOf('Macintosh') != -1
                                && navigator.userAgent.indexOf('Camino') == -1)) {
      this.options.durations = {hide: 0, show: 0, move: 0}
    }

    this.setupPage();
  },

  setupPage: function() {
    // creates the tooltip element that's used, and sets up the events like
    // mouseover and mouseout
    Event.observe(document, "dom:loaded", this.setupPageStructure.bind(this));
  },

  setupPageStructure: function() {
    this._createTooltip();
    this._observeTooltipEvents();

    if (this.options.className) {
      // applies the tweaks to all the elements that want it
      $$('.' + this.options.className).each(function(element) {
        if(!element.isHoverable)
          new AbleTooltip.Hoverable(element, this);
      }.bind(this));
    }
  },

  applyTo: function(element) {
    return new AbleTooltip.Hoverable(element, this);
  },

  _createTooltip: function() {
    this.tooltipElement = $(this.options.tooltip);
    if (!this.tooltipElement) {
      this._createTooltipElement();
    }
    this.tooltipElement.setStyle({display: 'none', position: 'absolute', 'z-index': 1000, top: '0px', left: '0px', visibility: 'hidden'});
    this._contentDiv = this.tooltipElement.down('div.content');

    // create a clone of the tooltip so we can use it for sizing
    this._sizingTooltip = this.tooltipElement.cloneNode(true);
    this._sizingTooltip.id = this._sizingTooltip.id + '_sizer';
    this._sizingTooltip.style.display = 'block';
    document.body.appendChild(this._sizingTooltip);
    this._sizingContentDiv = this._sizingTooltip.down('div.content');
  },

  _createTooltipElement: function() {
    document.body.appendChild(this.tooltipElement = $(div({id:'able_tooltip', className:'able-tooltip'},[
      table({border:'0'},[
        tr({},[
          td({className:'tooltip-top-left'}),
          td({className:'tooltip-top'}, [
            img({src:'/images/tooltips/able-tooltip-top-tail.png', className:'tail'})
          ]),
          td({className:'tooltip-top-right'})
        ]),
        tr({},[
          td({className:'tooltip-left'},[
            div()
          ]),
          td({className:'content', valign:'top'}, [
            div({className:'content'})
          ]),
          td({className:'tooltip-right'})
        ]),
        tr({},[
          td({className:'tooltip-bottom-left'}),
          td({className:'tooltip-bottom'}),
          td({className:'tooltip-bottom-right'})
        ])
      ])
    ])));
  },

  _observeTooltipEvents: function() {
    this.tooltipElement.observe('mouseover', function(event) {
      if (this._goingToHide) {
        this._hide.cancel();
        this._hiding = false;
      }
      this.tooltipElement.stopObserving('mouseout');
      this.tooltipElement.observe('mouseout', function(event) {
        if (this._hiding) return;
        this._hideTooltip();
      }.bind(this));
    }.bind(this));
  },

  _hideTooltip: function() {
    this._hide = new Effect.DropOut(this.tooltipElement, {
      delay: this.options.hideDelay,
      duration: this.options.durations.hide,
      queue: {position: 'end', scope: 'hide', limit: 2},
      beforeStart: function() {
        this._goingToHide = true;
      }.bind(this),
      afterUpdate: function() {
        this._goingToHide = false;
        this._hiding = true;
      }.bind(this),
      afterFinish: function() {
        this.tooltipElement.setStyle({visibility: 'hidden'});
        this._hiding = false;
        this._goingToHide = false;
        this._resetCurrentHoverable();
      }.bind(this)
    });
  },

  _resetCurrentHoverable: function() {
    if( this.currentHoverable ) {
      this.currentHoverable.reset();
      this.currentHoverable = null;
    }
  },

  _positionWithPointer: function(v, position, dimensions, sizer) {
    if (v == 'top') {
      return position.top + dimensions.height - 5;
    } else {
      var viewPortPosition = document.viewport.getScrollOffsets();
      var viewPortWidth = document.viewport.getWidth();
      var left = false;
      var difference = false;
      var proposedLeft = ((position.left - (sizer.width / 2)) + (dimensions.width / 2));
      if (proposedLeft + sizer.width - viewPortPosition.left > viewPortWidth) {
        difference = (viewPortWidth + viewPortPosition.left) - (proposedLeft + sizer.width);
        left = viewPortWidth + viewPortPosition.left - sizer.width;
      }
      if (proposedLeft < viewPortPosition.left) {
        difference = viewPortPosition.left - proposedLeft;
        left = viewPortPosition.left;
      }
      var tail = this.tooltipElement.down('img.tail');
      if (difference) {
        tail.writeAttribute({style: 'position: relative; left: ' + -(difference) + 'px;'});
      } else {
        tail.removeAttribute('style');
      }
      return (left === false) ? proposedLeft : left;
    }
  },

  resizeFor: function(hoverable) {
    // puts the contents that will eventually go into the tooltip, so the
    // dimensions can be calculated, and restricts the width
    this._sizingContentDiv.setStyle({width: 'auto'});
    this._sizingContentDiv.innerHTML = "";
    this._sizingContentDiv.appendChild(hoverable.contentElement.cloneNode(true));
    this._sizingContentDivDimensions = this._sizingContentDiv.getDimensions();
    if (this._sizingContentDivDimensions.width > this.options.maxWidth) {
      this._sizingContentDiv.setStyle({width: this.options.maxWidth + 'px'});
      this._sizingContentDivDimensions.width = this.options.maxWidth;
      this._sizingContentDivDimensions.height = this._sizingContentDiv.getHeight();
    }
    this._sizingTooltipDimensions = this._sizingTooltip.getDimensions();
  },

  resizeCurrent: function() {
    this.resizeFor(this.currentHoverable);
    this.renderContentsFor(this.currentHoverable);
  },

  renderContentsFor: function(hoverable) {
    // moves the tooltip when it's already visible (fading it back in is
    // handled below)
    var visible = (this.tooltipElement.getStyle('visibility') != 'hidden');
    if (visible) {
      // cancels any movement we're currently in
      if (this._moving) {
        this._move.cancel();
        if(this._resize)
          this._resize.cancel();
      }

      // resizes and moves the tooltip to the new location (could use parallel
      // here?)
      if (this.currentHoverable != hoverable) {
        this._resize = new Effect.Morph(this._contentDiv, {
          duration: this.options.durations.move / 2,
          queue: {position: 'end', scope: 'resize', limit: 1},
          style: {
            width: this._sizingContentDivDimensions.width + 'px',
            height: this._sizingContentDivDimensions.height + 'px'
          },
          afterFinish: function() {
            this._contentDiv.appendChild(hoverable.contentElement);
          }.bind(this)
        });
      } else {
        this._contentDiv.setStyle({width: this._sizingContentDivDimensions.width + 'px'});
        this._contentDiv.setStyle({height: this._sizingContentDivDimensions.height + 'px'});
        this._contentDiv.appendChild(hoverable.contentElement);
      }

      this._move = new Effect.Morph(this.tooltipElement, {
        duration: this.options.durations.move,
        queue: {position: 'end', scope: 'move', limit: 1},
        style: {
          top: this._positionWithPointer('top', hoverable.position, hoverable.dimensions)  + 'px',
          left: this._positionWithPointer('left', hoverable.position, hoverable.dimensions, this._sizingTooltipDimensions)  + 'px'
        },
        beforeStart: function() {
          this._moving = true;
        }.bind(this),
        afterFinish: function() {
          this._moving = false;
        }.bind(this)
      });
    } else {
      this._contentDiv.appendChild(hoverable.contentElement);
    }

    if (this.currentHoverable != hoverable) {
      this._resetCurrentHoverable();
    }
    this.currentHoverable = hoverable;
  },

  showTooltip: function(hoverable) {
    // makes the tooltip appear if it's hiding or hidden, which can work in
    // conjunction with the move animation
    var visible = (this.tooltipElement.getStyle('visibility') != 'hidden');
    if (!visible || this._goingToHide || this._hiding) {
      this.tooltipElement.setStyle({visibility: 'visible'});

      // cancels the hiding animation
      if (this._hide) this._hide.cancel();
      this._hiding = false;
      this._goingToHide = false;

      // moves the tooltip to where it should be before showing it if it's not
      // moving
      if (!this._moving) {
        this.tooltipElement.setStyle({
          top: this._positionWithPointer('top', hoverable.position, hoverable.dimensions) + 'px',
          left: this._positionWithPointer('left', hoverable.position, hoverable.dimensions, this._sizingTooltipDimensions) + 'px',
          visibility: 'visible'
        });
        this._contentDiv.setStyle({
          width: this._sizingContentDivDimensions.width + 'px',
          height: this._sizingContentDivDimensions.height + 'px'
        });
      }

      // shows the tooltip by fading it in
      this._showing = new Effect.Appear(this.tooltipElement, {
        duration: this.options.durations.show,
        queue: {position: 'front', scope: 'show', limit: 1},
        delay: hoverable.delayTooltip,
        afterFinish: function() { this._showing = false; }.bind(this)
      });
    }
  },

  _contentDiv: null, _sizingTooltip: null, _sizingContentDiv: null, _hide: null, _move: null,
  _goingToHide: false, _hiding: false, _moving: false, _showing: false
});

AbleTooltip.Hoverable = Class.create({
  hoverElement: null,
  position: null,
  dimensions: null,
  active: false,
  delayTooltip: 0,

  initialize: function(element, tooltip) {
    this._tooltipInstance = tooltip;
    this.hoverElement = element;
    this.hoverElement.isHoverable = true;
    
    // delays showing the tooltip by half a second if there's a with-delay
    // classname on the element
    if (Element.hasClassName(element, 'with-delay')) {
      this.delayTooltip = .25;
    }

    // gets the contents that will be put into the tooltip
    this.contentElement = $A(this.hoverElement.childNodes).find(function(e) { return e.className && e.className.match('tooltip') });
    if(!this.contentElement) {
      return;
    }

    this.hoverElement.observe('mouseover', function(event) {
      this.active = true;
      // gets the position and dimensions for the element being hovered over
      this.position = this.hoverElement.cumulativeOffset();
      this.dimensions = this.hoverElement.getDimensions();

      this._tooltipInstance.resizeFor(this);

      this._tooltipInstance.renderContentsFor(this);

      this._tooltipInstance.showTooltip(this);
    }.bind(this));

    // sets up the observer for the mouseout event for the current element
    this._observeHoverableEvents();
  },

  _observeHoverableEvents: function() {
    this.hoverElement.observe('mouseout', function(event) {
      if (this._tooltipInstance.currentHoverable == this) {
        this._tooltipInstance._hideTooltip();
      }
    }.bind(this));
  },

  reset: function() {
    this.active = false;
    this.hoverElement.appendChild(this.contentElement);
  },

  _tooltipInstance: null
});




var AbleExpandable = {
  version: '0.7',
  options: {
    className: 'able-expandable',
    collapsibleClassName: 'collapsible',
    durations: {toggle: 0.1}
  },

  initialize: function(options) {
    Object.extend(this.options, options);
  },

  toggle: function(element, url) {
    var expandable = Element.up(element, '.' + this.options.className);
    var collapsible = expandable.down('.' + this.options.collapsibleClassName);
    if (!collapsible) return;
    if (url && !expandable.hasClassName('expanded')) {
      expandable.addClassName('loading');
      new Ajax.Updater(collapsible, url, { 
        asynchronous: true,
        evalScripts: true,
        method: 'get',
        onComplete: function() {
          this._toggle(expandable, collapsible);
          expandable.removeClassName('loading');
        }.bind(this)
      });
    } else {
      this._toggle(expandable, collapsible);
    }
  },
  
  _toggle: function(expandable, collapsible) {
    Effect.toggle(collapsible, 'blind', {
      duration: this.options.durations.toggle,
      afterFinish: function() {
        expandable.toggleClassName('expanded');
      }
    });
  }
}

AbleExpandable.initialize({className: 'able-expandable'});


var AbleNotice = Class.create({
  version: '1.0',
  options: {
    className: 'able-notice',
    containerClassName: 'able-notice-container',
    shadeClassName: 'able-notice-shade',
    dismissClassName: 'able-notice-dismiss',
    maxWidth: '400',
    callback: false
  },

  initialize: function(options) {
    Object.extend(this.options, options);

    this.buildNoticeElements();

    this.setupPage();
  },

  setupPage: function() {
    document.observe("dom:loaded", function() {
      $$('.' + this.options.className).each(function(element) {
        if (document.cookie.indexOf(element.id + "_notice=") == -1) {
          this._elements.push(element);
        }
      }.bind(this));

      if (this._elements.length) this.showNotices();
    }.bind(this));
  },

  buildNoticeElements: function() {
    this._shade = new Element('div', {'class': this.options.shadeClassName});
    this._shade.observe('click', this.hideNotices.bind(this));
    this._container = new Element('div', {'class': this.options.containerClassName});
    this._container.observe('click', this.hideNotices.bind(this));

    this._dismiss = $(div({className: this.options.dismissClassName}, [
      input({type: 'reset', value: 'Never Show Me Again'}),
      input({type: 'submit', value: 'Dismiss'})
    ]));
    this._dismiss.observe('click', function(event) {
      element = Event.element(event);
      this.hideNotices((element.type == 'reset') ? true : false)
      Event.stop(event);
    }.bind(this));
  },

  showNotices: function() {
    this._showing = true;

    if (Prototype.Browser.IE) {
      $$('html, body').invoke('setStyle', {overflow: 'hidden'}); // turn off the scroll bar
      $$('select').invoke('setStyle', {visibility: 'hidden'}); // hide all the selects
      this._peIE = new PeriodicalExecuter(function() {window.scrollTo(0, 0)}, .1);
    }

    Event.observe(window, 'resize', function() {
      this._windowResize()
    }.bind(this));
    
    this._elements.each(function(element) {
      this._container.appendChild(element);
    }.bind(this));
    
    this._container.appendChild(this._dismiss);

    document.body.appendChild(this._shade);
    document.body.appendChild(this._container);

    this._windowResize();
  },
  
  hideNotices: function(neverAgain) {
    this._showing = false;

    if (neverAgain == true) {
      if (this.options.callback) {
        this.options.callback();
      }

      var expireDate = new Date();
      expireDate.setDate(expireDate.getDate() + 10000);
      expiAbleNoticereDate = expireDate.toGMTString();
      this._elements.each(function(element) {
        document.cookie = element.id + "_notice=false;expires=" + expireDate;
      });
    }

    this._container.hide();
    this._shade.hide();
    
    if (Prototype.Browser.IE) {
      this._peIE.stop();
      $$('html, body').invoke('setStyle', {overflow: ''});
      $$("select").invoke('setStyle', {visibility: ''});
    }
  },
  
  _windowResize: function() {
    if (!this._showing) return;

    var viewportDimensions = document.viewport.getDimensions();
    var containerDimensions = this._container.getDimensions();

    this._container.setStyle({
      top: ((viewportDimensions.height / 2) - (containerDimensions.height / 2)) + 'px',
      left: ((viewportDimensions.width / 2) - (containerDimensions.width / 2)) + 'px'
    });

    if (Prototype.Browser.IE) {
      this._shade.setStyle({height: viewportDimensions.height + 'px'});
      if (containerDimensions.width > this.options.maxWidth) {
        this._container.setStyle({width: this.options.maxWidth});
      }
    }
  },
  
  _elements: [], _shade: null, _container: null, _dismiss: null, _showing: false
});

var AbleUploader = Class.create({  
  version: '1.0',
  options:{
    inputClass: 'able-uploader',
    autoSubmit: true,
    label: 'Choose a file...'
  },
  
  initialize: function(hookElement, options) {
    this.hookElement = hookElement; 
    this.fileInput = $(this.hookElement);
    this.fileForm = this.fileInput.up();
    Object.extend(this.options, options);
    this._setup();
  },
  
  _setup: function(){
    this.fileInput.addClassName(this.options.inputClass); 
    this.fileInput.setAttribute('size', 1);

    this.container = $(div({ className: 'able-uploader-container'})); 

    this.fileInput.insert({before: this.container});
    this.container.insert({top: this.fileInput});         

    this.anchor = $(a({href: '#', className: 'able-button blue able-uploader-browse'})).update(this.options.label);
    this.fileInput.insert({after: this.anchor}); 

    if (this.options.autoSubmit) {
      new Event.observe(this.fileInput, 'change', this.submit.bind(this));
      
      //hide submit since it will submit form on file input change.
      this.fileForm.getInputs('submit').each(function(element){ element.hide() }); 

      //add a span for a loading icon.
      this.loadIcon = $(span({className: 'able-uploader-load-icon'}));
      this.loadIcon.hide();
      this.anchor.insert({after: this.loadIcon})
    }
		
    new Event.observe(this.anchor, 'mousemove', this._moveFileInput.bind(this));
    new Event.observe(this.fileInput, 'mouseover', function() { this.anchor.addClassName('hover'); }.bind(this) );
    new Event.observe(this.fileInput, 'mouseout', function() { this.anchor.removeClassName('hover'); }.bind(this));

    this.anchor.setStyle({top: this.fileInput.style.top, left: this.fileInput.style.left});
  },

  submit: function() {
    if(this.fileForm.onsubmit) {
      var result = this.fileForm.onsubmit();
      if(result !== false) {
        this.fileForm.submit();
      }
    } else {
      this.fileForm.submit();
    }
    this.loadIcon.show();
  },
  
  _moveFileInput: function(e) {    
    // that's right folks we are moving the input field around 
    // to be under the mouse when it is over the anchor tag
    if (typeof e == 'undefined') e = window.event;
    if (typeof e.pageY == 'undefined' &&  typeof e.clientX == 'number' && document.documentElement) {
      e.pageX = e.clientX + document.documentElement.scrollLeft;
      e.pageY = e.clientY + document.documentElement.scrollTop;
    }

    var ox = oy = 0;
    var elem = this.container;
    if (elem.offsetParent) {
      ox = elem.offsetLeft;
      oy = elem.offsetTop;
      while (elem = elem.offsetParent) {
        ox += elem.offsetLeft;
        oy += elem.offsetTop;
      }
    }

    var x = e.pageX - ox;
    var y = e.pageY - oy;
    var w = this.fileInput.offsetWidth;
    var h = this.fileInput.offsetHeight;

    this.fileInput.style.top = y - (h / 2)  + 'px';
    this.fileInput.style.left = x - (w - 30) + 'px';
  }
});

LikeMe.Facebook = {
  invite_users: function() {
    FB.Facebook.apiClient.users_getLoggedInUser(function(userId){
      if(userId) {
        var dialog = new FB.UI.FBMLPopupDialog('Invite your friends to join', '');
        var fbml = "<fb:fbml>" +
                     '<fb:request-form style="width:630px; height:540px;" action="' +
                         document.location.href +
                          '" invite="true" type="LikeMe"'+
                          'content="Ever walk into a place and feel right at home? That\'s what you get with LikeMe. '+
                          'LikeMe has developed a next generation search engine to match you up with people who have similar ' +
                          'tastes and give you their best recommendations. Check out LikeMe to discover tried and tested ' +
                          'places to eat, drink, dance, shop and explore - in the city you live in, and the cities you visit. ' +
                          '<fb:req-choice url=\'http://www.likeme.net\' label=\'Check out LikeMe!\' />"' +
                     '>'+
                      '<fb:multi-friend-selector showborder="false" '+
                          'actiontext="Invite your friends to join LikeMe" rows="5" bypass="cancel"'+
                          'showborder="false" email_invite="false" />'+
                     '</fb:request-form>'+
                   '</fb:fbml>';
        dialog.setFBMLContent(fbml);
        dialog.setContentWidth(630);
        dialog.setContentHeight(540);
        dialog.show();
      } else {
        FB.Connect.logout(Services.page.logout);
      }
    });
  },

  pop_out: function() {
    if (top.location != location) {
      top.location.href = document.location.href;
    }
  },

  share_url: function(url) {
    var win = window.open('http://www.facebook.com/sharer.php?u='+encodeURIComponent(url),'sharer','toolbar=0,status=0,width=626,height=436');
    win.focus();
    return false;
  }
};

var Services = {

  initCommandQueue: function() {
    this.commandQueue = new Pivotal.CommandQueue(new Pivotal.ServerProxy());
  },

  initGeocoder: function() {
    this.geocoder = new LikeMe.Service.Geocoder();
  },

  reset: function() {
    this.initCommandQueue();
    this.initGeocoder();
    this.userService = new LikeMe.Service.UserService();
    this.ratableService = new LikeMe.Service.RatableService();
  }

};


LikeMe.Command.Follow = Class.create({
  // This callback function receives the guide as the only parameter.
  initialize: function(guide, callback) {
    this.guide = guide;
    this.callback = callback;
  },

  asAjaxPayload: function() {
    return {
      url: Routing.user_follow_path({user_id: Services.userService.currentUser().toParam, id: this.guide.toParam}),
      method: 'post'
    }
  },

  responseFromServer: function(guideJson) {
    Services.userService.register(guideJson);
    if (this.callback) { this.callback(Services.userService.findById(guideJson.id)); }
  }
});

LikeMe.Command.Follow.phrase = {
  simpleFollow: "Follow",
  becomeFan: "Follow",
  unfollow: "Unfollow",
  suggestFollow: "Suggest to Follow",
  removeFrom: "from those you Follow",
  followCount: "Follows",
  sendEmailUponFollow: "Send me email when someone Follows me on LikeMe",
  shareActionLink: "Share with Followers",
  relationFollowing: "Following",
  relationFollowed: "Followed",
  relationNone: "None"
}

LikeMe.Command.Follow.send = function(guide, callback) {
  Services.commandQueue.enqueue(new LikeMe.Command.Follow(guide, callback));
}


LikeMe.Command.Generic = Class.create({

  initialize: function(options) {
    this.options = {
      paramHash: {},
      method: 'post'
    };
    Object.extend(this.options, options);
  },

  asAjaxPayload: function() {
    return {
      url: this.options.url,
      method: this.options.method,
      paramHash: this.options.paramHash
    }
  },
  
  executeLocally: function() {
    if (this.options.executeLocally) { this.options.executeLocally(); }
  },

  responseFromServer: function(responseJson) {
    if (this.options.callback) { this.options.callback(responseJson); }
  },

  undo: function() {
    if (this.options.undo) { this.options.undo(); }
  }
});

LikeMe.Command.Generic.send = function(options) {
  Services.commandQueue.enqueue(new LikeMe.Command.Generic(options));
}

LikeMe.Command.PaginatedLoader = Class.create({
  initialize: function(url, lo, hi, callback) {
    this.url = url;
    this.lo = lo;
    this.hi = hi;
    this.callback = callback;
  },

  asAjaxPayload: function() {
    return {
      url: this.url,
      method: 'get',
      paramHash: {
        "lo": this.lo,
        "hi": this.hi
      }
    };
  },

  responseFromServer: function(responseJson) {
    this.callback(responseJson, this.lo, this.hi);
  }

});

LikeMe.Command.PaginatedLoader.send = function(url, lo, hi, callback) {
  Services.commandQueue.enqueue(new LikeMe.Command.PaginatedLoader(url, lo, hi, callback));
}

LikeMe.Command.Rate = Class.create({
  initialize: function(url,rating) {
    this.url = url;
    this.rating = rating;
  },

  asAjaxPayload: function() {
    return {
      url: this.url,
      method: "put",
      paramHash: {"review[rating]": this.rating}
    };
  }
});

LikeMe.Command.Rate.send = function(url,rating) {
  Services.commandQueue.enqueue(new LikeMe.Command.Rate(url,rating));
}

LikeMe.Command.RemoveRanking = Class.create({
  initialize: function(rankedItem) {
    this.ratable = rankedItem.ratable;
    this.scroller = rankedItem.scroller;
  },

  asAjaxPayload: function() {
    var url;
    if ( this.scroller instanceof LikeMe.View.ToDoScroller ) {
      url = Routing.user_to_do_group_place_path({
        user_id: Services.userService.currentUser().toParam,
        to_do_group_id: this.scroller.rankingGroup.toParam,
        id: this.ratable.toParam
      })
    } else if(this.scroller instanceof LikeMe.View.FollowScroller) {
      url = Routing.user_follow_path({
        user_id: Services.userService.currentUser().toParam,
        id: this.ratable.toParam
      })
    } else {
      url = Routing.user_recommendation_group_place_path({
        user_id: Services.userService.currentUser().toParam,
        recommendation_group_id: this.scroller.rankingGroup.toParam,
        id: this.ratable.toParam
      })
    }
    return {
      url: url,
      method: "delete"
    }
  },

  executeLocally: function() {
    this.ratablePosition = this.scroller.rankingGroup.ratableIndex(this.ratable);
    this.scroller.rankingGroup.removeRatable(this.ratable);
    this.scroller.paint();
  },

  undo: function() {
    this.scroller.rankingGroup.undoRemoveRatable(this.ratable, this.ratablePosition);
    this.scroller.paint();
  }
});

LikeMe.Command.RemoveRanking.send = function(rankedItem) {
  Services.commandQueue.enqueue(new LikeMe.Command.RemoveRanking(rankedItem));
}

LikeMe.Command.ReorderRankings = Class.create({
  initialize: function(rankingGroup, url, page, pageSize, ratableIds) {
    this.rankingGroup = rankingGroup;
    this.url = url;
    this.page = page;
    this.pageSize = pageSize;
    this.ratableIds = ratableIds;
  },

  asAjaxPayload: function() {
    return {
      url: this.url,
      paramHash: {positions: this.ratableIds, page:this.page}
    }
  },

  executeLocally: function() {
    this.rankingGroup.reorderRatables(this.page, this.pageSize, this.ratableIds);
  }
});

LikeMe.Command.ReorderRankings.send = function(rankingGroup, url, page, pageSize, ratableIds) {
  Services.commandQueue.enqueue(new LikeMe.Command.ReorderRankings(rankingGroup, url, page, pageSize, ratableIds));
}

LikeMe.Command.InlineEditorUpdate = Class.create({
  initialize: function(editor) {
    this.editor = editor;
    this.oldText = editor.options.text;
    this.newText = editor.newText();
  },

  executeLocally: function() {
    this.editor.setText(this.newText);
  },

  undo: function() {
    this.editor.options.text = this.oldText;
    this.editor.paint();
  },

  asAjaxPayload: function() {
    var paramHash = {};
    paramHash[this.editor.options.parameter] = this.newText;
    return {
      url: this.editor.options.updateUrl,
      method: this.editor.options.method,
      paramHash: paramHash
    };
  }
});

LikeMe.Command.InlineEditorUpdate.send = function(editor) {
  Services.commandQueue.enqueue(new LikeMe.Command.InlineEditorUpdate(editor));
}

LikeMe.Command.UploadPhoto = Class.create({
  initialize: function(photoUrl, srcUrl, callback) {
    this.photoUrl = photoUrl;
    this.srcUrl = srcUrl;
    this.callback = callback;
  },

  asAjaxPayload: function() {
    return {
      url: this.photoUrl,
      paramHash: {"photo[file]":this.srcUrl}
    }
  },

  responseFromServer: function(ratableJson) {
    this.callback(ratableJson);
  }
});

LikeMe.Command.UploadPhoto.send = function(photoUrl, srcUrl, callback) {
  Services.commandQueue.enqueue(new LikeMe.Command.UploadPhoto(photoUrl, srcUrl, callback));
}

LikeMe.Model.Category = {};
LikeMe.Model.Category.all = {toParam: 'all', id: '', name:'place', pluralName:'places'};
LikeMe.Model.Category.toEnglishMap = {
  bar: "a Bar or Nightlife",
  hotel: "a Hotel",
  restaurant: "a Restaurant",
  activity: "an Activity",
  shopping: "Shopping",
  other: "Other Stuff",
  fitness: "a Fitness Center or Spa",
  event: "an Event"
}

LikeMe.Model.Category.categories = [{"name":"restaurant","toParam":"restaurant","id":4,"pluralValue":"Restaurants","value":"Restaurant","pluralName":"restaurants"},{"name":"bar","toParam":"bar","id":1,"pluralValue":"Bars and Nightlife","value":"Bar and Nightlife","pluralName":"bars"},{"name":"event","toParam":"event","id":10,"pluralValue":"Live Music & Events","value":"Live Music & Event","pluralName":"events"},{"name":"shopping","toParam":"shopping","id":6,"pluralValue":"Shopping","value":"Shopping","pluralName":"shopping"},{"name":"fitness","toParam":"fitness","id":9,"pluralValue":"Fitness\/Spas\/Beauty","value":"Fitness\/Spa\/Beauty","pluralName":"fitness\/spas"},{"name":"activity","toParam":"activity","id":7,"pluralValue":"Activities","value":"Activity","pluralName":"activities"},{"name":"hotel","toParam":"hotel","id":2,"pluralValue":"Hotels","value":"Hotel","pluralName":"hotels"},{"name":"other","toParam":"other","id":8,"pluralValue":"Other Stuff","value":"Other Stuff","pluralName":"other"}];


LikeMe.Model.DisplayNameUpdater = Class.create({

  initialize: function(display_name_id, first_name_id, last_name_id) {
    this.display_name = $(display_name_id);
    this.first_name= $(first_name_id);
    this.last_name= $(last_name_id);

    this.display_name_manually_updated = (this.display_name.value != '');

    Event.observe(this.first_name, 'keyup', this.onFirstOrLastNameUpdate.bindAsEventListener(this));
    Event.observe(this.last_name, 'keyup', this.onFirstOrLastNameUpdate.bindAsEventListener(this));
    Event.observe(this.display_name, 'keyup', this.onDisplayNameManualUpdate.bindAsEventListener(this));
  },

  onFirstOrLastNameUpdate: function() {
    if (this.display_name_manually_updated) {
      return;
    }
    this.display_name.value = this.first_name.value.substr(0,24).concat(this.last_name.value.charAt(0));
  },

  onDisplayNameManualUpdate: function() {
    this.display_name_manually_updated = true;
  }
});

LikeMe.Model.RankingGroup = Class.create({

  size: undefined,
  editable: false,
  row_count: undefined,
  includesHidden: false,
  ratableGroupIdObject: 'recommendationGroupIds',

  initialize: function(rankingGroupJson) {
    Object.extend(this, rankingGroupJson);
    this.replaceRatables(rankingGroupJson.ratables);
  },

  registerRatable: function(ratable) {
    return Services.ratableService.register(ratable);
  },

  replaceRatables: function(newRatables) {
    this.ratables = newRatables.collect(function(ratable) {
      return this.registerRatable(ratable);
    }.bind(this));
  },

  addRatables: function(newRatables, updateSize) {
    this.loadRatables(newRatables);
    this.size += (updateSize ? newRatables.length : 0);
  },

  loadRatables: function(newRatables) {
    this.ratables = this.ratables.concat(newRatables.collect(function(ratable) {return this.registerRatable(ratable);}.bind(this)));
  },

  ratableIndex: function(ratable) {
    return this.ratables.indexOf(ratable);
  },

  removeRatable: function(ratable) {
    var ratableIndex = this.ratableIndex(ratable);
    if(ratableIndex >= 0) {
      this.ratables.splice(ratableIndex, 1);
    }
    if(ratable[this.ratableGroupIdObject]) {
      var rankingGroupIndex = ratable[this.ratableGroupIdObject].indexOf(this.id);
      if(rankingGroupIndex >= 0) {
        ratable[this.ratableGroupIdObject].splice(rankingGroupIndex, 1);
      }
    }
    this.size = this.size - 1;
  },

  undoRemoveRatable: function(ratable, position) {
    if(position >= 0 && ratable) {
      this.ratables.splice(position, 0, ratable);
    }
    this.size = this.size + 1;
  },

  reorderRatables: function(page, pageSize, newOrder) {
    var start = (page - 1) * pageSize;
    var end = start + newOrder.length;
    var sliceToOrder = this.ratables.slice(start, end);
    sliceToOrder.sort(function(a,b) {
      var indexA = newOrder.indexOf(a.id);
      var indexB = newOrder.indexOf(b.id);
      if (indexA < indexB)
        return -1;
      if (indexB < indexA)
        return 1;
      return 0;
    });
    for(var i = 0, j = newOrder.length; i < j; i++)
    {
      this.ratables.splice(start + i, 1, sliceToOrder[i]);
    }
  },

  containsRatable: function(ratable) {
    return ratable[this.ratableGroupIdObject].indexOf(this.id) != -1;
  },
  
  items: function() {
    return this.ratables;
  },

  user: function() {
    return Services.userService.findById(this.userId);
  }

});

LikeMe.Model.Ratable = Class.create({

  initialize: function(ratableJson) {
    Object.extend(this, ratableJson);
    if(this.nextStartDate) {
      this.nextStartDate = Date.parse(this.nextStartDate);
    }
    this._views = [];
  },

  recommended: function() {
    return (this.recommendationGroupIds.length != 0);
  },

  inToDo: function() {
    return (this.toDoGroupIds.length != 0);
  },

  registerView: function( view ) {
    if ( -1 == this._views.indexOf( view ))
      this._views.push( view );
  },

  unregisterView: function( view ) {
    var index = this._views.indexOf( view );
    if ( -1 != index )
      this._views.splice( index, 1 );
  },

  repaintViews: function() {
    this._views.each(function(view) {
      if(view.paintTooltip) {view.paintTooltip(true)};
      if(view.paintLinks) {view.paintLinks()};
      if(view.paintUnderImage) {view.paintUnderImage()};
    });
  },

  hideFromScrollers: function() {
    var viewsCopy = this._views.slice(0);
    viewsCopy.each(function(view) {
      view.scroller.hideItem(this);
    }.bind(this));
  },
  
  photoUrl: function(version) {
    return this.photoUrlPrefix + version + this.photoUrlSuffix;
  },

  recommendationLink: function(options) {
    if( Services.userService.loggedIn() ) {
      if ( this.recommended() ) {
        var removeRankedLink = a({className:'action remove clickable'}, 'Remove from Recommendations');
        Event.observe(removeRankedLink, 'click', this.removeFromGuidebook.bind(this, options));
        return removeRankedLink;
      } else {
        var rankedLink = a({className:'action add-recommendation clickable'}, 'Add to Recommendations');
        Event.observe(rankedLink, 'click', this.addToGuidebook.bind(this, options));
        return rankedLink;
      }
    } else {
      var link = div({className: 'hoverable with-delay'}, [
        a({className: 'action add-recommendation', href: '/users/new'}, 'Add to Recommendations'),
        span({className: 'tooltip'}, [
          h6('You must create a recommendation profile to complete this action.')
        ])
      ]);
      Services.page.tooltip().applyTo(link);
      return link;
    }
  },

  addToGuidebook: function (options) {
    LikeMe.Command.Generic.send({
      url: Routing.place_recommendations_path({place_id: this.toParam}),
      executeLocally: function() {
        this.recommendationGroupIds = [-1];
        this.repaintViews();
        this.handleRecommended(options);
      }.bind(this),
      undo: function() {
        this.recommendationGroupIds = [];
        this.repaintViews();
      }.bind(this),
      callback: function(responseRatable) {
        Services.page.trackUrl('/events/add_to_recommendations');
        Services.ratableService.register(responseRatable);
      }
    });
  },

  handleRecommended: function(options) {
    if (!this.reviewed) {
      this.showInsideWordBox(options);
      Services.page.tooltip().tooltipElement.setStyle({visibility: 'hidden'});
    }
  },

  showInsideWordBox: function(options) {
    var params = {ratable: this};
    if(options) {
      params.category = options.category;
      params.context = options.context;
    }
    var box = new LikeMe.View.InsideWordModalBox(params)
    box.paint();
    return box;
  },

  removeFromGuidebook: function (options) {
    var recommendationGroupIds = null;
    LikeMe.Command.Generic.send({
      url: Routing.place_recommendations_path({place_id: this.toParam}),
      method: "delete",
      executeLocally: function() {
        recommendationGroupIds = this.recommendationGroupIds;
        this.recommendationGroupIds = [];
        this.repaintViews();
      }.bind(this),
      undo: function() {
        this.recommendationGroupIds = recommendationGroupIds;
        this.repaintViews();
      }.bind(this),
      callback: function(responseRatable) {
        Services.ratableService.register(responseRatable);
      }
    });
  },

  toDoLink: function(options) {
    if( Services.userService.loggedIn() ) {
      if ( this.inToDo() ) {
        var removeToDoLink = a({className:'action remove clickable'}, 'Remove from To Do List');
        Event.observe(removeToDoLink, 'click', this.removeFromToDo.bind(this, options));
        return removeToDoLink;
      } else {
        var toDoLink = a({className:'action add-todo clickable'}, 'Add to To Do List');
        Event.observe(toDoLink, 'click', this.addToToDo.bind(this, options));
        return toDoLink;
      }
    } else {
      var link = div({className: 'hoverable with-delay'}, [
        a({className: 'action add-todo', href: '/users/new'}, 'Add to To Do List'),
        span({className: 'tooltip'}, [
          h6('You must create a recommendation profile to complete this action.')
        ])
      ]);
      Services.page.tooltip().applyTo(link);
      return link;
    }
  },

  addToToDo: function (options) {
    LikeMe.Command.Generic.send({
      url: Routing.place_to_dos_path({place_id: this.toParam}),
      executeLocally: function() {
        this.toDoGroupIds = [-1];
        this.repaintViews();
      }.bind(this),
      undo: function() {
        this.toDoGroupIds = [];
        this.repaintViews();
      }.bind(this),
      callback: function(responseRatable) {
        Services.page.trackUrl('/events/add_to_to_do');
        Services.ratableService.register(responseRatable);
      }
    });
  },

  removeFromToDo: function (options) {
    var toDoGroupIds = null;
    LikeMe.Command.Generic.send({
      url: Routing.place_to_dos_path({place_id: this.toParam}),
      method: "delete",
      executeLocally: function() {
        toDoGroupIds = this.toDoGroupIds;
        this.toDoGroupIds = [];
        this.repaintViews();
      }.bind(this),
      undo: function() {
        this.toDoGroupIds = toDoGroupIds;
        this.repaintViews();
      }.bind(this),
      callback: function(responseRatable) {
        Services.ratableService.register(responseRatable);
      }
    });
  }


});


LikeMe.Model.TextfieldPinger = Class.create({

  initialize: function(textfield, timeout, action) {
    this.textfield = $(textfield);
    this.timeout = timeout;
    this.action = action;

    Event.observe(this.textfield, 'keypress', this.onKeyPress.bindAsEventListener(this));
  },

  onKeyPress: function() {
    if(this.observer) clearTimeout(this.observer);
    this.observer = setTimeout(this.action.bind(this), this.timeout);
  }

});


LikeMe.Model.User = Class.create({
  initialize: function(userJson) {
    Object.extend(this, userJson);
    this._views = [];    
  },
  
  registerView: function( view ) {
    if ( -1 == this._views.indexOf( view ))
      this._views.push( view );
  },

  unregisterView: function( view ) {
    var index = this._views.indexOf( view );
    if ( -1 != index )
      this._views.splice( index, 1 );
  },

  repaintViews: function() {
    this._views.each(function(view) {
      view.paintTooltip(true);
      if (view.paintUnderImage) {view.paintUnderImage()};
    });
  },

  hideFromScrollers: function() {
    this._views.each(function(view) {
      view.scroller.hideItem(this);
    }.bind(this));
  },

  photoUrl: function(version) {
    return this.photoUrlPrefix + version + this.photoUrlSuffix;
  },
  
  genderAgeLocation: function() {
    return [this.gender, this.age, this.location].compact().join(' / ');
  },

  messageableByCurrentUser: function() {
    if(Services.userService.currentUser() == this || !this.messageable) {
      return false;
    }
    if (Services.userService.currentUser() && Services.userService.currentUser().admin) {
      return true;
    }

    return this.relation == LikeMe.Command.Follow.phrase.relationFollowed;
  },

  sendMessageLink: function() {
    if ( this.messageableByCurrentUser() )
    {
      return a({
        className:'action send-message',
         href:'/messages/new?message[recipient_ids][]=' + this.id },
        'Send Message');
    }
  },

  follow: function() {
    /*alert('in follow');*/
    var user = Services.userService.register(this);

    LikeMe.Command.Generic.send({
      url: '/users/' + Services.userService.currentUser().toParam + '/follow',
      paramHash: {id: user.toParam},
      callback: function(responseUser) {
        Services.userService.register(responseUser);
        user.repaintViews();
      }
    });

  },

  removeFollow: function () {
    var relationStatus = null;
    LikeMe.Command.Generic.send({
      url: Routing.user_follow_path({user_id: Services.userService.currentUser().toParam, id: this.toParam}),
      method: 'delete',
      executeLocally: function() {
        relationStatus = this.relation;
        this.relation = LikeMe.Command.Follow.phrase.relationNone;
        this.repaintViews();
      }.bind(this),
      undo: function() {
        this.relation = relationStatus;
        this.repaintViews();
      }.bind(this)
    });
  },

  followLink: function() {
    if(Services.userService.loggedIn()) {
      //alert(this.relation);
      if ( this.relation == LikeMe.Command.Follow.phrase.relationNone) {
        var addFollowLink = a({className:'action add-follow'}, ((this.guestOrOfficial) ? LikeMe.Command.Follow.phrase.becomeFan : LikeMe.Command.Follow.phrase.simpleFollow));
        Event.observe(addFollowLink, 'click', this.follow.bind(this));
        return addFollowLink;
      } else if (this.relation == LikeMe.Command.Follow.phrase.relationFollowed) {
        var removeFollowLink = a({className:'action remove'}, LikeMe.Command.Follow.phrase.unfollow);
        Event.observe(removeFollowLink, 'click', this.removeFollow.bind(this));
        return removeFollowLink;
      }
    }
  }
});


LikeMe.Model.UserRankingGroup = Class.create(LikeMe.Model.RankingGroup, {

  registerRatable: function(user) {
    return Services.userService.register(user);
  }

});

LikeMe.Model.ToDoRankingGroup = Class.create(LikeMe.Model.RankingGroup, {

  ratableGroupIdObject: 'toDoGroupIds'

});

LikeMe.Page = Class.create({
  initialize: function(userJson) {
    Services.reset();
    Services.page = this;

    this.myViews = [];
    this.activeRatableAddWizard = null;
    this.registerCurrentUser(userJson);
    this._viewsForExpando = {};
    this.mainFlashError = new LikeMe.View.Flash($('flash-error'));
    this.mainFlashNotice = new LikeMe.View.Flash($('flash-notice'));
    this.setupIsAble();
    this.setupTooltip();
    this.setupTracking();
    this.setupButtons();
    this.trackingCookieName = 'url_to_track';
  },

  addView: function(view) {
    this.myViews.push(view);
    view.paint();
  },

  repaintAll: function() {
    this.myViews.each(function(view) {
      if(view.paint) view.paint();
    });
  },

  registerCurrentUser: function(userJson) {
    if (userJson) {
      Services.userService.register(userJson)
    }
  },

  toggleElement: function(anchor, element, hiddenText, visibleText) {
    anchor.innerHTML = (Element.visible(element)) ? hiddenText : visibleText;
    new Effect.toggle(element, 'blind', {
      duration: 0.2,
      queue: {position: 'end', scope: 'toggleElement'}
    });
  },

  focusOnFirstSensibleField: function() {
    for (var i = 0; i < document.forms.length; ++i) {
      var form = document.forms[i];
      if (!this.isSearchForm(form)) {
				this.focusErrorOrFirstFormField(form) 
				return;
      }
    }
    
    if ($('search_form')) {
			this.focusFirstInputField($('search_form'));
		}
  },

	focusErrorOrFirstFormField: function(form) {
		var errorElement = $(this.findFirstErrorElement(form));
    if (errorElement != null) {
      errorElement.activate();
    }
    else {
      form.focusFirstElement();
    }
	},
	
	focusFirstInputField: function(form) {
		Element.down(form, "input").activate();
	},

  isSearchForm: function(form) {
    return form.id == "search_form";
  },

  findFirstErrorElement: function(form) {
    var containingDiv = Element.down(form, ".fieldWithErrors");
    if (containingDiv) {
      return Element.firstDescendant(containingDiv);
    }
		return null;
  },

  tooltip: function() {
    return this._tooltip;
  },
  
  toggleChecked: function(togglebox, restrictTo) {
    if (!restrictTo) restrictTo = document.body;
    Element.getElementsBySelector($(restrictTo), '.checkbox').each(function(checkbox) {
      checkbox.checked = (togglebox.checked) ? true : false;
    });
  },

  importContacts: function(from, into) {
    var newAddresses = [];
    Element.getElementsBySelector($(from), '.checkbox').each(function(checkbox) {
      if (checkbox.checked) newAddresses.push(checkbox.value);
    });

    into = $(into);
    var currentAddresses = into.value.split(',');
    into.value = newAddresses.join(', ') + ', ' + currentAddresses.join(', ');
  },

  setupIsAble: function() {
    // adds a class to the html tag so we know it's capable
    var html = document.body.parentNode;
    if (!Element.hasClassName(html, 'HAS')) Element.addClassName(html, 'HAS AND-IS-ABLE');
  },

  setupTracking: function() {
    document.observe("dom:loaded", this.handleTrackingCookie.bind(this));
  },
  
  handleTrackingCookie: function() {
    // call trackUrl to submit analytics request, and delete the cookie
    var url = Cookie.get(this.trackingCookieName);
    if (!url) {
      return;
    }
    this.trackUrl(url);
    Cookie.unset(this.trackingCookieName);
  },

  trackUrl: function(url) {
    // invokes google analytics to track a dynamic request without an actual page load
    if(this.pageTracker) {
      decodedUrl = decodeURIComponent(url)
      this.pageTracker._trackPageview(decodedUrl);
    }
  },

  submitSearchForm: function() {
    var near_value = $F('near').strip();
    if (near_value != '') {
      this.gotoUrl(Routing.discover_near_path({
        near: near_value,
        category_plural_name: $F('category_plural_name')
      }));
    } else {
      Element.addClassName($('near'), 'required');
    }
    return false;
  },

  gotoUrl: function(url) {
    window.location = url;
  },

  setupButtons: function() {
    // Applies the AbleButtons to all the A, and INPUT elements with a given
    // classname, which you can customize here.
    AbleButton.initialize({className: 'able-button'});
  },

  setupTooltip: function() {
    this._tooltip = new AbleTooltip({tooltip: false});
  },

  logout: function() {
    var f = document.createElement('form');
    f.style.display = 'none';
    document.body.appendChild(f);
    f.method = 'POST';
    f.action = '/login';
    var m = document.createElement('input');
    m.setAttribute('type', 'hidden');
    m.setAttribute('name', '_method');
    m.setAttribute('value', 'delete');
    f.appendChild(m);
    f.submit();
  }
  
});

LikeMe.Page.getCurrentHostname = function() {
  return window.location.protocol + '//' + window.location.host;
};

LikeMe.Service.Geocoder = Class.create({
  initialize: function() {
    // window.google exists in windows-based chrome browsers for google gears functionality!
    // (window.google) alone doesn't mean what you think it means... sml @ 20090918
    if ( window.google && google && google.load ) {
        google.load("maps", "2");
        google.setOnLoadCallback(this.afterLoad.bind(this));
    }
  },

  afterLoad: function() {
    this.geocoder = new google.maps.ClientGeocoder();
  },

  GOOGLE_GEOCODE_SUCCESS: 200,
  GOOGLE_GEOCODE_SERVER_ERROR: 500,

  GEOCODE_SUCCESS: 0,
  GEOCODE_SERVER_ERROR: 1,
  GEOCODE_QUERY_FAILURE: 2,

  geocode: function(query, callback) {
    if (query.blank()) {
      callback(this.GEOCODE_SUCCESS, {});
    }
    else if (!this.geocoder) {
      callback(this.GEOCODE_SERVER_ERROR, null);
    }
    else {
      this.geocoder.getLocations(query, function(response) {
        var status = this._parseGeocodeStatus(response.Status.code);
        callback(status, (status == this.GEOCODE_SUCCESS) ? this.parseLocation(response) : null);
      }.bind(this));
    }
  },

  _parseGeocodeStatus: function(responseCode) {
    switch (responseCode) {
      case this.GOOGLE_GEOCODE_SUCCESS:
        return this.GEOCODE_SUCCESS;
      case this.GOOGLE_GEOCODE_SERVER_ERROR:
        return this.GEOCODE_SERVER_ERROR;
      default:
        return this.GEOCODE_QUERY_FAILURE;
    }
  },

  // Parameters:
  // @form: Form element to scrape query string(s) from, and plug results into.
  // @state: The current state of the object in question.  This must contain a key named
  //   'className' which corresponds to the type of object (eg className: 'user' for a user
  //   object).  The className key will be used as the element name prefix for field elements
  //   for all fields listed in the next two parameters.
  // @queryNames: An array of strings; each string is the <name> of an input element in the
  //   specified form containing a piece of the query string to geocode.  The geocoder will
  //   concatenate these values together, separated by spaces, to create the query string.
  //   These strings will be used as the inner piece of form field names using the format
  //   'prefix[inner_piece]'.  The className key of the state hash will be the prefix piece
  //   of the <name>.
  // @fieldNames: An array of strings; each string is the inner piece of <name> of an input
  //   element in the specified form which will receive the results of the geocoding.  Each
  //   name must be in the form 'prefix[inner_piece]'; the className key of the state
  //   parameter defines the prefix portion of the name.  Possible fieldName values are
  //   listed below (eg state, city).
  //
  processForm: function(form, state, queryNames, fieldNames, errorMessage) {
    Services.page.mainFlashError.clearErrors();
    if (!this.formHasChanges(form, state, queryNames)) {
      return true;
    }
    var query = this.concatenateFormFields(form, queryNames, state.className);
    this.geocode(query, function(status, location) {
      if (status == this.GEOCODE_SUCCESS) {
        this.setFormValue(form, fieldNames, state.className, 'street', location.street);
        this.setFormValue(form, fieldNames, state.className, 'city', location.city);
        this.setFormValue(form, fieldNames, state.className, 'state', location.state);
        this.setFormValue(form, fieldNames, state.className, 'zip', location.zip);
        this.setFormValue(form, fieldNames, state.className, 'country', location.countryCode);
        this.setFormValue(form, fieldNames, state.className, 'full_address', location.fullAddress);
        this.setFormValue(form, fieldNames, state.className, 'lat', location.lat);
        this.setFormValue(form, fieldNames, state.className, 'lng', location.lng);
        this.submitForm(form);
      } else if (status == this.GEOCODE_SERVER_ERROR) {
        this.clearResultFields(form, queryNames, fieldNames, state.className);
        this.submitForm(form);
      } else {
        Services.page.mainFlashError.addError(errorMessage, this.findElementsFromNames(form, queryNames, state.className));
        Services.page.focusOnFirstSensibleField();
      }
    }.bind(this));
    return false;
  },

  formHasChanges: function(form, state, queryNames) {
    return queryNames.any(function(queryName) {
      return ($F(form[state.className + '[' + queryName + ']']) != state[queryName])
    });
  },

  findElementsFromNames: function(form, elementNames, namePrefix) {
    var results = [];
    elementNames.each(function(elementName) {
      results.push(form[namePrefix + '[' + elementName + ']']);
    });
    return results;
  },

  concatenateFormFields: function(form, queryNames, namePrefix) {
    var queryElements = this.findElementsFromNames(form, queryNames, namePrefix);
    var query = '';
    queryElements.each(function(queryElement) {
      if (query.length != 0) { query += ' '; };
      query += queryElement.value;
    });
    return query;
  },

  clearResultFields: function(form, queryNames, fieldNames, namePrefix) {
    fieldNames.each(function(fieldName) {
      if (!queryNames.include(fieldName)) {
        form[namePrefix + '[' + fieldName +']'].value = '';
      }
    });
  },

  submitForm: function(form) {
    form.submit();
  },

  setFormValue: function(form, fieldNames, namePrefix, name, value) {
    if (fieldNames.include(name)) {
      var element = form[namePrefix + '[' + name +']'];
      if (element && value) {
        element.value = value;
      }
    }
  },

  parseLocation: function(response) {
    var location = {};
    var placemark = response.Placemark[0];

    location.fullAddress = this.parseFullAddress(placemark);
    location.countryCode = this.parseObjectFor(placemark, "CountryNameCode");
    location.lat = this.parseLatitude(placemark);
    location.lng = this.parseLongitude(placemark);
    location.state = this.parseObjectFor(placemark, "AdministrativeAreaName");
    location.city = this.parseObjectFor(placemark, "LocalityName");
    location.zip = this.parseObjectFor(placemark, "PostalCodeNumber");
    location.street = this.parseObjectFor(placemark, "ThoroughfareName");

    return location;
  },

  parseObjectFor: function(obj, searchValue) {
    var keys = Object.keys(obj).select(function(key){return !Object.isFunction(obj[key]) });
    for(var i = 0; i < keys.length; i++) {
      if(keys[i] == searchValue) {
        return obj[keys[i]];
      }
      else if(!this._isPrimative(obj[keys[i]])) {
        var result = this.parseObjectFor(obj[keys[i]], searchValue);
        if(result) {
          return result;
        }
      }
    }
    return false;
  },

  _isPrimative: function(obj) {
    return Object.isNumber(obj) || Object.isString(obj);
  },

  parseFullAddress: function(placemark) {
    return placemark.address;
  },

  parseLatitude: function(placemark) {
    return placemark.Point.coordinates[1];
  },

  parseLongitude: function(placemark) {
    return placemark.Point.coordinates[0];
  }

});

LikeMe.Service.RatableService = Class.create({
  initialize: function() {
    this._ratables = new Hash();
  },

  findById: function(id) {
    return this._ratables.get(id);
  },

  register: function(ratable) {
    var existing_ratable = this._ratables.get(ratable.id);
    if (existing_ratable) {
      Object.extend(existing_ratable, ratable);
    }
    else {
      this._ratables.set(ratable.id, new LikeMe.Model.Ratable(ratable));
    }
    return this._ratables.get(ratable.id);
  },

  registerAll: function(ratables) {
    for(var i = 0; i < ratables.length; i++) {
      ratables[i] = this.register(ratables[i]);
    }
    return ratables;
  }

})


LikeMe.Service.UserService = Class.create({
  initialize: function() {
    this._users = new Hash();
  },

  register: function(userJson) {
    var existing_user = this._users.get(userJson.id);
    if (existing_user) {
      Object.extend(existing_user, userJson);
    }
    else {
      var user = this._users.set(userJson.id, new LikeMe.Model.User(userJson));
      if (user.relation == "Self") {
        this._currentUser = user;
      }
    }
    return this._users.get(userJson.id);
  },

  registerAll: function(users) {
    for(var i = 0; i < users.length; i++) {
      users[i] = this.register(users[i]);
    }
    return users;
  },

  allUsers: function() {
    return this._users.values();
  },

  findById: function(id) {
    return this._users.get(id);
  },

  currentUser: function() {
    return this._currentUser;
  },

  loggedIn: function() {
    return !!this._currentUser;
  },

  logOut: function() {
    this._currentUser = undefined;
  }
});

LikeMe.View.Partial.ThumbnailFollow = {
  renderFollow: function(follow) {
    return div({className: 'follow'}, [
      img({className: 'thumbnail-photo thumbnail-photo-borders', src: follow.photoUrl('thumbnail')}),
      div({className: 'information'}, [
        h3(follow.name),
        h5(follow.genderAgeLocation())
      ])
    ]);
  }
}

LikeMe.View.Helpers = {

  renderColumns: function(dataList, columnContainer, columns, paintFunction) {
    var rowCount = 0;
    var perColumn = Math.ceil(dataList.length / columns);

    var column = div({className: 'column'});
    columnContainer.appendChild(column);
    dataList.each(function(data) {
      if (rowCount == perColumn) {
        column = div({className: 'column'});
        columnContainer.appendChild(column);
        rowCount = 0;
      }
      paintFunction(column, data);
      rowCount++;
    }.bind(this));
  },

  renderRows: function(dataList, rowContainer, columns, paintFunction) {
    var row = div({className: 'row'});
    var colCount = 0;
    rowContainer.appendChild(row);
    dataList.each(function(data) {
      if (colCount == columns) {
        row = div({className: 'row'});
        rowContainer.appendChild(row);
        colCount = 0;
      }
      paintFunction(row, data);
      colCount++;
    }.bind(this));
  }

}

LikeMe.View.Pagination = {
  ALL_LETTERS: ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],

  hasPreviousPage: function(currentOffset) {
    return currentOffset > 0;
  },

  hasNextPage: function(currentOffset) {
    return (currentOffset + this.perPage) <= this.maxOffset();
  },

  previousPageOffset: function(currentOffset) {
    var difference = currentOffset - this.perPage;
    return (difference <= 0) ? 0 : difference;
  },

  nextPageOffset: function(currentOffset) {
    var nextPage = currentOffset + this.perPage;
    return (nextPage > this.maxOffset()) ? currentOffset : nextPage;
  },
  
  offsetToPage: function(currentOffset) {
    return Math.floor(currentOffset / this.perPage) + 1;
  },

  shouldPaginate: function() {
    return this.maxOffset() >= this.perPage;
  },

  requiresLoad: function(offset) {
    var data = this.paginate(offset);
    for(var i = 0; i < data.length; i++) {
      if(!data[i]) {
        return true;
      }
    }
    return false;
  },

  paginate: function(offset) {
    return this.allItems().slice(offset, offset + this.perPage);
  },

  paginationPreviousLink: function(currentOffset, paintFunction) {
    var prevText = (this.paginationStyle == 'compact_letters') ? 'BACK' : '&nbsp;'
    if (this.hasPreviousPage(currentOffset)) {
      return a({className: 'prev_page', href: '#', onclick: function() { paintFunction(this.previousPageOffset(currentOffset)); return false; }.bind(this) }, prevText);
    } else {
      return a({className: 'disabled prev_page'}, prevText);
    }
  },

  paginationNextLink: function(currentOffset, paintFunction) {
    var nextText = (this.paginationStyle == 'compact_letters') ? 'NEXT' : '&nbsp;'
    if (this.hasNextPage(currentOffset)) {
      return a({className: 'next_page', href: '#', onclick: function() { paintFunction(this.nextPageOffset(currentOffset)); return false; }.bind(this) }, nextText);
    } else {
      return a({className: 'disabled next_page'}, nextText);
    }
  },

  paintPagination: function (paginationElement, currentOffset, paintFunction) {
    if (!this.shouldPaginate()) return;

    if(this.paginationStyle == 'compact_letters') {

        this.paintLetters(paginationElement, paintFunction);
        paginationElement.appendChild(div({className: 'compact-arrows'}, [
          this.paginationPreviousLink(currentOffset, paintFunction),
          ' | ',
          this.paginationNextLink(currentOffset, paintFunction)
        ]));

    } else {

      paginationElement.appendChild(this.paginationPreviousLink(currentOffset, paintFunction));

      if(this.paginationStyle == 'letters') {
        this.paintLetters(paginationElement, paintFunction);
      } else if (this.paginationStyle == 'numbers') {
        this.paintNumbers(paginationElement, paintFunction, this.offsetToPage(currentOffset));
      } else {
        throw "paginationStyle not supported.";
      }

      paginationElement.appendChild(this.paginationNextLink(currentOffset, paintFunction));

    }

  },

  paintLetters: function(paginationElement, paintFunction) {
    this.ALL_LETTERS.each(function(letter) {
      if(this.letterMap[letter] != undefined) {
        var link = a({href: '#', onclick: function() { paintFunction(this.letterMap[letter]); return false; }.bind(this) }, letter);
        paginationElement.appendChild(link);
      }
      else {
        paginationElement.appendChild(a({className: 'disabled'}, letter));
      }
    }.bind(this));
  },

  paintNumbers: function(paginationElement, paintFunction, currentPage) {
    var numPages = Math.ceil((this.maxOffset() + 1) / this.perPage);
    $R(1,numPages).each(function(pageNumber) {
      var link = a({href: '#', onclick: function() { paintFunction((pageNumber - 1) * this.perPage); return false; }.bind(this) }, pageNumber);
      if(currentPage == pageNumber){
        link.className = 'current';
      }  
      paginationElement.appendChild(link);
    }.bind(this));
  }
}

LikeMe.View.Autocompleter = Class.create({

  initialize: function(fieldId, options) {
    this.textInput = $(fieldId);
    this.id = this.textInput.id;
    this.suggestions = [];
    this.setOptions(options);
    this.injectSuggestBehavior();
    this.lastSearch = ''

    if(!this.options.searcher) {
      this.searcher = new LikeMe.View.Autocompleter.AjaxSearcher(this.options);
    } else {
      this.searcher = this.options.searcher;
    }

    this.searcher.setCallbacks({
      onLoading: this.onLoading.bind(this),
      onDoneLoading: this.onDoneLoading.bind(this),
      processResults: this.processResults.bind(this),
      onServerFailure: this.options.onServerFailure
    });
//    This hiding on blur is flaky due to the blur event happening before the click even sometimes when they are trying to select an item.  Then the click event never gets fired.
//    Event.observe(this.textInput, 'blur', this.hideSuggestions.bind(this));
  },

  setOptions: function(options) {
    this.options = {
      useInputForStatus: true,
      suggestDivClassName: 'autocomplete-dropdown',
      suggestionClassName: 'suggestion',
      selectedClassName: "selected",
      matchClassName: 'match',
      matchTextWidth: true,
      count: 20,
      delay: 400,
      minChars: 3,
      positionedParentId: null,
      url: "/ajax/show_suggestions",
      additionalParameters: "",
      onSelectionUpdated: function(selection) {},
      onItemChosen: function(selection) {},
      onItemChosenByClick: function(selection) {},
      onNothingSelected: function() {},
      onUnknownSelection: function(text) {},
      onServerFailure: function(response) {},
      cancelSearch: function(text) {return false},
      messageContents: function(count, text) {}
    };

    Object.extend(this.options, options || {});

    var spin = img({src:'/images/ajax-loader-small.gif', style:"display:none"});
    Element.hide(spin);
    this.spinner = this.textInput.parentNode.insertBefore(spin, this.textInput.nextSibling);
  },

  destroy: function() {
    this.testSuggestKeyHandler.destroy();
    this.destroySuggestionsDiv();
  },

  clearCache: function() {
    this.searcher.clearCache();
  },

  injectSuggestBehavior: function() {
    if (LikeMe.View.Utils.isIE) {
      this.textInput.autocomplete = "off";
    } else {
      this.textInput.setAttribute("autocomplete","OFF");
    }
    this.testSuggestKeyHandler = new LikeMe.View.Autocompleter.TextSuggestKeyHandler(this);
    this.createSuggestionsDiv();
  },

  createSuggestionsDiv: function() {
    this.suggestionsDiv = document.createElement("div");
    this.suggestionsDiv.className = this.options.suggestDivClassName;
    var divStyle = this.suggestionsDiv.style;
    divStyle.position = 'absolute';
    divStyle.display = 'none';
    this.textInput.parentNode.appendChild(this.suggestionsDiv);
  },

  destroySuggestionsDiv: function() {
    if(this.suggestionsDiv.parentNode) {
      this.suggestionsDiv.parentNode.removeChild(this.suggestionsDiv);
    }
    delete this.suggestionsDiv;
  },

  positionSuggestionsDiv: function() {
    var textPos = Element.cumulativeOffset(this.textInput);
    var parentPos = {left:0, top:0}
    if (this.options.positionedParentId != null) {
      parentPos = Element.cumulativeOffset($(this.options.positionedParentId));
    }

    var divStyle = this.suggestionsDiv.style;
    divStyle.top = ((textPos.top - parentPos.top) + LikeMe.View.Utils.heightWithBorders(this.textInput)) + "px";
    divStyle.left = "0px";

    if (this.options.matchTextWidth) {
      divStyle.width = LikeMe.View.Utils.width(this.textInput) + "px";
    }
  },

  updateSuggestionsDiv: function() {
    this.suggestionsDiv.innerHTML = "";
    var suggestLines = this.createSuggestionElements();
    for (var i=0; i<suggestLines.length; ++i) {
      this.suggestionsDiv.appendChild(suggestLines[i]);
    }
    var messageElement = this.options.messageContents(suggestLines.length, this.getTextInputValue());
    if (messageElement != null) {
      this.appendMessageSpan(messageElement);
    }
    this.suggestionsDiv.style.height = (suggestLines.length > 10) ? "180px" : "auto";
  },

  appendMessageSpan: function(messageElement) {
    this.suggestionsDiv.appendChild(this.createMessageSpan(messageElement));
  },
	
  createMessageSpan: function(messageElement) {
    return div({className: 'suggestion-message', notSelectable: true}, [messageElement]);
  },

  createSuggestionElements: function() {
    var suggestionSpans = [];
    for (var i=0; i<this.suggestions.length; ++i) {
      suggestionSpans.push(this.createSuggestionElement(i));
    }
    return suggestionSpans;
  },

  createSuggestionElement: function(n) {
    var suggestion = this.suggestions[n];
    var suggestionElement = div({className: this.options.suggestionClassName});
    Event.observe(suggestionElement, 'mouseover', this.mouseoverHandler.bindAsEventListener(this));
    Event.observe(suggestionElement, 'click', this.itemClickHandler.bindAsEventListener(this));

    // Simpler, no splitting or highlighting.
    suggestionElement.innerHTML = suggestion.display ? suggestion.display : suggestion.text;
    return suggestionElement;
  },

  handleTextInput: function(newString) {
    if (newString == "") {
      this.clearSuggestions();
    }

    if (newString.indexOf(this.lastSearch) < 0) {
      this.lastSearch = newString
      this.searcher.loading = false;
      this.hideSuggestions();
    }

    if (newString.length >= this.options.minChars && !this.options.cancelSearch(newString)) {
      this.lastSearch = newString
      this.searcher.search(newString);
    }
  },

  processResults: function(results) {
    this.suggestions = results;
    if (this.suggestions.length == 0 || this.getTextInputValue() == "") {
      this.clearSuggestions();
    } else if (this.isExactMatch()) {
      this.hideSuggestions();
      this.updateSuggestionsDiv();
      this.updateSelection();
    } else {
      this.updateSuggestionsDiv();
      this.showSuggestions();
      this.updateSelection();
    }
    return true;
  },

  isExactMatch: function() {
    for( var i in this.suggestions ) {
      if ( i == parseInt(i) && this.suggestions[i].text.toLowerCase() == this.getTextInputValue().toLowerCase() ) {
        this.textInput.value = this.suggestions[i].text;
        return true;
      }
    }
    return false;
  },

  clearSuggestions: function() {
    this.hideSuggestions();
    if (this.getTextInputValue().length == 0) {
      this.options.onNothingSelected();
    } else {
      this.options.onUnknownSelection(this.getTextInputValue());
    }
  },

  padding: function() {
    return 0;
  },

  moveSelectionUp: function() {
    if (this.selectedItem && this.selectedItem.previous() && this.suggestionsVisible()) {
      this.updateSelection(this.selectedItem.previous());
      this.suggestionsDiv.scrollTop = this.selectedItem.offsetTop;
    }
  },

  moveSelectionDown: function() {
    var nextItem = this.getNextItem();
    if (nextItem && this.suggestionsVisible() && !nextItem.notSelectable) { //some property
      this.updateSelection(nextItem);
      this.suggestionsDiv.scrollTop = this.selectedItem.offsetTop;
    }
  },

  getNextItem: function() {
    if (this.selectedItem) {
      return this.selectedItem.next();
    }
    else {
      return this.getFirstItem();
    }
  },

  getFirstItem: function() {
    return Element.childElements(this.suggestionsDiv)[0];
  },

  updateSelection: function(newItem) {
    var oldSelection = this.selectedItem;
    if (oldSelection) {
      Element.removeClassName(oldSelection, this.options.selectedClassName);
    }
    this.selectedItem = newItem;
    if (this.selectedItem) {
      Element.addClassName(this.selectedItem, this.options.selectedClassName);
      this.currentSuggestion = this.suggestions[this.selectedItem.previousSiblings().length];
      this.options.onSelectionUpdated(this.currentSuggestion);
    } else {
      this.currentSuggestion = null;
      this.options.onNothingSelected();
    }
  },

  setInputFromSelection: function() {
    if(this.currentSuggestion  && this.suggestionsVisible()) {
      this.textInput.value = this.currentSuggestion.text.unescapeHTML();
      this.textInput.blur();
      this.textInput.focus();
    }
  },

  getTextInputValue: function() {
    return this.textInput.value;
  },

  mouseoverHandler: function(e) {
    var src = e.srcElement ? e.srcElement : e.target;
    this.updateSelection(src);
  },

  itemClickHandler: function(e) {
    this.mouseoverHandler(e);
    this.setInputFromSelection();
    this.hideSuggestions();
    this.textInput.focus();
    this.options.onItemChosenByClick(this.currentSuggestion);
  },

  showSuggestions: function() {
    if (this.suggestionsVisible() ||
      this.getTextInputValue().length == 0) {
      return;
    }
    this.positionSuggestionsDiv();
    Element.show(this.suggestionsDiv);
    this.suggestionsDiv.scrollTop = 0;
  },

  suggestionsVisible: function() {
    return Element.visible(this.suggestionsDiv);
  },

  hideSuggestions: function() {
    Element.hide(this.suggestionsDiv);
  },

  onLoading: function() {
    if (this.options.useInputForStatus) {
      Element.addClassName(this.textInput, 'autocomplete-loading');
    } else {
      Element.show(this.spinner);
    }
  },
  
  onDoneLoading: function() {
    if (this.options.useInputForStatus) {
      Element.removeClassName(this.textInput, 'autocomplete-loading');
    } else {
      Element.hide(this.spinner);
    }
  }
});

LikeMe.View.Autocompleter.TextSuggestKeyHandler = Class.create({
  initialize: function(textSuggest) {
    this.lastKeyHandled_AppleJSBugWorkaround = null;
    this.textSuggest = textSuggest;
    this.input = this.textSuggest.textInput;
    this.addKeyHandling();
  },

  destroy: function() {
    Event.stopObserving(this.input, "keydown", this.keyDownHandlerFunction);
    Event.stopObserving(this.input, "keypress", this.keyPressHandlerFunction);
    Event.stopObserving(this.input, "keyup", this.keyUpHandlerFunction);        
  },

  addKeyHandling: function() {
    this.keyDownHandlerFunction = this.keyDownHandler.bindAsEventListener(this);
    this.keyPressHandlerFunction = this.keyPressHandler.bindAsEventListener(this);
    this.keyUpHandlerFunction = this.keyUpHandler.bindAsEventListener(this);
    Event.observe(this.input, "keydown", this.keyDownHandlerFunction);
    Event.observe(this.input, "keypress", this.keyPressHandlerFunction);
    Event.observe(this.input, "keyup", this.keyUpHandlerFunction);
  },

  keyDownHandler: function(e) {
    //if (new Date() - this.lastKeyHandled_AppleJSBugWorkaround < 20) {
    //  return;
    //}
    if (e.keyCode == Event.KEY_UP) {
      this.textSuggest.moveSelectionUp();
      this.textSuggest.setInputFromSelection();
    } else if (e.keyCode == Event.KEY_DOWN) {
      this.textSuggest.moveSelectionDown();
      this.textSuggest.setInputFromSelection();
    } else if (e.keyCode == Event.KEY_ESC) {
      this.textSuggest.hideSuggestions();
      Event.stop(e);
    }
    //this.lastKeyHandled_AppleJSBugWorkaround = new Date();
  },

  keyPressHandler: function(e) {
    if ( !this.handledSpecialKeys(e)) {
      if(this.isCharacterInput(e)) {
        if(this.observer) clearTimeout(this.observer);
        this.observer = setTimeout(function() {
            this.textSuggest.handleTextInput(this.textSuggest.getTextInputValue());
          }.bind(this), this.textSuggest.options.delay);
      }
    }
  },

  keyUpHandler: function(e) {
    if (this.input.length == 0) {
      this.textSuggest.hideSuggestions();
    }
  },

  isCharacterInput: function(e) {
    if(LikeMe.View.Utils.isIE || LikeMe.View.Utils.isOpera) {
      return true;
    } else {
      return e.charCode != 0;
    }
  },

  inputChar: function(e) {
    if(LikeMe.View.Utils.isIE || LikeMe.View.Utils.isOpera) {
      return String.fromCharCode(e.keyCode);
    } else {
      return String.fromCharCode(e.charCode);
    }
  },

  handledSpecialKeys: function(e) {
    if (e.keyCode == Event.KEY_UP || e.keyCode == Event.KEY_DOWN || e.keyCode == Event.KEY_ESC || e.keyCode == Event.KEY_RIGHT || e.keyCode == Event.KEY_LEFT) {
      if(e.keyCode == Event.KEY_ESC) {
        Event.stop(e);
      }
      return true;
    }
    else if (e.keyCode == Event.KEY_RETURN || e.keyCode == Event.KEY_TAB) {
      if (this.textSuggest.suggestionsVisible()) {
        Event.stop(e); 
      }
      if (this.textSuggest.lastChosen != this.textSuggest.currentSuggestion) {
        this.textSuggest.options.onItemChosen(this.textSuggest.currentSuggestion, e);
        this.textSuggest.hideSuggestions();
        this.textSuggest.lastChosen = this.textSuggest.currentSuggestion;
      }
      return true;

    }
    return false;
  },

  moveCaretToEnd: function() {
    var pos = this.input.value.length;
    if (this.input.setSelectionRange) {
      this.input.setSelectionRange(pos,pos);
    }
    else if (this.input.createTextRange) {
      var m = this.input.createTextRange();
      m.moveStart('character', pos);
      m.collapse();
      m.select();
    }
  }

});

LikeMe.View.Autocompleter.AjaxSearcher = Class.create({
  initialize: function(options) {
    this.options = options;
    this.cachedResults = {};
    this.loading = false;
  },

  setCallbacks: function(callbacks) {
    this.callbacks = callbacks;
  },

  search: function(string) {
    if (this.cachedResults[string.toLowerCase()] != null) {
      this.callbacks.processResults(this.cachedResults[string.toLowerCase()]);
    } else if (!this.loading) {
      this.loading = true;
      this.sendRequestForSuggestions(string);
    }
  },

  sendRequestForSuggestions: function(string) {
    var parameters = "count=" + this.options.count + "&query=" + encodeURIComponent(string) + this.options.additionalParameters;
    this.callbacks.onLoading();
    new Ajax.Request(this.options.url, {
      onSuccess: this.onAjaxSuccess.bind(this),
      onFailure: this.onAjaxFailure.bind(this),
      parameters: parameters
    });
  },

  onAjaxFailure: function(response) {
    this.loading = false;
    this.callbacks.onDoneLoading();
    this.callbacks.onServerFailure(response);
  },

  onAjaxSuccess: function(response) {
    this.loading = false;
    this.callbacks.onDoneLoading();
    eval("var jsonResults= " + response.responseText);
    var key = jsonResults["query"].toLowerCase();
    this.cachedResults[key] = jsonResults["response"];
    this.callbacks.processResults(jsonResults["response"]);
  },

  clearCache: function() {
    this.cachedResults = {};
  }
});

LikeMe.View.BulkImageUploader = Class.create({

  initialize: function(hookElement, url, checkboxForm, checkboxSelector) {
    this.hookElement = hookElement;
    this.url = url;
    this.checkboxForm = checkboxForm;
    this.checkboxSelector = checkboxSelector;
  },

  paint: function() {
    this.hookElement.appendChild(this.submitForm = form({
      action: this.url,
      enctype: 'multipart/form-data',
      method: 'post'
    }, [
      this.fileInput = input({type: 'file', name:'photo[file]', style: {cssFloat: 'left', marginRight: '10px'}}),
      this.uploadButton = a({href: "#", onclick: this._prepareBulkPhotoUploadForm.bind(this)}, 'Upload')
    ]));
    this.uploadButton = AbleButton.applyTo(this.uploadButton, 'able-button blue');
  },

  _inputHasContents: function() {
    return !!this.fileInput.value;
  },

  _prepareBulkPhotoUploadForm: function() {
    if(!this.submitted && this._inputHasContents()) {
      var checkedBoxes = Form.getInputs(this.checkboxForm, 'checkbox').select(function(box){
        return (box.checked && box.name == this.checkboxSelector);
      }.bind(this));
      checkedBoxes.each(function(box) {
        this.submitForm.appendChild(
          input({type: 'hidden', name: 'ratable_ids[]', value: box.value})
        );
      }.bind(this));
      this.submitForm.submit();
      this.submitted = true;
    }
    return false;
  }
});

LikeMe.View.ItemSelector = Class.create(LikeMe.View.Helpers, {

  initialize: function(hookElement, options) {
    this.options = options || {};
    this.hookElement = $(hookElement);
    this.selectedItems = [];
    this.currentScroll = 0;
    if(this.options.fetchUrl) {
      LikeMe.Command.Generic.send({
        url: this.options.fetchUrl,
        method: 'get',
        callback: this.loadItems.bind(this)
      });
    } else {
      this.loadItems([]);
    }
  },

  loadItems: function(items) {
    this.items = this.options.itemService.registerAll(items);
    if(this.options.additionalItems) {
      this.items = this.items.concat(this.options.itemService.registerAll(this.options.additionalItems)).uniq();
      delete this.options.additionalItems;
    }
    if(this.itemSorter) {
      this.items.sort(this.itemSorter);
    }
    if(this.options.selectedItemIds) {
      this.selectedItems = this.options.selectedItemIds.collect(function(id) {return this.options.itemService.findById(id)}.bind(this));
      delete this.options.selectedItemIds;
    }
    if(this.options.beforeInitialPaint) {
      this.options.beforeInitialPaint();
    }
    this.paint();
    if(this.options.afterInitialPaint) {
      this.options.afterInitialPaint();
    }
  },

  getTitle: function() {
    return this.options.title;
  },

  itemsToPaint: function() {
    return this.items;
  },

  paint: function() {
    if(this.options.beforePaint) {this.options.beforePaint();}
    if(this.itemsElement) {
      this.currentScroll = this.itemsElement.scrollTop;
    }
    this.hookElement.innerHTML = '';
    if(this.options.beforeItemsPaint) {this.options.beforeItemsPaint(this.hookElement);}
    this.hookElement.appendChild(this.itemsElement = div({className: this.options.containerClassName}));
    var itemsToUse = this.itemsToPaint();
    this.renderRows(itemsToUse, this.itemsElement, 2, this.paintItem.bind(this));
    if(this.options.afterItemsPaint) {this.options.afterItemsPaint(this.hookElement);}
    this.hookElement.appendChild(div({className: 'clear'}));
    this.itemsElement.scrollTop = this.currentScroll;
  },

  paintItem: function(container, item) {
    var itemDiv = this.renderItem(item);
    if(this.selectedItems.include(item)) {
      Element.addClassName(itemDiv, 'selected');
    }
    container.appendChild(itemDiv);
    Event.observe(itemDiv, 'click', function() {this.toggleSelected(item)}.bind(this));
  },

  toggleSelected: function(item) {
    if(this.selectedItems.include(item)) {
      this.selectedItems = this.selectedItems.without(item);
    } else {
      if(this.options.maxSelected && this.selectedItems.length >= this.options.maxSelected) {
        return;
      }
      this.selectedItems.push(item);
      if(this.itemSorter) {
        this.selectedItems.sort(this.itemSorter);
      }
    }
    this.paint();
  }

});

LikeMe.View.FollowSelector = Class.create(LikeMe.View.ItemSelector, LikeMe.View.Partial.ThumbnailFollow, {

  initialize: function($super, options) {
    options = Object.extend(options || {}, {
      fetchUrl: Routing.people_path({user_id: Services.userService.currentUser().toParam}),
      itemService: Services.userService,
      containerClassName: 'scrolling-follow-list',
      beforeInitialPaint: this.beforeInitialPaint.bind(this),
      afterInitialPaint: this.afterInitialPaint.bind(this)
    });
    var hookElement = div({id: 'modalbox_loading', className:'suggest-follow-modal'});
    $super(hookElement, options);
    this.showModalBox();
  },

  beforeInitialPaint: function() {
    this.hookElement.style.visibility = 'hidden';
  },

  afterInitialPaint: function() {
    this.hookElement.id = '';
    Modalbox.resizeToContent({afterResize: function() {this.hookElement.style.visibility = 'visible';}.bind(this)});
  },

  itemSorter: function(l, r) {
    var ln = l.name.toLowerCase(), rn = r.name.toLowerCase()
    return ln < rn ? -1 : ln > rn ? 1 : 0;
  },

  showModalBox: function() {
    Modalbox.show(this.hookElement, {title: this.getTitle(), height: 267, width: 700, disableSpinner: true});
  },
  
  close: function() {
    Modalbox.hide();
  },

  renderItem: function(item) {
    return this.renderFollow(item);
  }

});





LikeMe.View.DiscoverAs = Class.create(LikeMe.View.Partial.ThumbnailFollow, {

  initialize: function(options) {
    this.options = options;
    this.paint();
    this.showModalBox();
  },

  showModalBox: function() {
    Modalbox.show(this.hookElement, {title: 'Search Like This Person', height: 210, width: 600, disableSpinner: true});
  },

  close: function() {
    Modalbox.hide();
  },

  paint: function() {
    this.hookElement = div({className: 'discover-as'}, [
      div({style: {position: 'relative'}}, [
        textNode('Find '),
        this.categoryField = select({name: 'category_plural_name'}, LikeMe.Model.Category.categories.collect(function(category) {
          return option({value: category.pluralName, text: category.pluralValue});
        })),
        textNode(' Near '),
        this.nearContainer = span({style: {position: 'relative'}}, [this.nearField = input({name: 'near', type: 'text', value: $F("near")})])
      ]),
      div({style: {cssFloat: 'left'}}, [
        br(),
        h5('Searching Like: '),
        this.renderFollow(this.options.user)
      ]),
      div({className: 'buttons right', style: {paddingTop: '10px'}}, [
        this.submitButton = AbleButton.applyTo(a({href: '#', onclick: this.submit.bind(this) }, 'View Results'), 'able-button blue')
      ]),
      div({className: 'clear'}),
      this.errorsDiv = LikeMe.View.Flash.errorDiv()
    ]);

    this.errorsFlash = new LikeMe.View.Flash(this.errorsDiv, {useVisibility: true});
    
    new LikeMe.View.Autocompleter(this.nearField, {
      url: Routing.city_state_autocompletions_path(),
      positionedParentId: this.nearContainer,
      delay: 50
    });
  },
  
  submit: function() {
    this.errorsFlash.clearErrors();
    if(this.nearField.value.strip()) {
      Services.page.gotoUrl(Routing.discover_near_path({
        category_plural_name: $F(this.categoryField),
        near: this.nearField.value,
        'search[as_users][]': this.options.user.toParam
      }));
    } else {
      this.errorsFlash.addError('Please enter a location');
    }
    return false;
  }


});

LikeMe.View.DiscoverMultiple = Class.create(LikeMe.View.FollowSelector, {
  
  initialize: function($super, options) {
    var defaultOptions = {title: "Search Like Friends"};
    options = Object.extend(defaultOptions, options || {});
    if(options.selectedItemIds && Services.userService.currentUser()) {
      if(options.selectedItemIds.include(Services.userService.currentUser().id)) {
        options.selectedItemIds = options.selectedItemIds.without(Services.userService.currentUser().id);
        this.oldSearchAsSelf = true;
      }
    }
    $super(Object.extend(options, {
      beforePaint: this.beforePaint.bind(this),
      beforeItemsPaint: this.paintTop.bind(this),
      afterItemsPaint: this.paintBottom.bind(this),
      maxSelected: 3
    }));
  },
  
  beforePaint: function() {
    this.options.nearText = this.nearField ? this.nearField.value : (this.options.nearText || '');
    this.options.categoryIndex = this.categoryField ? this.categoryField.selectedIndex : this.options.categoryIndex;
    this.oldSearchAsSelf = this.searchAsSelfField ? this.searchAsSelfField.checked : this.oldSearchAsSelf;
  },
  
  paintTop: function(hookElement) {  
    hookElement.appendChild(div({className: 'discover-as'}, [
      div({style: {position: 'relative'}}, [
        textNode('Find '),
        this.categoryField = select({name: 'category_plural_name', selectedIndex: this.options.categoryIndex}, LikeMe.Model.Category.categories.collect(function(category) {
          return option({value: category.pluralName, text: category.pluralValue});
        })),
        textNode(' Near '),
        this.nearContainer = span({style: {position: 'relative'}}, [this.nearField = input({name: 'near', value: this.options.nearText, type: 'text'})]),
        br(),
        textNode(' Recommended By People Who Are:')
      ]),
      div({}, [
        span({className: 'like-me'}, 'Like Me'),
        this.searchAsSelfField = input({type: 'checkbox'}),
        span({className: 'blue', style: {paddingLeft: '10px'}}, ' And / Or '),
        textNode('(Select up to three friends below to get personalized results like them)')
      ]),
      div({className: 'hr'})
    ]));
    this.searchAsSelfField.checked = this.oldSearchAsSelf;
    new LikeMe.View.Autocompleter(this.nearField, {
      url: Routing.city_state_autocompletions_path(),
      positionedParentId: this.nearContainer,
      delay: 50
    });
  },
  
  paintBottom: function(hookElement) {
    hookElement.appendChild(div({className: 'clear'}));
    hookElement.appendChild(this.errorsDiv = LikeMe.View.Flash.errorDiv());
    hookElement.appendChild(
      div({className: 'buttons right', style: {paddingTop: '10px'}}, [
        this.submitButton = AbleButton.applyTo(a({href: '#', onclick: this.submit.bind(this) }, 'View Results'), 'able-button blue')
      ])
    );

    this.errorsFlash = new LikeMe.View.Flash(this.errorsDiv, {useVisibility: true});
  },

  usersToSearchAs: function() {
    var users = this.searchAsSelfField.checked ? [Services.userService.currentUser()] : [];
    return users.concat(this.selectedItems);
  },
  
  submit: function() {
    this.errorsFlash.clearErrors();
    var errors = false;
    var userParams = this.usersToSearchAs().collect(function(user) {return user.toParam});
    if(!this.nearField.value.strip()) {
      errors = true;
      this.errorsFlash.addError('Please enter a location.');
    }
    if(userParams.length == 0) {
      errors = true;
      this.errorsFlash.addError('Please select a user to search as.');
    }
    if(!errors) {
      Services.page.gotoUrl(Routing.discover_near_path({
        category_plural_name: $F(this.categoryField),
        near: this.nearField.value,
        search: {as_users: userParams}
      }));
    }
    return false;
  }
  
});


LikeMe.View.PhotoScroller = Class.create({

  initialize: function(hookElement, options) {
    if (hookElement.id == null) {
      throw "Cannot create a photo scroller without an id";
    }

    this.hookElement = hookElement;
    this.id = this.hookElement.id;

    this.page = 1;
    this.views = [];
    this.options = this.defaultOptions();
    Object.extend(this.options, options || {});

    this.nextPageFunction = this.nextPage.bindAsEventListener(this);
    this.previousPageFunction = this.previousPage.bindAsEventListener(this);
    this.moreLinkFunction = this.moreLink.bindAsEventListener(this);
    this.nextPageImageElement = null;

    if ( this.options['useRingArrows'] ) {
      this.arrowInfo = this.arrowRingInfo;
    }
  },

  defaultOptions: function() {
    return {
      endless: false,
      itemsPerRow: 6,
      isShowAll: false,
      rowCount: 1,
      containerClassName: "",
      cellPadding: 5,
      name: "Recommendations",
      expandRowsLink: "Show All",
      titleSize: '5',
      moreLink: null,
      useRingArrows: false
    };
  },

  title: function() {
    return this.options.title;
  },

  singularTitle: function() {
    return this.options.singularTitle;
  },

  destroy: function() {
    this.clearContent();
  },

  clearContent: function() {
    this.hookElement.innerHTML = "";
    this.views.each(function( view ) {
      view.destroy();
    });
    this.views = [];
  },

  paint: function() {
    if (this.numItems() == 0) {
      this.clearContent();
      this._paintEmptyScroller();
      return true;
    }

    this.verifyCurrentPageExists();

    if(this.requiresLoadForPage(this.page)) {
      this.loadPage(this.page, true);
      return false;
    }

    this.clearContent();
    this.bodyContainer = div({className: this.options.containerClassName});
    this.bodyContainer.style.position = "relative";
    this.resizeScroller();

    this._paintSecondaryLinks();

    this._paintTitle();

    this.hookElement.appendChild(this.bodyContainer);

    this._paintPreviousPageLink();

    this._paintItems();

    this._paintNextPageLink();

    this.bodyContainer.appendChild(div({className:"clear"}, [br({clear: 'both'})]));

    return true;
  },

  _paintEmptyScroller: function() {
  },

  clickExpandRows: function() {
    var previousRowCount = this.numRows();
    this.options.isShowAll = !this.options.isShowAll;
    this.recalculatePage(previousRowCount);
    this.paint();
  },

  clickSearchLink: function() {
    // We only do this for Events right now.  Add to this.options later if you need other categories.
    Services.page.gotoUrl(Routing.discover_near_path({near: this.options.searchNear , category_plural_name: 'events'}));
  },

  recalculatePage: function(previousRowCount) {
    var first_item_index = (this.page - 1) * previousRowCount * this.options.itemsPerRow + 1;
    this.page = Math.floor( ((first_item_index - 1) / (this.options.itemsPerRow * this.numRows())) + 1);
  },

  _setupSecondaryLinks: function() {
    var links = [];
    if ((this.numPages() > 1 || this.options.isShowAll) && !this.options.endless) {
      this.expandRowsLink = a({className:"clickable"}, this.options.isShowAll ? "Hide" : this.options.expandRowsLink);
      Event.observe(this.expandRowsLink, 'click', this.clickExpandRows.bindAsEventListener(this));
      links.push(this.expandRowsLink);
    } else {
      delete this.expandRowsLink;
    }
    if (this.options.searchLink) {
      this.searchLink = a({className:"clickable"}, this.options.searchLink);
      Event.observe(this.searchLink, 'click', this.clickSearchLink.bindAsEventListener(this));
      links.push(this.searchLink);
    }

    return links;
  },

  _paintSecondaryLinks: function() {
    var links = this._setupSecondaryLinks();
    if (links != null) {
      var linkDiv = div({className:"secondary-links"});
      for (var i=0; i<links.length; ++i) {
        if (i>0) {
          linkDiv.appendChild(document.createTextNode(" | "));
        }
        linkDiv.appendChild(links[i]);
      }
      this.hookElement.appendChild(linkDiv);
    }
  },

  _paintTitle: function() {
    var title = (this.title() != '') ? this.title() : '&nbsp;';

    if( this.options.titleSize == '2' ) this.hookElement.appendChild(h2(title, this.helpText()));
    else                                this.hookElement.appendChild(h5(title, this.helpText()));
  },

  helpText: function() {
    var helpText = [];
    if (this.options.helpText) {
      helpText = [span({className: 'hoverable'}, [
        span("&nbsp;&nbsp;What's This?"),
        span({className: 'tooltip'}, [
          h6(this.options.helpText)
        ])
      ])];
    }
    return helpText;
  },

  arrowInfo: {
    leftArrowSrc: "/images/arrow-left.gif",
    rightArrowSrc: "/images/arrow-right.gif",
    rightArrowMoreSrc: "/images/arrow-ring-seeall.gif",
    rightArrowDisabledSrc: "/images/arrow-right-disabled.gif",
    leftArrowDisabledSrc: "/images/arrow-left-disabled.gif",
    hOffset: -18,
    height: 16
  },

  arrowRingInfo: {
    leftArrowSrc: "/images/arrow-ring-left.gif",
    rightArrowSrc: "/images/arrow-ring-right.gif",
    rightArrowMoreSrc: "/images/arrow-ring-seeall.gif",
    rightArrowDisabledSrc: "/images/arrow-right-disabled.gif",
    leftArrowDisabledSrc: "/images/arrow-left-disabled.gif",
    hOffset: -30,
    height: 44
  },
 
  _paintPreviousPageLink: function() {
    if (this.hasPreviousPage()) {
      this.previousPageImageElement = img({src: this.arrowInfo.leftArrowSrc, className:"clickable arrow-left"});
      this.previousPageImageElement.style.position = "absolute";
      this.previousPageImageElement.style.left = this.arrowInfo.hOffset + "px";
      this.previousPageImageElement.style.top = (Math.ceil(this.scrollerHeight()/2) - this.arrowInfo.height/2) + "px";
      Event.observe(this.previousPageImageElement, 'click', this.previousPageFunction);
      this.bodyContainer.appendChild(this.previousPageImageElement);
    } else {
      this.previousPageImageElement = null;
    }
  },

  _paintNextPageLink: function() {
    if (this.hasNextPage()) {
      this.nextPageImageElement = img({src: this.arrowInfo.rightArrowSrc, className: "clickable arrow-right"});
      this.nextPageImageElement.style.position = "absolute";
      this.nextPageImageElement.style.right = this.arrowInfo.hOffset + "px";
      this.nextPageImageElement.style.top = (Math.ceil(this.scrollerHeight()/2) - this.arrowInfo.height/2) + "px";
      Event.observe(this.nextPageImageElement, 'click', this.nextPageFunction);
      this.bodyContainer.appendChild(this.nextPageImageElement);
    } else if (this.hasMoreLink()) {
      this.nextPageImageElement = img({src: this.arrowInfo.rightArrowMoreSrc, className: "clickable arrow-right"});
      this.nextPageImageElement.style.position = "absolute";
      this.nextPageImageElement.style.right = this.arrowInfo.hOffset + "px";
      this.nextPageImageElement.style.top = (Math.ceil(this.scrollerHeight()/2) - this.arrowInfo.height/2) + "px";
      Event.observe(this.nextPageImageElement, 'click', this.moreLinkFunction);
      this.bodyContainer.appendChild(this.nextPageImageElement);
    } else {
      this.nextPageImageElement = null;
    }
  },

  _paintItems: function() {
    this.itemsContainer = div({id: this.id + "_items", className: 'items-container'});
    this.itemsContainerContainer = div({className: 'items-container-container'});
    this.itemsContainerContainer.appendChild(this.itemsContainer);
    this.bodyContainer.appendChild(this.itemsContainerContainer);
    this.displayedItems().each(function(item) {
      var view = this._createItemView(item);
      this.views.push(view);
      this.itemsContainer.appendChild(view.hookElement);
      view.paint();
    }.bind(this));
  },

  _createItemView: function(item) {
    return new this.options.itemViewClass(this, item);
  },

  editable: function() {
    return false;
  },

  displayedItems: function() {
    return this.items().slice((this.page-1)*this.itemsPerPage(), this.page*this.itemsPerPage());
  },

  itemsPerPage: function() {
    return this.options.itemsPerRow * this.numRows();
  },

  currentPageRange: function() {
    return $R((this.page-1)*this.itemsPerPage()+1, Math.min(this.page*this.itemsPerPage(), this.numItems()));
  },

  numPages: function() {
    return Math.ceil(this.numItems() / this.itemsPerPage());
  },

  numRows: function($super) {
    if (this.options.isShowAll) {
      var calcMaxRows = Math.floor((this.numItems() - 1)/ this.options.itemsPerRow) + 1;
      if(this.options.maxRows) {
        return Math.min(this.options.maxRows, calcMaxRows);
      } else {
        return calcMaxRows;
      }
    } else {
      return this.options.rowCount;
    }
  },

  numDisplayedRows: function() {
    var numItems = this.numItems();
    var rowCount;
    if (this.collapseEmptyRows()) {
      if (numItems < this.itemsPerPage()) {
        rowCount = Math.ceil(numItems / this.options.itemsPerRow);
      } else {
        rowCount = this.numRows();
      }
    } else {
      rowCount = this.numRows();
    }
    return rowCount;
  },

  hasNextPage: function() {
    return this.numItems() > this.itemsPerPage() * this.page
  },

  hasPreviousPage: function() {
    return this.page > 1;
  },

  nextPage: function() {
    if(this.requiresLoadForPage(this.page+1)) {
      this.loadPage(this.page+1);
    } else {
      this.page++;
      this.paint();
    }
  },

  previousPage: function() {
    this.page--;
    this.paint();
  },

  requiresLoadForPage: function(page) {
    return (this.numItems() != this.items().length  &&
            this.items().length < page * this.itemsPerPage());
  },

  loadPage: function(page, dont_move_to_next) {
    if(!this.loadingPage) {
      this.loadingPage = true;
      this.dont_move_to_next_after_load = dont_move_to_next
      if(this.expandRowsLink) {
        var newElement = span("Loading...");
        this.expandRowsLink.parentNode.replaceChild(newElement, this.expandRowsLink);
        this.expandRowsLink = newElement;
      }
      if(this.nextPageImageElement) {
        Event.stopObserving(this.nextPageImageElement, 'click', this.nextPageFunction);
        this.nextPageImageElement.src = this.arrowInfo.rightArrowDisabledSrc;
        Element.removeClassName(this.nextPageImageElement, 'clickable');
      }
      if(this.previousPageImageElement) {
        Event.stopObserving(this.previousPageImageElement, 'click', this.previousPageFunction);
        this.previousPageImageElement.src = this.arrowInfo.leftArrowDisabledSrc;
        Element.removeClassName(this.previousPageImageElement, 'clickable');
      }
      LikeMe.Command.PaginatedLoader.send(this.loadPageUrl(), this.items().length+1, page*this.itemsPerPage(), this.loadPageComplete.bind(this));
    }
  },

  loadPageComplete: function(responseJson, requestedLo, requestedHi) {
    this.processLoadPageJson(responseJson, requestedLo, requestedHi);
    this.loadingPage = false;
    if(this.dont_move_to_next_after_load) { this.paint(); } else { this.nextPage(); }
  },

  verifyCurrentPageExists: function() {
    while (this.page > 1 && this.numItems() <= this.itemsPerPage() * (this.page - 1)) {
      this.page;
    }
  },

  collapseEmptyRows: function() {
    return !this.editable();
  },

  resizeScroller: function() {
    var colCount = this.options.itemsPerRow;
    if (this.options.itemViewClass.prototype.height != null) {
      this.bodyContainer.style.height = this.scrollerHeight() + "px";
    }
    this.bodyContainer.style.width = (colCount * (this.options.itemViewClass.prototype.width + this.options.cellPadding)) + this.paddingAdjustment() + "px";
  },

  scrollerHeight: function() {
    var rowCount = this.numDisplayedRows();
    return (rowCount * (this.options.itemViewClass.prototype.height + this.options.cellPadding)) + this.paddingAdjustment();
  },

  paddingAdjustment: function() {
    return (this.options.cellPadding > 0 ? this.options.cellPadding : 0)
  },

  loadPageUrl: function() {
    return this.options.loadPageUrl;
  },

  hasMoreLink: function() {
    return (this.options['moreLink']);
  },

  moreLink: function() {

    if ( this.nextPageImageElement ) {
      $(this.nextPageImageElement).src = '/images/ajax-loader-small.gif';
      $(this.nextPageImageElement).setStyle({paddingTop: '10px', right: '-20px'});
      Event.stopObserving(this.nextPageImageElement, 'click', this.moreLinkFunction);
    }

    this.redirectTo.delay(0.25, this.options['moreLink']);
  },

  redirectTo: function(url) {
    location.href = url;
  }

});


LikeMe.View.PhotoGallery = Class.create({
  
  initialize: function(options) {
    this.currentElement = $(options.startElement);

    this.options = this.defaultOptions();
    this.addOptions(options);

    this.content = div();
    this.paint();
    this.showModalBox();
  },

  addOptions: function(options) {
    Object.extend(this.options, options || {} );
  },

  defaultOptions: function() {
    return {
      size: 'large',
      startElement: null,
      showNextPrev: true
    };
  },

  paint: function() {
    this.renderContent();
  },

  renderContent: function() {
    this.content.innerHTML = '';
    this.centerDiv = div({className: 'image-gallery'}, []);

    if( this.options['showNextPrev'] && this.currentElement.up('div').previous() ){
      this.prevButton = AbleButton.applyTo(a({href: '#', onclick: this.prevImage.bind(this) }, 'Previous'), 'able-button blue')
      }else{
        this.prevButton = AbleButton.applyTo(a({href: '#'}, 'Previous'), 'able-button blue')
        this.prevButton.style.visibility = 'hidden';
    }
    this.centerDiv.appendChild(this.prevButton);

    this.currentImageElement = this.getLargePhotoImg();
    this.centerDiv.appendChild(this.currentImageElement);

    if( this.options['showNextPrev'] && this.currentElement.up('div').next() ){
      this.nextButton = AbleButton.applyTo(a({href: '#', onclick: this.nextImage.bind(this) }, 'Next'), 'able-button blue')
      this.centerDiv.appendChild(this.nextButton);
    }else{
      delete this.nextButton
    }
    this.content.appendChild(this.centerDiv);
  },
  
  getLargePhotoImg: function() {
    var url = this.currentElement.src.replace(this.options['size'], 'large');
    return img({src: url, className: 'large-photo large-photo-borders'});
  },
  
  showModalBox: function() {
    Modalbox.show(this.content,{title: 'Photo Gallery', height: 320, width: 550});
  },
  
  nextImage: function() {
    this.currentElement = this.currentElement.up('div').next().down('img');
    this.paint();
  },

  prevImage: function() {
    this.currentElement = this.currentElement.up('div').previous().down('img');
    this.paint();
  }
  
});

LikeMe.View.RankingScroller = Class.create(LikeMe.View.PhotoScroller, {
  initialize: function($super, hookElement, rankingGroup, options) {
    this.rankingGroup = rankingGroup;
    $super(hookElement, options);

    if (this.editable()) {
      this.createFindDrawer();
    }
  },

  defaultOptions: function($super) {
    return Object.extend($super(), {
      findDrawerOptions: {},
      containerClassName: "ratable-rankings-container",
      emptyText: ""
    });
  },

  reorderUrl: function() {
    return Routing.reorder_user_recommendation_group_path({
      user_id: this.rankingGroup.user().toParam,
      id: this.rankingGroup.toParam
    });
  },

  paint: function($super) {
    var success = $super();
    if (!success) { return false; }

    if (this.editable() && !this.rankingGroup.filtered && this.views.length != 0) {
      var exampleHookElement = this.views[0].hookElement;
      Sortable.create(this.itemsContainer, {
          constraint: '',
          handle: 'ratable-img',
          onUpdate: function(){
            LikeMe.Command.ReorderRankings.send(
              this.rankingGroup,
              this.reorderUrl(),
              this.page,
              this.itemsPerPage(),
              this.getRatableIds());
            }.bind(this),
          only: exampleHookElement.className,
          overlap: 'horizontal',
          tag: 'div'
      });
    }
    return true;
  },

  _setupSecondaryLinks: function($super) {
    var links = $super();
    if (this.editable() && !this.rankingGroup.filtered && this.findDrawer != null) {
      this.findMoreLink = a({className:"clickable"}, "Add More");
      Event.observe(this.findMoreLink, 'click', this.clickFindMore.bindAsEventListener(this));
      links.push(this.findMoreLink);
    }
    return links;
  },

  clickFindMore: function() {
    if(this.findDrawer) {
      this.findDrawer.toggle();
    }
  },

  resetFindDrawer: function() {
    if (!this.findDrawer)
      return
        
    this.findDrawer.reset();
  },

  _paintEmptyScroller: function() {
     if(this.editable() && !this.rankingGroup.filtered) {
       this.bodyContainer = div({id:this.id + "_items", className:this.options.containerClassName + " clickable tempting-empty-container"});

       this.resizeScroller();

       this._paintSecondaryLinks();

       this._paintTitle();
       
       this.hookElement.appendChild(this.bodyContainer);
       var emptyTextDiv = div(this.options.emptyText);
       this.bodyContainer.appendChild(emptyTextDiv);

       // Vertically center
       emptyTextDiv.style.marginTop = ((this.scrollerHeight() - LikeMe.View.Utils.height(emptyTextDiv)) / 2) + "px";
       Event.observe(this.bodyContainer, "click", this.clickFindMore.bindAsEventListener(this));
       Event.observe(this.bodyContainer, "mouseover", function() {Element.addClassName(this.bodyContainer, "tempting-empty-container-hover");}.bindAsEventListener(this));
       Event.observe(this.bodyContainer, "mouseout", function() {Element.removeClassName(this.bodyContainer, "tempting-empty-container-hover");}.bindAsEventListener(this));
     }
  },

  getRatableIds: function() {
    var regex = /ranking_(.*)$/;
    return Element.childElements(this.itemsContainer).collect(function(el){return parseInt(regex.exec(el.id)[1])});
  },

  editable: function() {
    return this.rankingGroup.editable;
  },
  
  items: function() {
    return this.rankingGroup.items();
  },
  
  numItems: function() {
    if(this.rankingGroup && this.rankingGroup.size)
      return this.rankingGroup.size;
    else
      return 0;
  },

  processLoadPageJson: function(json, requestedLo, requestedHi) {
    if (requestedHi - requestedLo + 1 != json.length) {
      this.rankingGroup.size = requestedLo - 1 + json.length;
    }
    this.rankingGroup.loadRatables(json);
  },

  createFindDrawer: function() {
  },
  
  hideItem: function(ratable) {
    this.rankingGroup.removeRatable(ratable);
    this.paint();
  }
});

LikeMe.View.Flash = Class.create({
  initialize: function(element, options) {
    this.element = $(element);
    this.options = options || {};
    this.options.fadeOptions = {};
    if(this.element) {
      this.element.appendChild(this.errorListElement = ul());
    }
    if(this.options.useVisibility) {
      this.options.fadeOptions.afterFinishInternal = function(effect) {
        effect.element.setStyle({opacity: ""});
        effect.element.setStyle({visibility: "hidden"});
      }
    }
  },

  clearFlash: function() {
    this.hide();
    this.errorListElement.innerHTML = '';
  },

  clearErrors: function() {
    this.clearFlash();
    this.clearErrorHighlighting();
    clearTimeout(this._timeoutId);
  },

  show: function() {
    if(this.options.useVisibility) {
      this.element.style.visibility = 'visible';
    } else {
      Element.show(this.element);
    }
  },

  hide: function() {
    if(this.options.useVisibility) {
      this.element.style.visibility = 'hidden';
    } else {
      Element.hide(this.element);
    }
  },

  showFlash: function(message) {
    this.show();
    this.errorListElement.appendChild(li(message));
    if(this._timeoutId) {
      clearTimeout(this._timeoutId);
    }
    this._timeoutId = setTimeout(function() { if(!this.element.id || $(this.element.id) == this.element) { Effect.Fade(this.element, this.options.fadeOptions) } }.bind(this), 10000);
  },

  addError: function(error, elements) {
    this.showFlash(error);
    if (elements) {
      elements.each(function(element) {
        var newParent = div({className:'fieldWithErrors'});
        element.parentNode.appendChild(newParent);
        newParent.appendChild(element);
      });
    }
  },

  addErrors: function(errors, elements) {
    errors.each(function(error) {
      this.addError(error, elements);
      elements = null;
    }.bind(this));
  },

  clearErrorHighlighting: function() {
    for (var i = 0; i < document.forms.length; ++i)
    {
      var errorDivs = Element.select(document.forms[i], ".fieldWithErrors");
      errorDivs.each(function(errorDiv) {
        $A(errorDiv.childNodes).each(function(child) {
          errorDiv.parentNode.appendChild(child);
        });
        errorDiv.parentNode.removeChild(errorDiv);
      });
    }
  }
});
LikeMe.View.Flash.errorDiv = function() {
  return div({className: "errorExplanation", style: {visibility: "hidden"}});
}

LikeMe.View.InputHelpText = Class.create( {

  initialize: function(element, options) {
    this.element = $(element);
    this.defaultText = options.defaultText;
    this.remoteForm = options.remoteForm;
    this.helpTextContainer = options.helpTextContainer || this.element.form;
    this.passwordField = this.element.type == 'password';
    this._addEvents();
    this.setDefaultText();
  },

  _removeListeners: function() {
    Event.stopObserving(this.element, 'focus', this.focusHandler);
    Event.stopObserving(this.element, 'blur', this.blurHandler);
  },

  destroy: function() {
    this._removeListeners();

    if(this.remoteForm) {
      if(this.helpTextContainer.inputHelpTextArray.indexOf(this) != -1) {
        this.helpTextContainer.inputHelpTextArray.splice(this.helpTextContainer.inputHelpTextArray.indexOf(this), 1);
      }
    } else {
      Event.stopObserving(this.element.form, 'submit', this.submitHander);
    }
  },

  _switchElementType: function(type, forceFocus) {
    this._removeListeners();
    var newElement = input({type: type, className: this.element.className, id: this.element.id, name: this.element.name});
    this.element.parentNode.replaceChild(newElement, this.element);
    this.element = newElement;
    if(forceFocus) {
      setTimeout(function() {Form.Element.focus(this.element);}.bind(this), 1);
    }
    this._addListeners();
  },

  unsetDefaultText: function(forceFocus) {
    if(Element.hasClassName(this.element, 'default-text')) {
      this.setText('');
      if(this.passwordField) {
        this._switchElementType('password', forceFocus);
      }
      Element.removeClassName(this.element, 'default-text');
    }
  },

  setDefaultText: function() {
    if(!this.getText() || this.getText() == this.defaultText) {
      Element.addClassName(this.element, 'default-text');
      if(this.passwordField) {
        this._switchElementType('text');
      }
      this.setText(this.defaultText);
    }
  },

  _focusHandler: function() {
    this.unsetDefaultText(true);
  },

  _blurHandler: function() {
    setTimeout(this.setDefaultText.bind(this), 1);
  },

  _addListeners: function() {
    this.focusHandler = this._focusHandler.bind(this);
    Event.observe(this.element, "focus", this.focusHandler);
    this.blurHandler = this._blurHandler.bind(this);
    Event.observe(this.element, "blur", this.blurHandler);
  },

  _addEvents: function() {
    this._addListeners();
    
    if(this.remoteForm) {
      if(!this.helpTextContainer.inputHelpTextArray) {
        this.helpTextContainer.inputHelpTextArray = [];
      }
      this.helpTextContainer.inputHelpTextArray.push(this);
    } else {
      this.submitHandler = this.unsetDefaultText.bind(this);
      Event.observe(this.element.form, "submit", this.submitHandler);
    }
  },

  setText: function(text) {
    this.element.value = text;
  },

  getText: function() {
    return this.element.value.strip();
  }

});

LikeMe.View.InputHelpText.prepareFormForSubmit = function(formToPrepare) {
  if(formToPrepare.inputHelpTextArray) {
    formToPrepare.inputHelpTextArray.each(function(inputHelpText) {
      inputHelpText.unsetDefaultText();
    });
  }
}

LikeMe.View.InsideWordModalBox = Class.create( {

  initialize: function(options) {
    this.options = Object.extend({
      instructiveText: {
        event: "What's the ambiance or crowd like?\nWhat should people know before they go?",
        restaurant: "What are your favorite dishes?\nWhat occasion do you like to go for?\nHow expensive is it?\nHow difficult are reservations or are they even necessary?",
        bar: "What's the ambiance or crowd like?\nWhat's the best night to go?\nIs there a cover charge or door list to get in?",
        hotel: "What's the location like?\nWhat is the vibe of the hotel and rooms?\nAre there any special amenities?",
        activity: "What makes this so fun?\nWhat should people know before they go?\nAre there special arrangements, attire or equipment needed?",
        shopping: "What kind of cool stuff do they sell?\nAre there perks for shopping here or with a specific employee?",
        other: "Why are you into this?\nWhy should people like you check it out?",
        tags: "Enter tags separated by commas"
      }
    }, options);

    this.ratable = options.ratable;
    this.context = (options.context) ? options.context : 'none';
    this.category = (options.category) ? options.category : {};
    this.reloadPageOnClose = options.reloadPageOnClose;
    this._content = div({className:'modal'});
  },

  paint: function() {
    this.renderContent();
    Modalbox.show(this.content(),{title: 'Inside Word', height: 580, width: 700});
    $(Modalbox.MBclose).stopObserving("click", Modalbox.MBclose.hideObserver);
    $(Modalbox.MBclose).observe("click", this.close.bind(this));
  },

  close: function() {
    this.reloadPageOnClose ? this._reloadPage() : Modalbox.hide();
  },

  _reloadPage: function() {
    window.location = window.location;
  },

  content: function() {
    return this._content;
  },

  renderContent: function( ) {
    this._content.innerHTML = "";

    var ratableElement;
    this._content.appendChild(ratableElement = div());

    var insideWordElement;
    this._content.appendChild(insideWordElement = div());

    this._renderRatable(ratableElement);
    this._renderInsideWord(insideWordElement);
  },

  showSpinner: function() {
      Element.show(this.searchSpinner);
  },

  hideSpinner: function() {
      Element.hide(this.searchSpinner);
  },

  _renderRatable: function(element) {
    element.appendChild(LikeMe.View.RatableView.renderRatable(this.ratable));
  },

  _renderInsideWord: function(element) {
    var errorsDiv;
    var recommendation = '';
    var inside_word = h2('Give Us Your Inside Word.');

    if ( this.context == 'recommendation' ) {
      recommendation = h2('Recommendation Added!');
      inside_word = h3('Now Give Us Your Inside Word:');
    }

    element.appendChild(div({className: "inside_word_form"}, [
      this.form = form({action: Routing.place_reviews_path({format: "json", place_id: this.ratable.toParam}), method: 'post', enctype: "multipart/form-data", onsubmit: this._submitForm.bind(this)}, [
        recommendation,
        inside_word,
        table({id: 'review_form'},[
          tr({}, [td({className: "label_column"}, ["Title:"]), td({className: "field_column"}, [this.insideWordTitle = input({type: "text", name:"review[title]", className: "input_field"})])]),
          tr({}, [td({className: "label_column"}, ["Your Word:"]), td({className: "field_column"}, [this.insideWordText = textarea({cols: 47, rows: 8, name:"review[text]"})])]),
          tr({}, [td({className: "label_column"}, ["Already known for:"]), this.insideWordTags = td({id: "insideWordTags",className: "field_column"}, [])]),
          tr({}, [td({className: "label_column"}, ["Known for:"]), td({className: "field_column"}, [this.insideWordTag = input({type: "text",  name:"tag[name]", className: "input_field"})])]),
          this.buttonRow = tr({className: "button_row"}, [td({}, []), td({}, [
            this.insideWordSubmitButton = input({id: 'review_submit', type: 'submit', value: 'Add Inside Word'}),
            this.insideWordCancelButton = input({type: 'button', value: 'No Comment'})
          ])])
        ]),
        this.searchSpinner = img({src:'/images/ajax-loader-large.gif', style:{display:"none"}}),
        div({className: 'clear'}),
        errorsDiv = LikeMe.View.Flash.errorDiv(),
        div({className: 'clear'})
      ])
    ]));

    var instructiveText = this.options.instructiveText[this.category.toParam] || '';
    new LikeMe.View.InputHelpText(this.insideWordTitle, {defaultText: 'Optional', remoteForm: true});
    new LikeMe.View.InputHelpText(this.insideWordText, {defaultText: instructiveText, remoteForm: true});
    new LikeMe.View.InputHelpText(this.insideWordTag, {defaultText: this.options.instructiveText['tags'], remoteForm: true});
    new Ajax.Updater(this.insideWordTags, Routing.place_tags_path({place_id: this.ratable.toParam}), {method:'get'});

    this.insideWordSubmitButton = AbleButton.applyTo(this.insideWordSubmitButton, 'able-button blue');
    this.insideWordCancelButton = AbleButton.applyTo(this.insideWordCancelButton, 'able-button red');
    this.insideWordErrorsFlash = new LikeMe.View.Flash(errorsDiv, {useVisibility: true});

    Event.observe(this.form, 'submit', function() { }.bind(this));

    Event.observe(this.insideWordCancelButton, 'click', this.close.bind(this));
  },

  _submitForm: function() {
    if(!this.sendingInsideWord) {
      this.showSpinner();
      Modalbox.resizeToContent();      
      this.sendingInsideWord = true;
      LikeMe.View.InputHelpText.prepareFormForSubmit(this.form);
      this.insideWordErrorsFlash.clearErrors();

      return AIM.submit(this.form, {onComplete: function(response) {
        response = JSON.parse(response);
        AbleButton.resetDoubleClick(this.insideWordSubmitButton);
        this.sendingInsideWord = false;
        this.hideSpinner();
        if (response.errors) {
          this.insideWordErrorsFlash.addErrors(response.errors);
          Modalbox.resizeToContent();      
        }
        else {
          this.ratable.reviewed = true;
          Services.page.repaintAll( );
          // add tracking code
          Services.page.trackUrl("/events/add_review");
          this.close();
        }
      }.bind(this)});

/*      LikeMe.Command.Generic.send({
        url: Routing.formatted_place_reviews_path({format: "json", place_id: this.ratable.toParam}),
        paramHash: {'review[title]': this.insideWordTitle.value, 'review[text]': this.insideWordText.value},
        callback: function(response) {
          this.sendingInsideWord = false;
          this.hideSpinner();
          if (response.errors) {
            this.insideWordErrorsFlash.addErrors(response.errors);
          }
          else {
            this.ratable.reviewed = true;
            Services.page.repaintAll( );
            // add tracking code
            Services.page.trackUrl("/events/add_review");
            this.close();
          }
        }.bind(this),
        undo: function() {
          this.hideSpinner();
          this.sendingInsideWord = false;
        }.bind(this)
      });*/
    }
  }
});


LikeMe.View.PopularGenericRatablesSelector = Class.create(LikeMe.View.Pagination, {

  paginationStyle: 'numbers',
  perPage: 8,

  initialize: function(hookElement, options) {
    this.hookElement = $(hookElement);

    this.views = [];

    this.options = Object.extend({
      category: LikeMe.Model.Category.all
    }, options);

    if (options.rankingGroup) {
      this.paint(0);
    } else {
      this.loadRatables();
    }
  },

  destroy: function() {
    this.destroyed = true;
    this.clearContent();
  },

  loadRatables: function() {
    if(this.options.onBeginLoading) {
      this.options.onBeginLoading();
    }
    var urlHash = {
      category: this.options.category.toParam
    };

    if(this.options.near){
      urlHash.near = this.options.near;
    }

    LikeMe.Command.Generic.send({
      url: Routing.popular_places_path(urlHash),
      method: 'get',
      callback: function(ratableGroupJSON) {
        if(!this.destroyed) {
          this.options.rankingGroup = new LikeMe.Model.RankingGroup(ratableGroupJSON);
          this.paint(0);

          if(this.options.onFinishedLoading) {
            this.options.onFinishedLoading();
          }
        }
      }.bind(this)
    });
  },

  paintRatable: function(column, ratable) {
    var ratableView = new LikeMe.View.RatableSummary(ratable, {imageSize: 'medium'})
    this.views.push(ratableView);
    column.appendChild(ratableView.hookElement);
  },

  clearContent: function() {
    this.hookElement.innerHTML = "";
    this.views.each(function( view ) {
      view.destroy();
    });
    this.views = [];
  },

  paint: function(offset) {
    this.clearContent();

    var items = this.paginate(offset);

    var placeList = div({className: 'place-list'});
    // container
    this.hookElement.appendChild(placeList);

    //items
    if(items.length > 0) {
      var columnDiv = div({});
      placeList.appendChild(columnDiv);
      LikeMe.View.Helpers.renderColumns(items, columnDiv, 2, this.paintRatable.bind(this));
    }
    else {
      placeList.appendChild(div({className: 'empty'}, [
        span({}, 'No popular places found')
      ]));
    }

    //pagination links
    this.hookElement.appendChild(div({className: 'clear'}));
    this.hookElement.appendChild(br());
    this.paginationElement = div({className: 'pagination'});
    this.hookElement.appendChild(div({className: 'pagination_container'}, [
      this.paginationElement
    ]));
    this.paintPagination(this.paginationElement, offset, this.paint.bind(this));
  },

  allItems: function() {
    return this.options.rankingGroup.items();
  },

  maxOffset: function() {
    return this.options.rankingGroup.size - 1;
  },

  _addToRecommendations: function(ratable) {
    if(this.options.addRatable) {
      this.options.addRatable(ratable);
    } else {
      LikeMe.Command.Generic.send({
        url: Routing.place_recommendations_path({place_id: ratable.toParam})
      })
    }
  },

  _removeFromRecommendations: function(ratable) {
    if(this.options.removeRatable) {
      this.options.removeRatable(ratable);
    } else {
      LikeMe.Command.Generic.send({
        url: Routing.place_recommendations_path({place_id: ratable.toParam}),
        method: 'delete'
      })
    }
  },

  _isRatablePresent: function(ratable) {
    if(this.options.isRatablePresent) {
      return this.options.isRatablePresent(ratable);
    } else {
      return ratable.recommended()
    }
  }
});


LikeMe.View.PopularRatablesForm = Class.create({
  initialize: function(hookElement, options) {
    this.hookElement = $(hookElement);
    this.options = {
      near: $F('near'),
      popularSelectorClass: LikeMe.View.PopularGenericRatablesSelector
    };

    this.options = Object.extend(this.options, options || {});

    this.options.popularSelectorOptions = this.options.popularSelectorOptions || {};

    this.passedSelectorBeginLoading = this.options.popularSelectorOptions.onBeginLoading;
    this.passedSelectorFinishedLoading = this.options.popularSelectorOptions.onFinishedLoading;

    this.options.popularSelectorOptions.onBeginLoading = this.selectorBeginLoading.bind(this);
    this.options.popularSelectorOptions.onFinishedLoading = this.selectorFinishedLoading.bind(this);

    this.paint();
    this.searchForm.onsubmit();
  },

  destroy: function() {
    if(this.popularSelector) { this.popularSelector.destroy(); this.popularSelector = null; }
    if(this.autocompleter) { this.autocompleter.destroy(); this.autocompleter = null; }
  },

  selectorBeginLoading: function() {
    this.showSpinner();
    if(this.passedSelectorBeginLoading) {
      this.passedSelectorBeginLoading();
    }
  },

  selectorFinishedLoading: function() {
    this.hideSpinner();
    if(this.passedSelectorFinishedLoading) {
      this.passedSelectorFinishedLoading();
    }
  },

  showSpinner: function() {
    Element.show(this.searchSpinner);
  },

  hideSpinner: function() {
    Element.hide(this.searchSpinner);
  },

  paint: function() {
    var autocompleterParent;
    this.hookElement.innerHTML = '';

    this.hookElement.appendChild(div({}, [
      this.searchForm = form({className: 'near-fields',
        onsubmit: function() {
          if(this.popularSelector) {this.popularSelector.destroy();}
          this.popularSelector = new this.options.popularSelectorClass(this.resultsArea, Object.extend(this.options.popularSelectorOptions, {
            near: this.nearField.value
          }));
          return false;
        }.bind(this)
      }, [
        autocompleterParent = div({className: 'form-left'}, [
          label({htmlFor: 'popular_places_near'}, 'Near (City AND State, OR Zip;)'),
          br(),
          this.nearField = input({id: 'popular_places_near', type: 'text', value: this.options.near})
        ]),
        div({className: 'form-button'}, [
          this.searchButton = input({type: 'submit', value: 'Search'})
        ])
      ]),
      div({className: 'clear'}, ''),
      this.searchSpinner = div({className: 'spinner', style:{display: "none"}}, [img({src: '/images/ajax-loader-small.gif'})]),
      br(),
      this.resultsArea = div({className:'popular-place-controller'})
	  ]));

    this.autocompleter = new LikeMe.View.Autocompleter(this.nearField, {
      url: Routing.city_state_autocompletions_path(),
      positionedParentId: autocompleterParent,
      delay: 50,
      onItemChosen: function() { this.nearField.form.onsubmit(); }.bind(this),
      cancelSearch: function(text) { return (/^[\s\d]*$/.test(text)) }.bind(this)
    });
    this.searchButton = AbleButton.applyTo(this.searchButton, 'able-button blue', {allowDoubleClick: true});
  }
});

LikeMe.View.PopularRatablesSelector = Class.create(LikeMe.View.PopularGenericRatablesSelector, {
  addText: 'Add to Recommendations',
  removeText: 'Remove from Recommendations',
  addLinkClass: 'add-recommendation',

  paintRatable: function(column, ratable) {
    var addLink, removeLink;
    var addRemoveLinks = [
      addLink = a({className: 'action '+this.addLinkClass}, this.addText),
      removeLink = a({className: 'action remove'}, this.removeText)
    ];
    
    if(this._isRatablePresent(ratable)) {
      Element.hide(addLink);
    } else {
      Element.hide(removeLink);
    }

    Event.observe(addLink, 'click', function() {
      this._addToRecommendations(ratable);
      Element.hide(addLink);
      Element.show(removeLink);
    }.bindAsEventListener(this))

    Event.observe(removeLink, 'click', function() {
      this._removeFromRecommendations(ratable);
      Element.hide(removeLink);
      Element.show(addLink);
    }.bindAsEventListener(this))

    var ratableDiv = LikeMe.View.RatableView.renderRatable(ratable, {additionalSummaryElements:addRemoveLinks, imageSize:'medium'});

    column.appendChild(ratableDiv);
  }
});


LikeMe.View.ToDoPopularRatablesSelector = Class.create(LikeMe.View.PopularRatablesSelector, {
  addText: 'Add to To Do List',
  removeText: 'Remove from To Do List',
  addLinkClass: 'add-todo'
});


LikeMe.View.SimpleScroller = Class.create({

  initialize: function(hookElement, elementContainer, options) {
    if (hookElement.id == null) {
      throw "Cannot create a simple scroller without an id";
    }

    if (elementContainer.id == null) {
      throw "The element container must have an id";
    }

    this.hookElement = $(hookElement);
    this.elementContainer = $(elementContainer);

    this.options = this.defaultOptions();
    this.addOptions(options);
    this._sanityCheckOptions();

    this.offset = 0;
    this.elements = Array();
    this.prevElement = null;
    this.nextElement = null;

    this.nextPageFunction = this.nextPage.bindAsEventListener(this);
    this.prevPageFunction = this.prevPage.bindAsEventListener(this);

    this.reloadMarkupElements();
  },

  arrowInfo: {
    leftArrowSrc: "/images/arrow-left.gif",
    rightArrowSrc: "/images/arrow-right.gif",
    rightArrowDisabledSrc: "/images/arrow-right-disabled.gif",
    leftArrowDisabledSrc: "/images/arrow-left-disabled.gif",
    hOffset: -18,
    height: 16
  },

  addOptions: function(options) {
    Object.extend(this.options, options || {} );
  },

  defaultOptions: function() {
    return {
      minDisplay: 4,
      scrollBy: 1
    };
  },

  reloadMarkupElements: function() {
    if (this.elementContainer && this.elementContainer.childElements && this.elementContainer.childElements().length > 0) {
      this.elements = this.elementContainer.childElements();
    }
  },

  addElement: function(newElement) {
    var container = div({className: "simple-scroller-element"});
    container.appendChild($(newElement));
    this.elements.push($(container));
    this.elementContainer.appendChild(container);
  },

  elementIndex: function(element) {
    for(var i = 0; i < this.elements.length; i++ ) {
      if (this.elements[i] == element || $(this.elements[i]).down() == element ) return i;
    }
    return -1;
  },

  getMaxOffset: function() {
    var max = this.elements.length - this.options['minDisplay'];
    if( max < 0 ) max = 0;
    return max;
  },

  hasNext: function() {
    var max = this.getMaxOffset();
    if (max < 0) max = 0;
    return (this.offset < max);
  },

  hasPrev: function() {
    return (this.offset > 0);
  },

  prevPage: function() {
    this.scrollBackward(this.offset - this.options['scrollBy']);
  },

  nextPage: function() {
    this.scrollForward(this.offset + this.options['scrollBy']);
  },

  scrollToOffset: function(index) {
    if( index > this.offset ) return this.scrollForward(index);
    if( index < this.offset ) return this.scrollBackward(index);
  },

  scrollForward: function(index) {
    for(var i = this.offset; i < index && i < this.getMaxOffset(); i++ ) this._nextElement();
    this.paintArrows();
  },

  scrollBackward: function(index) {
    for(var i = this.offset; i > index && i > 0; i-- ) { this._prevElement();}
    this.paintArrows();
  },

  scrollerHeight: function() {
    return $(this.hookElement).getHeight();
  },

  paintArrows: function() {
    this._paintNextPageLink();
    this._paintPrevPageLink();
  },

  paint: function() {
    this.paintArrows();
  },

  _paintPrevPageLink: function() {
    if( this.prevElement && this.prevElement.remove ) this.prevElement.remove();

    if (this.hasPrev()) {
      this.prevElement = img({src: this.arrowInfo.leftArrowSrc, className:"clickable arrow-left"});
      this.prevElement.style.position = "absolute";
      this.prevElement.style.left = this.arrowInfo.hOffset + "px";
      this.prevElement.style.top = (Math.ceil(this.scrollerHeight()/2) - this.arrowInfo.height/2) + "px";
      Event.observe(this.prevElement, 'click', this.prevPageFunction);
      this.hookElement.appendChild(this.prevElement);
    } else {
      this.prevElement = null;
    }
  },

  _paintNextPageLink: function() {
    if( this.nextElement && this.nextElement.remove ) this.nextElement.remove();

    if (this.hasNext()) {
      this.nextElement = img({src: this.arrowInfo.rightArrowSrc, className: "clickable arrow-right"});
      this.nextElement.style.position = "absolute";
      this.nextElement.style.right = this.arrowInfo.hOffset + "px";
      this.nextElement.style.top = (Math.ceil(this.scrollerHeight()/2) - this.arrowInfo.height/2) + "px";
      Event.observe(this.nextElement, 'click', this.nextPageFunction);
      this.hookElement.appendChild(this.nextElement);
    } else {
      this.nextElement = null;
    }
  },

  _nextElement: function() {
    if( this.hasNext() ) this._hideElement();
    this._offsetNext();
  },

  _prevElement: function() {
    if( this.hasPrev() )
    {
        this._offsetPrev();
        this._showElement();
    }
  },

  _offsetPrev: function() {
    this.offset--;
    this._boundOffset();
  },

  _offsetNext: function() {
    this.offset++;
    this._boundOffset();
  },

  _showElement: function() {
    $(this.elements[this.offset]).appear({duration: .25});
  },

  _hideElement: function() {
    $(this.elements[this.offset]).fade({duration: .25});
  },

  _boundOffset: function() {
    if( this.offset >= this.elements.length ) this.offset = this.getMaxOffset();
    if( this.offset < 0 ) this.offset = 0;
  },

  _sanityCheckOptions: function() {
    if ( this.options['minDisplay'] <= 0 ) throw "option: minDisplay must not be negative";
  }

});

LikeMe.View.NavScroller = Class.create(LikeMe.View.SimpleScroller, {

  initialize: function($super, hookElement, elementContainer, options) {
    $super(hookElement, elementContainer, options);
  },

  defaultOptions: function($super) {
    var options = $super();

    Object.extend(options, { maxDisplay: 4} );

    return options;
  },

  nextPageWhenCutoff: function(index) {
    if( index < 0 ) return;

    if( index >= this.offset + this.options['minDisplay'] ) {
      this.scrollForward(this.offset + (this.options['scrollBy']));
    }
  },

  displayWidth: function() {
    var width = 0;

    var elements = this.getVisibleElements();

    for( var i = 0; i < elements.length; i++ ) {
      width += $(elements[i]).getWidth();
    }

    return width;
  },

  getVisibleElements: function() {
    var elements = [];

    var len = this.offset + this.options['maxDisplay'];
    if( len > this.elements.length ) len = this.elements.length;

    for( var i = this.offset; i < len; i++ ) {
      elements.push( this.elements[i] );
    }

    return elements;
  }

});

LikeMe.View.ScrollerGroupComponent = Class.create({

  // TODO: sLawson @ 2009-09-15
  // + make a toggle for overflow:hidden on navclipContainer toggle
  // ? setCurrentElement to SimpleScroller

  initialize: function(hookElement, options) {
    if (hookElement.id == null) {
      throw "Cannot create a simple scroller without an id";
    }

    this.hookElement = $(hookElement);
    this.setOptions(options);

    this.elementContainer  = div({id: hookElement.id + '_elements', className: 'scroller-group-elements', style: {width: this.options['elementsWidth']+'px'}});
    this.navContainer      = div({id: hookElement.id + '_nav',      className: 'scroller-group-nav',      style: {position: 'relative', display: 'block', width: this.options['visibleWidth']+'px', margin: '0px 18px 0px 18px'}});
    this.scrollerContainer = div({id: hookElement.id + '_scroller', className: 'ranking-scroller scroller-group-scroller'});

    this.navScroller = new LikeMe.View.NavScroller(this.navContainer, this.elementContainer, options);

    this.scroller = null;
    this.currentElement = null;
    this.clearElement = div({className: 'clear'}, '<br class="clear"/>');

    this.clickElementFunction = this.clickElement; // IE6
  },

  setOptions: function(options) {
    this.options = {
      visibleWidth: 400,
      elementsWidth: 1000,
      navClipBuffer: 6,
      loadingMessage: null,
      rankingGroupClass: LikeMe.Model.RankingGroup
    };

    Object.extend(this.options, options || {});
  },

  addScrollerElement: function(element, scrollerClass, rankingGroup, scrollerOptions) {
    $(element).store('scroller', new scrollerClass(this.scrollerContainer, rankingGroup, scrollerOptions));

    this.navScroller.addElement(element);
    if ( !this.currentElement ) this.setCurrentElement(element);

    Event.observe(element, 'click', this.clickElementFunction.bindAsEventListener(this, element));
  },

  addAjaxScrollerElement: function(element, scrollerClass, groupClass, scrollerOptions, options) {
    this.options.rankingGroupClass = groupClass;
    this.addScrollerElement(element, scrollerClass, new groupClass({"size":0,"ratables":[],"includesHidden":false,"userId":null,"editable":false}), scrollerOptions);

    if ( !options['loadOnClick'] ) this.loadScrollerJson(options['ajaxLoadUrl'], element);
    $(element).store('loadUrl', options['ajaxLoadUrl']);
  },

  loadScrollerJson: function(url, element) {
    this.startLoading(element);

    new Ajax.Request(url, {
      method: 'get',
      evalJS: 'force',
      evalJSON: 'force',
      onSuccess: function(e, response) {
        try {
          if( !response.responseJSON ) { throw 'Sorry, unable to make any recommendations at this time.'; }
          $(e).retrieve('scroller').rankingGroup = new this.options.rankingGroupClass(response.responseJSON);
          this.navScroller.paint();
          if( e == this.currentElement ) { this.paintScroller(); }
        }
        catch( err ) {
          this.stopLoading(e);
          this.paintFailedScroller(err);
        }
      }.bind(this, element),
      onFailure: function(e) {
        this.stopLoading(e);
        this.paintFailedScroller('Request failed, please try again.');
      }.bind(this, element)
    });
  },

  startLoading: function(element) {
    $(element).store('loading', true);
  },

  stopLoading: function(element) {
    $(element).store('loading', false);
  },

  isLoading: function(element) {
    return $(element).retrieve('loading', false);
  },

  canLoad: function(element) {
    return ($(element).retrieve('loadUrl','').length > 0);
  },

  setCurrentElement: function(element) {
    if ( this.currentElement ) $(this.currentElement).removeClassName('dark');

    this.navScroller.nextPageWhenCutoff(this.navScroller.elementIndex(element));

    this.currentElement = $(element);
    $(this.currentElement).addClassName('dark');

    if ( element.retrieve('scroller') ) {
      this.scroller = element.retrieve('scroller');
      this.paintScroller();
    }

    if ( this.canLoad(element) && !this.isLoading(element) ) {
      this.loadScrollerJson(element.retrieve('loadUrl'), element);
    }
  },

  clickElement: function(event, element) {
    this.setCurrentElement(element);
  },

  paint: function() {
    this.hookElement.innerHTML = '';

    var headerContainer  = div({id: this.hookElement.id + '_header', className: 'scroller-group-header'});
    var navclipContainer = div({id: this.hookElement.id + '_navclip', className: 'scroller-group-navclip', style: {position: 'relative', overflow: 'hidden', width: this.options['visibleWidth']+'px'}});

    this.navContainer.innerHTML = '';
    navclipContainer.appendChild(this.elementContainer);
    this.navContainer.appendChild(navclipContainer);

    headerContainer.appendChild(this.navContainer);

    var bodyContainer = div({id: this.hookElement.id + '_body', className: 'top-list scroller-group-body'});
    bodyContainer.appendChild(this.scrollerContainer);
    bodyContainer.appendChild(this.clearElement);

    this.hookElement.appendChild(headerContainer);
    this.hookElement.appendChild(bodyContainer);

    if ( this.navScroller.displayWidth() > this.navScroller.options['visibleWidth'] + this.options['navClipBuffer'] ) {
      this.navScroller.options['minDisplay'] -= 1;
    }

    this.navScroller.paint();
    this.paintScroller();
  },

  paintScroller: function() {
    if ( this.scroller && this.scroller.numItems() > 0 ) {
      this.scroller.paint();
    } else {
      this.paintLoadingScroller();
    }
  },

  paintLoadingScroller: function() {
      this.scrollerContainer.innerHTML = "";
      this.scrollerContainer.appendChild(div({className: 'spinner'},
                                             [img({src: '/images/ajax-loader-small.gif'}), span({},this.options['loadingMessage'])])
      );
  },

  paintFailedScroller: function(msg) {
      this.scrollerContainer.innerHTML = "";
      this.scrollerContainer.appendChild(div({className: 'spinner'}, msg ) );
  }

});

LikeMe.View.FrontpageCarouselComponent = Class.create(LikeMe.View.SimpleScroller, {

  initialize: function($super, hookElement, elementContainer, options) {
    $super(hookElement, elementContainer, options);

    this.addOptions({
      arrowDelay: 1,
      alwaysLoad: true
    });

    this.addOptions(options);

    this.delayed_elements = [];
    this.arrowInfo = this.arrowRingInfo;

    this.last_element = null;
    this.at_zero = false;
  },

  loadingText: 'Loading...',
  noResultText: 'Sorry! Unable to locate the user',
  jsonErrorText: 'Error loading data, please try again later',
  requestFailText: 'Network error, check your connection and try again',

  arrowRingInfo: {
    leftArrowSrc: "/images/arrow-ring-left.gif",
    rightArrowSrc: "/images/arrow-ring-right.gif",
    rightArrowMoreSrc: "/images/arrow-ring-seeall.gif",
    rightArrowDisabledSrc: "/images/arrow-right-disabled.gif",
    leftArrowDisabledSrc: "/images/arrow-left-disabled.gif",
    hOffset: -44,
    height: 44
  },

  staticElement: function(json) {
    if ( json && json.ratable_group && json.ratable_group.ratables ) {
      this.createElement(json);
    }
  },

  createElement: function(json, container) {
    if ( !container ) {
      container = this.createContainer(this.loadingText);
    }

    if ( !json || !json.ratable_group || !json.name || !json.ratable_group.ratables )
    {
      return this.addError(container, this.noResultText);
    }

    var imageContainer   = div({id: this.hookElement.id + '_image', className: 'images'});
    var detailContainer  = div({id: this.hookElement.id + '_detail', className: 'top-list'});
    var itemContainer    = div({id: this.hookElement.id + '_items', className: 'ranking-scroller'});

    imageContainer.appendChild(a({href: Routing.user_guidebook_path({user_id: json.uname})}, [
      img({src: json.image, className: 'large-photo', alt: json.name})
    ]));

    try {
      var scroller = new LikeMe.View.RatableScroller(
        itemContainer,
        new LikeMe.Model.RankingGroup(json.ratable_group),
        {
          itemViewClass: LikeMe.View.RankedRatableUnder,
          containerClassName: "detail-container",
          itemsPerRow: 4,
          rowCount: 1,
          isShowAll: false,
          cellPadding: 21,
          endless: true,
          title: ""
        }
      );
    } catch (e) {
      return this.addError(container, e);
    }

    detailContainer.appendChild( h2({}, [ a({href: Routing.user_guidebook_path({user_id: json.uname})}, json.name), em({}, json.tagline) ]) );
    detailContainer.appendChild(itemContainer);

    scroller.paint();

    $(container).store('scroller', scroller);

    container.innerHTML = '';
    container.appendChild(imageContainer);
    container.appendChild(detailContainer);
    container.appendChild(div({className: 'clear'}, '<br class="clear"/>'));

    return container;
  },

  createContainer: function(msg) {
    var container = div({});
    container.appendChild(div({className: 'spinner'},
                              [img({src: '/images/ajax-loader-large.gif'}), div({}, msg)]));

    this.elements.push( container );

    return container;
  },

  addError: function(container, msg) {
    container.innerHTML = '';
    container.appendChild(div({className: 'spinner'},
                              [img({src: '/images/icon-failure.gif'}), div({}, msg)]));

    $(container).store('hasError', true);
  },

  paint: function($super) {
    $super();

    var element = this.elements[this.offset];

    if ( this.last_element ) { this.elementContainer.removeChild(this.last_element); }

    this.last_element = element;
    this.elementContainer.innerHTML = '';
    this.elementContainer.appendChild(element);
  },

  loadNext: function(when) {
    if ( this.delayed_elements.length > 0 && !this.at_zero && (this.options['alwaysLoad'] || this.offset >= when) ) {
      this.loadElement(this.delayed_elements.pop());
    }
  },

  loadElement: function(uname) {
    var container = this.createContainer('Loading ' + uname);

    new Ajax.Request('/frontpage_user/'+uname, {
      method: 'get',
      evalJS: 'force',
      evalJSON: 'force',
      onSuccess: function(c, response) {
        try {
          if( !response.responseJSON ) { throw this.jsonErrorText; }
          this.createElement(response.responseJSON, c);
        }
        catch( err ) {
          this.addError(c, err);
        }
      }.bind(this, container),
      onFailure: function(c) {
        this.addError(c, this.requestFailText);
      }.bind(this, container)
    });  
  },

  spinNextArrow: function(delay) {
    if ( this.nextElement ) {
      $(this.nextElement).src = '/images/ajax-loader-small.gif';
      $(this.nextElement).setStyle({paddingTop: '8px', right: '-35px', height: '24px', width: '24px'});
      Event.stopObserving(this.nextElement, 'click', this.nextPageFunction);
      this._paintNextPageLink.bind(this).delay(delay);
    }
  },

  spinPrevArrow: function(delay) {
    if ( this.prevElement ) {
      $(this.prevElement).src = '/images/ajax-loader-small.gif';
      $(this.prevElement).setStyle({paddingTop: '8px', left: '-35px', height: '24px', width: '24px'});
      Event.stopObserving(this.prevElement, 'click', this.prevPageFunction);
      this._paintPrevPageLink.bind(this).delay(delay);
    }
  },

  nextPage: function($super) {
    this.loadNext(this.getMaxOffset());
    this.at_zero = false;
    this._offsetNext();
    this.paint();
    this.spinNextArrow(this.options['arrowDelay']);
  },

  prevPage: function($super) {
    this.at_zero = (this.offset == 0);
    this._offsetPrev();
    this.paint();
    this.spinPrevArrow(this.options['arrowDelay']);
  },

  hasNext: function($super) {
    return true;
  },

  hasPrev: function($super) {
    return true;
  },

  _boundOffset: function($super) {
    if( this.offset >= this.elements.length ) this.offset = 0;
    if( this.offset < 0 ) this.offset = this.getMaxOffset();
  }

});

LikeMe.View.RankedItem = Class.create({
  height: 100,
  width: 100,
  hookElementClass: 'ratable-ranking-area',
  hide_text: "Hide",
  unhide_text: "Unhide",

  initialize: function(scroller, ratable) {
    this.scroller = scroller;
    this.ratable = ratable;

    var hookElementStyle = {width: this.width + 'px'};
    if(this.height) { hookElementStyle["height"] = this.height + 'px' }
    this.hookElement = div({className: this.hookElementClass, id: "ranking_" + this.ratable.id, style: hookElementStyle});
    this.mouseUpListener = this.handleMouseUp.bindAsEventListener(this);
    this.setRatableImg();

    this.tooltipElement = span({className: this.tooltipElementClass()});
    this.ratable.registerView( this );
  },

  destroy: function() {
    this.ratable.unregisterView( this );    
  },

  setRatableImg: function() {
    this.ratableImg = img({className:"ratable-img summary-photo",src:this.ratable.photoUrl('summary'),alt:this.getRatableImgAlt()});
  },

  getRatableImgAlt: function() {
    return "";
  },

  withPercentClassName: undefined,

  paintTooltipDetails: function(hookElement) {
    var event_details = [a({href: this.ratable.url},this.ratable.name)];

    if(this.ratable.location) {
      event_details.push(em(" / " + this.ratable.location));
    }
    hookElement.appendChild(
      h5({className: this.detailTitleClass()}, event_details)
    );

    this.paintSubDetails(hookElement);
  },

  paintTooltip: function( recalculateSize ) {
    this.tooltipElement.innerHTML = "";

    this.paintTooltipDetails(this.tooltipElement);

    var actionElements = this.getActionElements();

    if(actionElements && actionElements.length > 0) {
      var actionsList = h6();
      actionElements.each( function (action, index) {
        actionsList.appendChild( action );
        if ( index != actionElements.length )
          actionsList.appendChild(br());
      });
      this.tooltipElement.appendChild(actionsList);
    }

    if ( recalculateSize )
      this.resizeHover();
  },

  resizeHover: function() {
    if(this.hoverable.active) {
      Services.page.tooltip().resizeCurrent();
    }
  },

  paintSubDetails: function(containerElement) {
  },

  tooltipElementClass: function() {
    return 'tooltip';
  },

  // hook for rankedPerson guest or official star
  detailTitleClass: function() {
    return "";
  },

  showSimilarity: function() {
    return this.ratable.similarity && Services.userService.currentUser();
  },

  paint: function() {
    this.hookElement.innerHTML = "";
    this.contentDiv = div({className:"ratable-ranking"});
    this.hookElement.appendChild(this.contentDiv);

    this.contentDiv.appendChild(this.ratableImg);

    var hoverDiv = span({className:"hoverable"});

    if (this.showSimilarity()) {
      hoverDiv.appendChild(textNode( this.ratable.similarity.toFixed(0) + "%"));
    }
    if (this.withPercentClassName) {
      Element.addClassName(hoverDiv, this.withPercentClassName);
    }

    if (this.withoutPercentClassName) {
      Element.addClassName(hoverDiv, this.withoutPercentClassName);
    }

    this.paintTooltip();
    
    hoverDiv.appendChild(this.tooltipElement);

    Event.observe(this.ratableImg, "mousedown", this.handleMouseDown.bindAsEventListener(this));

    this.hookElement.appendChild(hoverDiv);

    this.underImage = div({className:"details"});
    this.hookElement.appendChild(this.underImage);
    this.paintUnderImage();

    this.hoverable = Services.page.tooltip().applyTo(hoverDiv);
  },

  getActionElements: function() {
    return [this.removeLink()].compact();
  },
  
  removeLink: function() {
    if(this.scroller && this.scroller.editable()) {
      var remove_text = (this.scroller.options.title && this.scroller.options.title != "&nbsp;") ? 'Remove from ' + this.scroller.options.title : "Remove";
      var removeLink = a({className:'action remove clickable'}, remove_text);
      Event.observe(removeLink, 'click', this.handleRemove.bind(this));
      return removeLink;
    }
  },

  handleRemove: function() {
    if( window.confirm(this.confirmMessage()) ) {
      LikeMe.Command.RemoveRanking.send(this);
    }
  },

  confirmMessage: function() {
    return 'Are you sure you want to remove '+ this.ratable.name + ' from your ' + this.scroller.options.name + '?';
  },

  handleMouseDown: function(event) {
    this.mouseDownX = Event.pointerX(event);
    this.mouseDownY = Event.pointerY(event);
    Event.observe(this.ratableImg, "mouseup", this.mouseUpListener);
    setTimeout(this.clearMouseUp.bind(this), 300);
  },

  handleMouseUp: function(event) {
    if(Math.abs(Event.pointerX(event) - this.mouseDownX) < 2
        && Math.abs(Event.pointerY(event) - this.mouseDownY) < 2) {
      this.redirectToRatable();
    }
  },

  clearMouseUp: function() {
    Event.stopObserving(this.ratableImg, "mouseup", this.mouseUpListener);
  },

  redirectToRatable: function() {
    window.location.href = this.ratable.url;
  },

  generateShareLink: function() {
    if(Services.userService.currentUser()) {
      var sharingLink = a({className:'action share-place clickable'}, LikeMe.Command.Follow.phrase.shareActionLink);
      Event.observe(sharingLink, 'click', this.shareThis.bind(this));
      return sharingLink;
    }
  },

  generateFacebookShareLink: function() {
    var fbShareLink = a({className:'action facebook clickable'}, 'Share with Facebook Friends');
    var url = LikeMe.Page.getCurrentHostname() + this.ratable.url;
    Event.observe(fbShareLink, 'click', LikeMe.Facebook.share_url.bind(this, url));
    return fbShareLink;
  },

  generateUnhideLink: function() {
    var unhideLink = a({className:'action remove'}, this.unhide_text);
    var clicked = false;
    Event.observe(unhideLink, 'click', function() {
      if (!clicked) {
        this.unhideThis();
        clicked = true;
      }
    }.bind(this));
    return unhideLink;
  },

  unhideThis: function() {
    LikeMe.Command.Generic.send({
      url: Routing.user_hidden_flaggings_path({user_id: Services.userService.currentUser().toParam}),
      method: 'delete',
      paramHash: {flaggable_type: this.flaggable_type, flaggable_id: this.ratable.id},
      executeLocally: function() {
        this.ratable.hideFromScrollers();
        Services.page.tooltip()._hideTooltip();
      }.bind(this)
    });    
  },

  createHideLinkElement: function() {
    var hideLink = a({className:'action remove'}, this.hide_text);
    var clicked = false;
    Event.observe(hideLink, 'click', function() {
      if (!clicked) {
        this.hideThis();
        clicked = true;
      }
    }.bind(this));
    return hideLink;
  },

  hideThis: function () {
    LikeMe.Command.Generic.send({
      url: Routing.user_hidden_flaggings_path({user_id: Services.userService.currentUser().toParam}),
      paramHash: {flaggable_type: this.flaggable_type, flaggable_id: this.ratable.id},
      executeLocally: function() {
        this.ratable.hideFromScrollers();
        Services.page.tooltip()._hideTooltip();
      }.bind(this)
    });
  },

  paintUnderImage: function() {
  }

});

LikeMe.View.RankedPerson = Class.create(LikeMe.View.RankedItem, {
  flaggable_type: "User",
  hide_text: "Hide",
  unhide_text: "Unhide",

  initialize: function($super, scroller, ranking) {
    $super(scroller, ranking);

    if (this.ratable.similarity)
      this.withPercentClassName = 'with-percent';
  },

  generateFollowLink: function() {
    var followLink = this.ratable.followLink();
    if(followLink) {
      //if(this.scroller.options && this.scroller.options.subject && this.scroller.options.subject.match(/FollowsMe/i)) {
      //  return null;
      //}
      if(followLink.innerHTML.match(/unfollow/i) != null) {
        return followLink;
      }
      if(!this.scroller || !this.scroller.editable()) {
        return followLink;
      }
    }
  },

  generateSendMessageLink: function() {
    return this.ratable.sendMessageLink();
  },

  generateSearchAsLink: function() {
    if (this.ratable.canSearchLike && Services.userService.currentUser() && (Services.userService.currentUser() != this.ratable))
    {
      var searchAsLink = a({className:'action search-as'}, 'Search Like This Person');
      Event.observe(searchAsLink, 'click', this.searchLikeThisPerson.bind(this));
      return searchAsLink;
    }
  },

  generateHideLink: function() {
    if ( Services.userService.currentUser() && (Services.userService.currentUser() != this.ratable) && this.scroller && !this.scroller.rankingGroup.includesHidden)
    {
      if ( this.ratable.relation == LikeMe.Command.Follow.phrase.relationNone)
      {
        return this.createHideLinkElement();
      }
    }
  },

  generateShareLink: function($super) {
    if ( Services.userService.currentUser() && (Services.userService.currentUser() != this.ratable))
    {
      return $super();
    }
  },

  generateFeedLink: function() {
    return a({className:'action make-default', href:'/activity/user/'+this.ratable.toParam},'View Activity');
  },

  getActionElements: function($super) {
    var actionElements = [];// = $super();
    actionElements.push(this.generateFollowLink());
    actionElements.push(this.generateFeedLink());
    actionElements.push(this.generateShareLink());
    actionElements.push(this.generateFacebookShareLink());
    actionElements.push(this.generateSendMessageLink());
    actionElements.push(this.generateSearchAsLink());
    actionElements.push(this.generateHideLink());
    this.actionElements = actionElements.compact();
    return this.actionElements;
  },

  detailTitleClass: function() {
    return this.ratable.guestOrOfficial ? "tooltip-star" : "";
  },

  paint: function($super) {
    $super();
  },

  shareThis: function () {
    var path = Routing.share_message_path( {"message[target_id]": this.ratable.id, "message[target_type]": "User"} );
    Modalbox.show(path, {title: LikeMe.Command.Follow.phrase.shareActionLink, width: 600, autoFocusing: true});
  },

  confirmMessage: function() {
    return 'Are you sure you want to remove '+ this.ratable.name + LikeMe.Command.Follow.phrase.removeFrom + '?';
  },
  
  searchLikeThisPerson: function() {
    new LikeMe.View.DiscoverAs({user: this.ratable});
  }

});


LikeMe.View.RankedPersonFull = Class.create(LikeMe.View.RankedPerson, {
  height: null,
  
  paintUnderImage: function() {

    var reclink = this.generateFollowLink();

    if (!reclink || reclink.innerHTML.match(/unfollow/i)) reclink = this.generateFeedLink();

    var sendmsg = this.generateSendMessageLink();
    if ( !sendmsg ) sendmsg = span({});
    else reclink = sendmsg;

    this.underImage.innerHTML = '';
    this.underImage.appendChild(
      div({},[
        h5({}, [
          a({href: this.ratable.url},this.ratable.name),
          em(this.ratable.location)
        ]),
        hr({}),
        reclink
      ])
    );
  }

});


LikeMe.View.RankedPersonDetailed = Class.create(LikeMe.View.RankedPerson, {
  height: null,

  paintTooltip: function() {
    this.tooltipElement.innerHTML = "";

    this.tooltipElement.appendChild(
      div({}, [
        h5({className: this.detailTitleClass()}, [
          a({href: this.ratable.url},this.ratable.name),
          em(this.ratable.location)
        ])
      ])
    );
  },

  tooltipElementClass: function() {
    return 'details';
  },

  paint: function() {
    this.hookElement.innerHTML = "";
    this.contentDiv = div({className:"ratable-ranking"});
    this.hookElement.appendChild(this.contentDiv);

    this.contentDiv.appendChild(this.ratableImg);
    
    this.paintTooltip();

    this.hookElement.appendChild(this.tooltipElement);
    Event.observe(this.ratableImg, "mousedown", this.handleMouseDown.bindAsEventListener(this));
  }

});


LikeMe.View.RankedPersonMedium = Class.create(LikeMe.View.RankedPerson, {
  height: 80,
  width: 80,


  initialize: function($super, scroller, ranking) {
    $super(scroller, ranking);

    if (this.ratable.similarity)
      this.withPercentClassName = 'with-percent-medium';
    else
      this.withoutPercentClassName = 'hoverable-medium';
  },

  setRatableImg: function() {
    this.ratableImg = img({src: this.ratable.photoUrl('medium'), alt: this.ratable.name, className: "ratable-img medium-photo"});
  }
});


LikeMe.View.RankedPersonMinimal = Class.create(LikeMe.View.RankedPerson, {
  height: 100,
  width: 100,
  hookElementClass: 'ranked-person-minimal-container',


  initialize: function($super, ratable) {
    $super(null, ratable);
  },

  paintTooltipDetails: function() {
  },

  paint: function() {
    this.hookElement.innerHTML = "";
    this.contentDiv = div({className:'ranked-person-minimal'});
    this.hookElement.appendChild(this.contentDiv);

    this.contentDiv.appendChild(a({href: Routing.user_guidebook_path({user_id: this.ratable.toParam})}, [
      this.ratableImg
    ]));

    var hoverDiv = span({className:"hoverable with-percent"}, [
      textNode( this.ratable.similarity.toFixed(0) + "%")
    ]);

    this.paintTooltip();

    hoverDiv.appendChild(this.tooltipElement);

    this.hookElement.appendChild(hoverDiv);
    this.hoverable = Services.page.tooltip().applyTo(hoverDiv);
  },

  setRatableImg: function() {
    this.ratableImg = img({src: this.ratable.photoUrl('summary'), alt: this.ratable.name, className: "summary-photo summary-photo-borders"});
  }

});


LikeMe.View.RankedPersonFeed = Class.create(LikeMe.View.RankedPerson, {
  height: 82,
  width: 82,
  hookElementClass: 'ranked-person-minimal-container',


  initialize: function($super, ratable, options) {
    this.options = this.defaultOptions();
    this.addOptions(options);

    $super(null, ratable);
  },

  addOptions: function(options) {
    Object.extend(this.options, options || {} );
  },

  defaultOptions: function() {
    return {
      photoSize: 'medium',
      photoClass: 'feed-photo'
    };
  },

  paintTooltipDetails: function() {
  },

  paint: function() {
    this.hookElement.innerHTML = "";
    this.contentDiv = div({className:'ranked-person-minimal'});
    this.hookElement.appendChild(this.contentDiv);

    this.contentDiv.appendChild(a({href: Routing.user_guidebook_path({user_id: this.ratable.toParam})}, [
      this.ratableImg
    ]));

    var hoverDiv = span({className:"hoverable with-percent"}, [
      textNode( this.ratable.similarity.toFixed(0) + "%")
    ]);

    this.paintTooltip();

    hoverDiv.appendChild(this.tooltipElement);

    this.hookElement.appendChild(hoverDiv);
    this.hoverable = Services.page.tooltip().applyTo(hoverDiv);
  },

  setRatableImg: function() {
    this.ratableImg = img({src: this.ratable.photoUrl(this.options.photoSize), alt: this.ratable.name, className: this.options.photoClass});
  }

});


LikeMe.View.HiddenRankedPerson = Class.create(LikeMe.View.RankedPerson, {
  getActionElements: function() {
    return this.actionElements = [this.generateUnhideLink()];
  }
});

LikeMe.View.RankedRatable = Class.create(LikeMe.View.RankedItem, {
  flaggable_type: "Ratable",
  hide_text: "Not Like Me",
  
  initialize: function($super, scroller, ratable) {
    $super(scroller, ratable);
  },

  getRatableImgAlt: function() {
    return this.ratable.name + ", " + this.ratable.location;
  },
  
  generateRecommendationLink: function(overrideEditableCheck) {
    if ( Services.userService.currentUser() ) {
      if ( !this.scroller.editable() || overrideEditableCheck) {
        return this.ratable.recommendationLink({category:this.scroller.rankingGroup.category, context:'recommendation'});
      }
    }
  },

  generateToDoLink: function(overrideEditableCheck) {
    if ( Services.userService.currentUser() ) {
      if ( !this.scroller.editable() || overrideEditableCheck) {
        return this.ratable.toDoLink();
      }
    }
  },
  
  generateInsideWordLink: function() {
    if ( Services.userService.currentUser() && !this.ratable.reviewed ) {
      var insidewordLink = a({className:'action add-inside-word clickable'}, 'Add your Inside Word');
      Event.observe(insidewordLink, 'click', this.ratable.showInsideWordBox.bind(this.ratable, {category:this.scroller.rankingGroup.category}));
      return insidewordLink;
    }
  },

  generateHideLink: function() {
    if ( Services.userService.currentUser() && !this.ratable.recommended() && !this.scroller.rankingGroup.includesHidden) {
      return this.createHideLinkElement();
    }
  },

  getActionElements: function() {
    var actionElements = [];
    actionElements.push(this.removeLink());
    actionElements.push(this.generateRecommendationLink());
    actionElements.push(this.generateToDoLink());
    actionElements.push(this.generateInsideWordLink());
    actionElements.push(this.generateShareLink());
    actionElements.push(this.generateFacebookShareLink());
    actionElements.push(this.generateHideLink());

    this.actionElements = actionElements.compact();
    return this.actionElements;
  },

  shareThis: function () {
    var path = Routing.share_message_path( {"message[target_id]": this.ratable.id, "message[target_type]": "Ratable"} );
    Modalbox.show(path, {title: LikeMe.Command.Follow.phrase.shareActionLink, width: 600, autoFocusing: true});
  }
});


LikeMe.View.RankedEvent = Class.create(LikeMe.View.RankedRatable, {
  paintTooltipDetails: function(hookElement) {
    var event_details = [a({href: this.ratable.url}, this.ratable.name)];
    event_details.push(br());

    if (this.ratable.nextStartDate >= Date.today()) {
      event_details.push(em(this.ratable.nextStartDate.toString('MMM d') + ', '));
    }
    event_details.push(em(this.ratable.venueName));

    event_details.push(br());
    event_details.push(em(this.ratable.location));

    event_details.push(br());
    event_details.push(em("<strong>" + this.ratable.event_categories + "</strong>"));

    hookElement.appendChild(
    h5({className: this.detailTitleClass()}, event_details)
    );
  }
});


LikeMe.View.RankedEventFull = Class.create(LikeMe.View.RankedRatable, {
  height: null,

  paintUnderImage: function() {

    var reclink = this.generateRecommendationLink();
    if ( reclink ) {
       reclink.innerHTML = reclink.innerHTML.replace('Add to Recommendations','Recommend');
       reclink.innerHTML = reclink.innerHTML.replace('Remove from Recommendations','Unrecommend');
    } else {
       reclink = span({});
    }

    var hidelink = this.generateHideLink();
    if ( !hidelink ) {
       hidelink = span({});
    }

    var when = '';
    if (this.ratable.nextStartDate >= Date.today()) {
      when = this.ratable.nextStartDate.toString('MMMM dd');
    }

    this.underImage.innerHTML = '';
    this.underImage.appendChild(
      div({},[
        h5({style: {minHeight: '60px'}}, [
          a({href: this.ratable.url},this.ratable.name),
          em(when),
          em(this.ratable.venueName)
        ]),
        hr({}),
        reclink,
        br({}),
        hidelink,
        hr({}),
        em(this.ratable.event_categories),
        em(this.ratable.location)
      ])
    );
  },

  paintTooltipDetails: function(hookElement) {
    var event_details = [a({href: this.ratable.url}, this.ratable.name)];
    event_details.push(br());

    if (this.ratable.nextStartDate >= Date.today()) {
      event_details.push(em(this.ratable.nextStartDate.toString('MMM d') + ', '));
    }
    event_details.push(em(this.ratable.venueName));

    event_details.push(br());
    event_details.push(em(this.ratable.location));

    event_details.push(br());
    event_details.push(em("<strong>" + this.ratable.event_categories + "</strong>"));

    hookElement.appendChild(
    h5({className: this.detailTitleClass()}, event_details)
    );
  }
});


LikeMe.View.RankedRatableDetailed = Class.create(LikeMe.View.RankedRatable, {
  height: null,

  paintTooltip: function() {
    this.tooltipElement.innerHTML = "";

    if( this.ratable.event_categories ) {
      var extra = span({className: 'small'}, this.ratable.event_categories);
    }
    else {
      var extra = span({className: 'small'}, '' + this.ratable.rankingsCount + ( this.ratable.rankingsCount == 1 ? ' recommendation' : ' recommendations'));
    }

    this.tooltipElement.appendChild(
      div({}, [
        h5({className: this.detailTitleClass()}, [
          a({href: this.ratable.url},this.ratable.name),
          em(this.ratable.location)
        ]),
        extra
      ])
    );
  },

  tooltipElementClass: function() {
    return 'details';
  },

  paint: function() {
    this.hookElement.innerHTML = "";
    this.contentDiv = div({className:"ratable-ranking"});
    this.hookElement.appendChild(this.contentDiv);

    this.contentDiv.appendChild(this.ratableImg);

    this.paintTooltip();

    this.hookElement.appendChild(this.tooltipElement);
    Event.observe(this.ratableImg, "mousedown", this.handleMouseDown.bindAsEventListener(this));
  }

});


LikeMe.View.RankedRatableUnder = Class.create(LikeMe.View.RankedRatable, {
  height: null,
  width: 120,

  paintUnderImage: function() {
    this.underImage.innerHTML = "";

    var show_title = '';

    if ( this.ratable.name.unescapeHTML ) {
      var name = this.ratable.name.unescapeHTML();
      if ( name.length >= 30 ) {
        show_title = name;
        name = name.substr(0,27).strip() + '...';
        this.ratable.name = name.escapeHTML();
      }
    }

    this.underImage.appendChild(
      h5({}, [ a({href: this.ratable.url, title: show_title},this.ratable.name),
               em(this.ratable.location) ]
        )
    );
  },

  paint: function() {
    this.hookElement.innerHTML = "";
    this.contentDiv = div({className:"ratable-ranking", style: {width: '90px'}});
    this.hookElement.appendChild(this.contentDiv);

    this.contentDiv.appendChild(this.ratableImg);

    Event.observe(this.ratableImg, "mousedown", this.handleMouseDown.bindAsEventListener(this));

    this.underImage = div({className:"descript"});
    this.hookElement.appendChild(this.underImage);
    this.paintUnderImage();
  }

});


LikeMe.View.RankedRatableFull = Class.create(LikeMe.View.RankedRatable, {
  height: null,

  paintUnderImage: function() {

    var reclink = this.generateRecommendationLink();
    if ( reclink ) {
       reclink.innerHTML = reclink.innerHTML.replace('Add to Recommendations','Recommend');
       reclink.innerHTML = reclink.innerHTML.replace('Remove from Recommendations','Unrecommend');
    } else {
       reclink = span({});
    }

    var hidelink = this.generateHideLink();
    if ( !hidelink ) {
       hidelink = span({});
    }

    this.underImage.innerHTML = '';
    this.underImage.appendChild(
      div({},[
        h5({}, [
          a({href: this.ratable.url},this.ratable.name),
          em(this.ratable.location)
        ]),
        hr({}),
        reclink,
        br({}),
        hidelink
      ])
    );
  }

});


LikeMe.View.RankedRatableToDo = Class.create(LikeMe.View.RankedRatable, {
  paintSubDetails: function(containerElement) {
   containerElement.appendChild(this.noteHookElement = div({className: 'inline-editor-hook'}, ''));
   this.inlineNoteEditor = new LikeMe.View.InlineEditor(this.noteHookElement, {
      minimumRows: 2,
      width: 300,
      parameter: "text",
      defaultText: "Click to add a note.",
      text: this.ratable.note,
      editable: true,
      multiline: false,
      maximumLength: 255,
      updateUrl: Routing.place_note_path({
        place_id: this.ratable.toParam
      }),
      method: 'put',
      afterStateChange: this.resizeHover.bind(this),
      customUpdateHandler: this.updateNoteText.bind(this)
    });
    this.inlineNoteEditor.paint();
  },

  updateNoteText: function() {
    this.ratable.note = this.inlineNoteEditor.newText();
    LikeMe.Command.InlineEditorUpdate.send(this.inlineNoteEditor);
    this.ratable.repaintViews();
  },

  getActionElements: function() {
    var actionElements = [];
    actionElements.push(this.removeLink());
    actionElements.push(this.generateRecommendationLink(true));
    actionElements.push(this.generateShareLink());
    actionElements.push(this.generateFacebookShareLink());

    return actionElements.compact();
  }
});

LikeMe.View.HiddenRankedRatable = Class.create(LikeMe.View.RankedRatable, {
  getActionElements: function() {
    return this.actionElements = [this.generateUnhideLink()];
  }
});

LikeMe.View.RatableAddWizard = Class.create({

  initialize: function(options) {
    this.category = options.category;
    if(!this.category) {
      this.category = LikeMe.Model.Category.all;
      this.hasNoCategory = true;
    }
    this.name = options.name;
    if(!this.name) {
      this.name = "Recommendations";
    }
    this._content = div({className:'modal'});
    this.tabMenu = div({className: 'tabs'}, [ ]);
    this.addToScroller = options.addToScroller;
    this.removeFromScroller = options.removeFromScroller;
    this.inScroller = options.inScroller;
  },

  toggle: function() {
    var title = (this.name == 'Recommendations') ? 'Recommend ' + LikeMe.Model.Category.toEnglishMap[this.category.name] : 'Add to your To Do list';
    Services.page.activeRatableAddWizard = this;
    Modalbox.show(this.content(),{title: title, height: 267, width: 800, afterLoad: this.performAfterLoad.bind(this)});
    this.renderSearch();
  },

  close: function() {
    Services.page.activeRatableAddWizard = null;
    Modalbox.hide();
  },

  content: function() {
    return this._content;
  },

  populateTabMenu: function(current) {

    if(current == 'search') {
      this.popularLink = li({}, [a({className: 'clickable', id: 'popular_tab'}, 'Browse Popular')])
      this.searchLink = li({className: 'current', id: 'search_tab'}, [span({}, 'Search by Name')])
      this.browseLink = li({}, [a({className: 'clickable', id: 'browse_tab'}, 'Browse Listings')])
    }else if(current == 'browse') {
      this.popularLink = li({}, [a({className: 'clickable', id: 'popular_tab'}, 'Browse Popular')])
      this.searchLink = li({}, [a({className: 'clickable', id: 'search_tab'}, 'Search by Name')])
      this.browseLink = li({className: 'current', id: 'browse_tab'}, [span({}, 'Browse Listings')])
    } else {
      this.popularLink = li({className: 'current', id: 'popular_tab'}, [span({}, 'Browse Popular')])
      this.searchLink = li({}, [a({className: 'clickable', id: 'search_tab'}, 'Search by Name')])
      this.browseLink = li({}, [a({className: 'clickable', id: 'browse_tab'}, 'Browse Listings')])
    }

    this.tabMenu.innerHTML = "";
    this.tabMenu.appendChild(
    		ul({}, [
          this.searchLink,
          this.browseLink,
          this.popularLink
    		]));
  },

  cleanup: function() {
    if(this.searchForm) { this.searchForm.destroy(); this.searchForm = null; }
    if(this.popularSelector) { this.popularSelector.destroy(); this.popularSelector = null; }
    this.ratableBrowser = undefined;
    this._content.innerHTML = "";
    if(this.autocompleter) { this.autocompleter.destroy(); this.autocompleter = null; }
  },

  renderSearch: function() {
    this.cleanup();
    var searchHookElement = div();
    this.searchForm = new LikeMe.View.RatableSearchForm(searchHookElement, {
      category: this.category,
      ratableRenderer: this._renderRatable.bind(this),
      beforeRenderResults: function() {this.searchForm.resultsArea.style.visibility = "hidden"}.bind(this),
      afterRenderResults: this.resizeModalbox.bind(this, function() {this.searchForm.resultsArea.style.visibility = ""}.bind(this))
    });

    this._content.appendChild(div({}, [
			this.tabMenu,
			div({className: 'clear'}, ''),
      searchHookElement
    ]));

    this.populateTabMenu('search');

    Event.observe(this.browseLink, "click", this.renderBrowseStep.bind(this));
    Event.observe(this.popularLink, "click", this.renderPopularStep.bind(this));

    Modalbox.setHeight(267);
  },

  performAfterLoad: function() {
    this.searchForm.titleField.focus();
  },

  showSpinner: function() {
    this.resultsArea.innerHTML = "";
    Element.show(this.searchSpinner);
  },

  hideSpinner: function() {
    Element.hide(this.searchSpinner);
  },

  resizeModalbox: function(callback) {
    Modalbox.resizeToContent({ afterResize: function() { if(this.resultsArea) {this.resultsArea.style.visibility = "";} if(callback) {callback();} }.bind(this) });
  },

  renderStepThreeContent: function(ratable) {
    this._content.innerHTML = "";
    this._content.appendChild(p(ratable.name+' was succesfully added to your '+this.category.pluralName+'!'));
    this._content.appendChild(p('Now what do you want to do?'));
    this._content.appendChild(br());

    if ( !ratable.reviewed ) {
      var insidewordLink = a({className:'action add-inside-word clickable'}, 'Add your Inside Word');
      Event.observe(insidewordLink, 'click', function(){
        new LikeMe.View.InsideWordModalBox({ratable: ratable, category: this.category}).paint();
      }.bind(this));
      this._content.appendChild(insidewordLink);
      this._content.appendChild(br());
    }  

    var uploadLink = a({className:'action more-photos clickable'}, 'Upload a photo for this recommendation');
    Event.observe(uploadLink, 'click', function(){
      window.location = Routing.place_photos_path({place_id: ratable.toParam});
    }.bind(this));
    this._content.appendChild(uploadLink);
    this._content.appendChild(br());

    var shareLink = a({className:'action share-place clickable'}, 'Share this place with your followers');
    Event.observe(shareLink, 'click', function(){ Modalbox.show('/share_message?message%5Btarget_id%5D='+ ratable.id +'&message%5Btarget_type%5D=Ratable', {title: LikeMe.Command.Follow.phrase.shareActionLink, width: 600, autoFocusing: true});});
    this._content.appendChild(shareLink);
    this._content.appendChild(br());

    var addLink = a({className:'action add-reccomendation clickable'}, 'Add another place');
    Event.observe(addLink, 'click', this.renderSearch.bind(this));
    this._content.appendChild(addLink);
    this._content.appendChild(br());

    var returnLink = a({className:'action return clickable'}, 'Return to my recommendations');
    Event.observe(returnLink, 'click', this.close.bind(this));
    this._content.appendChild(returnLink);

    this.resizeModalbox();
  },

  renderBrowseStep: function()  {
    this.cleanup();
		this._content.appendChild(div({}, [
			this.tabMenu = div({className: 'tabs'}, [ ]),
			div({className: 'clear'}, ''),
      this.browseArea = div()
    ]));

		this.populateTabMenu("browse");
    Event.observe(this.searchLink, "click", this.renderSearch.bind(this));
    Event.observe(this.popularLink, "click", this.renderPopularStep.bind(this));

    var ratableBrowserElement = div({className: 'ratable_browser'});

    this.browseArea.appendChild( ratableBrowserElement );

    this.ratableBrowser = new LikeMe.View.RatableBrowser(ratableBrowserElement, {
      category: this.category,
      renderRatable: function(ratable) {
        return this._renderRatable( null, ratable);
      }.bind(this)
    });
    this.resizeForBrowse();
    this.ratableBrowser.paint();
  },

  renderPopularStep: function()  {
    this.cleanup();
    var isRatablePresent = this.inScroller ? this.inScroller : undefined;
    var popularRatableSelectorClass = (this.name == 'Recommendations') ? LikeMe.View.PopularRatablesSelector : LikeMe.View.ToDoPopularRatablesSelector;

    var defaultOptions = {
      addRatable: this._addRatableToScroller.bind(this),
      removeRatable: this._removeRatableFromScroller.bind(this),
      isRatablePresent: isRatablePresent,
      onBeginLoading: function() { Modalbox.setHeight(270); }.bind(this),
      onFinishedLoading: function() { Modalbox.resizeToContent(); }.bind(this),
      category: this.category
    };

    var popularHookElement = div();
    this.popularSelector = new LikeMe.View.PopularRatablesForm(popularHookElement, {
      popularSelectorClass: popularRatableSelectorClass,
      popularSelectorOptions: defaultOptions
    });

    this._content.appendChild(div({}, [
			this.tabMenu = div({className: 'tabs'}, [ ]),
			div({className: 'clear'}, ''),
      popularHookElement
    ]));

    this.populateTabMenu("popular");
    Event.observe(this.browseLink, "click", this.renderBrowseStep.bind(this));
    Event.observe(this.searchLink, "click", this.renderSearch.bind(this));
  },

  resizeForBrowse: function() {
    Modalbox.setHeight(450);
  },

  _doEventTracking: function(action) {
    var trackingUrl = (this.name == 'Recommendations') ? action + "_recommendations" : action + "_to_do"
    Services.page.trackUrl('/events/'+ trackingUrl);
  },

  _removeRatableFromScroller: function(ratable) {
    this._doEventTracking("remove_from");
    this.removeFromScroller(ratable);
  },

  _addRatableToScroller: function(ratable) {
    this._doEventTracking("add_to");
    this.addToScroller(ratable);
  },

  _addRatableToScrollerAndDoNextStep: function(ratable) {
    this._doEventTracking("add_to");
    this._addToScroller(ratable);
  },

  _renderRatable: function(element, ratable) {
    var ratableAddButtons = [];

    if (!this.inScroller(ratable)) {
      var addRatableToScroller = function() { this._addRatableToScrollerAndDoNextStep(ratable) }
      var addLink = AbleButton.applyTo(a({onclick: function() {return false;}}, 'Add to '+this.name), 'able-button blue');
      Event.observe(addLink, 'click', addRatableToScroller.bindAsEventListener(this));
      ratableAddButtons = [div({className:'add_button'}, [addLink])];
    }

    var detailsDiv = LikeMe.View.RatableView.renderRatable(ratable, {additionalSummaryElements: ratableAddButtons});

    if(element) {
      element.appendChild(detailsDiv);
    } else {
      return detailsDiv;
    }
  },

  _addToScroller: function(ratable) {
    this.addToScroller(ratable);
    if(ratable.reviewed || this.name != "Recommendations") {
      this.close();
    } else {
      this.renderStepThreeContent(ratable);
    }
  }

});


LikeMe.View.RatableBrowser = Class.create(LikeMe.View.Pagination, {

  paginationStyle: 'letters',

  initialize: function(hookElement, options) {
    this.hookElement = $(hookElement);
    this.views = [];

    this.options = Object.extend({
      renderRatable: this.renderRatable.bind(this)
    }, options);
    this.options.nonCategorical = (!this.options.category || (this.options.category.toParam == 'all'));
    this.citiesData = {};
    this.ratablesData = {};
    this.category = this.options.category;
    this.state = Cookie.get("browse_state");
    this.city = Cookie.get("browse_city");
    if(this.options.nonCategorical) {
      this.contexts = ['categories', 'states', 'cities', 'places', 'ratable'];
    } else {
      this.contexts = ['states', 'cities', 'places', 'ratable'];
    }
  },

  statesUrl: function() {
    return Routing.browse_states_path({category: this.category.toParam});
  },

  citiesUrl: function() {
    return Routing.browse_cities_path({category: this.category.toParam, state: this.state});
  },

  ratablesUrl: function() {
    return Routing.browse_state_city_path({category: this.category.toParam, state: this.state, city: this.city});
  },

  paint: function() {
    if( this.options.nonCategorical ) {
      this.setContext('categories');
      this.paintContent(0);
    } else if (this.city && this.state) {
      this.requestRatables(this.city);
    } else if (this.state) {
      this.requestCities(this.state);
    } else {
      this.requestStates(this.category);
    }
  },

  startLoading: function() {
    this.loading = true;
    this.clearContent();
    this.hookElement.appendChild(div({className: 'spinner'}, [img({src: '/images/ajax-loader-small.gif'})]));
  },

  doneLoading: function() {
    this.loading = false;
  },

  requestStates: function(category) {
    this.state = null;
    this.city = null;
    if(!this.loading) {
      this.startLoading();
      this.category = category;
      LikeMe.Command.Generic.send({
        url: this.statesUrl(),
        method: 'get',
        callback: this.storeStates.bind(this),
        undo: this.doneLoading.bind(this)
      });
    }
  },

  storeStates: function(states) {
    this.states = states;
    this.setContext('states');
    this.paintContent(0);
  },

  requestCities: function(state) {
    if(!this.loading) {
      this.startLoading();
      this.state = state;
      LikeMe.Command.Generic.send({
        url: this.citiesUrl(),
        method: 'get',
        callback: this.storeCities.bind(this),
        undo: this.doneLoading.bind(this)
      });
    }
  },

  storeCities: function (cities) {
    this.citiesData = {cities: cities, size: cities.length};
    this.calculateCitiesPagination();
    this.setContext('cities');
    this.paintContent(0);
  },

  calculateCitiesPagination: function () {
    var cities = this.citiesData.cities;
    this.citiesData.letters = {};
    var letters = this.citiesData.letters;
    if (!cities || cities.length == 0) {
      return;
    }

    var firstLetters = cities.collect(function(city) {
      if(!city.charAt(0).toUpperCase().match(/[A-Z]/)) {
        return '#';
      }
      else {
        return city.charAt(0).toUpperCase();
      }
    });

    var position = 0;
    this.ALL_LETTERS.each(function(letter) {
      if(firstLetters[position] == letter) {
        letters[letter] = position;
        while(firstLetters[position] == letter) {
          position++;
        }
      }
    });
  },

  requestRatables: function (city) {
    if(!this.loading) {
      this.startLoading();
      this.city = city;
      LikeMe.Command.Generic.send({
        url: this.ratablesUrl(),
        method: 'get',
        paramHash: {lo: 0, hi: 44},
        callback: this.storeRatables.bind(this),
        undo: this.doneLoading.bind(this)
      });
    }
  },

  loadRatables: function (offset) {
    this.startLoading();
    LikeMe.Command.Generic.send({
      url: this.ratablesUrl(),
      method: 'get',
      paramHash: {lo: offset, hi: offset + this.perPage - 1},
      callback: function(ratableData) {
        this.mergeRatables(ratableData.ratables, offset);
        this.paintContent(offset);
      }.bind(this),
      undo: this.doneLoading.bind(this)
    });
  },

  storeRatables: function (ratableData) {
    var ratables = ratableData.ratables;
    this.ratablesData = ratableData;
    this.ratablesData.ratables = new Array(this.ratablesData.size);
    this.mergeRatables(ratables, 0);
    this.setContext('places');
    this.paintContent(0);
  },

  mergeRatables: function (ratables, offset) {
    var argsForSplice = [offset, ratables.length].concat(ratables);
    // Use apply so we can specify the full args list since splice takes a list of things to splice in, not a single array
    this.ratablesData.ratables.splice.apply(this.ratablesData.ratables, argsForSplice);
  },
  
  paintBackLinks: function() {
    var tmp_array = this.contexts.slice(0, this.contexts.indexOf(this.itemContext));
    
    var backLinks = div({className: 'back-links'});
    this.hookElement.appendChild(backLinks);
    
    tmp_array.each( 
      function(x){ 
        backLinks.appendChild(a({href:'#', onclick: function() { this.goBack(x); return false; }.bind(this) }, 'Back to '+x ));
        if( x != tmp_array[tmp_array.length - 1]){
          backLinks.appendChild(textNode(' | ')); 
        }  
      }.bind(this)
    );

  },

  clearContent: function() {
    this.hookElement.innerHTML = '';
    this.views.each(function( view ) {
      view.destroy();
    });
    this.views = [];
  },

  paintTitle: function() {
    this.hookElement.appendChild(h3(this.title));
  },

  paintContent: function (offset) {
    if(this.requiresLoad(offset)) {
      this.loadRatables(offset);
      return;
    }

    this.clearContent();

    this.paintBackLinks();

    this.paintTitle();

    this.hookElement.appendChild(div({className: 'clear'}));
    this.hookElement.appendChild(br());

    this.paintItems(offset);

    this.hookElement.appendChild(div({className: 'clear'}));
    this.hookElement.appendChild(br());

    var footerElement = div({className: 'footer'});
    this.hookElement.appendChild(footerElement);
    this.renderPagination(footerElement, offset);
    
    this.paintAddPlacePrompt(footerElement);

    this.doneLoading();
  },

  paintAddPlacePrompt: function(hookElement) {
    if(Services.userService.loggedIn()) {
      hookElement.appendChild(div({}, [
        span({}, ["Not finding what you're looking for? "]),
        a({href: Routing.new_place_path({title: '',
            category_id: (this.category) ? this.category.id : 0,
            state: (this.city) ? this.state : '',
            city: (this.city) ? this.city : ''})}, 'Add a new place to our database')
      ]));
    }
  },

  paintItems: function(offset) {
    var items = this.paginate(offset);
    if(items.length > 0) {
      var columnDiv = div({className: this.itemContext});
      this.hookElement.appendChild(columnDiv);
      LikeMe.View.Helpers.renderColumns(items, columnDiv, this.columns, this.itemPaintFunction.bind(this));
    }
    else {
      this.hookElement.appendChild(div({className: 'empty'}, [
        a({href: '#', onclick: function() { this.goBack(); return false; }.bind(this) }, 'No results found.  Click here to go back.')
      ]));
    }
  },

  renderPagination: function(hookElement, offset) {
    hookElement.appendChild(this.paginationElement = div({className: 'pagination'}));
    this.paintPagination(this.paginationElement, offset, this.paintContent.bind(this));
  },

  goBack: function (context) {
    if(!context) {
      context = this.contexts[this.contexts.indexOf(this.itemContext) - 1];
    }
    this.setContext(context);
    if (this.itemContext == 'cities' && this.items.length == 0) {
      this.requestCities(this.state);
    } else if (this.itemContext == 'states' && this.items.length == 0) {
      this.requestStates(this.category);
    } else {
      this.paintContent(0);
    }
  },

  contextVariables: {
    categories: {
      columns: 1,
      perPage: 60
    },
    states: {
      columns: 4,
      perPage: 60
    },
    cities: {
      columns: 4,
      perPage: 60
    },
    places: {
      columns: 3,
      perPage: 45
    },
    ratable: {
    }
  },

  setContext: function(context) {
    this.itemContext = context;
    this.columns = this.contextVariables[this.itemContext].columns;
    this.perPage = this.contextVariables[this.itemContext].perPage;
    switch (this.itemContext) {
      case 'categories':
        this.items = LikeMe.Model.Category.categories;
        this.title = 'Categories';
        this.itemPaintFunction = this.paintCategory;
        this.maxItemsOffset = this.items.length - 1;
        break;
      case 'states':
        this.items = this.states|| [];
        this.title = 'States';
        this.itemPaintFunction = this.paintState;
        this.maxItemsOffset = this.items.length - 1;
        break;
      case 'cities':
        this.items = this.citiesData.cities || []
        this.letterMap = this.citiesData.letters;
        this.title = 'Cities in '+this.state;
        this.itemPaintFunction = this.paintCity;
        this.maxItemsOffset = this.citiesData.size - 1;
        break;
      case 'places':
        this.items = this.ratablesData.ratables || [];
        this.letterMap = this.ratablesData.letters;
        this.title = this.category.pluralValue + ' in ' + this.city + ', ' + this.state;
        this.itemPaintFunction = this.paintRatable;
        this.maxItemsOffset = this.ratablesData.size - 1;
        break;
      case 'ratable':
        break;
    }
  },

  maxOffset: function() {
    return this.maxItemsOffset;
  },

  paintCategory: function(element, category) {
    var link = a({href: '#', onclick: function() {
      this.requestStates(category);
      return false; }.bind(this)}, category.pluralValue);
    element.appendChild(link)
  },

  paintState: function(element, state) {
    var link = a({href: '#', onclick: function() { this.requestCities(state[0]); return false;}.bind(this)}, state[1]);
    element.appendChild(link);
  },

  paintCity: function(element, city) {
    var link = a({href: '#', onclick: function() { this.requestRatables(city); return false;}.bind(this)}, city);
    element.appendChild(link);
  },

  paintRatable: function(element, ratable) {
    var link = a({href: '#', onclick: function(){ this.selectRatable(ratable.url); return false;}.bind(this) }, ratable.truncated_name)
    element.appendChild(link);
  },
 
  selectRatable: function (url) {
    this.setContext('ratable');
    this.startLoading( );
    LikeMe.Command.Generic.send({
      url: url + "?category_id="+this.category.id,
      method: 'get',
      callback: function(ratableJson) {
        Services.ratableService.register(ratableJson);
        var ratable = Services.ratableService.findById(ratableJson.id)
        this.clearContent();
        this.doneLoading();
        this.paintBackLinks();
        this.hookElement.appendChild(div({className: 'clear'}));
        this.hookElement.appendChild(this.options.renderRatable(ratable));
      }.bind(this)
    });
  },

  renderRatable: function(ratable) {
    var ratableView = new LikeMe.View.RatableSummary(ratable, {imageSize: 'medium'})
    this.views.push(ratableView);
    return ratableView.hookElement;
  },

  allItems: function() {
    return this.items;
  }
});


LikeMe.View.RatableInsideWord = Class.create({

  initialize: function(hookElement, options) {
    this.hookElement = hookElement;

    this.options = Object.extend({
    }, options);

  },

  paint: function() {
    this.hookElement.innerHTML = "";
    this.hookElement.appendChild(div({},[
      h3("Give us your Inside Word"),
      p("What's so great about this place? What can you tell people like you so they can have the best experience?"),
      h4("Title"),
      this.titleElement = input({type: 'text', name: 'insideWordTitle'}),
      h4("Your Word"),
      this.bodyElement = textarea({name: "insideWordBody"}),
      this.submitButton = AbleButton.applyTo(a({}, 'Submit'), 'able-button blue')
    ]));

    Event.observe(this.submitButton, 'click', function() {
      LikeMe.Command.Generic.send({
        url: this.options.ratable.url + '/reviews',
        paramHash: {
          title: this.titleElement.value,
          body: this.bodyElement.value
        },
        method: 'post',
        callback: function() { this.options.onComplete(); }.bind(this)
      })
    }.bind(this));
  }


});


LikeMe.View.RatableSearchForm = Class.create(LikeMe.View.Pagination, {

  paginationStyle: 'numbers',
  perPage: 4,

  initialize: function(hookElement, options) {
    this.hookElement = $(hookElement);
    this.views = [];
    this.options = {
      ratableRenderer: this.renderRatable.bind(this),
      category: LikeMe.Model.Category.all,
      near: $F('near')
    };

    this.options['perPage'] = 4;
    this.options = Object.extend(this.options, options || {});
    this.perPage = this.options['perPage'];

    this.paint();
  },

  destroy: function() {
    this.destroyed = true;
    if(this.autocompleter) { this.autocompleter.destroy(); this.autocompleter = null; }
    this.clearContent();
  },

  clearContent: function() {
    if( this.resultsArea ) { this.resultsArea.innerHTML = ''; };
    this.views.each(function( view ) {
      view.destroy();
    });
    this.views = [];
  },

  showSpinner: function() {
    Element.show(this.searchSpinner);
  },

  hideSpinner: function() {
    Element.hide(this.searchSpinner);
  },

  paint: function() {
    var autocompleterParent;
    this.hookElement.appendChild(div({}, [
      form({action: '/', onsubmit: this.performSearch.bind(this)},[
        div({className: 'form-left'}, [
          label({htmlFor: 'add_place_title'}, 'Name'),
          br(),
          this.titleField = input({id: 'add_place_title', type: 'text'})
        ]),

        autocompleterParent = div({className: 'form-right'}, [ 
          label({htmlFor: 'add_place_near'}, 'Near (City AND State, OR Zip)'),
          br(),
          this.nearField = input({id: 'add_place_near', type: 'text'})
        ]),
        div({className: 'form-button'}, [
          this.searchButton = input({type: 'submit', value: 'Find It'})
        ])
      ]),
      br(),
      br(),
      div({className: 'clear'}),
      this.searchSpinner = div({className: 'spinner', style:{display: "none"}}, [img({src: '/images/ajax-loader-small.gif'})]),
      this.resultsArea = div({className:'add_place_results'})
	  ]));

    this.searchButton = AbleButton.applyTo(this.searchButton, 'able-button blue', {allowDoubleClick: true});

    this.autocompleter = new LikeMe.View.Autocompleter(this.nearField, {
      url: Routing.city_state_autocompletions_path(),
      positionedParentId: autocompleterParent,
      delay: 50,
      onItemChosen: function() { this.nearField.form.onsubmit(); }.bind(this),
      cancelSearch: function(text) { return (/^[\s\d]*$/.test(text)) }.bind(this)
    });
  },

  performSearch: function() {
    if(!this.searching) {
      this.searching = true;
      this.clearContent();
      this.showSpinner();
      LikeMe.Command.Generic.send({
        url: '/categories/' + this.options.category.toParam + '/place_search',
        paramHash: {'title': this.titleField.value, 'near': this.nearField.value},
        callback: function(response) {
          this.searching = false;
          if(!this.destroyed) {
            this.hideSpinner();
            this.processSearchResults(response);
          }
        }.bind(this),
        undo: function() {
          this.searching = false;
        }
      });
    }
    return false;
  },

  processSearchResults: function(resultsSet) {
    resultsSet.results.each(function(ratable, index) {
      Services.ratableService.register(ratable);
      resultsSet.results[index] = Services.ratableService.findById(ratable.id);
    })
    this.resultsContainExactMatch = false;
    resultsSet.results = resultsSet.results.sortBy(function(result) {
      if (result.name.toLowerCase().unescapeHTML() == resultsSet.query.toLowerCase()) {
        this.resultsContainExactMatch = true;
        return 0;
      }
      return 1;
    }.bind(this));
    this.resultsSet = resultsSet;
    if(this.options.beforeRenderResults) { this.options.beforeRenderResults(); };
    this.renderResults();
    if(this.options.afterRenderResults) { this.options.afterRenderResults(); };
  },

  renderResults: function(offset) {
    offset = offset || 0
    this.clearContent();

    if ( this.allItems().length == 0 ) {
      this.resultsArea.appendChild(h2("Nothing found. Please try again."));
      if(Services.userService.loggedIn()) {
        this.resultsArea.appendChild(p({}, ["Not finding what you're looking for? ", a({href: Routing.new_place_path({title: this.titleField.value, category_id: this.options.category.id})}, 'Add a new place to our database')]));
      }
    } else {
      if (this.resultsContainExactMatch) {
        this.resultsArea.appendChild(h2("Select Place"));
      } else {
        this.resultsArea.appendChild(h2({className: 'notice'}, "Sorry we couldn't find an exact match. Did you mean one of these?"));
      }

      var placeList = div({className: 'place-list'});
      this.resultsArea.appendChild(placeList);
      LikeMe.View.Helpers.renderColumns(this.paginate(offset), placeList, 2, this.options.ratableRenderer);

      this.resultsArea.appendChild(div({className: 'clear'}));
      this.paginationElement = div({className: 'pagination'});
      this.resultsArea.appendChild(this.paginationElement);
      this.paintPagination(this.paginationElement, offset, this.renderResults.bind(this));

      if(Services.userService.loggedIn()) {
        this.resultsArea.appendChild(p({}, ["Not finding what you're looking for? ", a({href: Routing.new_place_path({title: this.titleField.value, category_id: this.options.category.id})}, 'Add a new place to our database')]));
      }
    }
  },

  renderRatable: function(column, ratable) {
    var ratableView = new LikeMe.View.RatableSummary(ratable, {imageSize: 'medium'})
    this.views.push(ratableView);
    column.appendChild(ratableView.hookElement);
  },

  allItems: function() {
    return this.resultsSet.results;
  },

  maxOffset: function() {
    return this.resultsSet.results.length - 1;
  }
});

LikeMe.View.RatableSelector = Class.create(LikeMe.View.ItemSelector, {
  initialize: function($super, hookElement, options) {
    options = Object.extend(options || {}, {
      itemService: Services.ratableService,
      containerClassName: 'ratables',
      afterItemsPaint: function() {setTimeout(this.paintSelectedItems.bind(this), 1)}.bind(this)
    });
    hookElement = this.buildSelectorStructure($(hookElement));
    $super(hookElement, options);
  },

  buildSelectorStructure: function(hookElement) {
    var newHook;
    hookElement.appendChild(
      div({className: 'ratables_wrapper'},
        [h2("Select"), newHook = div()]
      )
    );
    hookElement.appendChild(
      div({className: 'ratables_wrapper ratables_wrapper_selected'},
        [h2("Selected"), this.selectedItemsContainer = div()]
      )
    );
    return newHook;
  },

  renderItem: function(ratable) {
    var checkbox = input({type: "checkbox"});
    if(this.selectedItems.include(ratable)) {
      checkbox.checked = "selected";
    }
    return div({className: 'ratable'}, [
      div({className: 'selector'}, [checkbox]),
      img({className: 'medium-photo medium-photo-borders', src: ratable.photoUrl('medium')}),
      div({className: 'information'}, [h4(ratable.name), p(ratable.address)])
    ]);
  },

  paintSelectedItems: function() {
    var selectedCurrentScroll = 0;
    if(this.selectedItemsElement) {
      selectedCurrentScroll = this.selectedItemsElement.scrollTop;
    }
    this.selectedItemsContainer.innerHTML = '';
    this.selectedItemsContainer.appendChild(this.selectedItemsElement = div({className: this.options.containerClassName}))
    this.selectedItems.each(function(ratable) {
      this.selectedItemsElement.appendChild(this.renderSelectedRatable(ratable));
    }.bind(this));
    this.selectedItemsElement.scrollTop = selectedCurrentScroll;
  },

  renderSelectedRatable: function(ratable) {
    return div({className: 'ratable'}, [
      img({className: 'thumbnail-photo thumbnail-photo-borders', src: ratable.photoUrl('thumbnail')}),
      div({className: 'information'}, [h5(ratable.name)])
    ]);
  },

  submit: function() {
    this.selectedRatablesForm = form({method: "post", action: Routing.recommendation_collections_path()});
    this.selectedItems.each(function(ratable) {
      this.selectedRatablesForm.appendChild(input({type: "hidden", name:"ratable_ids[]", value:ratable.toParam}));
    }.bind(this));
    this.hookElement.appendChild(this.selectedRatablesForm);
    this.submitSelectedRatableForm();
  },

  submitSelectedRatableForm: function() {
    this.selectedRatablesForm.submit();
  }
});

LikeMe.View.RatableSummary = Class.create({

  initialize: function(ratable, options) {
    this.ratable = ratable;
    this.hookElement = div();
    this.options = Object.extend({imageSize:'summary'}, options);

    this.ratable.registerView( this );
    this.paint();
  },

  destroy: function() {
    this.ratable.unregisterView( this );
  },

  paint: function() {
    this.hookElement.innerHTML = '';
    this.hookElement.appendChild(div({className: 'place_details place-with-' + this.options.imageSize + '-photo'},[
      div({className: 'image_container'}, [
        a({href: this.ratable.url},[
          img({src: this.ratable.photoUrl(this.options.imageSize),
            alt:this.ratable.name,
            className: this.options.imageSize+'-photo '+this.options.imageSize+'-photo-borders'})
        ])
      ]),
      div({className: "place_summary"}, [
        div({className: "title"}, [a({href: this.ratable.url}, this.ratable.name)]),
        p({className: "address"}, this.ratable.address),
        this.linksDiv = div()
      ]),
      div({className: 'clear'})
    ]));

    this.paintLinks();
  },

  paintLinks: function() {
    this.linksDiv.innerHTML = '';
    this.linksDiv.appendChild(this.ratable.recommendationLink())
    this.linksDiv.appendChild(this.ratable.toDoLink())
  }

});

LikeMe.View.RatableView = {

  renderRatable: function(ratable, options) {
    options = Object.extend({imageSize:'summary', additionalSummaryElements: []}, options);
    var placeSummary = null;
    var detailsDiv = div({className: 'place_details place-with-' + options.imageSize + '-photo'},[
      div({className: 'image_container'}, [
        a({href: ratable.url},[
          img({src: ratable.photoUrl(options.imageSize),
            alt:ratable.name,
            className: options.imageSize+'-photo '+options.imageSize+'-photo-borders'})
        ])
      ]),
      placeSummary = div({className: "place_summary"}, [
        div({className: "title"}, [a({href: ratable.url}, ratable.name)]),
        p({className: "address"}, ratable.address)
      ]),
      div({className: 'clear'})
    ]);
    placeSummary.appendChild(
      div({className: "additional_summary_elements"}, options.additionalSummaryElements)
    )
    return detailsDiv;
  }

}

LikeMe.View.ImageUploader = Class.create({
  initialize: function(hookElement, options) {
    this.options = {
    };
    Object.extend(this.options, options);
    this.hookElement = hookElement;
  },

  paint: function() {
    this.hookElement.appendChild(
      p({}, [
        this.photoUploadLink = a({href: "#", onclick: function() {return false;} }, ["Upload"]),
        textNode(" "),
        this.photoUploadText = span("a photo")
      ]));

    this.hookElement.appendChild(this.photoUploadElement = div({className: "popup", style: {display: "none"}}));
    Event.observe(this.photoUploadLink, 'click', this._showPhotoUploadElement.bindAsEventListener(this));
  },

  paintPhotoUploadElement: function() {
    this.photoUploadElement.innerHTML = "";

    this.photoUploadElement.appendChild(this.photoUploadForm = form({action: this.options.url,
          enctype: 'multipart/form-data',
          method: 'post',
          onsubmit: this._photoUploadFormOnclick.bind(this)}, [
        textNode("Upload a new photo:"),
        br(),
        this.fileInputElement = input({type:'file', name:'photo[file]'}),
        br(),
        this.uploadButton = input({type: 'submit', value: 'Upload'}),
        AbleButton.applyTo(a({onclick: function() {Element.hide(this.photoUploadElement);}.bind(this)}, 'Cancel'), 'able-button')
      ])
    );
    this.uploadButton = AbleButton.applyTo(this.uploadButton, 'able-button blue');
  },

  _showPhotoUploadElement: function() {
    this.paintPhotoUploadElement();
    Element.show(this.photoUploadElement);
    Form.Element.activate(this.fileInputElement);
  },

  _photoUploadFormOnclick: function() {
    return AIM.submit(this.photoUploadForm, {'onComplete' : function(response) {
        this.options.afterPhotoUpload(JSON.parse(response), this);
      }.bind(this)
    });
  }

});

LikeMe.View.InlineEditor = Class.create({

    // States for text display:
    //   1: simple text display, mouse is not hovering
    //   2: hot-zone text display, mouse is hovering, text is outlined and a 'edit text' image is displayed at the top
    //   3: text is displayed in an text area element, along with submit and cancel buttons.
    //
    //
    //   1: simple text display
    //
    //   +- div: hookElement ----------------------------------+
    //   | +- div: .inline-editor-text-box ------------------+ |
    //   | | +- div: .inline-editor-text-box-bubble (hidden) | |
    //   | | |                                             | | |
    //   | | +---------------------------------------------+ | |
    //   | | +- p: .inline-editor-text --------------------+ | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | +---------------------------------------------+ | |
    //   | +-------------------------------------------------+ |
    //   +-----------------------------------------------------+
    //
    //
    //   2: hot-zone text display
    //
    //   +- div: hookElement ----------------------------------+
    //   | +- div: .inline-editor-text-box-hover ------------+ |
    //   | | +- div: .inline-editor-text-box-bubble -------+ | |
    //   | | |                                             | | |
    //   | | +---------------------------------------------+ | |
    //   | | +- p: .inline-editor-text --------------------+ | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | +---------------------------------------------+ | |
    //   | +-------------------------------------------------+ |
    //   +-----------------------------------------------------+
    //
    //
    //   3: text is displayed in an text area element, along with submit and cancel buttons.
    //
    //   +- div: hookElement ----------------------------------+
    //   | +- div: .inline-editor-input-box -----------------+ |
    //   | | +- textarea: .inline-editor-input ------------+ | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | |                                             | | |
    //   | | +---------------------------------------------+ | |
    //   | | +- div: .inline-editor-buttons ---------------+ | |
    //   | | | +- input -------------+- span ------------+ | | |
    //   | | | |                     |                   | | | |
    //   | | | +---------------------+-------------------+ | | |
    //   | | +---------------------------------------------+ | |
    //   | +-------------------------------------------------+ |
    //   +-----------------------------------------------------+

  initialize: function(hookElement, options) {
    this.hookElement = hookElement;
    this.options = Object.extend({
      minimumRows: 3,
      text: "",
      parameter: "",
      updateUrl: "",
      multiline: true,
      defaultText: "",
      method: "put",
      buttonsClass: 'right'
    }, options);
    if(this.hookElement.innerHTML) {
      this.options.text = this.convertBrToNewlines(this.hookElement.innerHTML);
    }
    if (!this.options.text) { this.options.text = ""; }
    this._inEditMode = false;
  },

  charWidth: 0.11637931034,

  setText: function(text) {
    this.options.text = text.escapeHTML();
  },

  getDisplayText: function() {
    return this.options.text.replace(/\n|\r\n/g, '<br />');
  },

  convertBrToNewlines: function(txt) {
    return txt.replace(/\<br\s?\/?\>/ig, '\n');
  },

  paint: function() {
    this.hookElement.innerHTML = "";
    this.cancelButton = undefined;
    this.updateButton = undefined;
    this.inputElement = null;
    this.textBoxBubbleElement = null;
    
    var displayed_text = this.getDisplayText();
    if (this._inEditMode) {
      this.hookElement.appendChild(div({className: "inline-editor-input-box"}, [this.createTextInputElement()]));
      this.hookElement.appendChild(div({className: "inline-editor-buttons buttons " + this.options.buttonsClass}, [
        this.createUpdateButtonElement(),
        this.createCancelButtonElement()
      ]));

      if (this.options.prompt) {
        if (this.inputElement.focus) { this.inputElement.focus(); }
      } else {
        if (this.inputElement.activate) { this.inputElement.activate(); }
      }
    } else {
      if (!this.options.text && this.options.editable) {
        displayed_text = this.options.defaultText;
      }

      this.hookElement.appendChild(
        this.inlineEditorTextBox = div({className: "inline-editor-text-box"},[
          this.textBoxBubbleElement = div({className: 'inline-editor-text-box-bubble'}, [
            img({src:"/images/edit-text.png", alt:"Edit Text"})
          ]),
          this.inlineEditorTextElement = this.createTextElement(displayed_text)
        ])
      );

      if (this.options.editable) {
        Event.observe(this.inlineEditorTextBox, 'click', this.handleTextClick.bind(this));
        Event.observe(this.inlineEditorTextBox, 'mouseover', this.handleTextEnterHover.bind(this));
        Event.observe(this.inlineEditorTextBox, 'mouseout', this.handleTextLeaveHover.bind(this));
      }
    }
  },

  newText: function() {
    return this.inputElement.value;
  },

  createTextElement: function(displayed_text) {
    var textContainer = p({className: "inline-editor-text"});
    if (!this.options.editable && this.options.maximumDisplayLength && displayed_text.length > this.options.maximumDisplayLength) {
      textContainer.innerHTML = this.truncate(displayed_text);
      textContainer.appendChild(this.readMoreLink = a({className: 'clickable'}, "read more"));
      Event.observe(this.readMoreLink, 'click', this.handleReadMoreClick.bindAsEventListener(this));
    } else {
      textContainer.innerHTML = displayed_text;
    }
    return textContainer;
  },

  handleReadMoreClick: function() {
    this.options.maximumDisplayLength = null;
    this.paint();
  },

  truncate: function(text) {
    return text.substr(0, this.options.maximumDisplayLength) + '...';
  },

  createTextInputElement: function() {
    var attributes = {
      className: "inline-editor-input",
      style: { width: this.options.width + 'px' }
    };

    if (this.options.multiline) {
      attributes.rows = this.numRowsToShow();
      this.inputElement = textarea(attributes);
			this.inputElement.value = this.getTextOrPrompt().stripTags().unescapeHTML();
    } else {
      attributes.type = 'text';
      if(this.options.maximumLength) {
        attributes.maxLength = this.options.maximumLength;
      }
      attributes.value = this.getTextOrPrompt();
      this.inputElement = input(attributes);
    }

    return this.inputElement;
  },

  getTextOrPrompt: function() {
    return this.options.prompt ? this.options.prompt : this.options.text
  },

  numRowsToShow: function() {
    if (this.options.text.length > this.options.width * this.charWidth * this.options.minimumRows) {
      return Math.round(this.options.text.length / (this.options.width * this.charWidth)) + 1;
    }
    return this.options.minimumRows;
  },

  createParagraphElement: function(displayed_text) {
    return p({className: "inline-editor-div"}, displayed_text);
  },

  createUpdateButtonElement: function() {
    this.updateButton = AbleButton.applyTo(a({href: '#', onclick: function() {return false;}}, 'Update'), 'able-button blue');
    Event.observe(this.updateButton, 'click', this.handleUpdateButtonClick.bind(this));
    return this.updateButton;
  },

  createCancelButtonElement: function() {
    this.cancelButton = AbleButton.applyTo(a({href: '#', onclick: function() {return false;}}, "Cancel"), 'able-button');
    var cancelClickHandler = this.handleCancelClick.bind(this);
    Event.observe(this.cancelButton, 'click', cancelClickHandler);
    return div('', [this.cancelButton, br(), br()]);
  },

  handleCancelClick: function() {
    this.setViewMode();
  },

  handleTextClick: function() {
    this.setEditMode();
  },

  handleUpdateButtonClick: function() {
    if (this.options.prompt && this.options.prompt == this.inputElement.value) {
      this.inputElement.value = "";
    }

    if (this.options.customUpdateHandler && !this.options.customUpdateHandler()) {
      return false;
    }

    LikeMe.Command.InlineEditorUpdate.send(this);
    this.setViewMode();
  },

  setEditMode: function() {
    this._inEditMode = true;
    this.paint();
    this.callAfterStateChange();
  },

  setViewMode: function() {
    this._inEditMode = false;
    this.paint();
    this.callAfterStateChange();
  },

  callAfterStateChange: function() {
    if(this.options.afterStateChange)
      this.options.afterStateChange();
  },

  handleTextEnterHover: function() {
    this.inlineEditorTextBox.className = "inline-editor-text-box-hover";
    Element.setStyle(this.textBoxBubbleElement, ({display : 'block'}));
  },

  handleTextLeaveHover: function() {
    this.inlineEditorTextBox.className = "inline-editor-text-box";
    if (this.textBoxBubbleElement) {
      Element.setStyle(this.textBoxBubbleElement, ({display : 'none'}));
    }
  }
});


LikeMe.View.MessageAddresser = Class.create ({

  initialize: function(hookElement, options) {
    this.options = options;
    this.hookElement = hookElement;
    this.resolvedUsers = [];
    if(this.options.resolvedIds) {
      this.options.resolvedIds.each(function(userId) {
        this.addUser(this.options.searchableUsers.find(function(user) {return user.id == userId}.bind(this)));
      }.bind(this))
    }
    this.searcher = new LikeMe.View.MessageAddresser.LocalSearcher(this.options.searchableUsers, this.resolvedUsers);
    Event.observe(this.hookElement, 'click', function() { this.currentInput.focus() }.bind(this));
  },

  paint: function(){
    if(this.inputHelpText) {
      this.inputHelpText.destroy();
    }
    this.hookElement.innerHTML = "";

    this.resolvedUsers.each(function(user) {
      var recipientElement = null;
      this.hookElement.appendChild(
        recipientElement = span({}, [
          a({}, [
              textNode(user.name),
              input({type: 'hidden', name: 'message[recipient_ids][]', value: user.id})
            ]),
          textNode(', ')
          ]));
      Event.observe(recipientElement, 'click', function() { this.removeUser(user); }.bind(this));
    }.bind(this));

    this.hookElement.appendChild( this.currentInput = input({id: 'message_recipient_ids', className: 'message_addresser_input', type: 'text'}) );
    this.autocompleter = new LikeMe.View.Autocompleter( this.currentInput, {
      positionedParentId: this.hookElement,
      searcher: this.searcher,
      delay: 0,
      minChars: 1,
      onItemChosen: this.addSelection.bind(this),
      onItemChosenByClick: this.addSelection.bind(this)
    });
    
    Event.observe(this.currentInput, 'keypress', this.handleSpecialKeys.bindAsEventListener(this));

    if(this.options.defaultTextOptions) {
      this.inputHelpText = new LikeMe.View.InputHelpText(this.currentInput, this.options.defaultTextOptions);
    }
  },

  handleSpecialKeys: function(e) {
    if (e.keyCode == Event.KEY_BACKSPACE && this.currentInput.value == '') {
      this.removeUser(this.resolvedUsers[this.resolvedUsers.length - 1]);
      this.currentInput.focus();
      e.stop();
    }
  },

  addSelection: function(selection, event) {
    this.addUser(selection.user);
    this.currentInput.value = "";
    this.paint();
    this.currentInput.focus();
    if(this.options.afterAdd) {
      this.options.afterAdd();
    }
  },

  addUser: function(user) {
    if(user) {
      this.resolvedUsers.push(user);
    }
  },

  removeUser: function(user) {
    var index = this.resolvedUsers.indexOf( user );
    if( -1 != index ) {
      this.resolvedUsers.splice( index, 1 );
    }
    this.paint();
    if(this.options.afterRemove) {
      this.options.afterRemove();
    }
  }

});

LikeMe.View.MessageAddresser.LocalSearcher = Class.create({
  initialize: function(users, usersToExclude) {
    this.users = users;
    this.usersToExclude = usersToExclude;
    this.escapeRegExp = /[^A-Za-z0-9]/g;
  },

  setCallbacks: function(callbacks) {
    this.callbacks = callbacks;
  },

  search: function(string) {
    var matchingUsers = this.users.select(function(user) {
      if(this.usersToExclude.any(function(u) { return u == user } )) return false;
      var regex = new RegExp(string.replace(this.escapeRegExp, '.'), 'gi');
      return user.name.match(regex) || user.toParam.match(regex);
    }.bind(this));
    var results = matchingUsers.collect(function(user) {
      return {
        text: user.name + ' (' + user.toParam + ')',
        user: user
      }
    });
    this.callbacks.processResults(results);
  },

  clearCache: function() {
  }
  
});

LikeMe.View.FollowScroller = Class.create(LikeMe.View.RankingScroller, {

  defaultOptions: function($super) {
    return Object.extend($super(), {
      itemViewClass: LikeMe.View.RankedPerson
    });
  },

  _addToScroller: function(guide) {
    this.resetFindDrawer();
    LikeMe.Command.Follow.send(guide, function(guide) {
        this.rankingGroup.addRatables([guide], true);
        this.resetFindDrawer();
        this.paint();
    }.bind(this));
  },

  loadPageUrl: function() {
    return Routing.people_path({
      user_id: this.rankingGroup.user().toParam,
      finder: (this.options && this.options.subject ? this.options.subject : '')
    });
  },

  reorderUrl: function() {
    return Routing.reorder_user_friends_path({
      user_id: this.rankingGroup.user().toParam
    });
  },

  clickFindMore: function() {
    var inputElement = $("search[name]");
    if(inputElement) {
      Form.Element.activate(inputElement);
    } else {
      this.redirectToFindPeople();
    }
  },

  redirectToFindPeople: function() {
    window.location = Routing.find_people_path();
  }  
});



LikeMe.View.RatableCommunityScroller = Class.create(LikeMe.View.FollowScroller , {

  initialize: function($super, hookElement, ratable, rankingGroup, options) {
    $super(hookElement, rankingGroup, options);
    this.ratable = Services.ratableService.register(ratable);
  },

  defaultOptions: function($super) {
    return Object.extend($super(), {
    });
  },

  loadPageUrl: function() {
    return Routing.place_community_path({place_id: this.ratable.toParam});
  },

  processLoadPageJson: function(usersJson, requestedLo, requestedHi) {
    if (requestedHi - requestedLo + 1 != usersJson.length) {
      this.rankingGroup.size = requestedLo - 1 + usersJson.length;
    }
    var users = usersJson.collect(function(user) {
      Services.userService.register(user);
      return Services.userService.findById(user.id);
    });
    this.rankingGroup.addRatables(users);
  }

});



LikeMe.View.RatableScroller = Class.create(LikeMe.View.RankingScroller, {

  defaultOptions: function($super) {
    return Object.extend($super(), {
      itemViewClass: ((this.rankingGroup.category && this.rankingGroup.category.toParam == 'event') ? LikeMe.View.RankedEvent : LikeMe.View.RankedRatable)
    });
  },

  addItemUrl: function() {
    return Routing.user_recommendation_group_places_path({
      user_id: this.rankingGroup.user().toParam,
      recommendation_group_id: this.rankingGroup.toParam
    });
  },

  createFindDrawer: function() {
    this.findDrawer = new LikeMe.View.RatableAddWizard({category: this.rankingGroup.category,
      addToScroller: this._addToScroller.bind(this),
      inScroller: this._isRatableInScroller.bind(this),
      removeFromScroller: this._removeFromScroller.bind(this),
      name: this.options.name});
  },

  _isRatableInScroller: function(ratable) {
    return this.rankingGroup.containsRatable(ratable);
  },

  loadPageUrl: function() {
    return Routing.user_recommendation_group_path(Object.extend({
      user_id: this.rankingGroup.user().toParam,
      id: this.rankingGroup.toParam
    }, this.rankingGroup.additionalParams));
  },

  processLoadPageJson: function(json, requestedLo, requestedHi) {
    if (requestedHi - requestedLo + 1 != json.ratables.length) {
      this.rankingGroup.size = requestedLo - 1 + json.ratables.length;
    }
    this.rankingGroup.loadRatables(json.ratables);
  },

  _addToScroller: function(ratableToAdd) {
    if (arguments.length > 1) {
      throw 'SOMEBODY USING AFTER_ADD'
    }
    LikeMe.Command.Generic.send({
      url: this.addItemUrl(),
      paramHash: {id: ratableToAdd.toParam},
      executeLocally: function() {
        ratableToAdd[this.rankingGroup.ratableGroupIdObject].push(this.rankingGroup.id);
        this.addToRankingGroup(ratableToAdd);
      }.bind(this),
      undo: function() {
        this.removeFromRankingGroup(ratableToAdd);
      }.bind(this)
    });
  },

  addToRankingGroup: function(ratable) {
    if(this.numItems() == this.items().length) {
      this.rankingGroup.addRatables([ratable], true);
    } else {
      this.rankingGroup.size += 1;
      Services.ratableService.register(ratable);
    }
    this.paint();
  },

  removeFromRankingGroup: function(ratable) {
    var ratablePosition = this.rankingGroup.ratableIndex(ratable);
    this.rankingGroup.removeRatable(ratable);
    this.paint();
    return [ratable, ratablePosition];
  },

  _removeFromScroller: function(ratable) {
    var rankedItem = {scroller:this, ratable:ratable};
    LikeMe.Command.RemoveRanking.send(rankedItem);
  },

  undoRemoveFromRankingGroup: function(ratable, rankingPosition) {
    this.rankingGroup.undoRemoveRanking(ratable, rankingPosition);
    this.paint();
  }

});

LikeMe.View.SimilarPlacesSuggestor = Class.create({

  initialize: function(textInputId, resultsDivId, options) {
    this.textInput = $(textInputId);
    this.setupResultsDiv(resultsDivId);
    this.setOptions(options);
    this.injectSuggestBehavior();
    this.cachedResults = {};
    this.handleTextInput();
  },

  setupResultsDiv: function(resultsDivId) {
    this.resultsDiv = $(resultsDivId)
    this.resultsDiv.appendChild(this.boilerplateElement = h3({className: "boilerplate"}));
    this.resultsDiv.appendChild(this.suggestionsDiv = div({className: "suggestions"}));
  },

  setOptions: function(options) {
    this.options = {
      count: 10,
      delay: 400,
      url: "/ratable_autocompletions",
      onServerFailure: function(response) {}
    };
    Object.extend(this.options, options || {});
  },

  clearCache: function() {
    this.cachedResults = {};
  },

  injectSuggestBehavior: function() {
    if (LikeMe.View.Utils.isIE) {
      this.textInput.autocomplete = "off";
    } else {
      this.textInput.setAttribute("autocomplete","OFF");
    }
    new LikeMe.View.SimilarPlacesSuggestor.TextSuggestKeyHandler(this);
  },

  handleTextInput: function() {
    this.lastRequestString = this.getTextInputValue();
    if (this.lastRequestString == "") {
      this.clearResultsDiv();
    } else {
      if (!this.displayFromCache(this.lastRequestString.toLowerCase())) {
        this.sendRequestForSuggestions();
      }
    }
  },

  sendRequestForSuggestions: function() {
    var parameters = "count=" + this.options.count + "&query=" + encodeURIComponent(this.lastRequestString);
    new Ajax.Request(this.options.url, {
      onSuccess: this.onAjaxSuccess.bind(this),
      onFailure: this.onAjaxFailure.bind(this),
      parameters: parameters
    });
  },

  onAjaxFailure: function(response) {
    this.options.onServerFailure(response);
  },

  onAjaxSuccess: function(response) {
    eval("var jsonResults= " + response.responseText);
    var key = jsonResults["query"].toLowerCase();
    this.cachedResults[key] = jsonResults["response"];
    for(var i = 0; i < this.cachedResults[key].length; i++) {
      this.cachedResults[key][i].ratable = Services.ratableService.register(this.cachedResults[key][i].ratable);
    }
    this.displayFromCache(key);
  },
  
  displayFromCache: function(key) {
    var suggestions = this.cachedResults[key];
    if (suggestions == null) {
      return false;
    }
    this.suggestions = suggestions;
    if (suggestions.length == 0 || this.getTextInputValue() == "") {
      this.clearResultsDiv();
    } else {
      this.boilerplateElement.innerHTML = "Are you thinking of one of these?"
      this.updateSuggestionsDiv();
    }
    return true;
  },

  updateSuggestionsDiv: function() {
    this.clearSuggestions();
    this.suggestions.each(function(suggestion) {
      this.suggestionsDiv.appendChild(
      div({className: 'suggestion'}, [
        a({href: suggestion.ratable.url}, [
          img({src: suggestion.ratable.photoUrl('thumbnail'), alt: suggestion.ratable.name, align: 'left'})
        ]),
        a({href: suggestion.ratable.url}, suggestion.ratable.name), br({}),
        span({}, suggestion.ratable.location)
      ]))
    }.bind(this))
  },

  clearSuggestions: function() {
    this.suggestionsDiv.innerHTML = "";
  },
  
  clearResultsDiv: function() {
    this.boilerplateElement.innerHTML = "";
    this.clearSuggestions();
  },
  
  getTextInputValue: function() {
    return this.textInput.value;
  },
  
  paint: function() {
  }
});

LikeMe.View.SimilarPlacesSuggestor.TextSuggestKeyHandler = Class.create({
  initialize: function(suggestor) {
    this.lastKeyHandled_AppleJSBugWorkaround = null;
    this.suggestor = suggestor;
    this.input = this.suggestor.textInput;
    this.addKeyHandling();
  },
  
  addKeyHandling: function() {
    Event.observe(this.input, "keyup", this.keyupHandler.bindAsEventListener(this));
    if (LikeMe.View.Utils.isOpera) {
      Event.observe(this.input, "keypress", this.keyupHandler.bindAsEventListener(this));
    }
  },
  
  keyupHandler: function(e) {
    if(this.observer) clearTimeout(this.observer);
    this.observer = setTimeout(this.suggestor.handleTextInput.bind(this.suggestor), this.suggestor.options.delay);
  }
});


LikeMe.View.SuggestFriends = Class.create(LikeMe.View.FollowSelector, {

  initialize: function($super, options) {
    this.currentTab = 'all';
    $super(Object.extend(options, {
      beforePaint: this.beforePaint.bind(this),
      beforeItemsPaint: this.paintTop.bind(this),
      afterItemsPaint: this.paintBottom.bind(this)
    }));
    this.helpTextContainer = {};
  },
  
  itemsToPaint: function() {
    return this.currentTab == 'all' ? this.items : this.selectedItems;
  },

  beforePaint: function() {
    if(this.inputHelpText) {
      this.inputHelpText.destroy();
    }
    this.oldMessageText = this.messageArea ? this.messageArea.value : '';
  },
  
  paintTop: function(hookElement) {
    hookElement.appendChild(textNode('Select friends below whose recommendations you think ' + this.options.user.name + ' will enjoy.'));
    hookElement.appendChild(br());
    hookElement.appendChild(br());
    hookElement.appendChild(textNode('Personalize Your Message:'));
    hookElement.appendChild(br());
    hookElement.appendChild(this.messageArea = textarea({value: this.oldMessageText}));
    this.inputHelpText = new LikeMe.View.InputHelpText(this.messageArea, {defaultText: 'Add your personal message (optional)', remoteForm: true, helpTextContainer: this.helpTextContainer});
    hookElement.appendChild(this.tabMenu = div({className: 'tabs'}, [ ]));
    hookElement.appendChild(div({className: 'clear'}));
    this.paintTabs();
  },
  
  paintBottom: function(hookElement) {
    hookElement.appendChild(div({className: 'clear'}));
    hookElement.appendChild(this.errorsDiv = LikeMe.View.Flash.errorDiv());
    this.submitButton = AbleButton.applyTo(a({onclick: this.submit.bind(this)}, 'Send Suggestions'), 'able-button blue');
    this.cancelButton = AbleButton.applyTo(a({onclick: this.close.bind(this)}, 'Cancel'), 'able-button');
    hookElement.appendChild(div({className: 'buttons right'}, [
      this.submitButton,
      this.cancelButton
    ]));
    this.errorsFlash = new LikeMe.View.Flash(this.errorsDiv, {useVisibility: true});
  },

  paintTabs: function() {
    if(this.currentTab == 'all') {
      this.allTab = li({className:'current'}, [span({}, [textNode("View All")])]);
      this.selectedTab = li({}, [a({}, "Selected (" + this.selectedItems.length + ")")]);
      Event.observe(this.selectedTab, 'click', function() { this.currentTab = 'selected'; this.paint(); }.bind(this));
    } else {
      this.allTab = li({}, [a({}, [textNode("View All")])]);
      this.selectedTab = li({className:'current'}, [span({}, "Selected (" + this.selectedItems.length + ")")]);
      Event.observe(this.allTab, 'click', function() { this.currentTab = 'all'; this.paint(); }.bind(this));
    }

    this.tabMenu.appendChild(
      ul({}, [
        this.allTab,
        this.selectedTab
      ])
    );
  },

  submit: function() {
    if(this.submitting) {
      return true;
    }
    this.errorsFlash.clearErrors();
    if(this.selectedItems.length == 0) {
      this.errorsFlash.addError('Please select at least one friend.');
    } else {
      this.submitting = true;
      LikeMe.View.InputHelpText.prepareFormForSubmit(this.helpTextContainer);
      LikeMe.Command.Generic.send({
        url: Routing.share_message_path({format: 'json'}),
        paramHash: {message: {
          target_ids: this.selectedItems.pluck("id"),
          target_type: "FriendGroup",
          recipient_ids: [this.options.user.id],
          personal_message: this.messageArea.value
        }},
        method: 'post',
        callback: function() {
          this.close();
        }.bind(this)
      });
    }
  }

});

LikeMe.View.Tags = Class.create({

  initialize: function(hookElement, options) {
    this.options = options || {};
    this.hookElement = hookElement;
    this.tags = this.options.tags;
    delete this.options.tags;
    this.ratable = this.options.ratable;
    delete this.options.ratable;
  },

  paint: function() {
    this.hookElement.innerHTML = "";
    this.drawTags();

    if (this.loggedIn()) {
      this.drawTagEditArea();
    } else if (!this.hasReadOnlyTags()) {
      this.drawIncentiveMessage();
    }
  },

  tagForTagList: function(tag, last, className) {
    if (last) {
      this.hookElement.appendChild(
        span({}, [tag, span({}, ', ')])
      );
    } else {
      this.hookElement.appendChild(tag);
    }
  },

  drawTags: function() {
    this.readOnlyTags = this.tags.reject(function(tag) { return tag.myTag; });
    var tagsLength = this.tags.length;
    for (var i = 0; i != tagsLength; ++i) {
      var tag = this.tags[i];
      if (tag.myTag || this.options.admin) {
        var removeIcon = img({src: '/images/empty.gif', alt: 'x', title: "Remove", align: "absmiddle"});
        this.observeTagDelete(tag, removeIcon);
        this.tagForTagList(a({className: 'mine'}, [removeIcon, a({href: '#', title: tag.name}, tag.name)]), i + 1 < tagsLength);
      } else {
        this.tagForTagList(a({href: '#'}, tag.name), i + 1 < tagsLength);
      }
    }
  },

  observeTagDelete: function(myTag, elementToObserve) {
    Event.observe(elementToObserve, 'click',
      function(event) {
        this.deleteTagging(event, myTag);
      }.bind(this)
    );
  },

  deleteTagging: function(event, myTag) {
    var url = this.options.admin ? Routing.admin_ratable_tag_path({ratable_id: this.ratable.toParam, id: myTag.name}) : Routing.place_tag_path({place_id: this.ratable.toParam, id: myTag.name});
    LikeMe.Command.Generic.send({
      url: url,
      method: 'delete',
      callback: function() {
        this.afterDelete(myTag);
      }.bind(this)
    });
    event.stop();
  },

  loggedIn: function() {
    return Services.userService.currentUser();
  },

  hasReadOnlyTags: function() {
    return 0 != this.readOnlyTags.length
  },

  drawIncentiveMessage: function() {
    this.hookElement.appendChild(
      div({className: 'tag-message'}, [
        textNode("Log in to add tags to help describe this place better!")
      ])
    );
  },

  drawTagEditArea: function() {
    this.hookElement.appendChild(
      div({className: 'tag-input-form'}, [
        span('Tag this place:', [br()]),
        this.inputField = input({type: "text", className: 'text'}),
        this.addButton = input({type: 'button', value: 'Add'}),
        br({clear: 'both'})
      ])
    );

    this.autocompleter = new LikeMe.View.Autocompleter(this.inputField, {
      url: '/tag_autocompletions',
      positionedParentId: this.hookElement
    });
    
    Event.observe(this.inputField, "keyup", function(event) {
      if (event.keyCode == Event.KEY_RETURN) {
        this.handleAdd()
      }
    }.bindAsEventListener(this));
      
    this.addButton = AbleButton.applyTo(this.addButton, 'able-button blue');
    Event.observe(this.addButton, 'click', this.handleAdd.bindAsEventListener(this));
  },

  handleAdd: function() {
    LikeMe.Command.Generic.send({
      url: Routing.place_tags_path({place_id: this.ratable.toParam}),
      callback: this.afterAdd.bind(this),
      paramHash: {tag: {name: this.inputField.value}}
    });
  },

  afterAdd: function(tagsJson) {
    if (tagsJson) {
      tagsJson.each(function(tagJson) {
        if (!this.tags.any(function(tag) {return tag.name == tagJson.name})) {
          this.tags.push(tagJson)
        }
      }.bind(this));
      this.paint();
      $(this.inputField).focus();
    }
  },

  afterDelete: function(tagToRemove) {
    this.tags = this.tags.reject(function(tag){return tag == tagToRemove});
    this.paint();
  }
});



LikeMe.View.Utils = {
  heightWithPadding: function(elem) {
    var height = Element.getHeight(elem);
    if (height == 0) {
      return elem.offsetHeight;
    } else {
      return height;
    }
  },

  widthWithPadding: function(elem) {
    return Element.getWidth(elem);
  },

  heightWithBorders: function(elem) {
    var height = this.heightWithPadding(elem);
    height += this.styleAsNum(elem, "border-top-width");
    height += this.styleAsNum(elem, "border-bottom-width");
    return height;
  },

  widthWithBorders: function(elem) {
    var width = this.widthWithPadding(elem);
    width += this.styleAsNum(elem, "border-left-width");
    width += this.styleAsNum(elem, "border-right-width");
    return width;
  },

  widthWithBordersAndMargins: function(elem) {
    var width = this.widthWithBorders(elem);
    width += this.styleAsNum(elem, "margin-left");
    width += this.styleAsNum(elem, "margin-right");
    return width;
  },

  heightWithBordersAndMargins: function(elem) {
    var height = this.heightWithBorders(elem);
    height += this.styleAsNum(elem, "margin-top");
    height += this.styleAsNum(elem, "margin-bottom");
    return height;
  },

  width: function(elem) {
    var width = this.widthWithPadding(elem);
    width -= this.styleAsNum(elem, "padding-left");
    width -= this.styleAsNum(elem, "padding-right");
    return width;
  },

  height: function(elem) {
    var height = this.heightWithPadding(elem);
    height -= this.styleAsNum(elem, "padding-top");
    height -= this.styleAsNum(elem, "padding-bottom");
    return height;
  },

  styleAsNum: function(elem, mozillaProp) {
		var computedStyle = Element.getStyle(elem, mozillaProp);
    if (!computedStyle) {
      return 0;
    }

    computedStyle = computedStyle.substr(0, computedStyle.length-2);
    if (isNaN(computedStyle)) {
			return 0;
		}
		return parseInt(computedStyle);
	}
}

LikeMe.View.Utils.isIE = (navigator.userAgent.toLowerCase().indexOf("msie") != -1);
LikeMe.View.Utils.isOpera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);



LikeMe.View.VerticalRatableBrowser = Class.create(LikeMe.View.RatableBrowser, {

  paginationStyle: 'compact_letters',

  initialize: function($super, hookElement, options) {
    $super(hookElement, options);
    for (var key in this.contextVariablesOverrides) {
      Object.extend(this.contextVariables[key], this.contextVariablesOverrides[key]);
    }
  },

  contextVariablesOverrides: {
    states: {
      columns: 2
    },
    cities: {
      columns: 2,
      perPage: 50
    },
    places: {
      columns: 1,
      perPage: 25
    }
  },

  paintTitle: function() {
    this.hookElement.appendChild(h4(this.title));
  },

  paintBackLinks: function() {
    var tmp_array = this.contexts.slice(0, this.contexts.indexOf(this.itemContext));
    if(tmp_array.length == 0) {return;}
    var backLinks = div({className: 'back-links'}, 'Back To: ');
    this.hookElement.appendChild(backLinks);

    tmp_array.each(
      function(x){
        backLinks.appendChild(a({href:'#', onclick: function() { this.goBack(x); return false; }.bind(this) }, x.capitalize() ));
        if( x != tmp_array[tmp_array.length - 1]){
          backLinks.appendChild(textNode(' | '));
        }
      }.bind(this)
    );

  },

  paintContent: function (offset) {
    if(this.requiresLoad(offset)) {
      this.loadRatables(offset);
      return;
    }

    this.clearContent();

    this.paintBackLinks();
    this.hookElement.appendChild(div({className: 'clear'}));

    this.paintTitle();

    this.renderPagination(this.hookElement, offset);
    Element.addClassName(this.paginationElement, 'compact-pagination');

    this.hookElement.appendChild(div({className: 'clear'}));

    this.paintItems(offset);

    this.hookElement.appendChild(div({className: 'clear'}));
    this.hookElement.appendChild(br());

    this.paintAddPlacePrompt(this.hookElement);
    
    this.doneLoading();
  }
});

LikeMe.View.ToDoScroller = Class.create(LikeMe.View.RatableScroller, {

  defaultOptions: function($super) {
    return Object.extend($super(), {
      itemViewClass: LikeMe.View.RankedRatableToDo
    });
  },

  reorderUrl: function() {
    return Routing.reorder_user_to_do_group_path({
      user_id: this.rankingGroup.user().toParam,
      id: this.rankingGroup.toParam
    });
  },

  addItemUrl: function() {
    return Routing.user_to_do_group_places_path({
      user_id: this.rankingGroup.user().toParam,
      to_do_group_id: this.rankingGroup.toParam
    });
  }

});

LikeMe.View.UserFollowLink = Class.create({
  initialize: function(hookElement, user_json) {
    this.user = Services.userService.register(user_json);
    this.user.registerView(this);
    this.hookElement = hookElement;
    this.paintTooltip();
  },

  paintTooltip: function() {
    this.hookElement.innerHTML = "";
    this.hookElement.appendChild(this.user.followLink());
  }
})


LikeMe.View.VvmNewsletter = Class.create({
  
  initialize: function(fieldId, options) {
    this.textInput = $(fieldId);
    this.id = this.textInput.id;
    this.options= { url: "/users/new/vvm_newsletter", autoStart: true }
    
    Object.extend(this.options, options || {});
    
    if(this.options.autoStart)
    {
      this.observeField(this.textInput, this.textInput.value); 
    }
    this.setupObserver();
  },
  
  setupObserver: function() {
    this.observer = new Form.Element.Observer(this.textInput.id, 0.25, this.observeField.bind(this));
  },
  
  observeField: function(element, value) {
    if (value.length == 5){
      new Ajax.Request(this.options.url, {
        onSuccess: this.onSuccess.bind(this),
        parameters: { zip_code: value }
      });
    }
  },
  
  onSuccess: function(response){
    if( $('vvm_newsletter_text') != null )
    {
      $('vvm_newsletter_text').remove();
      $('vvm_newsletter_subscriptions').remove();
    }
    $('spacer_row').insert( { after: response.responseText } );
  }
  

});