 /*  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) {
    if (!element || !ancestor) {
        return false;
    }
    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 (!Prototype.Browser.IE && 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,
      offsetWidth:  0,
      offsetHeight: 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 + options.offsetWidth + 'px';
    if (options.setHeight) element.style.height = source.offsetHeight + options.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 && 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) {
    if (!element.getStyle) {
        return "";
    }

    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
    var value = element.style[style];

    if (!value && element.currentStyle) value = element.currentStyle[style];

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

    if (value == 'auto') {
      element = Element.extend(element);
      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 (typeof value === "function" && !(property in element))
        element[property] = value._methodized || 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 + ' ').indexOf(needle) != -1)
          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 && nv == v || nv && nv.startsWith(v); },
    '$=': function(nv, v) { return nv && nv == v || nv && nv.endsWith(v); },
    '*=': function(nv, v) { return nv && nv == v || nv && nv.include(v); },
    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
     '-').include('-' + (v || "").toUpperCase() + '-'); }
  },

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return matchingInputs;
  },

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

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

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

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

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

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

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

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

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

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

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


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

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

Form.Element.Methods = {

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

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

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

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

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

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

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

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

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

var Field = Form.Element;

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

    cache: {}
  };

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

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

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

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

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

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

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

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

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

    return Element.extend(node);
  }

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

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

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

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

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

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


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

    event.stopped = true;
  }

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

    element: element,
    findElement: findElement,

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

    stop: stop
  };


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

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

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

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

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

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

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

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

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

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

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

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

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

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

            if (parent === element) return;

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

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

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

  var CACHE = [];

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

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


  var _getDOMEventName = Prototype.K;

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

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

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

    if (!responder) return element;

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

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

    return element;
  }

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

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

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

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

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

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

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

    var responders = registry.get(eventName);

    if (!responders) return;

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

    var actualEventName = _getDOMEventName(eventName);

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

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

    return element;
  }

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

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

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

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

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

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

    return Event.extend(event);
  }


  Object.extend(Event, Event.Methods);

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

  Element.addMethods({
    fire:          fire,

    observe:       observe,

    stopObserving: stopObserving
  });

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

    observe:       observe.methodize(),

    stopObserving: stopObserving.methodize(),

    loaded:        false
  });

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

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

  var timer;

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

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

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

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

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

Element.addMethods();

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

Hash.toQueryString = Object.toQueryString;

var Toggle = { display: Element.toggle };

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

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

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

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

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

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

var Position = {
  includeScrollOffsets: false,

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

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

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

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

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

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

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


  cumulativeOffset: Element.Methods.cumulativeOffset,

  positionedOffset: Element.Methods.positionedOffset,

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

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

  realOffset: Element.Methods.cumulativeScrollOffset,

  offsetParent: Element.Methods.getOffsetParent,

  page: Element.Methods.viewportOffset,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/*--------------------------------------------------------------------------*/
// script.aculo.us scriptaculous.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009

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

var Scriptaculous = {
  Version: '1.8.3',
  require: function(libraryName) {
    try{
      // inserting via DOM fails in Safari 2.0, so brute force approach
      document.write('<script type="text/javascript" src="'+libraryName+'"><\/script>');
    } catch(e) {
      // for xhtml+xml served content, fall back to DOM methods
      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.src = libraryName;
      document.getElementsByTagName('head')[0].appendChild(script);
    }
  },
  REQUIRED_PROTOTYPE: '1.6.0.3',
  load: function() {
    function convertVersionString(versionString) {
      var v = versionString.replace(/_.*|\./g, '');
      v = parseInt(v + '0'.times(4-v.length));
      return versionString.indexOf('_') > -1 ? v-1 : v;
    }

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

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

// We don't actually need any of this functionality.  Will remove completely in web3
//Scriptaculous.load();// script.aculo.us effects.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009

// Copyright (c) 2005-2009 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, options) {
    element = $(element);
    effect  = (effect || 'appear').toLowerCase();

    return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({
      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
    }, 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) {
    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 controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009

// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//           (c) 2005-2009 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 - 1 // - 1 makes the typeahead share a border with input
          });
        }
        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) {
    this.onObserverEvent()
    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);
  },

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

  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());
    this.index = 0;
  },

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

        // reset the index if it's more than the number of choices
        if (this.index >= this.entryCount) {
          this.index = 0;
        }
      } else {
        this.entryCount = 0;
      }

      this.stopIndicator();

      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));
  }
});
// script.aculo.us builder.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009

// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// 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/

var Builder = {
  NODEMAP: {
    AREA: 'map',
    CAPTION: 'table',
    COL: 'table',
    COLGROUP: 'table',
    LEGEND: 'fieldset',
    OPTGROUP: 'select',
    OPTION: 'select',
    PARAM: 'object',
    TBODY: 'table',
    TD: 'table',
    TFOOT: 'table',
    TH: 'table',
    THEAD: 'table',
    TR: 'table'
  },
  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
  //       due to a Firefox bug
  node: function(elementName) {
    elementName = elementName.toUpperCase();

    // try innerHTML approach
    var parentTag = this.NODEMAP[elementName] || 'div';
    var parentElement = document.createElement(parentTag);
    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
    } catch(e) {}
    var element = parentElement.firstChild || null;

    // see if browser added wrapping tags
    if(element && (element.tagName.toUpperCase() != elementName))
      element = element.getElementsByTagName(elementName)[0];

    // fallback to createElement approach
    if(!element) element = document.createElement(elementName);

    // abort if nothing could be created
    if(!element) return;

    // attributes (or text)
    if(arguments[1])
      if(this._isStringOrNumber(arguments[1]) ||
        (arguments[1] instanceof Array) ||
        arguments[1].tagName) {
          this._children(element, arguments[1]);
        } else {
          var attrs = this._attributes(arguments[1]);
          if(attrs.length) {
            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
              parentElement.innerHTML = "<" +elementName + " " +
                attrs + "></" + elementName + ">";
            } catch(e) {}
            element = parentElement.firstChild || null;
            // workaround firefox 1.0.X bug
            if(!element) {
              element = document.createElement(elementName);
              for(attr in arguments[1])
                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
            }
            if(element.tagName.toUpperCase() != elementName)
              element = parentElement.getElementsByTagName(elementName)[0];
          }
        }

    // text, or array of children
    if(arguments[2])
      this._children(element, arguments[2]);

     return $(element);
  },
  _text: function(text) {
     return document.createTextNode(text);
  },

  ATTR_MAP: {
    'className': 'class',
    'htmlFor': 'for'
  },

  _attributes: function(attributes) {
    var attrs = [];
    for(attribute in attributes)
      attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) +
          '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'&quot;') + '"');
    return attrs.join(" ");
  },
  _children: function(element, children) {
    if(children.tagName) {
      element.appendChild(children);
      return;
    }
    if(typeof children=='object') { // array can hold nodes and text
      children.flatten().each( function(e) {
        if(typeof e=='object')
          element.appendChild(e);
        else
          if(Builder._isStringOrNumber(e))
            element.appendChild(Builder._text(e));
      });
    } else
      if(Builder._isStringOrNumber(children))
        element.appendChild(Builder._text(children));
  },
  _isStringOrNumber: function(param) {
    return(typeof param=='string' || typeof param=='number');
  },
  build: function(html) {
    var element = this.node('div');
    $(element).update(html.strip());
    return element.down();
  },
  dump: function(scope) {
    if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope

    var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
      "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
      "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
      "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
      "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
      "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);

    tags.each( function(tag){
      scope[tag] = function() {
        return Builder.node.apply(Builder, [tag].concat($A(arguments)));
      };
    });
  }
};
// script.aculo.us slider.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009

// Copyright (c) 2005-2009 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  = this.track.cumulativeOffset();
          this.event = event;
          this.setValue(this.translateToValue(
           (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
          ));
          var offsets  = this.activeHandle.cumulativeOffset();
          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  = this.activeHandle.cumulativeOffset();
            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 = this.track.cumulativeOffset();
    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;
  }
});
/*!	SWFObject v2.2 <http://code.google.com/p/swfobject/> 
	is released under the MIT License <http://www.opensource.org/licenses/mit-license.php> 
*/

var swfobject = function() {
	
	var UNDEF = "undefined",
		OBJECT = "object",
		SHOCKWAVE_FLASH = "Shockwave Flash",
		SHOCKWAVE_FLASH_AX = "ShockwaveFlash.ShockwaveFlash",
		FLASH_MIME_TYPE = "application/x-shockwave-flash",
		EXPRESS_INSTALL_ID = "SWFObjectExprInst",
		ON_READY_STATE_CHANGE = "onreadystatechange",
		
		win = window,
		doc = document,
		nav = navigator,
		
		plugin = false,
		domLoadFnArr = [main],
		regObjArr = [],
		objIdArr = [],
		listenersArr = [],
		storedAltContent,
		storedAltContentId,
		storedCallbackFn,
		storedCallbackObj,
		isDomLoaded = false,
		isExpressInstallActive = false,
		dynamicStylesheet,
		dynamicStylesheetMedia,
		autoHideShow = true,
	
	/* Centralized function for browser feature detection
		- User agent string detection is only used when no good alternative is possible
		- Is executed directly for optimal performance
	*/	
	ua = function() {
		var w3cdom = typeof doc.getElementById != UNDEF && typeof doc.getElementsByTagName != UNDEF && typeof doc.createElement != UNDEF,
			u = nav.userAgent.toLowerCase(),
			p = nav.platform.toLowerCase(),
			windows = p ? /win/.test(p) : /win/.test(u),
			mac = p ? /mac/.test(p) : /mac/.test(u),
			webkit = /webkit/.test(u) ? parseFloat(u.replace(/^.*webkit\/(\d+(\.\d+)?).*$/, "$1")) : false, // returns either the webkit version or false if not webkit
			ie = !+"\v1", // feature detection based on Andrea Giammarchi's solution: http://webreflection.blogspot.com/2009/01/32-bytes-to-know-if-your-browser-is-ie.html
			playerVersion = [0,0,0],
			d = null;
		if (typeof nav.plugins != UNDEF && typeof nav.plugins[SHOCKWAVE_FLASH] == OBJECT) {
			d = nav.plugins[SHOCKWAVE_FLASH].description;
			if (d && !(typeof nav.mimeTypes != UNDEF && nav.mimeTypes[FLASH_MIME_TYPE] && !nav.mimeTypes[FLASH_MIME_TYPE].enabledPlugin)) { // navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin indicates whether plug-ins are enabled or disabled in Safari 3+
				plugin = true;
				ie = false; // cascaded feature detection for Internet Explorer
				d = d.replace(/^.*\s+(\S+\s+\S+$)/, "$1");
				playerVersion[0] = parseInt(d.replace(/^(.*)\..*$/, "$1"), 10);
				playerVersion[1] = parseInt(d.replace(/^.*\.(.*)\s.*$/, "$1"), 10);
				playerVersion[2] = /[a-zA-Z]/.test(d) ? parseInt(d.replace(/^.*[a-zA-Z]+(.*)$/, "$1"), 10) : 0;
			}
		}
		else if (typeof win.ActiveXObject != UNDEF) {
			try {
				var a = new ActiveXObject(SHOCKWAVE_FLASH_AX);
				if (a) { // a will return null when ActiveX is disabled
					d = a.GetVariable("$version");
					if (d) {
						ie = true; // cascaded feature detection for Internet Explorer
						d = d.split(" ")[1].split(",");
						playerVersion = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)];
					}
				}
			}
			catch(e) {}
		}
		return { w3:w3cdom, pv:playerVersion, wk:webkit, ie:ie, win:windows, mac:mac };
	}(),
	
	/* Cross-browser onDomLoad
		- Will fire an event as soon as the DOM of a web page is loaded
		- Internet Explorer workaround based on Diego Perini's solution: http://javascript.nwbox.com/IEContentLoaded/
		- Regular onload serves as fallback
	*/ 
	onDomLoad = function() {
		if (!ua.w3) { return; }
		if ((typeof doc.readyState != UNDEF && doc.readyState == "complete") || (typeof doc.readyState == UNDEF && (doc.getElementsByTagName("body")[0] || doc.body))) { // function is fired after onload, e.g. when script is inserted dynamically 
			callDomLoadFunctions();
		}
		if (!isDomLoaded) {
			if (typeof doc.addEventListener != UNDEF) {
				doc.addEventListener("DOMContentLoaded", callDomLoadFunctions, false);
			}		
			if (ua.ie && ua.win) {
				doc.attachEvent(ON_READY_STATE_CHANGE, function() {
					if (doc.readyState == "complete") {
						doc.detachEvent(ON_READY_STATE_CHANGE, arguments.callee);
						callDomLoadFunctions();
					}
				});
				if (win == top) { // if not inside an iframe
					(function(){
						if (isDomLoaded) { return; }
						try {
							doc.documentElement.doScroll("left");
						}
						catch(e) {
							setTimeout(arguments.callee, 0);
							return;
						}
						callDomLoadFunctions();
					})();
				}
			}
			if (ua.wk) {
				(function(){
					if (isDomLoaded) { return; }
					if (!/loaded|complete/.test(doc.readyState)) {
						setTimeout(arguments.callee, 0);
						return;
					}
					callDomLoadFunctions();
				})();
			}
			addLoadEvent(callDomLoadFunctions);
		}
	}();
	
	function callDomLoadFunctions() {
		if (isDomLoaded) { return; }
		try { // test if we can really add/remove elements to/from the DOM; we don't want to fire it too early
			var t = doc.getElementsByTagName("body")[0].appendChild(createElement("span"));
			t.parentNode.removeChild(t);
		}
		catch (e) { return; }
		isDomLoaded = true;
		var dl = domLoadFnArr.length;
		for (var i = 0; i < dl; i++) {
			domLoadFnArr[i]();
		}
	}
	
	function addDomLoadEvent(fn) {
		if (isDomLoaded) {
			fn();
		}
		else { 
			domLoadFnArr[domLoadFnArr.length] = fn; // Array.push() is only available in IE5.5+
		}
	}
	
	/* Cross-browser onload
		- Based on James Edwards' solution: http://brothercake.com/site/resources/scripts/onload/
		- Will fire an event as soon as a web page including all of its assets are loaded 
	 */
	function addLoadEvent(fn) {
		if (typeof win.addEventListener != UNDEF) {
			win.addEventListener("load", fn, false);
		}
		else if (typeof doc.addEventListener != UNDEF) {
			doc.addEventListener("load", fn, false);
		}
		else if (typeof win.attachEvent != UNDEF) {
			addListener(win, "onload", fn);
		}
		else if (typeof win.onload == "function") {
			var fnOld = win.onload;
			win.onload = function() {
				fnOld();
				fn();
			};
		}
		else {
			win.onload = fn;
		}
	}
	
	/* Main function
		- Will preferably execute onDomLoad, otherwise onload (as a fallback)
	*/
	function main() { 
		if (plugin) {
			testPlayerVersion();
		}
		else {
			matchVersions();
		}
	}
	
	/* Detect the Flash Player version for non-Internet Explorer browsers
		- Detecting the plug-in version via the object element is more precise than using the plugins collection item's description:
		  a. Both release and build numbers can be detected
		  b. Avoid wrong descriptions by corrupt installers provided by Adobe
		  c. Avoid wrong descriptions by multiple Flash Player entries in the plugin Array, caused by incorrect browser imports
		- Disadvantage of this method is that it depends on the availability of the DOM, while the plugins collection is immediately available
	*/
	function testPlayerVersion() {
		var b = doc.getElementsByTagName("body")[0];
		var o = createElement(OBJECT);
		o.setAttribute("type", FLASH_MIME_TYPE);
		var t = b.appendChild(o);
		if (t) {
			var counter = 0;
			(function(){
				if (typeof t.GetVariable != UNDEF) {
					var d = t.GetVariable("$version");
					if (d) {
						d = d.split(" ")[1].split(",");
						ua.pv = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)];
					}
				}
				else if (counter < 10) {
					counter++;
					setTimeout(arguments.callee, 10);
					return;
				}
				b.removeChild(o);
				t = null;
				matchVersions();
			})();
		}
		else {
			matchVersions();
		}
	}
	
	/* Perform Flash Player and SWF version matching; static publishing only
	*/
	function matchVersions() {
		var rl = regObjArr.length;
		if (rl > 0) {
			for (var i = 0; i < rl; i++) { // for each registered object element
				var id = regObjArr[i].id;
				var cb = regObjArr[i].callbackFn;
				var cbObj = {success:false, id:id};
				if (ua.pv[0] > 0) {
					var obj = getElementById(id);
					if (obj) {
						if (hasPlayerVersion(regObjArr[i].swfVersion) && !(ua.wk && ua.wk < 312)) { // Flash Player version >= published SWF version: Houston, we have a match!
							setVisibility(id, true);
							if (cb) {
								cbObj.success = true;
								cbObj.ref = getObjectById(id);
								cb(cbObj);
							}
						}
						else if (regObjArr[i].expressInstall && canExpressInstall()) { // show the Adobe Express Install dialog if set by the web page author and if supported
							var att = {};
							att.data = regObjArr[i].expressInstall;
							att.width = obj.getAttribute("width") || "0";
							att.height = obj.getAttribute("height") || "0";
							if (obj.getAttribute("class")) { att.styleclass = obj.getAttribute("class"); }
							if (obj.getAttribute("align")) { att.align = obj.getAttribute("align"); }
							// parse HTML object param element's name-value pairs
							var par = {};
							var p = obj.getElementsByTagName("param");
							var pl = p.length;
							for (var j = 0; j < pl; j++) {
								if (p[j].getAttribute("name").toLowerCase() != "movie") {
									par[p[j].getAttribute("name")] = p[j].getAttribute("value");
								}
							}
							showExpressInstall(att, par, id, cb);
						}
						else { // Flash Player and SWF version mismatch or an older Webkit engine that ignores the HTML object element's nested param elements: display alternative content instead of SWF
							displayAltContent(obj);
							if (cb) { cb(cbObj); }
						}
					}
				}
				else {	// if no Flash Player is installed or the fp version cannot be detected we let the HTML object element do its job (either show a SWF or alternative content)
					setVisibility(id, true);
					if (cb) {
						var o = getObjectById(id); // test whether there is an HTML object element or not
						if (o && typeof o.SetVariable != UNDEF) { 
							cbObj.success = true;
							cbObj.ref = o;
						}
						cb(cbObj);
					}
				}
			}
		}
	}
	
	function getObjectById(objectIdStr) {
		var r = null;
		var o = getElementById(objectIdStr);
		if (o && o.nodeName == "OBJECT") {
			if (typeof o.SetVariable != UNDEF) {
				r = o;
			}
			else {
				var n = o.getElementsByTagName(OBJECT)[0];
				if (n) {
					r = n;
				}
			}
		}
		return r;
	}
	
	/* Requirements for Adobe Express Install
		- only one instance can be active at a time
		- fp 6.0.65 or higher
		- Win/Mac OS only
		- no Webkit engines older than version 312
	*/
	function canExpressInstall() {
		return !isExpressInstallActive && hasPlayerVersion("6.0.65") && (ua.win || ua.mac) && !(ua.wk && ua.wk < 312);
	}
	
	/* Show the Adobe Express Install dialog
		- Reference: http://www.adobe.com/cfusion/knowledgebase/index.cfm?id=6a253b75
	*/
	function showExpressInstall(att, par, replaceElemIdStr, callbackFn) {
		isExpressInstallActive = true;
		storedCallbackFn = callbackFn || null;
		storedCallbackObj = {success:false, id:replaceElemIdStr};
		var obj = getElementById(replaceElemIdStr);
		if (obj) {
			if (obj.nodeName == "OBJECT") { // static publishing
				storedAltContent = abstractAltContent(obj);
				storedAltContentId = null;
			}
			else { // dynamic publishing
				storedAltContent = obj;
				storedAltContentId = replaceElemIdStr;
			}
			att.id = EXPRESS_INSTALL_ID;
			if (typeof att.width == UNDEF || (!/%$/.test(att.width) && parseInt(att.width, 10) < 310)) { att.width = "310"; }
			if (typeof att.height == UNDEF || (!/%$/.test(att.height) && parseInt(att.height, 10) < 137)) { att.height = "137"; }
			doc.title = doc.title.slice(0, 47) + " - Flash Player Installation";
			var pt = ua.ie && ua.win ? "ActiveX" : "PlugIn",
				fv = "MMredirectURL=" + win.location.toString().replace(/&/g,"%26") + "&MMplayerType=" + pt + "&MMdoctitle=" + doc.title;
			if (typeof par.flashvars != UNDEF) {
				par.flashvars += "&" + fv;
			}
			else {
				par.flashvars = fv;
			}
			// IE only: when a SWF is loading (AND: not available in cache) wait for the readyState of the object element to become 4 before removing it,
			// because you cannot properly cancel a loading SWF file without breaking browser load references, also obj.onreadystatechange doesn't work
			if (ua.ie && ua.win && obj.readyState != 4) {
				var newObj = createElement("div");
				replaceElemIdStr += "SWFObjectNew";
				newObj.setAttribute("id", replaceElemIdStr);
				obj.parentNode.insertBefore(newObj, obj); // insert placeholder div that will be replaced by the object element that loads expressinstall.swf
				obj.style.display = "none";
				(function(){
					if (obj.readyState == 4) {
						obj.parentNode.removeChild(obj);
					}
					else {
						setTimeout(arguments.callee, 10);
					}
				})();
			}
			createSWF(att, par, replaceElemIdStr);
		}
	}
	
	/* Functions to abstract and display alternative content
	*/
	function displayAltContent(obj) {
		if (ua.ie && ua.win && obj.readyState != 4) {
			// IE only: when a SWF is loading (AND: not available in cache) wait for the readyState of the object element to become 4 before removing it,
			// because you cannot properly cancel a loading SWF file without breaking browser load references, also obj.onreadystatechange doesn't work
			var el = createElement("div");
			obj.parentNode.insertBefore(el, obj); // insert placeholder div that will be replaced by the alternative content
			el.parentNode.replaceChild(abstractAltContent(obj), el);
			obj.style.display = "none";
			(function(){
				if (obj.readyState == 4) {
					obj.parentNode.removeChild(obj);
				}
				else {
					setTimeout(arguments.callee, 10);
				}
			})();
		}
		else {
			obj.parentNode.replaceChild(abstractAltContent(obj), obj);
		}
	} 

	function abstractAltContent(obj) {
		var ac = createElement("div");
		if (ua.win && ua.ie) {
			ac.innerHTML = obj.innerHTML;
		}
		else {
			var nestedObj = obj.getElementsByTagName(OBJECT)[0];
			if (nestedObj) {
				var c = nestedObj.childNodes;
				if (c) {
					var cl = c.length;
					for (var i = 0; i < cl; i++) {
						if (!(c[i].nodeType == 1 && c[i].nodeName == "PARAM") && !(c[i].nodeType == 8)) {
							ac.appendChild(c[i].cloneNode(true));
						}
					}
				}
			}
		}
		return ac;
	}
	
	/* Cross-browser dynamic SWF creation
	*/
	function createSWF(attObj, parObj, id) {
		var r, el = getElementById(id);
		if (ua.wk && ua.wk < 312) { return r; }
		if (el) {
			if (typeof attObj.id == UNDEF) { // if no 'id' is defined for the object element, it will inherit the 'id' from the alternative content
				attObj.id = id;
			}
			if (ua.ie && ua.win) { // Internet Explorer + the HTML object element + W3C DOM methods do not combine: fall back to outerHTML
				var att = "";
				for (var i in attObj) {
					if (attObj[i] != Object.prototype[i]) { // filter out prototype additions from other potential libraries
						if (i.toLowerCase() == "data") {
							parObj.movie = attObj[i];
						}
						else if (i.toLowerCase() == "styleclass") { // 'class' is an ECMA4 reserved keyword
							att += ' class="' + attObj[i] + '"';
						}
						else if (i.toLowerCase() != "classid") {
							att += ' ' + i + '="' + attObj[i] + '"';
						}
					}
				}
				var par = "";
				for (var j in parObj) {
					if (parObj[j] != Object.prototype[j]) { // filter out prototype additions from other potential libraries
						par += '<param name="' + j + '" value="' + parObj[j] + '" />';
					}
				}
				el.outerHTML = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"' + att + '>' + par + '</object>';
				objIdArr[objIdArr.length] = attObj.id; // stored to fix object 'leaks' on unload (dynamic publishing only)
				r = getElementById(attObj.id);	
			}
			else { // well-behaving browsers
				var o = createElement(OBJECT);
				o.setAttribute("type", FLASH_MIME_TYPE);
				for (var m in attObj) {
					if (attObj[m] != Object.prototype[m]) { // filter out prototype additions from other potential libraries
						if (m.toLowerCase() == "styleclass") { // 'class' is an ECMA4 reserved keyword
							o.setAttribute("class", attObj[m]);
						}
						else if (m.toLowerCase() != "classid") { // filter out IE specific attribute
							o.setAttribute(m, attObj[m]);
						}
					}
				}
				for (var n in parObj) {
					if (parObj[n] != Object.prototype[n] && n.toLowerCase() != "movie") { // filter out prototype additions from other potential libraries and IE specific param element
						createObjParam(o, n, parObj[n]);
					}
				}
				el.parentNode.replaceChild(o, el);
				r = o;
			}
		}
		return r;
	}
	
	function createObjParam(el, pName, pValue) {
		var p = createElement("param");
		p.setAttribute("name", pName);	
		p.setAttribute("value", pValue);
		el.appendChild(p);
	}
	
	/* Cross-browser SWF removal
		- Especially needed to safely and completely remove a SWF in Internet Explorer
	*/
	function removeSWF(id) {
		var obj = getElementById(id);
		if (obj && obj.nodeName == "OBJECT") {
			if (ua.ie && ua.win) {
				obj.style.display = "none";
				(function(){
					if (obj.readyState == 4) {
						removeObjectInIE(id);
					}
					else {
						setTimeout(arguments.callee, 10);
					}
				})();
			}
			else {
				obj.parentNode.removeChild(obj);
			}
		}
	}
	
	function removeObjectInIE(id) {
		var obj = getElementById(id);
		if (obj) {
			for (var i in obj) {
				if (typeof obj[i] == "function") {
					obj[i] = null;
				}
			}
			obj.parentNode.removeChild(obj);
		}
	}
	
	/* Functions to optimize JavaScript compression
	*/
	function getElementById(id) {
		var el = null;
		try {
			el = doc.getElementById(id);
		}
		catch (e) {}
		return el;
	}
	
	function createElement(el) {
		return doc.createElement(el);
	}
	
	/* Updated attachEvent function for Internet Explorer
		- Stores attachEvent information in an Array, so on unload the detachEvent functions can be called to avoid memory leaks
	*/	
	function addListener(target, eventType, fn) {
		target.attachEvent(eventType, fn);
		listenersArr[listenersArr.length] = [target, eventType, fn];
	}
	
	/* Flash Player and SWF content version matching
	*/
	function hasPlayerVersion(rv) {
		var pv = ua.pv, v = rv.split(".");
		v[0] = parseInt(v[0], 10);
		v[1] = parseInt(v[1], 10) || 0; // supports short notation, e.g. "9" instead of "9.0.0"
		v[2] = parseInt(v[2], 10) || 0;
		return (pv[0] > v[0] || (pv[0] == v[0] && pv[1] > v[1]) || (pv[0] == v[0] && pv[1] == v[1] && pv[2] >= v[2])) ? true : false;
	}
	
	/* Cross-browser dynamic CSS creation
		- Based on Bobby van der Sluis' solution: http://www.bobbyvandersluis.com/articles/dynamicCSS.php
	*/	
	function createCSS(sel, decl, media, newStyle) {
		if (ua.ie && ua.mac) { return; }
		var h = doc.getElementsByTagName("head")[0];
		if (!h) { return; } // to also support badly authored HTML pages that lack a head element
		var m = (media && typeof media == "string") ? media : "screen";
		if (newStyle) {
			dynamicStylesheet = null;
			dynamicStylesheetMedia = null;
		}
		if (!dynamicStylesheet || dynamicStylesheetMedia != m) { 
			// create dynamic stylesheet + get a global reference to it
			var s = createElement("style");
			s.setAttribute("type", "text/css");
			s.setAttribute("media", m);
			dynamicStylesheet = h.appendChild(s);
			if (ua.ie && ua.win && typeof doc.styleSheets != UNDEF && doc.styleSheets.length > 0) {
				dynamicStylesheet = doc.styleSheets[doc.styleSheets.length - 1];
			}
			dynamicStylesheetMedia = m;
		}
		// add style rule
		if (ua.ie && ua.win) {
			if (dynamicStylesheet && typeof dynamicStylesheet.addRule == OBJECT) {
				dynamicStylesheet.addRule(sel, decl);
			}
		}
		else {
			if (dynamicStylesheet && typeof doc.createTextNode != UNDEF) {
				dynamicStylesheet.appendChild(doc.createTextNode(sel + " {" + decl + "}"));
			}
		}
	}
	
	function setVisibility(id, isVisible) {
		if (!autoHideShow) { return; }
		var v = isVisible ? "visible" : "hidden";
		if (isDomLoaded && getElementById(id)) {
			getElementById(id).style.visibility = v;
		}
		else {
			createCSS("#" + id, "visibility:" + v);
		}
	}

	/* Filter to avoid XSS attacks
	*/
	function urlEncodeIfNecessary(s) {
		var regex = /[\\\"<>\.;]/;
		var hasBadChars = regex.exec(s) != null;
		return hasBadChars && typeof encodeURIComponent != UNDEF ? encodeURIComponent(s) : s;
	}
	
	/* Release memory to avoid memory leaks caused by closures, fix hanging audio/video threads and force open sockets/NetConnections to disconnect (Internet Explorer only)
	*/
	var cleanup = function() {
		if (ua.ie && ua.win) {
			window.attachEvent("onunload", function() {
				// remove listeners to avoid memory leaks
				var ll = listenersArr.length;
				for (var i = 0; i < ll; i++) {
					listenersArr[i][0].detachEvent(listenersArr[i][1], listenersArr[i][2]);
				}
				// cleanup dynamically embedded objects to fix audio/video threads and force open sockets and NetConnections to disconnect
				var il = objIdArr.length;
				for (var j = 0; j < il; j++) {
					removeSWF(objIdArr[j]);
				}
				// cleanup library's main closures to avoid memory leaks
				for (var k in ua) {
					ua[k] = null;
				}
				ua = null;
				for (var l in swfobject) {
					swfobject[l] = null;
				}
				swfobject = null;
			});
		}
	}();
	
	return {
		/* Public API
			- Reference: http://code.google.com/p/swfobject/wiki/documentation
		*/ 
		registerObject: function(objectIdStr, swfVersionStr, xiSwfUrlStr, callbackFn) {
			if (ua.w3 && objectIdStr && swfVersionStr) {
				var regObj = {};
				regObj.id = objectIdStr;
				regObj.swfVersion = swfVersionStr;
				regObj.expressInstall = xiSwfUrlStr;
				regObj.callbackFn = callbackFn;
				regObjArr[regObjArr.length] = regObj;
				setVisibility(objectIdStr, false);
			}
			else if (callbackFn) {
				callbackFn({success:false, id:objectIdStr});
			}
		},
		
		getObjectById: function(objectIdStr) {
			if (ua.w3) {
				return getObjectById(objectIdStr);
			}
		},
		
		embedSWF: function(swfUrlStr, replaceElemIdStr, widthStr, heightStr, swfVersionStr, xiSwfUrlStr, flashvarsObj, parObj, attObj, callbackFn) {
			var callbackObj = {success:false, id:replaceElemIdStr};
			if (ua.w3 && !(ua.wk && ua.wk < 312) && swfUrlStr && replaceElemIdStr && widthStr && heightStr && swfVersionStr) {
				setVisibility(replaceElemIdStr, false);
				addDomLoadEvent(function() {
					widthStr += ""; // auto-convert to string
					heightStr += "";
					var att = {};
					if (attObj && typeof attObj === OBJECT) {
						for (var i in attObj) { // copy object to avoid the use of references, because web authors often reuse attObj for multiple SWFs
							att[i] = attObj[i];
						}
					}
					att.data = swfUrlStr;
					att.width = widthStr;
					att.height = heightStr;
					var par = {}; 
					if (parObj && typeof parObj === OBJECT) {
						for (var j in parObj) { // copy object to avoid the use of references, because web authors often reuse parObj for multiple SWFs
							par[j] = parObj[j];
						}
					}
					if (flashvarsObj && typeof flashvarsObj === OBJECT) {
						for (var k in flashvarsObj) { // copy object to avoid the use of references, because web authors often reuse flashvarsObj for multiple SWFs
							if (typeof par.flashvars != UNDEF) {
								par.flashvars += "&" + k + "=" + flashvarsObj[k];
							}
							else {
								par.flashvars = k + "=" + flashvarsObj[k];
							}
						}
					}
					if (hasPlayerVersion(swfVersionStr)) { // create SWF
						var obj = createSWF(att, par, replaceElemIdStr);
						if (att.id == replaceElemIdStr) {
							setVisibility(replaceElemIdStr, true);
						}
						callbackObj.success = true;
						callbackObj.ref = obj;
					}
					else if (xiSwfUrlStr && canExpressInstall()) { // show Adobe Express Install
						att.data = xiSwfUrlStr;
						showExpressInstall(att, par, replaceElemIdStr, callbackFn);
						return;
					}
					else { // show alternative content
						setVisibility(replaceElemIdStr, true);
					}
					if (callbackFn) { callbackFn(callbackObj); }
				});
			}
			else if (callbackFn) { callbackFn(callbackObj);	}
		},
		
		switchOffAutoHideShow: function() {
			autoHideShow = false;
		},
		
		ua: ua,
		
		getFlashPlayerVersion: function() {
			return { major:ua.pv[0], minor:ua.pv[1], release:ua.pv[2] };
		},
		
		hasFlashPlayerVersion: hasPlayerVersion,
		
		createSWF: function(attObj, parObj, replaceElemIdStr) {
			if (ua.w3) {
				return createSWF(attObj, parObj, replaceElemIdStr);
			}
			else {
				return undefined;
			}
		},
		
		showExpressInstall: function(att, par, replaceElemIdStr, callbackFn) {
			if (ua.w3 && canExpressInstall()) {
				showExpressInstall(att, par, replaceElemIdStr, callbackFn);
			}
		},
		
		removeSWF: function(objElemIdStr) {
			if (ua.w3) {
				removeSWF(objElemIdStr);
			}
		},
		
		createCSS: function(selStr, declStr, mediaStr, newStyleBoolean) {
			if (ua.w3) {
				createCSS(selStr, declStr, mediaStr, newStyleBoolean);
			}
		},
		
		addDomLoadEvent: addDomLoadEvent,
		
		addLoadEvent: addLoadEvent,
		
		getQueryParamValue: function(param) {
			var q = doc.location.search || doc.location.hash;
			if (q) {
				if (/\?/.test(q)) { q = q.split("?")[1]; } // strip question mark
				if (param == null) {
					return urlEncodeIfNecessary(q);
				}
				var pairs = q.split("&");
				for (var i = 0; i < pairs.length; i++) {
					if (pairs[i].substring(0, pairs[i].indexOf("=")) == param) {
						return urlEncodeIfNecessary(pairs[i].substring((pairs[i].indexOf("=") + 1)));
					}
				}
			}
			return "";
		},
		
		// For internal usage only
		expressInstallCallback: function() {
			if (isExpressInstallActive) {
				var obj = getElementById(EXPRESS_INSTALL_ID);
				if (obj && storedAltContent) {
					obj.parentNode.replaceChild(storedAltContent, obj);
					if (storedAltContentId) {
						setVisibility(storedAltContentId, true);
						if (ua.ie && ua.win) { storedAltContent.style.display = "block"; }
					}
					if (storedCallbackFn) { storedCallbackFn(storedCallbackObj); }
				}
				isExpressInstallActive = false;
			} 
		}
	};
}();
var _gat=new Object({c:"length",lb:"4.3",m:"cookie",b:undefined,cb:function(d,a){this.zb=d;this.Nb=a},r:"__utma=",W:"__utmb=",ma:"__utmc=",Ta:"__utmk=",na:"__utmv=",oa:"__utmx=",Sa:"GASO=",X:"__utmz=",lc:"http://www.google-analytics.com/__utm.gif",mc:"https://ssl.google-analytics.com/__utm.gif",Wa:"utmcid=",Ya:"utmcsr=",$a:"utmgclid=",Ua:"utmccn=",Xa:"utmcmd=",Za:"utmctr=",Va:"utmcct=",Hb:false,_gasoDomain:undefined,_gasoCPath:undefined,e:window,a:document,k:navigator,t:function(d){var a=1,c=0,h,
o;if(!_gat.q(d)){a=0;for(h=d[_gat.c]-1;h>=0;h--){o=d.charCodeAt(h);a=(a<<6&268435455)+o+(o<<14);c=a&266338304;a=c!=0?a^c>>21:a}}return a},C:function(d,a,c){var h=_gat,o="-",k,l,s=h.q;if(!s(d)&&!s(a)&&!s(c)){k=h.w(d,a);if(k>-1){l=d.indexOf(c,k);if(l<0)l=d[h.c];o=h.F(d,k+h.w(a,"=")+1,l)}}return o},Ea:function(d){var a=false,c=0,h,o;if(!_gat.q(d)){a=true;for(h=0;h<d[_gat.c];h++){o=d.charAt(h);c+="."==o?1:0;a=a&&c<=1&&(0==h&&"-"==o||_gat.P(".0123456789",o))}}return a},d:function(d,a){var c=encodeURIComponent;
return c instanceof Function?(a?encodeURI(d):c(d)):escape(d)},J:function(d,a){var c=decodeURIComponent,h;d=d.split("+").join(" ");if(c instanceof Function)try{h=a?decodeURI(d):c(d)}catch(o){h=unescape(d)}else h=unescape(d);return h},Db:function(d){return d&&d.hash?_gat.F(d.href,_gat.w(d.href,"#")):""},q:function(d){return _gat.b==d||"-"==d||""==d},Lb:function(d){return d[_gat.c]>0&&_gat.P(" \n\r\t",d)},P:function(d,a){return _gat.w(d,a)>-1},h:function(d,a){d[d[_gat.c]]=a},T:function(d){return d.toLowerCase()},
z:function(d,a){return d.split(a)},w:function(d,a){return d.indexOf(a)},F:function(d,a,c){c=_gat.b==c?d[_gat.c]:c;return d.substring(a,c)},uc:function(){var d=_gat.b,a=window;if(a&&a.gaGlobal&&a.gaGlobal.hid)d=a.gaGlobal.hid;else{d=Math.round(Math.random()*2147483647);a.gaGlobal=a.gaGlobal?a.gaGlobal:{};a.gaGlobal.hid=d}return d},wa:function(){return Math.round(Math.random()*2147483647)},Gc:function(){return(_gat.wa()^_gat.vc())*2147483647},vc:function(){var d=_gat.k,a=_gat.a,c=_gat.e,h=a[_gat.m]?
a[_gat.m]:"",o=c.history[_gat.c],k,l,s=[d.appName,d.version,d.language?d.language:d.browserLanguage,d.platform,d.userAgent,d.javaEnabled()?1:0].join("");if(c.screen)s+=c.screen.width+"x"+c.screen.height+c.screen.colorDepth;else if(c.java){l=java.awt.Toolkit.getDefaultToolkit().getScreenSize();s+=l.screen.width+"x"+l.screen.height}s+=h;s+=a.referrer?a.referrer:"";k=s[_gat.c];while(o>0)s+=o--^k++;return _gat.t(s)}});_gat.hc=function(){var d=this,a=_gat.cb;function c(h,o){return new a(h,o)}d.db="utm_campaign";d.eb="utm_content";d.fb="utm_id";d.gb="utm_medium";d.hb="utm_nooverride";d.ib="utm_source";d.jb="utm_term";d.kb="gclid";d.pa=0;d.I=0;d.wb="15768000";d.Tb="1800";d.ea=[];d.ga=[];d.Ic="cse";d.Gb="q";d.ab="google";d.fa=[c(d.ab,d.Gb),c("yahoo","p"),c("msn","q"),c("bing","q"),c("aol","query"),c("aol","encquery"),c("lycos","query"),c("ask","q"),c("altavista","q"),c("netscape","query"),c("cnn","query"),c("looksmart","qt"),c("about",
"terms"),c("mamma","query"),c("alltheweb","q"),c("gigablast","q"),c("voila","rdata"),c("virgilio","qs"),c("live","q"),c("baidu","wd"),c("alice","qs"),c("yandex","text"),c("najdi","q"),c("aol","q"),c("club-internet","query"),c("mama","query"),c("seznam","q"),c("search","q"),c("wp","szukaj"),c("onet","qt"),c("netsprint","q"),c("google.interia","q"),c("szukacz","q"),c("yam","k"),c("pchome","q"),c("kvasir","searchExpr"),c("sesam","q"),c("ozu","q"),c("terra","query"),c("nostrum","query"),c("mynet","q"),
c("ekolay","q"),c("search.ilse","search_for")];d.B=undefined;d.Kb=false;d.p="/";d.ha=100;d.Da="/__utm.gif";d.ta=1;d.ua=1;d.G="|";d.sa=1;d.qa=1;d.pb=1;d.g="auto";d.D=1;d.Ga=1000;d.Yc=10;d.nc=10;d.Zc=0.2};_gat.Y=function(d,a){var c,h,o,k,l,s,q,f=this,n=_gat,w=n.q,x=n.c,g,z=a;f.a=d;function B(i){var b=i instanceof Array?i.join("."):"";return w(b)?"-":b}function A(i,b){var e=[],j;if(!w(i)){e=n.z(i,".");if(b)for(j=0;j<e[x];j++)if(!n.Ea(e[j]))e[j]="-"}return e}function p(){return u(63072000000)}function u(i){var b=new Date,e=new Date(b.getTime()+i);return"expires="+e.toGMTString()+"; "}function m(i,b){f.a[n.m]=i+"; path="+z.p+"; "+b+f.Cc()}function r(i,b,e){var j=f.V,t,v;for(t=0;t<j[x];t++){v=j[t][0];
v+=w(b)?b:b+j[t][4];j[t][2](n.C(i,v,e))}}f.Jb=function(){return n.b==g||g==f.t()};f.Ba=function(){return l?l:"-"};f.Wb=function(i){l=i};f.Ma=function(i){g=n.Ea(i)?i*1:"-"};f.Aa=function(){return B(s)};f.Na=function(i){s=A(i)};f.Hc=function(){return g?g:"-"};f.Cc=function(){return w(z.g)?"":"domain="+z.g+";"};f.ya=function(){return B(c)};f.Ub=function(i){c=A(i,1)};f.K=function(){return B(h)};f.La=function(i){h=A(i,1)};f.za=function(){return B(o)};f.Vb=function(i){o=A(i,1)};f.Ca=function(){return B(k)};
f.Xb=function(i){k=A(i);for(var b=0;b<k[x];b++)if(b<4&&!n.Ea(k[b]))k[b]="-"};f.Dc=function(){return q};f.Uc=function(i){q=i};f.pc=function(){c=[];h=[];o=[];k=[];l=n.b;s=[];g=n.b};f.t=function(){var i="",b;for(b=0;b<f.V[x];b++)i+=f.V[b][1]();return n.t(i)};f.Ha=function(i){var b=f.a[n.m],e=false;if(b){r(b,i,";");f.Ma(f.t());e=true}return e};f.Rc=function(i){r(i,"","&");f.Ma(n.C(i,n.Ta,"&"))};f.Wc=function(){var i=f.V,b=[],e;for(e=0;e<i[x];e++)n.h(b,i[e][0]+i[e][1]());n.h(b,n.Ta+f.t());return b.join("&")};
f.bd=function(i,b){var e=f.V,j=z.p,t;f.Ha(i);z.p=b;for(t=0;t<e[x];t++)if(!w(e[t][1]()))e[t][3]();z.p=j};f.dc=function(){m(n.r+f.ya(),p())};f.Pa=function(){m(n.W+f.K(),u(z.Tb*1000))};f.ec=function(){m(n.ma+f.za(),"")};f.Ra=function(){m(n.X+f.Ca(),u(z.wb*1000))};f.fc=function(){m(n.oa+f.Ba(),p())};f.Qa=function(){m(n.na+f.Aa(),p())};f.cd=function(){m(n.Sa+f.Dc(),"")};f.V=[[n.r,f.ya,f.Ub,f.dc,"."],[n.W,f.K,f.La,f.Pa,""],[n.ma,f.za,f.Vb,f.ec,""],[n.oa,f.Ba,f.Wb,f.fc,""],[n.X,f.Ca,f.Xb,f.Ra,"."],[n.na,
f.Aa,f.Na,f.Qa,"."]]};_gat.jc=function(d){var a=this,c=_gat,h=d,o,k=function(l){var s=(new Date).getTime(),q;q=(s-l[3])*(h.Zc/1000);if(q>=1){l[2]=Math.min(Math.floor(l[2]*1+q),h.nc);l[3]=s}return l};a.O=function(l,s,q,f,n,w,x){var g,z=h.D,B=q.location;if(!o)o=new c.Y(q,h);o.Ha(f);g=c.z(o.K(),".");if(g[1]<500||n){if(w)g=k(g);if(n||!w||g[2]>=1){if(!n&&w)g[2]=g[2]*1-1;g[1]=g[1]*1+1;l="?utmwv="+_gat.lb+"&utmn="+c.wa()+(c.q(B.hostname)?"":"&utmhn="+c.d(B.hostname))+(h.ha==100?"":"&utmsp="+c.d(h.ha))+l;if(0==z||2==z){var A=
new Image(1,1);A.src=h.Da+l;var p=2==z?function(){}:x||function(){};A.onload=p}if(1==z||2==z){var u=new Image(1,1);u.src=("https:"==B.protocol?c.mc:c.lc)+l+"&utmac="+s+"&utmcc="+a.wc(q,f);u.onload=x||function(){}}}}o.La(g.join("."));o.Pa()};a.wc=function(l,s){var q=[],f=[c.r,c.X,c.na,c.oa],n,w=l[c.m],x;for(n=0;n<f[c.c];n++){x=c.C(w,f[n]+s,";");if(!c.q(x))c.h(q,f[n]+x+";")}return c.d(q.join("+"))}};_gat.i=function(){this.la=[]};_gat.i.bb=function(d,a,c,h,o,k){var l=this;l.cc=d;l.Oa=a;l.L=c;l.sb=h;l.Pb=o;l.Qb=k};_gat.i.bb.prototype.S=function(){var d=this,a=_gat.d;return"&"+["utmt=item","utmtid="+a(d.cc),"utmipc="+a(d.Oa),"utmipn="+a(d.L),"utmiva="+a(d.sb),"utmipr="+a(d.Pb),"utmiqt="+a(d.Qb)].join("&")};_gat.i.$=function(d,a,c,h,o,k,l,s){var q=this;q.v=d;q.ob=a;q.bc=c;q.ac=h;q.Yb=o;q.ub=k;q.$b=l;q.xb=s;q.ca=[]};_gat.i.$.prototype.mb=function(d,a,c,h,o){var k=this,l=k.Eb(d),s=k.v,q=_gat;if(q.b==
l)q.h(k.ca,new q.i.bb(s,d,a,c,h,o));else{l.cc=s;l.Oa=d;l.L=a;l.sb=c;l.Pb=h;l.Qb=o}};_gat.i.$.prototype.Eb=function(d){var a,c=this.ca,h;for(h=0;h<c[_gat.c];h++)a=d==c[h].Oa?c[h]:a;return a};_gat.i.$.prototype.S=function(){var d=this,a=_gat.d;return"&"+["utmt=tran","utmtid="+a(d.v),"utmtst="+a(d.ob),"utmtto="+a(d.bc),"utmttx="+a(d.ac),"utmtsp="+a(d.Yb),"utmtci="+a(d.ub),"utmtrg="+a(d.$b),"utmtco="+a(d.xb)].join("&")};_gat.i.prototype.nb=function(d,a,c,h,o,k,l,s){var q=this,f=_gat,n=q.xa(d);if(f.b==
n){n=new f.i.$(d,a,c,h,o,k,l,s);f.h(q.la,n)}else{n.ob=a;n.bc=c;n.ac=h;n.Yb=o;n.ub=k;n.$b=l;n.xb=s}return n};_gat.i.prototype.xa=function(d){var a,c=this.la,h;for(h=0;h<c[_gat.c];h++)a=d==c[h].v?c[h]:a;return a};_gat.gc=function(d){var a=this,c="-",h=_gat,o=d;a.Ja=screen;a.qb=!self.screen&&self.java?java.awt.Toolkit.getDefaultToolkit():h.b;a.a=document;a.e=window;a.k=navigator;a.Ka=c;a.Sb=c;a.tb=c;a.Ob=c;a.Mb=1;a.Bb=c;function k(){var l,s,q,f,n="ShockwaveFlash",w="$version",x=a.k?a.k.plugins:h.b;if(x&&x[h.c]>0)for(l=0;l<x[h.c]&&!q;l++){s=x[l];if(h.P(s.name,"Shockwave Flash"))q=h.z(s.description,"Shockwave Flash ")[1]}else{n=n+"."+n;try{f=new ActiveXObject(n+".7");q=f.GetVariable(w)}catch(g){}if(!q)try{f=
new ActiveXObject(n+".6");q="WIN 6,0,21,0";f.AllowScriptAccess="always";q=f.GetVariable(w)}catch(z){}if(!q)try{f=new ActiveXObject(n);q=f.GetVariable(w)}catch(z){}if(q){q=h.z(h.z(q," ")[1],",");q=q[0]+"."+q[1]+" r"+q[2]}}return q?q:c}a.xc=function(){var l;if(self.screen){a.Ka=a.Ja.width+"x"+a.Ja.height;a.Sb=a.Ja.colorDepth+"-bit"}else if(a.qb)try{l=a.qb.getScreenSize();a.Ka=l.width+"x"+l.height}catch(s){}a.Ob=h.T(a.k&&a.k.language?a.k.language:(a.k&&a.k.browserLanguage?a.k.browserLanguage:c));a.Mb=
a.k&&a.k.javaEnabled()?1:0;a.Bb=o?k():c;a.tb=h.d(a.a.characterSet?a.a.characterSet:(a.a.charset?a.a.charset:c))};a.Xc=function(){return"&"+["utmcs="+h.d(a.tb),"utmsr="+a.Ka,"utmsc="+a.Sb,"utmul="+a.Ob,"utmje="+a.Mb,"utmfl="+h.d(a.Bb)].join("&")}};_gat.n=function(d,a,c,h,o){var k=this,l=_gat,s=l.q,q=l.b,f=l.P,n=l.C,w=l.T,x=l.z,g=l.c;k.a=a;k.f=d;k.Rb=c;k.ja=h;k.o=o;function z(p){return s(p)||"0"==p||!f(p,"://")}function B(p){var u="";p=w(x(p,"://")[1]);if(f(p,"/")){p=x(p,"/")[1];if(f(p,"?"))u=x(p,"?")[0]}return u}function A(p){var u="";u=w(x(p,"://")[1]);if(f(u,"/"))u=x(u,"/")[0];return u}k.Fc=function(p){var u=k.Fb(),m=k.o;return new l.n.s(n(p,m.fb+"=","&"),n(p,m.ib+"=","&"),n(p,m.kb+"=","&"),k.ba(p,m.db,"(not set)"),k.ba(p,m.gb,"(not set)"),
k.ba(p,m.jb,u&&!s(u.R)?l.J(u.R):q),k.ba(p,m.eb,q))};k.Ib=function(p){var u=A(p),m=B(p);if(f(u,k.o.ab)){p=x(p,"?").join("&");if(f(p,"&"+k.o.Gb+"="))if(m==k.o.Ic)return true}return false};k.Fb=function(){var p,u,m=k.Rb,r,i,b=k.o.fa;if(z(m)||k.Ib(m))return;p=A(m);for(r=0;r<b[g];r++){i=b[r];if(f(p,w(i.zb))){m=x(m,"?").join("&");if(f(m,"&"+i.Nb+"=")){u=x(m,"&"+i.Nb+"=")[1];if(f(u,"&"))u=x(u,"&")[0];return new l.n.s(q,i.zb,q,"(organic)","organic",u,q)}}}};k.ba=function(p,u,m){var r=n(p,u+"=","&"),i=!s(r)?
l.J(r):(!s(m)?m:"-");return i};k.Nc=function(p){var u=k.o.ea,m=false,r,i;if(p&&"organic"==p.da){r=w(l.J(p.R));for(i=0;i<u[g];i++)m=m||w(u[i])==r}return m};k.Ec=function(){var p="",u="",m=k.Rb;if(z(m)||k.Ib(m))return;p=w(x(m,"://")[1]);if(f(p,"/")){u=l.F(p,l.w(p,"/"));if(f(u,"?"))u=x(u,"?")[0];p=x(p,"/")[0]}if(0==l.w(p,"www."))p=l.F(p,4);return new l.n.s(q,p,q,"(referral)","referral",q,u)};k.sc=function(p){var u="";if(k.o.pa){u=l.Db(p);u=""!=u?u+"&":u}u+=p.search;return u};k.zc=function(){return new l.n.s(q,
"(direct)",q,"(direct)","(none)",q,q)};k.Oc=function(p){var u=false,m,r,i=k.o.ga;if(p&&"referral"==p.da){m=w(l.d(p.ia));for(r=0;r<i[g];r++)u=u||f(m,w(i[r]))}return u};k.U=function(p){return q!=p&&p.Fa()};k.yc=function(p,u){var m="",r="-",i,b,e=0,j,t,v=k.f;if(!p)return"";t=k.a[l.m]?k.a[l.m]:"";m=k.sc(k.a.location);if(k.o.I&&p.Jb()){r=p.Ca();if(!s(r)&&!f(r,";")){p.Ra();return""}}r=n(t,l.X+v+".",";");i=k.Fc(m);if(k.U(i)){b=n(m,k.o.hb+"=","&");if("1"==b&&!s(r))return""}if(!k.U(i)){i=k.Fb();if(!s(r)&&
k.Nc(i))return""}if(!k.U(i)&&u){i=k.Ec();if(!s(r)&&k.Oc(i))return""}if(!k.U(i))if(s(r)&&u)i=k.zc();if(!k.U(i))return"";if(!s(r)){var y=x(r,"."),E=new l.n.s;E.Cb(y.slice(4).join("."));j=w(E.ka())==w(i.ka());e=y[3]*1}if(!j||u){var F=n(t,l.r+v+".",";"),I=F.lastIndexOf("."),G=I>9?l.F(F,I+1)*1:0;e++;G=0==G?1:G;p.Xb([v,k.ja,G,e,i.ka()].join("."));p.Ra();return"&utmcn=1"}else return"&utmcr=1"}};_gat.n.s=function(d,a,c,h,o,k,l){var s=this;s.v=d;s.ia=a;s.ra=c;s.L=h;s.da=o;s.R=k;s.vb=l};_gat.n.s.prototype.ka=
function(){var d=this,a=_gat,c=[],h=[[a.Wa,d.v],[a.Ya,d.ia],[a.$a,d.ra],[a.Ua,d.L],[a.Xa,d.da],[a.Za,d.R],[a.Va,d.vb]],o,k;if(d.Fa())for(o=0;o<h[a.c];o++)if(!a.q(h[o][1])){k=h[o][1].split("+").join("%20");k=k.split(" ").join("%20");a.h(c,h[o][0]+k)}return c.join("|")};_gat.n.s.prototype.Fa=function(){var d=this,a=_gat.q;return!(a(d.v)&&a(d.ia)&&a(d.ra))};_gat.n.s.prototype.Cb=function(d){var a=this,c=_gat,h=function(o){return c.J(c.C(d,o,"|"))};a.v=h(c.Wa);a.ia=h(c.Ya);a.ra=h(c.$a);a.L=h(c.Ua);a.da=
h(c.Xa);a.R=h(c.Za);a.vb=h(c.Va)};_gat.Z=function(){var d=this,a=_gat,c={},h="k",o="v",k=[h,o],l="(",s=")",q="*",f="!",n="'",w={};w[n]="'0";w[s]="'1";w[q]="'2";w[f]="'3";var x=1;function g(m,r,i,b){if(a.b==c[m])c[m]={};if(a.b==c[m][r])c[m][r]=[];c[m][r][i]=b}function z(m,r,i){return a.b!=c[m]&&a.b!=c[m][r]?c[m][r][i]:a.b}function B(m,r){if(a.b!=c[m]&&a.b!=c[m][r]){c[m][r]=a.b;var i=true,b;for(b=0;b<k[a.c];b++)if(a.b!=c[m][k[b]]){i=false;break}if(i)c[m]=a.b}}function A(m){var r="",i=false,b,e;for(b=0;b<k[a.c];b++){e=m[k[b]];if(a.b!=
e){if(i)r+=k[b];r+=p(e);i=false}else i=true}return r}function p(m){var r=[],i,b;for(b=0;b<m[a.c];b++)if(a.b!=m[b]){i="";if(b!=x&&a.b==m[b-1]){i+=b.toString();i+=f}i+=u(m[b]);a.h(r,i)}return l+r.join(q)+s}function u(m){var r="",i,b,e;for(i=0;i<m[a.c];i++){b=m.charAt(i);e=w[b];r+=a.b!=e?e:b}return r}d.Kc=function(m){return a.b!=c[m]};d.N=function(){var m=[],r;for(r in c)if(a.b!=c[r])a.h(m,r.toString()+A(c[r]));return m.join("")};d.Sc=function(m){if(m==a.b)return d.N();var r=[m.N()],i;for(i in c)if(a.b!=
c[i]&&!m.Kc(i))a.h(r,i.toString()+A(c[i]));return r.join("")};d._setKey=function(m,r,i){if(typeof i!="string")return false;g(m,h,r,i);return true};d._setValue=function(m,r,i){if(typeof i!="number"&&(a.b==Number||!(i instanceof Number)))return false;if(Math.round(i)!=i||i==NaN||i==Infinity)return false;g(m,o,r,i.toString());return true};d._getKey=function(m,r){return z(m,h,r)};d._getValue=function(m,r){return z(m,o,r)};d._clearKey=function(m){B(m,h)};d._clearValue=function(m){B(m,o)}};_gat.ic=function(d,a){var c=this;c.jd=a;c.Pc=d;c._trackEvent=function(h,o,k){return a._trackEvent(c.Pc,h,o,k)}};_gat.kc=function(d){var a=this,c=_gat,h=c.b,o=c.q,k=c.w,l=c.F,s=c.C,q=c.P,f=c.z,n="location",w=c.c,x=h,g=new c.hc,z=false;a.a=document;a.e=window;a.ja=Math.round((new Date).getTime()/1000);a.H=d;a.yb=a.a.referrer;a.va=h;a.j=h;a.A=h;a.M=false;a.aa=h;a.rb="";a.l=h;a.Ab=h;a.f=h;a.u=h;function B(){if("auto"==g.g){var b=a.a.domain;if("www."==l(b,0,4))b=l(b,4);g.g=b}g.g=c.T(g.g)}function A(){var b=g.g,e=k(b,"www.google.")*k(b,".google.")*k(b,"google.");return e||"/"!=g.p||k(b,"google.org")>-1}function p(b,
e,j){if(o(b)||o(e)||o(j))return"-";var t=s(b,c.r+a.f+".",e),v;if(!o(t)){v=f(t,".");v[5]=v[5]?v[5]*1+1:1;v[3]=v[4];v[4]=j;t=v.join(".")}return t}function u(){return"file:"!=a.a[n].protocol&&A()}function m(b){if(!b||""==b)return"";while(c.Lb(b.charAt(0)))b=l(b,1);while(c.Lb(b.charAt(b[w]-1)))b=l(b,0,b[w]-1);return b}function r(b,e,j){if(!o(b())){e(c.J(b()));if(!q(b(),";"))j()}}function i(b){var e,j=""!=b&&a.a[n].host!=b;if(j)for(e=0;e<g.B[w];e++)j=j&&k(c.T(b),c.T(g.B[e]))==-1;return j}a.Bc=function(){if(!g.g||
""==g.g||"none"==g.g){g.g="";return 1}B();return g.pb?c.t(g.g):1};a.tc=function(b,e){if(o(b))b="-";else{e+=g.p&&"/"!=g.p?g.p:"";var j=k(b,e);b=j>=0&&j<=8?"0":("["==b.charAt(0)&&"]"==b.charAt(b[w]-1)?"-":b)}return b};a.Ia=function(b){var e="",j=a.a;e+=a.aa?a.aa.Xc():"";e+=g.qa?a.rb:"";e+=g.ta&&!o(j.title)?"&utmdt="+c.d(j.title):"";e+="&utmhid="+c.uc()+"&utmr="+a.va+"&utmp="+a.Tc(b);return e};a.Tc=function(b){var e=a.a[n];b=h!=b&&""!=b?c.d(b,true):c.d(e.pathname+unescape(e.search),true);return b};a.$c=
function(b){if(a.Q()){var e="";if(a.l!=h&&a.l.N().length>0)e+="&utme="+c.d(a.l.N());e+=a.Ia(b);x.O(e,a.H,a.a,a.f)}};a.qc=function(){var b=new c.Y(a.a,g);return b.Ha(a.f)?b.Wc():h};a._getLinkerUrl=function(b,e){var j=f(b,"#"),t=b,v=a.qc();if(v)if(e&&1>=j[w])t+="#"+v;else if(!e||1>=j[w])if(1>=j[w])t+=(q(b,"?")?"&":"?")+v;else t=j[0]+(q(b,"?")?"&":"?")+v+"#"+j[1];return t};a.Zb=function(){var b;if(a.A&&a.A[w]>=10&&!q(a.A,"=")){a.u.Uc(a.A);a.u.cd();c._gasoDomain=g.g;c._gasoCPath=g.p;b=a.a.createElement("script");
b.type="text/javascript";b.id="_gasojs";b.src="https://www.google.com/analytics/reporting/overlay_js?gaso="+a.A+"&"+c.wa();a.a.getElementsByTagName("head")[0].appendChild(b)}};a.Jc=function(){var b=a.a[c.m],e=a.ja,j=a.u,t=a.f+"",v=a.e,y=v?v.gaGlobal:h,E,F=q(b,c.r+t+"."),I=q(b,c.W+t),G=q(b,c.ma+t),C,D=[],H="",K=false,J;b=o(b)?"":b;if(g.I){E=c.Db(a.a[n]);if(g.pa&&!o(E))H=E+"&";H+=a.a[n].search;if(!o(H)&&q(H,c.r)){j.Rc(H);if(!j.Jb())j.pc();C=j.ya()}r(j.Ba,j.Wb,j.fc);r(j.Aa,j.Na,j.Qa)}if(!o(C))if(o(j.K())||
o(j.za())){C=p(H,"&",e);a.M=true}else{D=f(j.K(),".");t=D[0]}else if(F)if(!I||!G){C=p(b,";",e);a.M=true}else{C=s(b,c.r+t+".",";");D=f(s(b,c.W+t,";"),".")}else{C=[t,c.Gc(),e,e,e,1].join(".");a.M=true;K=true}C=f(C,".");if(v&&y&&y.dh==t){C[4]=y.sid?y.sid:C[4];if(K){C[3]=y.sid?y.sid:C[4];if(y.vid){J=f(y.vid,".");C[1]=J[0];C[2]=J[1]}}}j.Ub(C.join("."));D[0]=t;D[1]=D[1]?D[1]:0;D[2]=undefined!=D[2]?D[2]:g.Yc;D[3]=D[3]?D[3]:C[4];j.La(D.join("."));j.Vb(t);if(!o(j.Hc()))j.Ma(j.t());j.dc();j.Pa();j.ec()};a.Lc=
function(){x=new c.jc(g)};a._initData=function(){var b;if(!z){a.Lc();a.f=a.Bc();a.u=new c.Y(a.a,g)}if(u())a.Jc();if(!z){if(u()){a.va=a.tc(a.Ac(),a.a.domain);if(g.sa){a.aa=new c.gc(g.ua);a.aa.xc()}if(g.qa){b=new c.n(a.f,a.a,a.va,a.ja,g);a.rb=b.yc(a.u,a.M)}}a.l=new c.Z;a.Ab=new c.Z;z=true}if(!c.Hb)a.Mc()};a._visitCode=function(){a._initData();var b=s(a.a[c.m],c.r+a.f+".",";"),e=f(b,".");return e[w]<4?"":e[1]};a._cookiePathCopy=function(b){a._initData();if(a.u)a.u.bd(a.f,b)};a.Mc=function(){var b=a.a[n].hash,
e;e=b&&""!=b&&0==k(b,"#gaso=")?s(b,"gaso=","&"):s(a.a[c.m],c.Sa,";");if(e[w]>=10){a.A=e;if(a.e.addEventListener)a.e.addEventListener("load",a.Zb,false);else a.e.attachEvent("onload",a.Zb)}c.Hb=true};a.Q=function(){return a._visitCode()%10000<g.ha*100};a.Vc=function(){var b,e,j=a.a.links;if(!g.Kb){var t=a.a.domain;if("www."==l(t,0,4))t=l(t,4);g.B.push("."+t)}for(b=0;b<j[w]&&(g.Ga==-1||b<g.Ga);b++){e=j[b];if(i(e.host))if(!e.gatcOnclick){e.gatcOnclick=e.onclick?e.onclick:a.Qc;e.onclick=function(v){var y=
!this.target||this.target=="_self"||this.target=="_top"||this.target=="_parent";y=y&&!a.oc(v);a.ad(v,this,y);return y?false:(this.gatcOnclick?this.gatcOnclick(v):true)}}}};a.Qc=function(){};a._trackPageview=function(b){if(u()){a._initData();if(g.B)a.Vc();a.$c(b);a.M=false}};a._trackTrans=function(){var b=a.f,e=[],j,t,v,y;a._initData();if(a.j&&a.Q()){for(j=0;j<a.j.la[w];j++){t=a.j.la[j];c.h(e,t.S());for(v=0;v<t.ca[w];v++)c.h(e,t.ca[v].S())}for(y=0;y<e[w];y++)x.O(e[y],a.H,a.a,b,true)}};a._setTrans=
function(){var b=a.a,e,j,t,v,y=b.getElementById?b.getElementById("utmtrans"):(b.utmform&&b.utmform.utmtrans?b.utmform.utmtrans:h);a._initData();if(y&&y.value){a.j=new c.i;v=f(y.value,"UTM:");g.G=!g.G||""==g.G?"|":g.G;for(e=0;e<v[w];e++){v[e]=m(v[e]);j=f(v[e],g.G);for(t=0;t<j[w];t++)j[t]=m(j[t]);if("T"==j[0])a._addTrans(j[1],j[2],j[3],j[4],j[5],j[6],j[7],j[8]);else if("I"==j[0])a._addItem(j[1],j[2],j[3],j[4],j[5],j[6])}}};a._addTrans=function(b,e,j,t,v,y,E,F){a.j=a.j?a.j:new c.i;return a.j.nb(b,e,
j,t,v,y,E,F)};a._addItem=function(b,e,j,t,v,y){var E;a.j=a.j?a.j:new c.i;E=a.j.xa(b);if(!E)E=a._addTrans(b,"","","","","","","");E.mb(e,j,t,v,y)};a._setVar=function(b){if(b&&""!=b&&A()){a._initData();var e=new c.Y(a.a,g),j=a.f;e.Na(j+"."+c.d(b));e.Qa();if(a.Q())x.O("&utmt=var",a.H,a.a,a.f)}};a._link=function(b,e){if(g.I&&b){a._initData();a.a[n].href=a._getLinkerUrl(b,e)}};a._linkByPost=function(b,e){if(g.I&&b&&b.action){a._initData();b.action=a._getLinkerUrl(b.action,e)}};a._setXKey=function(b,e,
j){a.l._setKey(b,e,j)};a._setXValue=function(b,e,j){a.l._setValue(b,e,j)};a._getXKey=function(b,e){return a.l._getKey(b,e)};a._getXValue=function(b,e){return a.l.getValue(b,e)};a._clearXKey=function(b){a.l._clearKey(b)};a._clearXValue=function(b){a.l._clearValue(b)};a._createXObj=function(){a._initData();return new c.Z};a._sendXEvent=function(b){var e="";a._initData();if(a.Q()){e+="&utmt=event&utme="+c.d(a.l.Sc(b))+a.Ia();x.O(e,a.H,a.a,a.f,false,true)}};a._createEventTracker=function(b){a._initData();
return new c.ic(b,a)};a._trackEvent=function(b,e,j,t){var v=true,y=a.Ab;if(h!=b&&h!=e&&""!=b&&""!=e){y._clearKey(5);y._clearValue(5);v=y._setKey(5,1,b)?v:false;v=y._setKey(5,2,e)?v:false;v=h==j||y._setKey(5,3,j)?v:false;v=h==t||y._setValue(5,1,t)?v:false;if(v)a._sendXEvent(y)}else v=false;return v};a.ad=function(b,e,j){a._initData();if(a.Q()){var t=new c.Z;t._setKey(6,1,e.href);var v=j?function(){a.rc(b,e)}:undefined;x.O("&utmt=event&utme="+c.d(t.N())+a.Ia(),a.H,a.a,a.f,false,true,v)}};a.rc=function(b,
e){if(!b)b=a.e.event;var j=true;if(e.gatcOnclick)j=e.gatcOnclick(b);if(j||typeof j=="undefined")if(!e.target||e.target=="_self")a.e.location=e.href;else if(e.target=="_top")a.e.top.document.location=e.href;else if(e.target=="_parent")a.e.parent.document.location=e.href};a.oc=function(b){if(!b)b=a.e.event;var e=b.shiftKey||b.ctrlKey||b.altKey;if(!e)if(b.modifiers&&a.e.Event)e=b.modifiers&a.e.Event.CONTROL_MASK||b.modifiers&a.e.Event.SHIFT_MASK||b.modifiers&a.e.Event.ALT_MASK;return e};a._setDomainName=
function(b){g.g=b};a.dd=function(){return g.g};a._addOrganic=function(b,e){c.h(g.fa,new c.cb(b,e))};a._clearOrganic=function(){g.fa=[]};a.hd=function(){return g.fa};a._addIgnoredOrganic=function(b){c.h(g.ea,b)};a._clearIgnoredOrganic=function(){g.ea=[]};a.ed=function(){return g.ea};a._addIgnoredRef=function(b){c.h(g.ga,b)};a._clearIgnoredRef=function(){g.ga=[]};a.fd=function(){return g.ga};a._setAllowHash=function(b){g.pb=b?1:0};a._setCampaignTrack=function(b){g.qa=b?1:0};a._setClientInfo=function(b){g.sa=
b?1:0};a._getClientInfo=function(){return g.sa};a._setCookiePath=function(b){g.p=b};a._setTransactionDelim=function(b){g.G=b};a._setCookieTimeout=function(b){g.wb=b};a._setDetectFlash=function(b){g.ua=b?1:0};a._getDetectFlash=function(){return g.ua};a._setDetectTitle=function(b){g.ta=b?1:0};a._getDetectTitle=function(){return g.ta};a._setLocalGifPath=function(b){g.Da=b};a._getLocalGifPath=function(){return g.Da};a._setLocalServerMode=function(){g.D=0};a._setRemoteServerMode=function(){g.D=1};a._setLocalRemoteServerMode=
function(){g.D=2};a.gd=function(){return g.D};a._getServiceMode=function(){return g.D};a._setSampleRate=function(b){g.ha=b};a._setSessionTimeout=function(b){g.Tb=b};a._setAllowLinker=function(b){g.I=b?1:0};a._setAllowAnchor=function(b){g.pa=b?1:0};a._setCampNameKey=function(b){g.db=b};a._setCampContentKey=function(b){g.eb=b};a._setCampIdKey=function(b){g.fb=b};a._setCampMediumKey=function(b){g.gb=b};a._setCampNOKey=function(b){g.hb=b};a._setCampSourceKey=function(b){g.ib=b};a._setCampTermKey=function(b){g.jb=
b};a._setCampCIdKey=function(b){g.kb=b};a._getAccount=function(){return a.H};a._getVersion=function(){return _gat.lb};a.kd=function(b){g.B=[];if(b)g.B=b};a.md=function(b){g.Kb=b};a.ld=function(b){g.Ga=b};a._setReferrerOverride=function(b){a.yb=b};a.Ac=function(){return a.yb}};_gat._getTracker=function(d){var a=new _gat.kc(d);return a};

var GA = {
    vars_to_set_later: [],
    setPendingVars: function() {
        while (GA.vars_to_set_later.length) {
            var v = GA.vars_to_set_later.pop();
            GA.pageTracker._setVar(v);
        }
    },
    setVar: function (v) {
        if (GA.pageTracker) {
            GA.pageTracker._setVar(v);
        } else {
            GA.vars_to_set_later.push(v);
        }
    }
}

/*
Copyright (c) Copyright (c) 2007, Carl S. Yestrau All rights reserved.
Code licensed under the BSD License: http://www.featureblend.com/license.txt
Version: 1.0.4
*/
var FlashDetect = new function(){
    var self = this;
    self.installed = false;
    self.raw = "";
    self.major = -1;
    self.minor = -1;
    self.revision = -1;
    self.revisionStr = "";
    var activeXDetectRules = [
        {
            "name":"ShockwaveFlash.ShockwaveFlash.7",
            "version":function(obj){
                return getActiveXVersion(obj);
            }
        },
        {
            "name":"ShockwaveFlash.ShockwaveFlash.6",
            "version":function(obj){
                var version = "6,0,21";
                try{
                    obj.AllowScriptAccess = "always";
                    version = getActiveXVersion(obj);
                }catch(err){}
                return version;
            }
        },
        {
            "name":"ShockwaveFlash.ShockwaveFlash",
            "version":function(obj){
                return getActiveXVersion(obj);
            }
        }
    ];
    /**
     * Extract the ActiveX version of the plugin.
     * 
     * @param {Object} The flash ActiveX object.
     * @type String
     */
    var getActiveXVersion = function(activeXObj){
        var version = -1;
        try{
            version = activeXObj.GetVariable("$version");
        }catch(err){}
        return version;
    };
    /**
     * Try and retrieve an ActiveX object having a specified name.
     * 
     * @param {String} name The ActiveX object name lookup.
     * @return One of ActiveX object or a simple object having an attribute of activeXError with a value of true.
     * @type Object
     */
    var getActiveXObject = function(name){
        var obj = -1;
        try{
            obj = new ActiveXObject(name);
        }catch(err){
            obj = {activeXError:true};
        }
        return obj;
    };
    /**
     * Parse an ActiveX $version string into an object.
     * 
     * @param {String} str The ActiveX Object GetVariable($version) return value. 
     * @return An object having raw, major, minor, revision and revisionStr attributes.
     * @type Object
     */
    var parseActiveXVersion = function(str){
        var versionArray = str.split(",");//replace with regex
        return {
            "raw":str,
            "major":parseInt(versionArray[0].split(" ")[1], 10),
            "minor":parseInt(versionArray[1], 10),
            "revision":parseInt(versionArray[2], 10),
            "revisionStr":versionArray[2]
        };
    };
    /**
     * Parse a standard enabledPlugin.description into an object.
     * 
     * @param {String} str The enabledPlugin.description value.
     * @return An object having raw, major, minor, revision and revisionStr attributes.
     * @type Object
     */
    var parseStandardVersion = function(str){
        var descParts = str.split(/ +/);
        var majorMinor = descParts[2].split(/\./);
        var revisionStr = descParts[3];
        return {
            "raw":str,
            "major":parseInt(majorMinor[0], 10),
            "minor":parseInt(majorMinor[1], 10), 
            "revisionStr":revisionStr,
            "revision":parseRevisionStrToInt(revisionStr)
        };
    };
    /**
     * Parse the plugin revision string into an integer.
     * 
     * @param {String} The revision in string format.
     * @type Number
     */
    var parseRevisionStrToInt = function(str){
        return parseInt(str.replace(/[a-zA-Z]/g, ""), 10) || self.revision;
    };
    /**
     * Is the major version greater than or equal to a specified version.
     * 
     * @param {Number} version The minimum required major version.
     * @type Boolean
     */
    self.majorAtLeast = function(version){
        return self.major >= version;
    };
    /**
     * Is the minor version greater than or equal to a specified version.
     * 
     * @param {Number} version The minimum required minor version.
     * @type Boolean
     */
    self.minorAtLeast = function(version){
        return self.minor >= version;
    };
    /**
     * Is the revision version greater than or equal to a specified version.
     * 
     * @param {Number} version The minimum required revision version.
     * @type Boolean
     */
    self.revisionAtLeast = function(version){
        return self.revision >= version;
    };
    /**
     * Is the version greater than or equal to a specified major, minor and revision.
     * 
     * @param {Number} major The minimum required major version.
     * @param {Number} (Optional) minor The minimum required minor version.
     * @param {Number} (Optional) revision The minimum required revision version.
     * @type Boolean
     */
    self.versionAtLeast = function(major){
        var properties = [self.major, self.minor, self.revision];
        var len = Math.min(properties.length, arguments.length);
        for(i=0; i<len; i++){
            if(properties[i]>=arguments[i]){
                if(i+1<len && properties[i]==arguments[i]){
                    continue;
                }else{
                    return true;
                }
            }else{
                return false;
            }
        }
    };
    /**
     * Constructor, sets raw, major, minor, revisionStr, revision and installed public properties.
     */
    self.FlashDetect = function(){
        if(navigator.plugins && navigator.plugins.length>0){
            var type = 'application/x-shockwave-flash';
            var mimeTypes = navigator.mimeTypes;
            if(mimeTypes && mimeTypes[type] && mimeTypes[type].enabledPlugin && mimeTypes[type].enabledPlugin.description){
                var version = mimeTypes[type].enabledPlugin.description;
                var versionObj = parseStandardVersion(version);
                self.raw = versionObj.raw;
                self.major = versionObj.major;
                self.minor = versionObj.minor; 
                self.revisionStr = versionObj.revisionStr;
                self.revision = versionObj.revision;
                self.installed = true;
            }
        }else if(navigator.appVersion.indexOf("Mac")==-1 && window.execScript){
            var version = -1;
            for(var i=0; i<activeXDetectRules.length && version==-1; i++){
                var obj = getActiveXObject(activeXDetectRules[i].name);
                if(!obj.activeXError){
                    self.installed = true;
                    version = activeXDetectRules[i].version(obj);
                    if(version!=-1){
                        var versionObj = parseActiveXVersion(version);
                        self.raw = versionObj.raw;
                        self.major = versionObj.major;
                        self.minor = versionObj.minor; 
                        self.revision = versionObj.revision;
                        self.revisionStr = versionObj.revisionStr;
                    }
                }
            }
        }
    }();
};
FlashDetect.JS_RELEASE = "1.0.4";/*global $, Element, Prototype*/

document.observe("dom:loaded", function () {
    var s = document.body.style;
    if (s.WebkitBoxShadow !== undefined || s.MozBoxShadow !== undefined || s.BoxShadow !== undefined || s.boxShadow !== undefined) {
        $(document.body).addClassName("has_box_shadow");
    }

    if (s.WebkitBorderRadius !== undefined || s.MozBorderRadius !== undefined || s.BorderRadius !== undefined || s.borderRadius !== undefined) {
        $(document.body).addClassName("has_border_radius");
    }

    var elm = new Element("a");
    var orig_color = "#ffffff";
    elm.style.color = orig_color;

    try {
        elm.style.color = 'rgba(1,1,1,0.5)';
    } catch (e) {
        //pass :0
    }

    if (elm.style.color != orig_color) {
        $(document.body).addClassName("has_rgba");
    }

    var supports_font_face = Prototype.Browser.WebKit || Prototype.Browser.Opera;
    if (supports_font_face) {
        $(document.body).addClassName("has_font_face");
    }

    for (var browser in Prototype.Browser) {
        if (Prototype.Browser[browser]) {
            $(document.body).addClassName(browser.toLowerCase());
        }
    }
});


Function.prototype.defer = Function.prototype.defer.wrap(
    function (defer) {
        var args = $A(arguments).slice(1);
        this.__tb__ = get_stack_rep();
        defer.apply(this, args);
    }
);

String.prototype.evalScripts = String.prototype.evalScripts.wrap(
    function (evalScripts) {
        var args = $A(arguments).slice(1);
        try {
            evalScripts.apply(this, args);
        } catch (e) {
            assert(0, e.toString());
        }
    }
);

Function.prototype.stop_calls_at = function (count) {
    var f = this;
    return function () {
        if (count-- > 0) {
            return f.apply(this, arguments);
        }
    };
};


Function.prototype.cached = function (expiration) {
    // holds the value of thunk (no-arg function) in a cache for a period of time (or forever)
    var r = Math.random();
    var f = this;
    return function () {
        var x = Jcached.get(r);
        if (x !== false) {
            return x;
        }

        x = f();
        Jcached.set(r, x, expiration);
        return x;
    };
};


Array.prototype.sort_by_key = function (f, reversed) {
    if (!reversed) {
        reversed = -1;
    }

    return this.sort(function (x, y) {
        x = f(x);
        y = f(y);

        if (x < y) {
            return reversed * -1;
        } else if (x > y) {
            return reversed * 1;
        } else {
            return 0;
        }
    });
};

Array.prototype.contains = function (item) {
    return this.indexOf(item) != -1;
};

Array.prototype.remove = function (from, to) {
    to = to || from + 1;
    this.splice(from, to - from);
};

Array.prototype.removeItem = function(item) {
    var index = this.indexOf(item);
    return index >= 0 ? this.remove(index) : false;
};

String.prototype.widthSplit = function (width) {
    width = width || 15;
    var out = [];
    var s = this;

    var start = 0;
    var snip = s.substring(start, start + width);
    while (snip !== '') {
        out.push(snip);
        start += width;
        snip = s.substring(start, start + width);
    }

    return out;
};

String.prototype.lpad = function (width, filler) {
    var str = this;
    filler = filler || "0";

    while (str.length < width) {
        str = filler + str;
    }
    return str.toString();
};

String.prototype.pad_nums = function () {
    return this.replace(/(\d+)/, function (s) {
        return s.lpad(10);
    });
};

String.prototype.reverse = function () {
    var splitext = this.split("");
    var revertext = splitext.reverse();
    var reversed = revertext.join("");
    return reversed;
};

String.prototype.replace_last = function (bad, good) {
    var rstr = this.reverse();
    var rbad = bad.reverse();
    var rgood = good.reverse();

    return rstr.replace(rbad, rgood).reverse();
};

String.prototype.create = function (s) {
    return s;
};

String.prototype.count = function (substring) {
    return (this.length - this.gsub(substring, "").length) / substring.length;
};

String.prototype.snippet = function (maxchars, where) {
    maxchars = maxchars || 26;
    where = where || 0.75;

    if (this.length <= maxchars) {
        return this;
    }

    var dot_pos = this.lastIndexOf(".");
    var ext = "";

    if (dot_pos > 0) {
        ext = this.substr(dot_pos);
        maxchars = maxchars - ext.length;
    } else {
        dot_pos = this.length;
        ext = "";
    }

    maxchars = maxchars - this.create("...").length;

    var left = Math.floor(maxchars * where);
    var right_count = maxchars - left;
    var right = dot_pos - right_count;
    var pre = this.substr(0, left);
    var post = this.substr(right, dot_pos - right);

    return pre + "..." + post + ext;
};

String.prototype.title = function () {
    return this.charAt(0).toUpperCase() + this.substr(1);
};

Effect.BlindFadeUp = function (elm, opts) {
    var ef1, ef2;
    ef1 = new Effect.BlindUp(elm, opts);
    ef2 = new Effect.Fade(elm, opts);
    this.cancel = function () {
        ef1.cancel();
        ef2.cancel();
    };
};

Effect.BlindFadeDown = function (elm, opts) {
    var ef1, ef2;
    ef1 = new Effect.BlindDown(elm, opts);
    ef2 = new Effect.Appear(elm, opts);
    this.cancel = function () {
        ef1.cancel();
        ef2.cancel();
    };
};

Effect.Flash = function (elm, opts, cycle) {
    assert(opts.startcolor && opts.endcolor, "Start and end colors must be specified");
    assert(opts.cycles, "Fade cycles must be specified");

    cycle = cycle || 0;
    new Effect.Highlight(elm, {duration: 1, startcolor: opts.startcolor, endcolor: opts.endcolor, restorecolor: opts.endcolor, afterFinish: function () {
        new Effect.Highlight(elm, {duration: 1, startcolor: opts.endcolor, endcolor: opts.startcolor, restorecolor: opts.startcolor, afterFinish: function () {
            if (cycle < opts.cycles) {
                Effect.Flash(elm, opts, cycle + 1);
            }
        }});
    }});
};

Ajax.DBRequest = Class.create(Ajax.Request,
{
    initialize: function ($super, url, options) {
        this.start_time = Util.time();
        options = options || {};
        options.method = 'post';
        options.parameters = options.parameters || {};
        options.parameters.t = Constants.TOKEN;
        var cleanUp = options.cleanUp || function (ok) {};

        if (options.job) {
            this.job_id = Util.nonce();
            options.parameters.job_id = this.job_id;
            ProgressWatcher.watch(this);
        }

        RequestWatcher.watch(this, !!options.job); //no_still_working=!!options.job

        var origOnFailure = options.onFailure;
        var origOnSuccess = options.onSuccess;
        var origOnComplete = options.onComplete;

        options.onFailure = function (req) {
            if (Job.handled(req.request.job_id)) {
                return;
            }
            if (!options.noAutonotify) {
                var msg;
                if (!Constants.IS_PROD && req.status === 500 && req.getHeader("X-Debug-Url")) {
                    var url = req.getHeader("X-Debug-Url");
                    msg = _("There was a problem completing this request.") + " <a href=\"" + url + "\">View debug</a>";
                }
                Notify.ServerError(msg);
            }
            cleanUp(false); // okay=false
            if (origOnFailure) {
                origOnFailure(req);
            }
            if ([404, 502].contains(req.status)) {
                assert(false, "Ajax " + req.status + " on " + req.request.url);
            }
        };

        options.onSuccess = function (req) {
            WIT._record("AJAX", "load", req.request.url, {
                'time': new Date().getTime() - req.request.start_time
            });
            if (Job.handled(req.request.job_id)) {
                return;
            }

            // the order here matters. i18n gets added on after query_log
            TranslationSuggest.update_i18n_messages_from_req(req);
            if (typeof(QueryLog) !== 'undefined') {
                QueryLog.update_query_log_from_req(req);
            }

            if (!req.responseText.length) {
                if (!options.job) {
                    if (!options.noAutonotify) {
                        if (!req || req.status !== 0) { // User interupts ajax request so lets not show a error notification
                            Notify.ServerError();
                        }
                    }

                    if (origOnFailure) {
                        origOnFailure(req);
                    }
                }
            } else if (req.responseText.indexOf('err:') === 0) {
                if (!options.noAutonotify) {
                    Notify.ServerError(req.responseText.substr(4));
                }

                if (origOnFailure) {
                    origOnFailure(req);
                }
            } else {
                if (origOnSuccess) {
                    if (req.responseText.indexOf('ok:') === 0) {
                        Notify.ServerSuccess(req.responseText.substr(3));
                    }
                    origOnSuccess(req);
                }
                if (origOnComplete) {
                    origOnComplete(req);
                }
            }

            cleanUp(true); // okay=true
        };

        if (options.job) {
            url += (url.indexOf("?") != -1 ? "&" : "?") + "long_running=1";
        }

        // store some request info for traceback purposes
        var ajax_info = $H({'url': url});
        if (options.parameters) {
            ajax_info.update(options.parameters);
            ajax_info.unset('t');
        }
        options.onSuccess.__tb_ajax_info__ = options.onFailure.__tb_ajax_info__ = ajax_info.toJSON();

        // make the request
        $super(url, options);
    }
});

Element.addMethods({
    db_observe: function (element, eventName, callback) {
        return element.observe(eventName, function (e) { callback(e, element); });
    }
});

// Used to get the element that has focus - http://ajaxandxml.blogspot.com/2007/11/emulating-activeelement-property-with.html
function _dom_trackActiveElement(evt) {
    if (evt && evt.target) {
        try {
            document.activeElement = evt.target == document ? null : evt.target;
        } catch (e) {
            //pass
        }
    }
}

function _dom_trackActiveElementLost(evt) {
    try {
        document.activeElement = null;
    } catch (e) {
        //pass
    }
}

if (document.addEventListener) {
    document.addEventListener("focus",_dom_trackActiveElement,true);
    document.addEventListener("blur",_dom_trackActiveElementLost,true);
}


var WIT, Jcached, AMC, MCLog;
var WIT = {
    enabled: Constants.WIT_ENABLED,
    reporting: false,
    start_time: 0,
    register: function () {
        if (WIT.enabled) {
            WIT.reportInterval = setInterval(WIT.report, 10000);
            $(document.body).observe("click", WIT.click);
        }
    },
    add_group: function (elm, name) {
        elm = $(elm);
        assert(elm, "WIT.add_group missing elm");

        elm.addClassName("wit_group");
        elm.setAttribute("name", name);
    },
    clear_group: function (elm) {
        if (elm.hasClassName("wit_group")) {
            elm.removeClassName("wit_group");
        }
    },
    get_group: function (elm) {
        var group_elm = elm.up(".wit_group");
        if (group_elm) {
            return group_elm.getAttribute("name");
        }
        return "ALL";
    },
    time_elapsed: function () {
        var diff = Util.time() - WIT.start_time;
        if (diff < 0) {
            diff = 0;
        }
        return diff;
    },
    click: function (e) {
        if (!WIT.enabled || !e || !e.target) {
            return;
        }
        var target = $(e.target);
        var tracking_elm;
        if (["input", "textarea", "checkbox"].indexOf(target.tagName.toLowerCase()) > -1) {
            tracking_elm = target;
        }
        if (!tracking_elm) {
            tracking_elm = Util.resolve_target(target, "a, .wit");
        }
        if (!tracking_elm || $(tracking_elm).hasClassName("ignore")) {
            return;
        }
        var label = tracking_elm.getAttribute("name") || tracking_elm.id || (tracking_elm.getValue && tracking_elm.getValue()) || tracking_elm.innerHTML.stripTags().strip() || "unknown " + tracking_elm.tagName;

        if (Constants.emessages[label]) {
            label = Constants.emessages[label];
        }
        var group = WIT.get_group(tracking_elm);
        assert(group, "Group missing");

        WIT.record_action(label, 'click', group, WIT.time_elapsed());
    },
    record_action: function (label, type, group, tti, extra_info) {
        extra_info = extra_info || {};
        extra_info.group = group;
        extra_info.type = type;
        extra_info.tti = tti;

        WIT.record("ACTION", label, extra_info);
    },
    record: function (event_type, label, extra_info) {
        extra_info = extra_info || {};
        WIT._record(event_type, label,  window.location.pathname.split("#")[0], extra_info);
    },
    _record: function (event_type, label, url, extra_info) {
        var key = "WIT_" + event_type + "_" + label;
        if (!WIT.enabled || Jcached.get(key) || WIT.IGNORE_URLS[url.split(Constants.WEBSERVER).last().split(/[\?\#]/)[0]]) { // Get just the pathname
            return;
        }

        assert(label, "Missing WIT label");
        assert(event_type, "Missing WIT event_type");
        assert(url, "Missing WIT url");
        var params = [event_type, label, url, extra_info || {}];

        WIT.add_to_cookie(params);
        Jcached.set(key, 1, 5000); // 5 sec
    },
    add_to_cookie: function (params) {
        if (Ajax.activeRequestCount) {
            // Anything added while an active ajax request is happening will get cleared by the wit cookie being set by the response
            setTimeout(function () {
                WIT.add_to_cookie(params);
            }, 500);
            return;
        }
        assert(WIT.enabled, "WIT Disabled.");
        var prev_val = WIT.get_cookie_val();
        prev_val.push(params);

        var json = Object.toJSON(prev_val);
        var encoded = encodeURIComponent(json);
        Util.create_cookie("wit", encoded, 365);
        if (encoded.length > 1024) {
            WIT.report();
        }
    },
    get_cookie_val: function () {
        var prev_str = Util.read_cookie("wit");
        var prev_val;
        if (prev_str) {
            try {
                prev_val = decodeURIComponent(prev_str).evalJSON();
            } catch (e) {
                prev_val = [];
            }
        } else {
            prev_val = [];
        }
        return prev_val;
    },
    IGNORE_URLS: {
        '/wit': true
    },
    report: function () {
        if (!WIT.enabled || WIT.reporting) {
            return;
        }

        var cookie_val = Util.read_cookie("wit");
        if (cookie_val) {
            WIT.reporting = true;
            new Ajax.Request("/wit", {
                onComplete: function () {
                    WIT.reporting = false;
                }
            });
            Util.create_cookie("wit", "", -1);
        }
    }
};
WIT.start_time = window.ST || new Date().getTime();
document.observe("dom:loaded", function () {
    WIT.record("LOAD", "ready", {
        'time':  Util.time() - WIT.start_time
    });
    WIT.register();
});
Event.observe(window, "load", function () {
    WIT.record("LOAD", "complete", {
        'time':  Util.time() - WIT.start_time
    });
});
Event.stop = Event.stop.wrap(function (f, e) {
    if (e) {
        if (e.type == "click") {
            WIT.click(e);
        }
        f.apply(this, $A(arguments).slice(1));
    }
});

var Jcached = {
    cache: {},
    set: function (name, value, expiration) {
        var c = Jcached.cache[name];

        if (!c) {
            Jcached.cache[name] = {};
            c = Jcached.cache[name];
        }

        c.value = value;
        c.expires = expiration ? (new Date()).getTime() + expiration : 0;
    },
    get: function (name) {
        var c = Jcached.cache[name];
        if (!c || (c.expires && (new Date()).getTime() > c.expires)) {
            delete Jcached.cache[name];
            return false;
        }

        return c.value;
    }
};

var AMC = {
    log_escape: function (kept) {
        $("top_notifier_container").remove();
        new Ajax.DBRequest("/log_escape", {
            parameters: {
                kept_locale: kept
            }
        });
    },
    log: function (label, values) {
        assert(label, "AMCLog missing label");
        var params = {
            'label': label
        };

        if (values) {
            Object.extend(params, values);
        }

        new Ajax.Request('/ajax_amc_log', {
            'parameters': params
        });
    },
    help_article_play: function (help_article_id) {
        if (!AMC.help_article_play_logged) {
            AMC.help_article_play_logged = 1;
            if (Help.article_id) {
                new Ajax.Request('/ajax_amc_help_video', {
                    parameters : {
                        article_id : Help.article_id
                    }
                });
            }
        }
    }
};

var ABTest = {
    log: function (abtest_name, event_name) {
        assert(abtest_name, "ABTest log missing abtest name");
        assert(event_name, "ABTest log missing event name");
        var params = {
            'abtest_name': abtest_name,
            'event_name': event_name
        };

        new Ajax.Request('/ajax_abtest_log', {
            'parameters': params
        });
    }
};

var MCLog = {
    log: function (label) {
        new Ajax.Request('/ajax_mc_log/' + label);
    }
};

function add_i18n_message(id, msg_display, english_display, clear_msg) {
    Constants.messages = Constants.messages || {};
    Constants.emessages = Constants.emessages || {};

    msg_display = msg_display.stripTags().friendly_format();
    english_display = english_display.stripTags();
    Constants.messages[msg_display] = id;
    Constants.emessages[msg_display] = english_display;
    if (clear_msg) { // we've substituted into original and don't need it anymore
        delete Constants.messages[clear_msg];
        delete Constants.emessages[clear_msg];
    }
    if (typeof(TranslationSuggest) != 'undefined') { // we index onload for messages we get before js is done loading
        TranslationSuggest.index_message(msg_display);
    }
}


// common plural functions
function singular_1(n) {
    return n == 1 ? 0 : 1;
}
function singular_01(n) {
    return n <= 1 ? 0 : 1;
}
function singular_all(n) {
    return 0;
}
var PLURAL_RULES = {
    'es_US' : singular_1,
    'de'    : singular_1,
    'es'    : singular_1,
    'fr'    : singular_01,
    'ja'    : singular_all,
    'pl'    : singular_1
};

Date.prototype.localize = function () {
    assert(Constants.date_format, "Date format missing.");
    return this.format(Constants.date_format);
};

Date.prototype.format = function (format_str) {
    assert(format_str, "Missing format string");
    assert(typeof(format_str) == "string", "Date format requires a format string");

    var format_lambdas = {
        yy: function (date) {
            return date.getFullYear().toString().substring(2);
        },
        yyyy: function (date) {
            return date.getFullYear().toString();
        },
        d: function (date) {
            return date.getDate().toString();
        },
        dd: function (date) {
            return date.getDate().toString().lpad(2);
        },
        M: function (date) {
            return (date.getMonth() + 1).toString();
        },
        MM: function (date) {
            return (date.getMonth() + 1).toString().lpad(2);
        }
    };

    var out = format_str.replace(/([a-zA-Z]+)/g, (function (s) {
        return (format_lambdas[s] && format_lambdas[s](this)) || s;
    }).bind(this));
    return out;
};

String.prototype.format_sub = function (sub_func) {
    // calls sub_func(full_match, key, truncate, type) with `this` bound to the orig string
    return this.replace(/%(\([a-z_\-]+\))?(.\d+)?(.)/g, sub_func.bind(this));
};

String.prototype.format = function string_format() {
    // example usage:
    // 'hello'.format();
    // 'hello %s'.format('world');
    // 'hello %s'.format(['world']);
    // '%s %s'.format('hello', 'world');
    // '%s %s'.format(['hello', 'world']);
    // '%(greeting)s %(thing)s'.format({'greeting': 'hello', 'thing': 'world'});
    //
    // currently only supports s, d and f.
    if (arguments.length === 0) {
        return this.toString();
    }
    var d;
    var curindex = 0;
    if (arguments.length == 1 && arguments[0] instanceof Object) {
        d = arguments[0];
    }
    else {
        d = $A(arguments);
    }
    function r(s, key, truncate, type) {
        var v;
        if (!key) {
            if (!Object.isArray(d)) {
                d = [d]; // handle case where an object is passed in for positional formatting
            }
            assert(curindex > -1, "Cannot mix named and positional indices in string formatting for string '" + this + "'.");
            assert(curindex < d.length, "Insufficient number of items in format for string '" + this + "', list " + $A(d).toJSON() + ".");

            v = d[curindex];
            curindex++;
        }
        else {
            key = key.slice(1, -1);
            assert(curindex <= 0, "Cannot mix named and positional indices in string formatting for string '" + this + "'.");
            curindex = -1;

            assert(key in d, "Key '" + key + "' not present during string substitution for string '" + this + "', dict " + $H(d).toJSON() + ".");
            v = d[key];
        }
        assert(typeof(v) != 'undefined', 'value for key "' + (key || '').toString() + '" is undefined');

        var result;
        if (type == 's') {
            result = v.toString();
        }
        else if (type == 'd') {
            result = parseInt(v, 10).toString();
        }
        else if (type == 'f') {
            result = Number(v).toString();
        }
        else if (type == '%') {
            return '%';
        }
        else {
            assert(false, "Unexpected format character '" + type + "' for string '" + this + "'.");
        }
        if (truncate) {
            truncate = parseInt(truncate.slice(1), 10);
            if (type == 'f') { // truncate by precision
                if (result.indexOf('.') == '-1') {
                    result = result + '.0';
                }
                var parts = result.split('.');
                return parts[0] + '.' + parts[1].slice(0, truncate);
            }
            else { // truncate by length
                return result.slice(0, truncate);
            }
        }
        return result;
    }

    var ret = this.format_sub(r);
    // split-todo i had to add a prefix "Constants.messages &&" to make this work post-split.
    // really, format() shouldn't be connected with add_i18n_message at all:
    // it's better if it stayed simple, but more importantly, is often used outside of translated strings,
    // e.g. for internal error messages such as
    // assert(["string", "number"].contains(type), "expected a string or a number, got %s".format(type));
    // revisit!
    if (Constants.messages && this in Constants.messages) {
        curindex = 0; // reset to allow for another substitution with same r function
        var eng_msg_id = Constants.emessages[this];
        var eng = String.prototype.format_sub.call(eng_msg_id, r);
        add_i18n_message(eng_msg_id, ret, eng, this);
    }
    return ret;
};

String.prototype.friendly_format = function () {
    // replace all format strings with [key] or [wordN]/[numberN]
    var numc = 1;
    var wordc = 1;

    function r(s, key, truncate, type) {
        if (!key) {
            // name based on type
            if (type == 's') {
                return "[word" + (wordc++) + "]";
            } else {
                return "[number" + (numc++) + "]";
            }
        } else {
            return "[" + key.slice(1, -1).replace("-", "_") + "]";
        }
    }

    return this.format_sub(r);
};

String.prototype.blank_format = function () {
    // replace all format strings with nothing
    function r() {
        return "";
    }

    return this.format_sub(r);
};

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

function _(s) {
    var r = LANGPACK[s] || s;
    add_i18n_message(s, r, s);
    return r;
}
function N_(s) {
    return s;
}
function ungettext(singular, plural, n) {
    assert(typeof(n) != 'undefined', "missing number parameter for ungettext");

    var ret;
    if (singular in LANGPACK) {
        if (Constants.USER_LOCALE) {
            var lookup = LANGPACK[singular];
            var plural_class = PLURAL_RULES[Constants.USER_LOCALE](n);
            assert(plural_class in lookup, 'bad plural lookup');
            ret = lookup[plural_class];
        }
    }
    var eng = n == 1 ? singular : plural;

    ret = ret || eng; // fallback to english str
    add_i18n_message(singular, ret, eng);
    return ret;
}
function localized_path(path, supported_locales) {
    // returns a localized path to a static resource: http://blah.com/vid.flv -> http://blah.com/vid__zh_TW.flv
    // falls back to input path if user's locale isn't in supported_locales.
    // when possible, use server-side localized_path instead. it has better fallback.
    if (!supported_locales) {
        supported_locales = [];
        for (var i = 0; i < Constants.LOCALES.length; i++) {
            var entry = Constants.LOCALES[i]; // code/display name pairs
            supported_locales.push(entry[0]);
        }
    }
    if (supported_locales.indexOf(Constants.USER_LOCALE) != -1) {
        return path.replace(/(\.[a-zA-Z0-9]{2,4})$/, "__%s$1".format(Constants.USER_LOCALE));
    }
    else {
        return path;
    }
}


function strip_comments(js) {
    return js.replace(/\/\/.*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
}

function fn_body(f) {
    // normalize some easy to handle cross-browser inconsistencies in Function.toString.
    // this isn't perfect, but some normalization is better than none
    var fbody = strip_comments(f.toString());
    fbody = fbody.replace(/[\s]+/g, ' ');

    if (fbody.startsWith('(')) {
        fbody = fbody.substr(1);
    }
    if (fbody.endsWith(')')) {
        fbody = fbody.substr(0, fbody.length - 1);
    }

    fbody = fbody.replace('function (', 'function(');

    return fbody;
}

function get_stack_rep() {
    var out = [];
    var seen = {};
    var caller = arguments.callee.caller;

    while (caller) {
        if (caller.__tb_ajax_info__) {
            // short-circuit at ajax.dbrequest if we set ajax info
            out.unshift("Ajax.DBRequest: " + caller.__tb_ajax_info__);
            break;
        }

        if (caller.__tb__) {
            // short-circuit with a tb we've pre-collected
            out = caller.__tb__.concat(out);
            break;
        }

        var fingerprint = fn_body(caller);

        if (fingerprint in seen) {
            break; // bail at the first sign of recursion
        }

        seen[fingerprint] = true;
        out.unshift(fingerprint);
        caller = caller.caller;
    }

    return out;
}

/* the "real" alert is now alertd.  We shouldn't really be using alerts on production.  We currently use them
   when we want to show an alert and another modal is already on screen. (rare) */
var alertd = window.alert;

function assert(cond, msg, force_report) {
    if (!cond) {
        msg = "Assertion Error: " + msg;
        if (!Constants.IS_PROD) {
            alert(msg);
        }

        var stack = get_stack_rep();
        stack.pop(); // remove the assert part of the stack

        global_report_exception(msg, window.location.href, '', stack.join("\n")); // force = true
        throw msg;
    }
}


var DBObserver = {
    watch: function (elm_id, callback) {
        setInterval(function () {
            var elm = $(elm_id);
            assert(elm, "Couldn't find watch element");

            var new_search = elm.getValue().strip();
            if (new_search != elm.last_search && !SuggestionInput.defaulted(elm)) {
                elm.last_search = new_search;
                callback(new_search);
            }
        }, 300);
    }
};


var Email = {
    mailto: function (link, name, domain, body) {
        if (!domain) {
            domain = "dropbox.com";
        }

        link.href = "mailto:" + name + "@" + domain;

        if (body) {
            link.href += "?body=" + body;
        }

        link.onMouseover = null;
    }
};

var SimpleSet = Class.create({
    // a set of strings or numbers => true; not for objects
    initialize: function (items) {
        this.length = 0;
        this.d = {};
        this.items = [];
        this.update(items);
    },
    update: function (items) {
        items = items || [];
        for (var i = 0, l = items.length; i < l; i++) {
            var item = items[i];
            this.add(item);
        }
    },
    _hash: function (item) {
        var type = typeof(item);
        assert(["string", "number"].contains(type), "expected a string or a number, got %s".format(type));
        return type + ":" + item;
    },
    add: function (item) {
        var hsh = this._hash(item);
        if (!this.d[hsh]) {
            this.items.push(item);
            this.length += 1;
        }
        this.d[hsh] = true;
    },
    contains: function (item) {
        var hsh = this._hash(item);
        return this.d[hsh] || false;
    },
    union: function () {
        var all = this.items;
        for (var i = 0, len = arguments.length; i < len; i += 1) {
            all = all.concat(arguments[i].items);
        }
        return new SimpleSet(all);
    },
    difference: function () {
        var args = $A(arguments);
        assert(args.length > 0, "Requires at least one SimpleSet");

        var argset;
        if (args.length > 1) {
            var base = args[0];
            argset = base.union.apply(base, args.slice(1));
        } else {
            argset = args[0];
        }
        var diff = [];
        for (var i = 0; i < this.items.length; i += 1) {
            var item = this.items[i];
            if (!argset.contains(item)) {
                diff.push(item);
            }
        }

        return new SimpleSet(diff);
    }
});


var Emstring = Class.create(
    {
        initialize: function (s) {
            this.s = s;
            this.info = this.widthInfo();
            this.length = s.length ? this.info[this.s.length - 1] : 0;
        },
        create: function (s) {
            return new Emstring(s);
        },
        widthInfo: function () {
            var r = {};
            r[-1] = 0;

            for (var i = 0; i < this.s.length; i++) {
                r[i] = r[i - 1] + this.ems(this.s.charAt(i));
            }

            return r;
        },
        findSpot: function (em) {
            if (!em) {
                return 0;
            }

            var s = 0;
            var e = this.s.length;
            var mid;
            while (s <= e) {
                mid = Math.floor(s / 2 + e / 2);
                var count = this.info[mid - 1];

                if (count > em) {
                    e = mid - 1;
                } else if (count < em) {
                    s = mid + 1;
                } else { //exact match
                    return mid;
                }
            }

            // missed exact match, but close
            if (s > mid) {
                return s;
            } else {
                return mid;
            }
        },
        ems: function (c) {
            // see python version
            var DEFAULT = 0.65;

            if (Emstring.THIN_CHARS.contains(c)) {
                return 0.3;
            }
            if (Emstring.WIDE_CHARS.contains(c)) {
                return 1;
            }
            var n = c.charCodeAt(0);
            if (n < 128) {
                return DEFAULT;
            }

            // japanese wide chars
            if (0x3000 < n && n < 0x30ff || 0xff00 < n && n < 0xff5f ||
                0x4e00 < n && n < 0x9faf || 0x3400 < n && n < 0x4db0) {
                return 1.1;
            }

            return DEFAULT;
        },
        substr: function (start, len) {
            start = this.findSpot(start);
            len = len !== null ? this.findSpot(start + len) : this.s.length;

            return new Emstring(this.s.substr(start, len));
        },
        indexOf: function (s) {
            var at = this.s.indexOf(s);
            return at > -1 ? this.info[at - 1] : -1;
        },
        lastIndexOf: function (s) {
            var backwards_pos = this.s.reverse().indexOf(s.reverse());
            if (backwards_pos < 0) {
                return -1;
            }

            return this.info[(this.s.length - backwards_pos) - s.length - 1];
        },
        toString: function () {
            return this.s;
        },
        snippet: String.prototype.snippet
    });

Emstring.THIN_CHARS = new SimpleSet(["!", "'", "(", ")", ",", "-", ".", "/", ":", ";", "I", "J",
                                     "[", "]", "f", "i", "j", "l", "r", "t", "|", "\\"]);
Emstring.WIDE_CHARS = new SimpleSet(["#", "%", "+", "<", "=", ">", "M", "W", "^", "m", "w", "~"]);


var DomUtil, Util; // these variables reference each other -> declare both ahead of time for jslint.
DomUtil = {
    fromElm: function (elm) {
        return $(elm).innerHTML;
    },
    updateFromElm: function (toElm, fromElm) {
        toElm = $(toElm);
        fromElm = $(fromElm);
        toElm.update(DomUtil.fromElm(fromElm));
        Util.live_joff(fromElm, toElm);
    },
    fillVal: function (val, className) {
        $$("." + className).each(
            function (x) {
                x = $(x);
                if (x.tagName == 'INPUT') {
                    x.value = val;
                    x.defaultValue = val;
                } else {
                    x.innerHTML = val;
                }
            });
    }
};


Util = {
    one_line_fit: function (selector) {
        var elms = $$(selector);
        if (elms.length < 2) {
            return;
        }

        var resize = function () {
            var first = elms[0];
            var last = elms[elms.length - 1];

            var first_top = first.cumulativeOffset().top,
                last_top = last.cumulativeOffset().top;

            if (first_top != last_top) {
                var current_size = parseInt(first.getStyle("font-size"), 10);
                var new_size = current_size - 1;
                if (new_size < 8) {
                    return;
                }
                elms.each(function (elm) {
                    elm.style.fontSize = new_size + 'px';
                });
                resize();
            }
        };
        resize();
    },
    timedelta: function (x, y) {
        var dt = x.getTime() - y.getTime();

        var ms_per_day = 86400000,
            ms_per_sec = 1000;

        var days = parseInt(dt / ms_per_day, 10);
        dt = dt % ms_per_day;

        var seconds = parseInt(dt / ms_per_sec, 10);
        dt = dt % ms_per_sec;

        return {
            'microseconds': parseInt(dt, 10),
            'seconds': seconds,
            'days': days
        };
    },
    ago: function (when, full) {
        var todays_date = new Date();
        var dt = Util.timedelta(todays_date, when);

        var arg;
        if (dt.days < 2) {
            var sec = dt.seconds + dt.days * 86400;

            if (sec < 60) {
                arg = sec;
                if (full) {
                    // TRANSLATORS for example "5 seconds"
                    return ungettext("%d second", "%d seconds", arg).format(arg);
                } else {
                    // TRANSLATORS abbreviation for seconds
                    return ungettext("%d sec", "%d secs", arg).format(arg);
                }
            } else if (sec < 3600) {
                arg = parseInt(sec / 60, 10);
                if (full) {
                    return ungettext("%d minute", "%d minutes", arg).format(arg);
                } else {
                    // TRANSLATORS abbreviation for minutes
                    return ungettext("%d min", "%d mins", arg).format(arg);
                }
            } else {
                arg = parseInt(sec / 3600, 10);
                if (full) {
                    return ungettext("%d hour", "%d hours", arg).format(arg);
                } else {
                    // TRANSLATORS abbreviation for hours
                    return ungettext("%d hr", "%d hrs", arg).format(arg);
                }
            }
        } else {
            var days = parseInt(dt.days + Math.round(dt.seconds / 86400.0), 10);

            if (days < 30) {
                return ungettext("%d day", "%d days", days).format(days);
            } else if (days < 56) {
                arg = parseInt(days / 7, 10);
                return ungettext("%d week", "%d weeks", arg).format(arg);
            } else if (days < 365) {
                arg = parseInt(days / 30, 10);
                return ungettext("%d month", "%d months", arg).format(arg);
            } else {
                arg = parseInt(days / 365, 10);
                return ungettext("%d year", "%d years", arg).format(arg);
            }
        }
    },
    nice_list: function (args) {
        if (!args) {
            return "";
        } else if (args.length == 1) {
            return args[0];
        } else if (args.length == 2) {
            return _(Constants.TWO_ITEM_LIST).format({
                'x': args[0],
                'y': args[1]
            });
        }

        var split = _(Constants.THREE_ITEM_LIST).split(/%\(x\)s|%\(y\)s|%\(z\)s/);
        assert(split.length == 4, "bad item list format " + Constants.THREE_ITEM_LIST);

        var before = split[0],
            between = split[1],
            ending = split[2],
            after = split[3];

        return [ before,
                 args.slice(0, -1).join(between),
                 ending,
                 args[args.length - 1],
                 after
               ].join("");

    },

    center: function (elm) {
        elm = $(elm);
        var l = (document.viewport.getWidth() - elm.getWidth()) / 2;
        elm.setStyle({left: Math.floor(l) + "px"});
    },
    pinTop: function (elm, slide) {
        elm = $(elm);
        if (slide) {
            elm.setStyle({top: window.pageYOffset + "px"});
        } else {
            new Effect.Move(elm, {y: window.pageYOffset, mode: 'absolute', duration: 0.25});
        }
    },
    getTickWaiter: function (n, action) {
        var ticks = 0;
        return function () {
            if (ticks == n) {
                action();
            }
            ticks++;
        };
    },
    calcBox: function (t1, l1, t2, l2, box) {
        box.top = Math.min(t1, t2);
        box.left = Math.min(l1, l2);
        box.width = Math.abs(l1 - l2);
        box.height = Math.abs(t1 - t2);
    },
    initBox: function (top, left, box) {
        box.top = top;
        box.left = left;
        box.width = 0;
        box.height = 0;
    },
    pointOnBox: function (left, top, box) {
        return (top  >= box.top  && top  <= box.top  + box.height &&
                left >= box.left && left <= box.left + box.width);
    },
    cmpBox: function (left, top, box) {
        // returns cmp-style results regarding the point's position relative to the box
        if (top < box.top || left < box.left) {
            return -1;
        }

        if (top > box.top  + box.height || left > box.left + box.width) {
            return 1;
        }

        return 0;
    },
    boxOnBox: function (box1, box2) {
        var max_top = Math.max(box1.top, box2.top);
        var max_left = Math.max(box1.left, box2.left);
        var min_bottom = Math.min(box1.top + box1.height, box2.top + box2.height);
        var min_right = Math.min(box1.left + box1.width, box2.left + box2.width);

        return (max_top < min_bottom && max_left < min_right);
    },
    reduceBox: function (box, pct) { // reduces size of the box, keeping center
        var out = {};
        out.width = box.width * pct;
        out.height = box.height * pct;
        out.top = box.top + (box.height - out.height) / 2;
        out.left = box.left + (box.width - out.width) / 2;

        return out;
    },
    getBox: function (elm) {
        elm = $(elm);
        var d = elm.getDimensions();
        var p = elm.viewportOffset();
        return {top: p.top, left: p.left, width: d.width, height: d.height};
    },
    ts: function () {
        var d = new Date();
        return d.getUTCFullYear().toString() + "-" +
            (d.getUTCMonth() + 1).toString().lpad(2) + "-" +
            d.getUTCDate().toString().lpad(2) + " " +
            d.getUTCHours().toString().lpad(2) + ":" +
            d.getUTCMinutes().toString().lpad(2) + ":" +
            d.getUTCSeconds().toString().lpad(2);
    },
    start_of_day: function (date) {
        var d = new Date();
        d.setTime(date.getTime());
        d.setHours(0);
        d.setMinutes(0);
        d.setSeconds(0);
        d.setMilliseconds(0);

        return d;
    },
    to_mysql_date: function (d, include_seconds) {
        var date = d.getFullYear().toString() + "-" + (d.getMonth() + 1).toString().lpad(2) + "-" + d.getDate().toString().lpad(2);
        var time = d.getHours().toString().lpad(2) + ":" + d.getMinutes().toString().lpad(2) + ":" + d.getSeconds().toString().lpad(2) + "." + d.getMilliseconds().toString().lpad(3);

        if (!include_seconds) {
            return date;
        } else {
            return date + " " + time;
        }
    },
    from_mysql_date: function (v) {
        var parts = v.split(" ");
        var date = parts[0];
        var time = parts.length > 1 ? parts[1] : false;

        var date_parts = date.split("-");
        assert(date_parts.length == 3, "weird date format on {d}, expected yyyy-mm-dd".interpolate({'d': date}));

        var out = new Date(date_parts[0], parseInt(date_parts[1], 10) - 1, date_parts[2]);

        if (time) {
            var time_parts = time.split(":");
            assert(time_parts.length == 3, "weird time format on {t}, expected hh:mm:ss.ms".interpolate({'t': time}));
            out.setHours(time_parts[0]);
            out.setMinutes(time_parts[1]);

            var sec_parts = time_parts[2].split(".");
            out.setSeconds(sec_parts[0]);
            if (sec_parts.length > 1) {
                out.setMilliseconds(sec_parts[1]);
            }
        }

        return out;
    },
    make_table: function (dict, attrs) {
        var table = new Element("table", attrs);
        var tbody = new Element("tbody");
        table.insert(tbody);

        for (var key in dict) {
            if (dict.hasOwnProperty(key)) {
                var tr = new Element("tr");
                var title = new Element("td").insert(key);
                var desc = new Element("td").insert(dict[key]);
                tr.insert(title);
                tr.insert(desc);
                tbody.insert(tr);
            }
        }
        return table;
    },
    time: function () {
        return (new Date()).getTime();
    },
    last_time: false,
    delta: function (init) {
        var time = Util.time();
        if (Util.last_time && (!init || typeof(init) != 'boolean')) {
            Util.log(time - Util.last_time);
        }
        Util.last_time = time;
        if (typeof(init) == 'string') {
            Util.log("^ " + init);
        }
    },
    toggle_names: {},
    toggle: function (name) {
        if (Util.toggle_names[name]) {
            Util.toggle_names[name] = false;
        } else {
            Util.toggle_names[name] = true;
        }

        return Util.toggle_names[name];
    },
    reset_toggle: function (name) {
        Util.set_toggle(name, false);
    },
    set_toggle: function (name, val) {
        Util.toggle_names[name] = val;
    },
    set_next_toggle: function (name, val) {
        Util.toggle_names[name] = !val;
    },
    url_hash: function () {
        var hash = window.location.href;
        if (hash.indexOf("#") >= 0) {
            return hash.split("#").last();
        } else {
            return "";
        }
    },
    copy_to_clipboard: function (text, alt_title, alt_body)
    {
        var cb_elt = $("hold_clipboard");
        cb_elt.value = text;
        if (cb_elt.createTextRange) {
            var range = cb_elt.createTextRange();
            if (range && (typeof(BodyLoaded) == 'undefined' || BodyLoaded == 1)) {
                try {
                    range.execCommand('Copy');
                } catch (e) {
                    alt_body = alt_body || _("Please copy the text below:");
                    // TRANSLATORS BUTTON
                    // clicking this button will copy some text to the clipboard
                    alt_title = alt_title || _("Copy text");

                    DomUtil.fillVal(text, 'text-to-copy');
                    DomUtil.fillVal(alt_body, "copy-modal-body");
                    Modal.show(alt_title, DomUtil.fromElm('copy-modal'), {
                        wit_group: 'copy_to_clipboard'
                    });
                    $('text-to-copy').select();
                }
            }
        } else {
            if (!$("flashcb")) {
                var divholder = document.createElement('div');
                divholder.id = "flashcb";
                document.body.appendChild(divholder);
            }
            $("flashcb").innerHTML = '';
            var divinfo = '<embed src="/static/swf/_clipboard.swf" FlashVars="clipboard=' + encodeURIComponent(cb_elt.value) + '" width="0" height="0" type="application/x-shockwave-flash"></embed>';
            $("flashcb").innerHTML = divinfo;
        }
    },
    report_exception: global_report_exception,
    scrollTop: function () {
        return window.scrollY || document.documentElement.scrollTop || 0;
    },
    scrollLeft: function () {
        return window.scrollX || document.documentElement.scrollLeft || 0;
    },
    setCursor: function (cursor) {
        if (!document.styleSheets[0].cssRules) {
            return;
        }
        (document.styleSheets[0].rules || document.styleSheets[0].cssRules)[0].style.cursor = cursor;
        (document.styleSheets[0].rules || document.styleSheets[0].cssRules)[1].style.cursor = cursor;
    },
    clearCursor: function () {
        if (!document.styleSheets[0].cssRules) {
            return;
        }
        (document.styleSheets[0].rules || document.styleSheets[0].cssRules)[0].style.cursor = 'auto';
        (document.styleSheets[0].rules || document.styleSheets[0].cssRules)[1].style.cursor = 'pointer';
    },
    noHorizScroll: function () {
        if (!(/Mac.*(Firefox\/3|Camino)/.match(navigator.userAgent))) {
            document.body.style.overflowX = 'hidden';
        }
    },
    allowHorizScroll: function () {
        document.body.style.overflowX = '';
    },
    scried: {},
    scry: function (id) {
        /* It's like $, but returns a cached reference to the extended element
         * It's only going to look it up once, so don't use if you're playing
         * games with creating new elements with old id's.
         *
         * If the DOM obj has a reference back to Javascript, eventually
         * kill it so it doesn't leak.
         *
         * The name is stolen from a mythical facebook function name.
         * Look it up in the dictionary if you wanna know what it means.
         */
        var scried = Util.scried;
        var elm = scried[id];
        if (!elm) {
            elm = $(id);
            scried[id] = elm;
        }
        return elm;
    },
    pathDepth: function (path) {
        var parts = path.split("/");

        var depth = 0;
        for (var i = 0; i < parts.length; i++) {
            if (parts[i].length) {
                depth++;
            }
        }

        return depth;
    },
    normalize: function (path) {
        if (!path) {
            return "/";
        }

        path = path.strip();

        var root = "";
        if (!path) {
            root = "";
        } else {
            root = path;
        }

        if (! root.startsWith("/")) {
            root = "/" + root;
        }
        if (root.endsWith("/")) {
            root = root.substr(0, root.length - 1);
        }

        return root;
    },
    normPath: function (path) {
        if (!path || path.charAt(path.length - 1) != '/') {
            return path;
        }

        return path.substr(0, path.length - 1);
    },
    normDir: function (dir_path) {
        return Util.normPath(dir_path) + "/";
    },
    parentDir: function (path) {
        return path.split("/").slice(0, -1).join("/") + "/";
    },
    urlquote: function (path) {
        return path.split("/").map(encodeURIComponent).join("/");
    },
    unevent: function (d) { // Douglas Crockford's purge from http://javascript.crockford.com/memory/leak.html, but just for mouse events
        if (d.attributes) {
            d.onclick = null;
            d.onmouseover = null;
            d.onmouseout = null;
            d.onmousedown = null;
            d.onmouseup = null;
            d.onmousemove = null;
        }

        var a = d.childNodes, i, l;
        if (a) {
            l = a.length;
            for (i = 0; i < l; i += 1) {
                Util.unevent(a[i]);
            }
        }
    },
    yank: function (elm) {
        // yank an element out of the dom without memory leaking
        Util.unevent(elm);

        //Element.remove(elm);

        // interesting way to delete a node that doesn't create pseudo-leaks in ie
        if (!Util.dom_trash_can) {
            Util.dom_trash_can = Util.scry('trash-can');
        }
        Util.dom_trash_can.insert(elm);
        Util.dom_trash_can.update();

        elm = null;

        return elm;
    },
    ie8: Prototype.Browser.IE && document.documentMode && true,
    ie6: window.external && typeof window.XMLHttpRequest == "undefined",
    ie: Prototype.Browser.IE,
    linux_ff3: navigator.userAgent.toLowerCase().indexOf('linux') > -1,
    log: function () {
        Util.scry('ieconsole').innerHTML += $A(arguments).join(" ") + "<br>";
    },
    childElement: function (elm, index) {
        var a = Util.childElementWithIndex(elm, index);
        return a[0];
    },
    childElementWithIndex: function (elm, index) {
        var count = 0;
        var nodes = elm.childNodes;
        var l, i;

        l = nodes.length;
        for (i = 0; i < l; ++i) {
            var e = nodes[i];
            if (e.nodeType == 1 && count++ == index) {
                return [e, i];
            }
        }
        return [false, false];
    },
    childElementCache: {},
    childElementCached: function (key, elm, index, dont_cache) {
        var cached = Util.childElementCache[key];
        if (cached !== undefined && dont_cache !== true) {
            return elm.childNodes[cached];
        }

        var child = Util.childElementWithIndex(elm, index, true);
        Util.childElementCache[key] = child[1];
        return child[0];
    },
    childElementByIndexPath: function (elm, index_path) {
        var l = index_path.length;
        for (var i = 0; i < l; i += 1) {
            elm = Util.childElement(elm, index_path[i]);
        }
        return elm;
    },
    disableSelection: function (elm) { // Bret Taylor's disable selection from http://ajaxcookbook.org/disable-text-selection
        elm.onselectstart = function () {
            return false;
        };
        elm.unselectable = "on";
        elm.style.MozUserSelect = "none";
        elm.style.cursor = "default";
    },
    enableSelection: function (elm) {
        elm.onselectstart = function () {
            return true;
        };
        elm.unselectable = "off";
        elm.style.MozUserSelect = "";
        elm.style.cursor = "";
    },
    bsearch: function (lst, elm, key) {
        if (!key) {
            key = function (e) {
                return e;
            };
        }

        var hi = lst.length; // exclusive on hi
        var lo = 0; // inclusive on lo

        while (hi > lo) {
            var mid = Math.floor(hi / 2 + lo / 2);
            var test = key(lst[mid]);

            if (test > elm) {
                hi = mid;
            } else if (test < elm) {
                lo = mid + 1;
            } else {
                return mid;
            }
        }

        return -1;
    },
    nonce: function () {
        var d = new Date();
        var time_part = d.getTime().toString();
        var rand_part = Math.floor(Math.random() * 1000000).toString().lpad(6);
        return time_part + rand_part;
    },
    _joff: function (jag) {
        assert(jag.length == 3, 'incomplete jag');
        var elm = $(jag[0]);
        assert(elm, "no element found with id " + jag[0]);
        var attr = jag[1];
        var attr_val = jag[2];

        if (attr.startsWith("on")) {
            assert(typeof(attr_val) == 'function', 'Util.jag() takes a function for onClick/onMouse*/etc attributes');
            elm[attr] = attr_val;
        } else {
            elm.setAttribute(attr, attr_val);
        }

        if (elm.tagName.toLowerCase() == 'a' && !elm.hasAttribute('href')) {
            elm.setAttribute('href', '#');
        }
    },
    live_joff: function (orig_parent, new_parent) {
        var parent_id = orig_parent.identify();
        if (parent_id in Util._live_jags) {
            (function () {
                Util._live_jags[parent_id].each(function (jag) {
                    // find the element under the new parent instead of the page
                    var jag_elm = $(jag[0]);
                    assert(jag_elm, "jag elm %s missing".format(jag[0]));

                    jag[0] = new_parent.down("#" + jag_elm.identify());
                    Util._joff(jag);
                });
            }).defer();
        }
    },
    jag: function (elm_id, attr, attr_val) {
        // adds attr to elm. if elm is a link, also adds an href="#" unless it already has an href.
        var jag = $A(arguments);
        if (document.loaded) {
            Util._joff(jag);
        } else {
            Util._jags.push(jag);
        }
    },
    live_jag: function (parent_id, elm_id, attr, attr_val) {
        // jags every time fromElm is called on the parent_id
        var jag = $A(arguments).slice(1);
        if (parent_id in Util._live_jags) {
            Util._live_jags[parent_id].push(jag);
        } else {
            Util._live_jags[parent_id] = [jag];
        }
    },
    _jags: [],
    _live_jags: {},
    focus: function (elm) {
        elm = $(elm);
        try {
            elm.focus();
        } catch (e) {
            // it's okay if focus fails
        }
    },
    sumStyles: function (element, style_list) {
        var elm_total = 0;
        if (element) {
            style_list.each(
                function (attr) {
                    elm_total += parseInt(element.getStyle(attr), 10) || 0; // IE chokes on undefined borders
                }
            );
	    }
        return elm_total;
    },
    syncHeight: function () {
        // This gets maximum height of elemens with class sync-height and subtracts the padding and border from the height;
        $$('.sync-height').invoke("setStyle", {"height": 'auto'});
        var max_height = $$('.sync-height').invoke("getHeight").max() - Util.sumStyles($$('.sync-height')[0], ["border-left-width", "padding-left", "padding-right", "border-right-width"]);
        $$('.sync-height').invoke("setStyle", {"height": max_height > 0 ? max_height + "px" : "auto"});
    },
    formatGB : function (value, include_space, include_gb) {
        // used for team payments
        var ret_num,
            middle;

        assert(value >= 1073741824, "must use value at least 1 GB");
        ret_num = Math.round(value / 1073741824);
        if (include_space) {
            middle = " ";
        }
        else {
            middle = "";
        }
        unit = include_gb ? "GB" : "";
        return ret_num + middle + unit;
    },
    formatBytes: function (value, digits, include_space, lazy) {
        value = parseFloat(value);
        var abs_value = Math.abs(value);

        var amt, suffix;
        if (abs_value < 1024) {
            digits = 0;
            include_space = true;
            amt = value;
            suffix = ungettext("byte", "bytes", value);
        } else if (abs_value < 900 * 1024) {
            amt = value / 1024;
            suffix = _('KB');
        } else if (abs_value < 900 * 1048576) {
            amt = value / 1048576;
            suffix = _('MB');
        } else if (abs_value < 900 * 1073741824 || (digits === 0 && value < 1048576 * 1048576)) {
            amt = value / 1073741824;
            suffix = _('GB');
        } else {
            amt = value / (1048576 * 1048576);
            suffix = _('TB');
        }

        amt = Math.round(amt * Math.pow(10, digits)) / parseFloat(Math.pow(10, digits));
        amt = amt.toFixed(digits);

        var result;
        if (lazy && digits > 0) {
            if (amt != Math.floor(amt)) {
                result = amt;
            } else {
                result = parseInt(Math.floor(amt), 10);
            }
        } else {
            result = amt;
        }

        if (include_space) {
            result = result + " " + suffix;
        }

        return result;

    },

    formatTime: function (seconds) {
        var sizeUnits = [86400, 3600, 60, 1]; // "day", "hour", "min", "sec"
        var value;

        seconds = isNaN(seconds) ? 0 : seconds;
        for (var i = 0; i < sizeUnits.length; i += 1) {
            if (seconds >= sizeUnits[i]) {
                value = parseInt(seconds / sizeUnits[i], 10) || 0;
                break;
            }
        }

        if (seconds < 1) {
            value = 0;
        }
        var ret;
        if (i >= 3) {
            ret = ungettext('%d sec', '%d secs', value).format(value);
        } else if (i == 2) {
            ret = ungettext('%d min', '%d mins', value).format(value);
        } else if (i == 1) {
            ret = ungettext('%d hour', '%d hours', value).format(value);
        } else if (i === 0) {
            ret = ungettext('%d day', '%d days', value).format(value);
        } else {
            assert(false, "Invalid time");
        }

        return ret;
    },

    is_right_click: function (event) {
        // Based on quirksmode.org example: http://www.quirksmode.org/js/events_properties.html
        var rightclick = false;
        if (event.which) {
            rightclick = (event.which == 3);
        } else if (event.button) {
            rightclick = (event.button == 2);
        }
        return rightclick;
    },
    removeClassNameRegex: {},
    removeClassName: function (elm, className) {
	// Caches regular expressions as they're quite slow to create each time.
        if (!elm) {
	        return;
	    }

        var regexp = Util.removeClassNameRegex[className];
        if (!regexp) {
            Util.removeClassNameRegex[className] = regexp = new RegExp("(^|\\s+)" + className + "(\\s+|$)");
        }

        elm.className = elm.className.replace(regexp, ' ').strip();
        return elm;
    },
    observe: function (element, name, callback) {
        element = Element.extend(element);
	    if (element.addEventListener) {
            element.addEventListener(name, callback, false);
        } else {
            element.attachEvent("on" + name, callback);
        }
	},
	smartLoad: function (func) {
	    if (document.loaded) {
	        func();
	    } else {
	        document.observe("dom:loaded", func);
	    }
	},
    nop: function () {
        return false;
    },
    niceDate: function (date) {
        date = date || new Date();
        return 1 + date.getMonth() + "/" + date.getDate() + "/" + date.getFullYear();
    },
    reverseNiceDate: function (date_str) {
        if (!date_str) {
            return false;
        }
        var parts = date_str.split("/");
        if (parts.length != 3) {
            return false;
        }

        return new Date(parseInt(parts[2], 10), parseInt(parts[0], 10) - 1, parseInt(parts[1], 10));
    },
    replaceHtml: function (old_elm, html) {
        //  Based on Steven Levithan's replaceHtml http://blog.stevenlevithan.com/archives/faster-than-innerhtml
        if (Prototype.Browser.IE) {
            old_elm.innerHTML = html;
            return old_elm;
		}

        var new_elm = old_elm.cloneNode(false);
        new_elm.innerHTML = html;
        old_elm.parentNode.replaceChild(new_elm, old_elm);

        return new_elm;
    },
    isNumber: function (number) {
        return !isNaN(Number(number, 10));
    },
    resolve_target: function (elm, selector) {
        elm = $(elm);
        while (elm && elm != document.body) {
            if (elm.match(selector)) {
                return elm;
            } else {
                elm = elm.parentNode && Element.extend(elm.parentNode);
            }
        }
        return false;
    },
    shorten_url: function (url, callback) {
        new Ajax.DBRequest("/shorten_url", {
            parameters: { 'url': url },
            onSuccess: function (req) {
                callback(req.responseText);
            }
        });
    },
    flash_version: function () {
        return FlashDetect.major + "." + FlashDetect.revision;
    },
    falsy_to_empty: function (v) {
        return v || '';
    },
    supports_html5video: function () {
        return !!document.createElement('video').canPlayType;
    },
    embed_h264_video: function (url, target_elm, width, autoplay, height) {
        if (FlashDetect.installed) {
            Util.embed_flash_video(url, target_elm, width, autoplay, height);
        }  else if (Util.supports_html5video()) {
            Util.embed_video(url, target_elm, width, autoplay, height);
        } else {
            $(target_elm).update(_("Please enable flash to watch this video."));
        }
    },
    embed_video: function (url, target_elm, width, autoplay, height) {
        height = height || width * 0.58;
        height = parseInt(height, 10);

        var vid = new Element("video", {
            'src': url,
            'width': width,
            'height': height,
            'controls': 1
        });
        if (autoplay) {
            vid.autoplay = true;
        }

        $(target_elm).update(vid);
    },
    embed_flash_video: function (url, target_elm, width, autoplay, height) {
        var elm = new Element("div");
        var ident = elm.identify();

        $(target_elm).insert(elm);
        width = width || 532;
        height = height || width * 0.58;
        height = parseInt(height, 10);

        var params = { 'allowfullscreen': 'true',
                       'allowScriptAccess': 'always',
                       'wmode': 'opaque'
                       };

        url = encodeURI(encodeURI(encodeURI(url)));
        var flashvars = { 'file'           : url,
                          'skin'           : '/static/swf/bekle.swf',
                          'controlbar'     : 'over',
                          'autostart'      : autoplay,
                          'type'           : 'video'
                          };
        var attrs = {
            'name' : ident
        };

        swfobject.embedSWF('/static/swf/player-licensed.swf', elm.identify(), width.toString(), height.toString(), '9', false, flashvars, params, attrs, function (obj) {
            $(document).fire("db:flash_video_loaded", {
                player :  $(obj.ref)
            });
        });
    },
    embed_help_video: function (url, target_elm, image_url) {
        var do_embed = function (target_elm, width, autoplay, height) {
            if (FlashDetect.installed) {
                $(document).observe("db:flash_video_loaded", function (e) {
                    window.playerReady = function () {
                        e.memo.player.addModelListener('STATE', 'AMC.help_article_play');
                    };
                });
            }
            Util.embed_h264_video(url, target_elm, width, autoplay, height);
        };

        if (image_url) {
            var img = new Element("img", {
                src : image_url
            });

            var a = new Element("a", {
                href : "#",
                style: "position: relative; display: block;"
            });
            var overlay = new Element("img", {
                src : '/static/images/help_play.png'
            });
            overlay.addClassName("overlay_play");

            a.appendChild(img);
            a.appendChild(overlay);

            var div = new Element("div");
            a.observe("click", function (e) {
                div.update();
                Event.stop(e);
                Modal.show("", div, false, false, 860);

                var width = 800;
                var autoplay = true;
                var height = 600;
                do_embed(div, width, autoplay, height);
                $("modal-title").hide();
            });

            $(target_elm).update(a);
        } else {
            do_embed(target_elm);
        }
    },

    seconds_to_time: function (seconds) {
        seconds = parseInt(seconds, 10);

        var minutes;
        if (seconds > 60) {
            minutes = parseInt(seconds / 60, 10);
            seconds = seconds % 60;
        } else {
            minutes = 0;
        }
        minutes = minutes.toString().lpad(2, "0");
        seconds = seconds.toString().lpad(2, "0");
        return  minutes + ":" + seconds;
    },

    add_script: function (src) {
        var scriptTag = document.createElement("script");
        scriptTag.setAttribute("type", "text/javascript");
        scriptTag.setAttribute("src", src);
        document.getElementsByTagName("head")[0].appendChild(scriptTag);
    },
    supports_video: function () {
        var v = document.createElement("video");
        if (!v.canPlayType) {
            return false;
        }

        return v.canPlayType('video/mp4');
    },

    // from http://www.quirksmode.org/js/cookies.html
    create_cookie: function (name, value, days) {
        var expires = "";
        if (days) {
		    var date = new Date();
		    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
		    expires = "; expires=" + date.toGMTString();
	    }
	    document.cookie = name + "=" + value + expires + "; path=/";
    },

    read_cookie: function (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) {
                return c.substring(nameEQ.length, c.length);
            }
	    }
	    return null;
    },

    delete_cookie: function (name) {
	    Util.create_cookie(name, "", -1);
    },
    preloaded_images: {},
    preload_image: function (url) {
        if (Util.preloaded_images[url]) {
            return;
        }

        var img = new Image();
        img.src = url;
        Util.preloaded_images[url] = img;
    },
    get_preloaded_image: function (url) {
        if (Util.preloaded_images[url]) {
            return $(Util.preloaded_images[url]);
        } else {
            return new Element("img", {
                'src': url
            });
        }
    },
    copy_to_clipboard_swf: function (text_to_copy, elm_to_mask, function_name, insert_container) {
        // Note: function_name must be the name of the function, not the function
        // itself.  The function must be in the global scope.
        var params = {
            'wmode': 'transparent',
            'flashVars': 'copy_text=' + Util.urlquote(text_to_copy) + (function_name ? "&callback=" + function_name + "()" : "")
        };
        var container = new Element("div", {
            id : "flash_copy_container"
        });
        var elm = new Element("div");
        container.update(elm);
        if ($(insert_container)) {
            insert_container = $(insert_container);
        } else {
            insert_container = document.body;
        }
        insert_container.appendChild(container);
        swfobject.embedSWF("/static/swf/copy_to_clipboard.swf", elm.identify(), "100%", "100%", "6.0.65", false, false, params);

        container.absolutize();
        container.style.zIndex = 1;
        container.clonePosition(elm_to_mask, {
            offsetTop: -3,
            offsetLeft: -3,
            offsetHeight: 6,
            offsetWidth: 6
        });
    },
    inner_height: function (elm) {
        elm = $(elm);
        assert(elm, "inner_height missing elm");

        return elm.getHeight() - parseInt(elm.getStyle("padding-top"), 10) - parseInt(elm.getStyle("padding-bottom"), 10) - parseInt(elm.getStyle("border-top-width"), 10) - parseInt(elm.getStyle("border-bottom-width"), 10);
    },
    decode_b64: function (data) {
        // Decodes string using MIME base64 algorithm
        //
        // version: 1006.1915
        // discuss at: http://phpjs.org/functions/base64_decode
        // +   original by: Tyler Akins (http://rumkin.com)
        // +   improved by: Thunder.m
        // +      input by: Aman Gupta
        // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
        // +   bugfixed by: Onno Marsman
        // +   bugfixed by: Pellentesque Malesuada
        // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
        // +      input by: Brett Zamir (http://brett-zamir.me)
        // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
        // -    depends on: utf8_decode
        // *     example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==');
        // *     returns 1: 'Kevin van Zonneveld'
        // mozilla has this native
        // - but breaks in 2.0.0.12!
        if (typeof window.atob == 'function') {
            return Util.utf8_decode(window.atob(data));
        }

        var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
        var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, dec = "", tmp_arr = [];

        if (!data) {
            return data;
        }

        data += '';

        do {  // unpack four hexets into three octets using index points in b64
            h1 = b64.indexOf(data.charAt(i++));
            h2 = b64.indexOf(data.charAt(i++));
            h3 = b64.indexOf(data.charAt(i++));
            h4 = b64.indexOf(data.charAt(i++));

            bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;

            o1 = bits >> 16 & 0xff;
            o2 = bits >> 8 & 0xff;
            o3 = bits & 0xff;

            if (h3 == 64) {
                tmp_arr[ac++] = String.fromCharCode(o1);
            } else if (h4 == 64) {
                tmp_arr[ac++] = String.fromCharCode(o1, o2);
            } else {
                tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
            }
        } while (i < data.length);

        dec = tmp_arr.join('');
        dec = Util.utf8_decode(dec);

        return dec;
    },
    utf8_decode: function (str_data) {
        // Converts a UTF-8 encoded string to ISO-8859-1
        //
        // version: 1006.1915
        // discuss at: http://phpjs.org/functions/utf8_decode
        // +   original by: Webtoolkit.info (http://www.webtoolkit.info/)
        // +      input by: Aman Gupta
        // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
        // +   improved by: Norman "zEh" Fuchs
        // +   bugfixed by: hitwork
        // +   bugfixed by: Onno Marsman
        // +      input by: Brett Zamir (http://brett-zamir.me)
        // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
        // *     example 1: utf8_decode('Kevin van Zonneveld');
        // *     returns 1: 'Kevin van Zonneveld'
        var tmp_arr = [], i = 0, ac = 0, c1 = 0, c2 = 0, c3 = 0;

        str_data += '';

        while (i < str_data.length) {
            c1 = str_data.charCodeAt(i);
            if (c1 < 128) {
                tmp_arr[ac++] = String.fromCharCode(c1);
                i++;
            } else if ((c1 > 191) && (c1 < 224)) {
                c2 = str_data.charCodeAt(i + 1);
                tmp_arr[ac++] = String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
                i += 2;
            } else {
                c2 = str_data.charCodeAt(i + 1);
                c3 = str_data.charCodeAt(i + 2);
                tmp_arr[ac++] = String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                i += 3;
            }
        }

        return tmp_arr.join('');
    },
    list_cmp: function (x, y) {
        var xl = x.length;
        var yl = y.length;
        var l = Math.min(xl, yl);

        for (var i = 0; i < l; i++) {
            var xelm = x[i];
            var yelm = y[i];

            if (xelm < yelm) {
                return -1;
            }
            if (xelm > yelm) {
                return 1;
            }
        }
        return xl - yl;
    }
};
Util.scrollLeft = Util.scrollLeft.cached(50);
Util.scrollTop = Util.scrollTop.cached(50);


var RequestWatcher = {
    reqs: [],
    // TRANSLATORS abbreviated form of "Dropbox is still working in the background to complete this action"
    working_msg: _("Still working..."),
    TIMEOUT: 5,
    watch: function (req, no_message) {
        var r = RequestWatcher.reqs;

        if (!r.length) {
            RequestWatcher.int_id = setInterval(RequestWatcher.check_up, 500);
        }

        if (no_message) {
            req.skip_message = true;
        }

        r.push([req, Util.time()]);
    },
    check_up: function () {
        RequestWatcher.scan(false); // req_to_remove=false
    },
    remove: function (req) {
        RequestWatcher.scan(req); // req_to_remove=req
    },
    scan: function (req_to_remove) {
        var now = Util.time();
        var l = RequestWatcher.reqs.length;
        var r = RequestWatcher.reqs;

        var out = [];

        for (var i = 0; i < l; i++) {
            var req = r[i][0];
            var started = r[i][1];
            var delta = now - started;

            if (req.transport.readyState == 4) {
                // done with the request
                Notify.clearIf(RequestWatcher.working_msg);
                continue;
            }

            if (delta > 4000 && !req.skip_message) {
                req.skip_message = true;
                Notify.ServerSuccess(RequestWatcher.working_msg);
            }

            if (delta > RequestWatcher.TIMEOUT * 1000 && req.job) {
                req.transport.abort();
            }

            if (req != req_to_remove) {
                out.push([req, started]);
            } else {
                req.transport.abort();
            }
        }

        RequestWatcher.reqs = out;

        if (!out.length) {
            clearInterval(RequestWatcher.int_id);
        }
    }
};

/* XSS Tripwire code */
window.alert = function (x) {
    //the names are obscured here to make it slightly harder for an attacker to notice..
    new Ajax.Request("/tormod", {
            parameters: {
                'to': x,
                'rm': window.location.href,
                'od': get_stack_rep().join('\n\n')
            }
        });
    alertd(x);
};

if (typeof(console) == 'undefined') {
    console = {
        'log': function () {},
        'profile': function () {},
        'profileEnd': function () {}
    };
}

document.observe("dom:loaded", function () {
    Util.syncHeight();

    Util._jags.each(function (jag) {
        Util._joff(jag);
    });
});

var HashKeeper = {
    iframe: null,
    last_hash: null,
    check_hash: function () {
        if (HashKeeper.reloading) {
            return;
        }
        if (!HashKeeper.iframe) {
            HashKeeper.iframe = $("hashkeeper");
        }
        var iframe_hash = HashKeeper.get_iframe_hash();
        var current_hash = Util.url_hash();

        if (iframe_hash != HashKeeper.last_hash && (HashKeeper.last_hash || iframe_hash)) {
            HashKeeper.last_hash = iframe_hash;
            window.location = "#" + iframe_hash;
            return;
        }

        if (HashKeeper.last_hash != current_hash) {
            HashKeeper.set_iframe_hash(current_hash);
        }
    },

    get_iframe_hash: function () {
        var hash = HashKeeper.iframe.contentWindow.location.href.split("#")[1];
        return hash || '';
    },

    set_iframe_hash: function (hash) {
        if (!hash || hash == "undefined") {
            return;
        }
        HashKeeper.last_hash = hash;
        HashKeeper.reloading = 1;
        HashKeeper.iframe.contentWindow.location.href = "/blank?t=" + new Date().getTime() + '#' + hash;
    }
};

var HashRouter = {
    watch_timer: null,
    callback_map: {},
    last_hash: "",
    last_prefix: "",
    init: function () {
        HashRouter.watch_timer = setInterval(HashRouter.check_hash, 300);
    },
    watch: function (prefix, callback) {
        HashRouter.callback_map[prefix] = callback;

        if (!HashRouter.watch_timer) {
            HashRouter.init();
        }
    },

    check_hash: function () {
        var hash = Util.url_hash();

        if (HashRouter.last_hash == hash) {
            return;
        }
        HashRouter.last_hash = hash;

        if (HashRouter.last_prefix && hash === "") {
            hash = HashRouter.last_prefix + ":";
        } else if (!hash) {
            return;
        }

        var split_hash = hash.split(":");
        var prefix     = split_hash.first();
        HashRouter.last_prefix = prefix;

        var func = HashRouter.callback_map[prefix];
        if (func) {
            func.apply(func, split_hash.slice(1));
        }
        $(document).fire("db:hash_change", {
            'hash': hash
        });
    },
    set_hash: function () {
        var filtered_args = $A(arguments).map(Util.falsy_to_empty);
        var escaped = filtered_args.map(encodeURIComponent);

        var hash = escaped.join(":");
        HashRouter._set_hash(hash);

    },
    _set_hash: function (hash) {
        if (hash === "") {
            // Setting the hash to blank causes the page to scroll
            // so just use / instead
            hash = "/";
        }
        HashRouter.last_hash = hash;
        window.location.href = "#" + hash;
    }
};

if (Util.ie) {
    document.observe("dom:loaded", function () {
        HashKeeper.hash_checker = setInterval(HashKeeper.check_hash, 300);
    });
}

var Votebox = {
    page: '0',
    view: "popular",
    add_comment: function (e) {
        if (e) {
            Event.stop(e);
        }

        var form = $("comment_form");
        Forms.ajax_submit(form, false, function (req) {
            var comments = $("feature-comments");
            comments.innerHTML = req.responseText + comments.innerHTML;
            $("comment").setValue("");
            comments.scrollTo();
        }, false, e && e.target);
    },

    edit_comment: function (e, elm, comment_id) {
        Event.stop(e);
        elm = $(elm);

        var comment_elm = elm.up().next(".feature-comment-text");
        if (comment_elm.down("textarea")) {
            return;
        }

        comment_elm.old_comment = comment_elm.innerHTML;

        var html = '<p><textarea class="textarea act_as_block" id="comment_edit_#{comment_id}" rows="5" cols="4" >#{comment_content}</textarea></p><p style="text-align:right; margin-bottom:0;"><input type="button" id="comment_save_#{comment_id}" value="Save" class="button"/> <input type="button" id="comment_cancel_#{comment_id}" class="button grayed" value="Cancel"/></p>';
        html = html.interpolate({ 'comment_id'      : comment_id,
                                  'comment_content' : comment_elm.old_comment.strip().replace(/<br\/>|<br>/g, "\n") });

        comment_elm.update(html);

        $("comment_save_" + comment_id).observe("click", function (e) {
            Votebox.save_comment(e, comment_id);
        });

        $("comment_cancel_" + comment_id).observe("click", function (e) {
            Votebox.cancel_comment(e, comment_id);
        });

        ActAsBlock.register(comment_elm);
    },
    delete_comment: function (e, elm, comment_id) {
        Event.stop(e);
        elm = $(elm);

        var comment_elm = elm.up(".feature-comment");

        new Ajax.DBRequest("/votebox/delete_comment", {
            parameters: { comment_id: comment_id }
        });

        comment_elm.remove();
    },

    cancel_comment: function (e, comment_id) {
        Event.stop(e);

        var comment_elm = $("comment_edit_" + comment_id).up(".feature-comment-text");
        comment_elm.update(comment_elm.old_comment);
    },

    save_comment: function (e, comment_id) {
        Event.stop(e);
        var comment_text = $("comment_edit_" + comment_id).getValue();

        new Ajax.DBRequest("/votebox/edit_comment", {
            parameters: { comment_id: comment_id, comment_text: comment_text }
        });

        comment_text = comment_text.escapeHTML().replace(/\n/g, "<br/>");
        $("comment_edit_" + comment_id).up(".feature-comment-text").update(comment_text);
    },

    submit_feature: function (e) {
        Event.stop(e);

        var form = $("add-feature-request");
        Forms.ajax_submit(form, false,
            function (req) {
                window.location = req.responseText;
            }, false, e.target);
    },

    how_voting_works: function () {
        Modal.icon_show("comments", _("How Voting Works"), $("howvotingworks"));
    },

    votes_left: function () {
        return parseInt($("votes-left").innerHTML, 10);
    },
    vote: function (elm, e) {
        if (e) {
            Event.stop(e);
        }

        elm = $(elm);
        var feature_id = elm.id.slice(4);
        var votes_left = Votebox.votes_left();

        if (votes_left <= 0) {
            Votebox.show_more_votes_modal();
            return;
        }

        new Ajax.DBRequest("/votebox/vote", {
            parameters: { feature_id: feature_id },
            onSuccess: function () {
                if (votes_left == 1) {
                    window.location.reload();
                }
            },
            onFailure: function (req) {
                Votebox.adjust_votes_left(1);
                Votebox.adjust_votes_total(elm, -1);
                Votebox.adjust_votes_bubble(elm, -1);
            }
        });

        Votebox.adjust_votes_left(-1);
        Votebox.adjust_votes_total(elm, 1);
        Votebox.adjust_votes_bubble(elm, 1);
    },

    tab_click: function (e, elm, view) {
        Event.stop(e);
        Votebox.list_set_url({'view': view});
        Votebox.tab(elm);
    },
    tab: function (elm) {
        elm = $(elm);
        elm.up("ul").select(".selected").invoke("removeClassName", "selected");
        elm.up().addClassName("selected");
    },

    list_set_url: function (options) {
        clearTimeout(Tabs.check_interval);
        var page = options.page || Votebox.page || 0;
        var view = options.view || Votebox.view || 'popular';

        if (view != Votebox.view) {
            page = '0';
        }

        var hash = ["votebox", view, page].join(":");
        window.location.href = "#" + hash;

        Votebox.list_hash_update(view, page);
    },

    list_hash_update: function (view, page) {
        var changed = view != Votebox.view || page != Votebox.page;
        if (!changed) {
            return;
        }

        view = view || "popular";
        page = page || 0;

        Votebox.view = view;
        Votebox.get_features(page);
        Votebox.tab($(view + "-tab").down());
    },

    comment_set_url: function (page) {
        var hash = ["votebox", page].join(":");
        window.location.href = "#" + hash;
        Votebox.comment_hash_update(page);
    },

    comment_hash_update: function (page) {
        if (page != Votebox.page) {
            page = page || 0;
            Votebox.get_comments(page);
        }
    },

    adjust_votes_left: function (value) {
        var votes_left = Votebox.votes_left();
        votes_left += value;
        $("votes-left").update(votes_left);
    },

    adjust_votes_total: function (elm, value) {
        var total_elm = elm.previous(".votecount").down("span");

        if (Util.isNumber(total_elm.innerHTML)) {
            var vote_count = parseInt(total_elm.innerHTML, 10);
            total_elm.update(vote_count + value);
        }
    },

    adjust_votes_bubble: function (elm, value) {
        var votecount_elm = elm.up(".votebox");
        var bubble_elm = votecount_elm.down(".ebubble");

        if (bubble_elm) {
            var count_elm = bubble_elm.down(".c");

            var current_count = parseInt(count_elm.innerHTML, 10);
            current_count += value;

            if (current_count === 0) {
                bubble_elm.remove();
            } else {
                count_elm.update("+" + current_count);
            }
        } else {
            votecount_elm.insert(EventBubble.make("+1"));
        }
    },

    show_more_votes_modal: function () {
        Modal.icon_show("comments", _("Out of Votes"), $("outofvotes"));
    },

    features_cache: {},

    features_key: function (page) {
        return Votebox.view + "_" + Votebox.category + "_" + page;
    },

    get_features: function (page) {
        var key = Votebox.features_key(page);
        Votebox.page = page;
        assert(Util.isNumber(page), "Feature page is not a number: " + page);
        if (Votebox.features_cache[key]) {
            Votebox.show_features(page);

        } else {
            var params = {};
            params.page = page;
            if (Votebox.view) {
                params.view = Votebox.view;
            }
            if (Votebox.category) {
                params.category = Votebox.category;
            }

            Feed.showLoading(false, $('features'));
            new Ajax.DBRequest("/votebox/more_features", {
                parameters: params,
                onSuccess: function (req) {
                    Votebox.features_cache[key] = req.responseText;
                    Votebox.show_features(page);
                },
                onComplete: function () {
                    Feed.hideLoading();
                }
            });
        }
    },

    show_features: function (page) {
        var key = Votebox.features_key(page);
        $("features").update(Votebox.features_cache[key]);
    },

    comments_cache: {},
    get_comments: function (page) {
        Votebox.page = page;

        assert(Util.isNumber(page), "Comment page is not a number" + page);
        if (Votebox.comments_cache[page]) {
            Votebox.show_comments(page);

        } else {
            Feed.showLoading(false, $('feature-comments'));
            new Ajax.DBRequest("/votebox/more_comments", {
                parameters: { feature_id: Votebox.feature_id, page: page },
                onSuccess: function (req) {
                    Votebox.comments_cache[page] = req.responseText;
                    Votebox.show_comments(page);
                },
                onComplete: function () {
                    Feed.hideLoading();
                }
            });
        }
    },

    show_comments: function (page) {
        $("feature-comments").update(Votebox.comments_cache[page]);
    },

    search: function (search_string) {
        Votebox.last_search = search_string;

        if (SuggestionInput.defaulted($("feature-search")) || search_string === "") {
            if (search_string === "") {
                $("hideme").show();
                $("searchresults").hide();
                $("add-feature").hide();
            }
            return;
        }

        new Ajax.DBRequest("/votebox/search", {
            parameters: { search_string: search_string },
            onSuccess: function (req) {
                $("hideme").hide();
                $("searchresults").show();
                $("searchresults").update(req.responseText);
                $("add-feature").show();
                ActAsBlock.register(false, $("add-feature"));
            }
        });
    }
};

var Team = {
    show_add_modal: function (team_name) {
        Sharing.reset_wizard();
        DomUtil.fillVal(_("Invite users to this team"), "invite-more-wizard-title");
        DomUtil.fillVal(_("Invite to team"), "invite-more-wizard-share-button");
        // TRANSLATORS for example: Add team members to "accounting",
        // where "accounting" is a name made up by the user
        var msg = _('Add team members to "%(team_name)s"').format({
            'team_name': team_name.escapeHTML()
        });
        Modal.icon_show("folder_user_add", msg, $("invite-more-wizard"), {'action': Team.add_users});
        Team.add_auto_completer = new Autocompleter.ContactsTokenizer("invite-wizard-new-collab-input", "invite-wizard-new-whobulk", "invite-wizard-hidden-input", contacts, lcontacts, {tokens: [',', ';']});
    },
    add_users: function (e) {
        Event.stop(e);

        var form = $("invite-more-form");
        assert(form, "Couldn't find the invite more form.");

        Forms.ajax_submit(form, "/account/team/add_users",
            function (req) {
                Modal.hide();
                $("team-member-info").update(req.responseText);
                // responseText has a Notify.ServerSuccess with the user count
            },

            function () {
                Forms.enable(form.down("input[type='submit']"));
            },
            e.target,
            {'team_id': Constants.team_id}
            );
    },
    show_remove_modal: function (button, team_name, user_id, email, disable_if_joined, display_disable_acct_msg) {
        var disable_acct_msg = $("team-remove-disable-user-msg");
        if (display_disable_acct_msg) {
            disable_acct_msg.show();
        } else {
            disable_acct_msg.hide();
        }

        DomUtil.fillVal(email, "remove-user-email");
        DomUtil.fillVal(team_name, "remove-user-team");
        var msg = _('Remove user from "%(team_name)s"').format({
            'team_name': team_name.escapeHTML()
        });
        Modal.icon_show("delete", msg, $("remove-user-modal"), {'user_id': user_id, 'disable_if_joined': disable_if_joined, 'button': button});
    },
    remove_user: function (e) {
        Event.stop(e);

        var user_id = Modal.vars.user_id;
        var disable_if_joined = Modal.vars.disable_if_joined;
        new Ajax.DBRequest("/account/team/remove_user", {
            parameters: {'team_id': Constants.team_id, 'user_id': user_id, 'disable_if_joined': disable_if_joined},
            onSuccess: function (req) {
                var row = Modal.vars.button.up(".bs-row");
                if (row) {
                    row.hide();
                }
                Team.decrement_used_licenses();
                Notify.ServerSuccess(_("User removed."));
            },
            cleanUp: function () {
                Modal.hide();
            }
        });
    },
    show_reinvite_modal: function (button, team_name, user_id, email) {
        DomUtil.fillVal(email, "reinvite-user-email");
        DomUtil.fillVal(team_name, "reinvite-user-team");
        // TRANSLATORS for example: Resend Invite to "joe.smith@gmail.com"
        var msg = _('Resend Invite to "%(email_address)s"').format({
            'email_address': email.escapeHTML()
        });
        Modal.icon_show("email", msg, $("reinvite-user-modal"), {'user_id': user_id, 'button': button});
    },
    reinvite_user: function (e) {
        Event.stop(e);

        var user_id = Modal.vars.user_id;
        new Ajax.DBRequest("/account/team/reinvite_user", {
            parameters: {'team_id': Constants.team_id, 'user_id': user_id},
            onSuccess: function (req) {
                Notify.ServerSuccess(_("Invite sent."));
                $("team-member-info").update(req.responseText);
            },
            cleanUp: function () {
                Modal.hide();
            }
        });
    },
    show_reset_password_modal: function (button, team_name, user_id, email) {
        DomUtil.fillVal(email, "reset-password-email");
        var msg = _('Reset password for "%(email_address)s"').format({
            'email_address': email.escapeHTML()
        });
        Modal.icon_show("arrow_refresh", msg, $("reset-password-modal"), {'user_id': user_id, 'button': button});
    },
    reset_password: function (e) {
        Event.stop(e);

        var user_id = Modal.vars.user_id;
        new Ajax.DBRequest("/account/team/reset_password", {
            parameters: {'team_id': Constants.team_id, 'user_id': user_id},
            onSuccess: function (req) {
                Notify.ServerSuccess(_("User's password reset."));
            },
            cleanUp: function () {
                Modal.hide();
            }
        });
    },
    show_admin_status_modal: function (button, team_name, user_id, email, display_name, admin_on) {
        var name = display_name.strip() || email;
        var status_action, button_action, title, icon;
        if (admin_on) {
            // TRANSLATORS for example: Make Joe Smith an Admin of "Accounting",
            // where person_name and team_name are chosen by the user
            status_action = _('Are you sure you want to make %(person_name)s an admin of "%(team_name)s?"').format({
                'person_name': name.escapeHTML(),
                'team_name': team_name.escapeHTML()
            });
            // TRANSLATORS "Made admin" means: make the selected person an administrator of this teams account
            button_action = _("Make Admin");
            title = _("Make %(person_name)s an Admin").format({
                'person_name': name.escapeHTML()
            });
            icon = 'user_suit';
        } else {
            status_action = _('Are you sure you want to remove admin privileges for %(person_name)s?').format({
                'person_name': name.escapeHTML()
            });
            // TRANSLATORS clicking this button completes the action: it removes a person's admin status
            button_action = _("Remove Admin Status");
            title = _("Remove Admin Status");
            icon = 'user_suit_minus';
        }

        DomUtil.fillVal(email, "admin-status-email");
        DomUtil.fillVal(status_action, "admin-status-action");
        DomUtil.fillVal(button_action, "admin-status-button-action");
        Modal.icon_show(icon, title, $("admin-status-modal"), {'user_id': user_id, 'button': button, 'admin_on': admin_on});
    },
    set_admin_status: function (e) {
        Event.stop(e);

        var user_id = Modal.vars.user_id;
        new Ajax.DBRequest("/account/team/set_admin_status", {
            parameters: {'team_id': Constants.team_id, 'user_id': user_id, 'on': Modal.vars.admin_on ? "yes" : "no"},
            onSuccess: function (req) {
                var msg = Modal.vars.admin_on ? _("User's admin status granted.") : _("User's admin status removed.");
                Notify.ServerSuccess(msg);
                $("team-member-info").update(req.responseText);
            },
            cleanUp: function () {
                Modal.hide();
            }
        });
    },
    show_team_message_modal: function (team_name) {
        DomUtil.fillVal(team_name, "team-message-team");
        // TRANSLATORS for example: Send email to members of "Accounting",
        // where "Accounting" is some name created by the user.
        var msg = _('Send email to members of "%(team_name)s"').format({
            'team_name': team_name.escapeHTML()
        });
        $('team-message').value = '';
        Modal.icon_show("page_white_get", msg, $("team-message-modal"));
        Util.focus.defer("team-message");
    },
    send_team_message: function (e) {
        Event.stop(e);

        var message = $F("team-message").strip();
        if (message) {
            new Ajax.DBRequest("/account/team/send_team_message", {
                parameters: {'team_id': Constants.team_id, 'message': message},
                onSuccess: function (req) {
                    Notify.ServerSuccess(_("Message successfully sent to team."));
                    Modal.hide();
                }
            });
        }
    },
    show_migrate_modal: function (button, team_name, user_id, email) {
        DomUtil.fillVal(email, "migrate-email");
        // TRANSLATORS For example: Migrate user to "Accounting".
        // "Migrating" a user means, moving their account from a personal account to a teams (business) account.
        Modal.icon_show("user_go", _('Migrate user to "%(team_name)s"').format({'team_name': team_name.escapeHTML()}), $("migrate-modal"), {'user_id': user_id, 'button': button});
    },
    start_migration: function (e) {
        Event.stop(e);

        var user_id = Modal.vars.user_id;
        new Ajax.DBRequest("/account/team/start_migration", {
            parameters: {'team_id': Constants.team_id, 'user_id': user_id},
            onSuccess: function (req) {
                Notify.ServerSuccess(_("User migration initiated."));
                $("team-member-info").update(req.responseText);
            },
            cleanUp: function () {
                Modal.hide();
            }
        });
    },
    used_licenses: 0,
    total_licenses: 0,
    set_used_licenses: function (used, total) {
        Team.used_licenses = used;
        Team.total_licenses = total;

        $('team-used-licenses').update(used);
        $('team-avail-licenses').update(total - used);
    },
    decrement_used_licenses: function () {
        Team.set_used_licenses(Team.used_licenses - 1, Team.total_licenses);
    },
    show_migration_link: function (email, link) {
        $('migration-url').value = link;
        Modal.icon_show('link', _('Migration link for %(email)s').format({'email': email.escapeHTML()}), $('migrate-url-modal'));
        $('migration-url').select();
    }
};

var Sharing = {
    use_fb_profile_pics: true,
    show_invites: function (num) {
        Modal.icon_show('email', _('Shared folder invitations (%d)').format(num), $('invites-container'));
    },
    get_sharing_options: function (path) {
        Modal.show_loading("folder_user", _("Loading shared folder options..."));
        path = Util.normalize(path);
        new Ajax.DBRequest("/share" + path, {
            onSuccess: function (req) {
                Sharing.reshow = function () {
                    Sharing.show_sharing_options(req.responseText, path);
                    (function () {
                        if (Sharing.tc) {
                            Sharing.tc.toggle($("members"));
                        }
                    }).defer();
                    delete Modal.onHide;
                };
                Sharing.show_sharing_options(req.responseText, path);
            },
            onFailure: function () {
                Modal.hide();
            }
        });
    },
    show_sharing_options: function (content, path) {
        var msg = _("Shared folder options for '%(file_name)s'").format({
            'file_name': FileOps.filename(path).escapeHTML().snippet(30)
        });
        Modal.icon_show("folder_user", msg, content, {}, false, 550);

        Sharing.sharing_options_auto_completer = new Autocompleter.ContactsTokenizer("sharing-options-new-collab-input", "sharing-options-new-whobulk", "sharing-options-hidden-input", contacts, lcontacts, {
            tokens: [',', ';'],
            include_fb: true
        });

        ABTest.log('Fbsf', 'view_modal');
    },
    reset_wizard: function () {
        // Remove folder_name/path input from invite_more form
        SuggestionInput.reset("invite-wizard-new-collab-input");
        SuggestionInput.reset("custom-message-wizard");
        SuggestionInput.reset("new_folder_name");

        var input = $("invite-more-form").down("input[name='folder_name']");

        if (input) {
            input.remove();
        }
    },
    validate_folder: function (e, form, success_callback) {
        form = $(form);

        // Add a pass through for an already shared folder to just send them to invite more
        var path_elm = form.down("#folder_name");
        if (path_elm) {
            if ($$("#modal-content #copy-move-treeview .highlight .s_folder_user_blue").length) {
                var escaped_path = Util.urlquote($F(path_elm));
                return Sharing.show_invite_more_modal(escaped_path);
            }
        }
        var target = $(e.target);

        if (target && target.tagName != "input") {
            var m = target.up("#modal-content");
            target = m.down("input.button");
        }

        assert(form, "Trying to validate a folder where the form doesn't exist");
        Forms.ajax_submit(form, false,
            function () {
                if (success_callback && typeof(success_callback) == "function") {
                    success_callback(e, form);
                }
            },
            false,
            target);
        return false;
    },

    start_wizard: function (event) {
        Sharing.reset_wizard();
        if (event) {
            Event.stop(event);
        }
        // TRANSLATORS BUTTON
        Modal.icon_show(BrowseActions.getIcon('share_new'), _("Share a folder"), $("shared-folder-wizard"));
        $("create-new-sf").focus();
    },

    wizard_next: function (event) {
        Event.stop(event);
        if ($('create-new-sf').checked) { // New shared folder
            Sharing.validate_folder(event, "validate-folder-name", Sharing.from_new_to_invitation);

        } else {
            Modal.show(new Element("span").insert(Sprite.make("folder_user", {}).addClassName("modal-h-img")).insert(_("Choose Folder to Share")), $("existing-shared-folder-wizard"));
            TreeView.move('copy-move-treeview', 'share-existing-treeview', {
                onSuccess: function () {
                    $("modal").observe("db:treeview_selected", function (e) {
                        $("folder_name").setValue(e.memo.path);
                    });

                    // prep by selecting the first
                    var first_link = $('first-treeview-link');
                    if (!Util.ie) {
                        first_link.onclick();
                    }
                }
            });
        }
        $$("#modal-content .suggestion-input").each(SuggestionInput.register);
    },

    from_new_to_invitation: function (e, form) {
        var folder_name = $F(form.down("#new_folder_name")).strip();
        assert(folder_name, "Moving from new folder to invite modal with no path.");

        Sharing.show_invite_more_wizard(folder_name);
        Modal.vars.action = Sharing.submit_share_new_wizard;
        Modal.vars.path = "/" + folder_name;

        var invite_form = $("invite-more-form");
        var folder_name_input = invite_form.down("input[name=folder_name]");
        if (!folder_name_input) {
            folder_name_input = new Element("input", {type: "hidden", name: "folder_name"});
            invite_form.insert(folder_name_input);
        }

        folder_name_input.setValue(folder_name);
        // TRANSLATORS BUTTON
        // "share" is a verb: clicking this button will share the selected folder
        $("share-invite-button").setValue(_("Share folder"));
    },

    from_existing_to_invitation: function (e, form, path) {
        if (!path) {
            path = $F(form.down("#folder_name")).strip();
        }
        assert(path, "Moving from choose a folder to invite modal with no path.");

        Sharing.show_invite_more_wizard(FileOps.raw_filename(path));
        Modal.vars.action = Sharing.submit_share_existing_wizard;
        Modal.vars.path = path.strip();

        var path_form = $("invite-more-form");
        var path_elm = path_form.down("input[name=path]");

        if (!path_elm) {
            path_elm = new Element("input", {type: "hidden", name: "path"});
            path_form.insert(path_elm);
        }
        path_elm.setValue(path);

        $("share-invite-button").setValue(_("Share folder"));

    },

    show_invite_more_wizard: function (folder_name) {
        assert(folder_name, "Folder name required");
        DomUtil.fillVal("folder", "invite-more-wizard-share-type");
        var msg = _('Share "%(folder_name)s" with others').format({
            'folder_name': folder_name.escapeHTML()
        });
        Modal.icon_show("folder_user", msg, $("invite-more-wizard"));

        if (Sharing.invite_wizard_auto_completer) {
            Sharing.invite_wizard_auto_completer.clearTokens();
        } else {
            Sharing.invite_wizard_auto_completer = new Autocompleter.ContactsTokenizer("invite-wizard-new-collab-input", "invite-wizard-new-whobulk", "invite-wizard-hidden-input", contacts, lcontacts, {
                tokens: [',', ';'],
                include_fb: true
            });
        }

        ABTest.log('Fbsf', 'view_modal');
    },

    show_cli: function () {
        Referral.select_all = 0;
        Sharing.old_state = Modal.vars;
        Modal.onHide = Sharing.hide_cli;
        Referral.show_login_modal();
    },

    hide_cli: function () {
        Referral.select_no_contacts();
        Sharing.add_from_cli();
        Sharing.load_contacts();
        delete Modal.onHide;
        return false;
    },

    add_from_cli: function () {
        var emails = Referral.get_selected_emails();

        Sharing.show_invite_more_wizard(FileOps.raw_filename(Sharing.old_state.path));
        Modal.vars = Sharing.old_state;

        var email_input = $("invite-wizard-new-collab-input");

        SuggestionInput.do_blank("invite-wizard-new-collab-input");
        var current_value = $F(email_input);

        if (emails) {
            email_input.setValue(current_value + (current_value.length > 0 ? ", " : "") + emails);
            email_input.addClassName('suggestion-input-unfaded');
        }

        delete Modal.onHide;
    },

    submit_share_new_wizard: function (e) {
        var form = $("invite-more-form");
        assert(form, "Couldn't find the invite more form.");

        Forms.ajax_submit(form, "/share_ajax/new",
            function () {
                Modal.hide();
                if (Sharing.is_new) {
                    window.location = "/home" + Util.urlquote(Modal.vars.path);
                } else {
                    var msg = _('The shared folder "%(folder_name)s" has been created.').format({
                        'folder_name': FileOps.raw_filename(Modal.vars.path)
                    });
                    Notify.ServerSuccess(msg);
                    Browse.reload_fqpath(Modal.vars.path);
                    TreeView.schedule_reset();
                }
            },

            function () {
                Forms.enable(form.down("input[type='submit']"));
            },
            e.target);
        return false;
    },

    submit_share_existing_wizard: function (e) {
        var form = $("invite-more-form");
        assert(form, "Couldn't find the invite more form.");

        Forms.ajax_submit(form, "/share_ajax/existing?long_running",
            function () {
                Modal.hide();
                if (Sharing.is_new) {
                    window.location = "/home" +  Util.urlquote(Modal.vars.path);
                } else {
                    var msg = _('Shared folder "%(folder_name)s" has been created.').format({
                        'folder_name': FileOps.raw_filename(Modal.vars.path).escapeHTML()
                    });
                    Notify.ServerSuccess(msg);
                    Browse.force_reload();
                }
            },
            function () {
                Forms.enable(form.down("input[type='submit']"));
            },
            e.target);
        return false;
    },
    show_share_existing_modal: function (path) {
        Sharing.reset_wizard();
        Sharing.from_existing_to_invitation(false, false, decodeURIComponent(path));
    },
    show_invite_more_modal: function (path) {
        Sharing.reset_wizard();
        Sharing.get_sharing_options(path);
    },
    submit_invite_more_wizard: function (e) {
        var form = $("invite-more-form");
        assert(form.down("input[name=ns_id]"), "Submit invite more wizard is missing ns_id");

        Forms.ajax_submit(
            form,
            "/share_ajax/invite_more",
            function () {
                // TRANSLATORS success message, meaning, some people were invited successfully
                Notify.ServerSuccess(_("Invited successfully."));
                Modal.hide();
                Browse.force_reload();
            },
            function () {},
            e.target);
        return false;
    },
    show_leave_modal: function (folder_path, ns_id) {
        Modal.onHide = Sharing.reshow;
        var title = _('Leave the Shared Folder "%(folder_name)s"').format({
            'folder_name': FileOps.filename(folder_path).snippet().escapeHTML()
        });
        Modal.icon_show("folder_user_delete", title, DomUtil.fromElm("leave-confirm"), {
            wit_group: 'share-leave-confirm'
        });
        var form = $("leave-share-form");
        form.action = "/share_ajax/leave?long_running";
        Modal.vars.ns_id = ns_id;
        Modal.vars.folder_path = folder_path;
    },
    submit_leave: function (e) {
        delete Modal.onHide;
        var path = Modal.vars.folder_path;
        assert(path.length, "submit_leave: No shared folder path.");
        assert(Modal.vars.ns_id, "submit_leave: missing ns_id");

        var form = $("leave-share-form");
        Forms.add_vars(form, {
            ns_id: Modal.vars.ns_id
        });
        Forms.ajax_submit(
            form,
            false,
            function () { // On Success
                var msg = _("You removed yourself from \"%(msg)s\".").format(
                    {msg: FileOps.filename(path).snippet().escapeHTML()}
                );

                Notify.ServerSuccess(msg);
                Modal.hide();
                if (Browse.current_nsid) {
                    // leaving from inside the shared folder on browse
                    BrowseURL.set_path_url('', Util.parentDir(Browse.current_fqpath()));
                } else if (typeof(Browse.current_path) !== "undefined") {
                    Browse.force_reload();
                } else if (window.SFController) {
                    window.SFController.convert_to_inactive(path);
                } else {
                    window.location.reload();
                }
            },
            function () {},
            e.target);
        return false;
    },

    show_unshare_modal: function (folder_path, ns_id) {
        Modal.onHide = Sharing.reshow;
        var title = _('Unshare "%(folder_name)s"').format({
            'folder_name': FileOps.filename(folder_path).snippet().escapeHTML()
        });
        Modal.icon_show("link_break", title, DomUtil.fromElm("unshare-confirm"), {
            wit_group: 'share-unshare-confirm'
        });
        var form = $("unshare-form");
        Modal.vars.path = folder_path;
        Modal.vars.ns_id = ns_id;
        form.action = "/share_ajax/unshare?long_running";
    },
    submit_unshare: function (e) {
        delete Modal.onHide;
        var form = $("unshare-form");

        var path = Browse.current_fqpath() || PAGE_PATH;
        assert(path.length, "submit_unshare: No shared folder path.");
        assert(Modal.vars.ns_id, "submit_unshare: missing ns_id");

        Forms.add_vars(form, {
            ns_id: Modal.vars.ns_id
        });

        Forms.ajax_submit(form, false,
            function () { // On Success
                Notify.ServerSuccess(_("You unshared the folder \"%(folder_name)s\"").format({'folder_name': FileOps.filename(Modal.vars.path).snippet().escapeHTML()}));
                Modal.hide();
                if (Browse.files && Browse.files.length) {
                    Browse.force_reload();
                } else if (window.SFController) {
                    SFController.remove_by_path(Modal.vars.path);
                } else {
                    window.location.reload();
                }
            },
            false,
            e.target);

        return false;
    },
    remove_div: function (me) {
        $(me).up('.bs-row').remove();
        return false;
    },
    leave: function () {
        // TRANSLATORS this is a confirmation message, meaning, "Leave (this) shared folder?"
        Modal.show(_("Leave Shared Folder?"), DomUtil.fromElm('leave-confirm'), {
            wit_group: 'share-leave-confirm'
        });
    },
    unshare: function () {
        // TRANSLATORS this is a confirmation message, meaning, "Unsuare (this) shared folder?"
        Modal.show(_("Unshare Folder?"), DomUtil.fromElm('unshare-confirm'), {
            wit_group: 'share-unshare-confirm'
        });
    },
    ignore: function (path, ns_id) {
        assert(ns_id, "Share ignore did not get an ns_id");
        var msg = _("Permanently Remove '%(folder_name)s'").format({ 'folder_name' : FileOps.filename(path).escapeHTML()});
        Modal.icon_show("folder_delete", msg, DomUtil.fromElm('ignore-confirm'), {
            wit_group: 'share-ignore-confirm'
        });
        var form = $("modal-content").down("form");
        form.action = '/share_action/ignore?longrunning';
        Forms.add_vars(form, {
            'ns_id': ns_id
        });

        Modal.vars.path = path;
    },
    submit_ignore: function (e) {
        if (e) {
            Event.stop(e);
        }

        var form = $("share-ignore-form");
        assert(form, "Missing submit_ignore_form");

        Forms.ajax_submit(form, false, function (req) {
            // Success
            window.SFController.remove_by_path(Modal.vars.path);
            var d = {'folder_name': FileOps.filename(Modal.vars.path).snippet().escapeHTML()};
            Notify.ServerSuccess(_("Permanently removed '%(folder_name)s'").format(d));
            Modal.hide();
        },
        function () {
            window.location.reload();
        }, e && e.target);
    },
    rejoin: function (path, ns_id) {
        assert(ns_id, "Rejoin didn't get an ns_id");
        var msg = _("Rejoin the Shared Folder '%(folder_name)s'?").format({ 'folder_name' : FileOps.filename(path).escapeHTML() });
        Modal.icon_show(BrowseActions.getIcon("rejoin"), msg, DomUtil.fromElm('rejoin-confirm'), {
            wit_group: 'share-rejoin-confirm'
        });
        var form = $("modal-content").down("form");
        form.action = '/share_action/rejoin?longrunning';
        Forms.add_vars(form, {
            'ns_id': ns_id
        });
        if (window.SFController) {
            Forms.add_vars(form, {
                'from_share': '1'
            });
        }
        Modal.vars.path = path;
    },
    submit_rejoin: function (e) {
        Event.stop(e);
        var form = $("rejoin-form");
        var path = Modal.vars.path;

        Forms.ajax_submit(form, false, function (req) {
                Modal.hide();
                Notify.ServerSuccess(_("Rejoined shared folder successfully."));
                if (window.SFController) {
                    window.SFController.convert_to_active(path, req.responseText);
                } else if (Browse.files.length) {
                    Browse.force_reload();
                } else {
                    window.location.reload();
                }
            },
            false,
            e.target);
        return false;
    },
    show_change_sf_owner_modal: function (name, who, ns_id, user_id) {
        DomUtil.fillVal(name, 'change_sf_owner-confirm-nickname');
        Modal.onHide = Sharing.reshow;
        var title = _("Make %(person_name)s the owner of this folder?").format({
            'person_name' : name.escapeHTML()
        });
        Modal.icon_show('user_go', title, DomUtil.fromElm('change_sf_owner-confirm'), {
            wit_group: 'share-change-sf-owner-confirm'
        });
        var form = $("change-sf-owner-form");
        form.action = "/share_ajax/change_sf_owner";
        Modal.vars.ns_id = ns_id;
        Modal.vars.user_id = user_id;
    },
    submit_change_sf_owner: function (e) {
        delete Modal.onHide;
        $('make-owner-button').disable();
        assert(Modal.vars.ns_id, "submit_change_sf_owner: missing ns_id");
        assert(Modal.vars.user_id, "submit_change_sf_owner: missing user_id");

        var form = $("change-sf-owner-form");
        Forms.add_vars(form, {
            ns_id: Modal.vars.ns_id,
            user_id: Modal.vars.user_id
        });
        Forms.ajax_submit(
            form,
            false,
            function () { // On Success
                var msg = _("Ownership changed successfully.");

                Notify.ServerSuccess(msg);
                Modal.hide();
            },
            function () {},
            e.target);
        return false;
    },
    cancel_user: function (email_or_fbname, ns_id, invite_id, button) {
        var msg = _("Are you sure you want to uninvite %(email_or_fbname)s?").format({ 'email_or_fbname' : email_or_fbname });
        if (confirm(msg)) {
            new Ajax.DBRequest("/share_ajax/cancel_invite", {
                parameters: {
                    'ns_id': ns_id,
                    'invite_id': invite_id
                },
                onSuccess: function (req) {
                    Sharing.remove_div(button);
                    Notify.ServerSuccess(email_or_fbname.escapeHTML() + " has been uninvited.");
                }
            });
        }
    },
    kick_user: function (name, who, ns_id, user_id, button, path) {
        DomUtil.fillVal(name, 'kick-confirm-nickname');
        var msg = _("Kick %(person_name)s out of Folder?").format({ 'person_name' : name });
        Modal.show(msg, DomUtil.fromElm('kick-confirm'), {
            button: button,
            victim: who,
            ns_id: ns_id,
            user_id: user_id,
            wit_group: 'share-kick-confirm'
        });
    },
    do_kick: function (ns_id, user_id, button) {
        var keep_files = $F("keep-files-check");
        new Ajax.DBRequest("/share_ajax/kick_user", {
            parameters: {
                'ns_id': ns_id,
                'user_id': user_id,
                'keep_files': keep_files
            },
            onSuccess: function () {
                Notify.ServerSuccess(_("User removed successfully."));
                Modal.hide();
            }
        });
    },
    reinvite_user: function (email_or_fbname, invite_id) {
        new Ajax.DBRequest("/share_ajax/reinvite_user/" + PAGE_PATH, {
            parameters: {
                'invite_id': invite_id
            },
            onSuccess: function (req) {
                Notify.ServerSuccess(_("%(email_or_fbname)s was reinvited successfully").format({
                    'email_or_fbname': email_or_fbname
                }));
            }
        });
        return false;
    },
    load_contacts: function (include_fb) {
        if (Sharing.loading_contacts) {
            return false;
        }

        Sharing.loading_contacts = true;

        new Ajax.DBRequest("/get_contacts", {
            parameters: {
                include_fb: include_fb && Constants.can_fb_invite
            },
            onSuccess: function (req) {
                var contacts_dict = req.responseText.evalJSON(false);
                window.contacts = contacts_dict.contacts;
                window.lcontacts = contacts_dict.lcontacts;
                Sharing.loading_contacts = false;

                if (Sharing.sharing_options_auto_completer) {
                    Sharing.sharing_options_auto_completer.options.array = window.contacts;
                    Sharing.sharing_options_auto_completer.options.larray = window.lcontacts;
                }

                if (Sharing.invite_wizard_auto_completer) {
                    Sharing.invite_wizard_auto_completer.options.array = window.contacts;
                    Sharing.invite_wizard_auto_completer.options.larray = window.lcontacts;
                }

            }
        });
    }
};


var SharingModel = {
    confirm_remove: function (name, tkey, redirect_to, from_share) {
        assert(tkey, "confirm_remove missing tkey");
        assert(name, "confirm_remove missing name");
        var msg = _('Remove link to "%(folder_name)s"').format({
            'folder_name': name.escapeHTML().snippet(34)
        });
        Modal.icon_show("link_delete", msg, $("disable-token-modal"), {
            'token': tkey,
            'redirect_to': redirect_to,
            'from_share': from_share,
            'name': name
        });
    },
    do_remove: function (opts) {
        var url = '/sm/disable/' + opts.token;
        new Ajax.DBRequest(url, {
            onSuccess: function () {
                if (opts.redirect_to) {
                    window.location.href = opts.redirect_to;
                    return;
                }

                if (opts.from_share) {
                    var file;
                    for (var i = 0; i < LinkController.files.length; i += 1) {
                        if (LinkController.files[i].tkey == opts.token) {
                            file = LinkController.files[i];
                            break;
                        }
                    }
                    if (!file) {
                        window.location.reload();
                    } else {
                        file.div.remove();
                        LinkController.files.removeItem(file);
                    }

                } else {
                    Browse.force_reload();
                }

                var msg = _("Linking of %(filename)s is disabled.").format({
                    filename: opts.name.escapeHTML().snippet(34)
                });
                Notify.ServerSuccess(msg);
                BrowseActions.hide_dropdown();
                Modal.hide();
            },
            onComplete: function () {
                Forms.remove_loading();
            }
        });
        Forms.add_loading($("modal-content").down("input"));
    },
    create_token: function (path, onSuccess) {
        var msg = _('Creating a link for "%(file_name)s"').format({
            'file_name': FileOps.filename(path).escapeHTML().snippet(34)
        });
        Modal.show_loading("link", msg);

        new Ajax.DBRequest("/sm/create" + Util.normalize(path), {
            onSuccess: function (req) {
                var token = req.responseText.evalJSON(true);
                if (onSuccess) {
                    onSuccess(token);
                }
            }
        });
    },
    get_token: function (path, onSuccess) {
        new Ajax.DBRequest("/sm/get" + Util.normalize(path), {
            onSuccess: function (req) {
                var token = req.responseText.evalJSON(true);
                if (onSuccess) {
                    onSuccess(token);
                }
            }
        });
    },
    toggle_advanced: function () {
        var opts = $("token-advanced-options");
        if (opts.animating) {
            return;
        }

        opts.animating = true;
        var duration = 0.5;
        if ($("token-advanced-options").style.display == "none") {
            new Effect.BlindDown("token-advanced-options", {
                duration: duration,
                afterFinish: function () {
                    opts.animating = false;
                }
            });
        } else {
            new Effect.BlindUp("token-advanced-options", {
                duration: duration,
                afterFinish: function () {
                    opts.animating = false;
                }
            });
        }
        Sprite.toggle($("advanced-toggle-bullet"), "bullet_plus", "bullet_minus");
    },
    update_access_options: function (target, e) {
        if (e) {
            Event.stop(e);
        }
        var form = $(target).up("form");

        Forms.ajax_submit(form, false,
            function () {
                $$("#modal-content .error-message").invoke("remove");
                Notify.ServerSuccess(_("Updated Successfully."));
            },
            false,
            $(target));
    },

    update_date: function (when) {
        var date_fmt = when.getMonth() + 1 + "/" + when.getDate() + "/" + when.getFullYear();
        $("token-expires-date").update(when.localize());
        $("expires").setValue(date_fmt);
        SharingModel.hide_calendar();
    },
    hide_calendar: function () {
        $("modal-calendar-container").hide();
    },
    show_calendar: function (e) {
        if (e) {
            Event.stop(e);
        }

        if (SharingModel.calendar) {
            $("modal-calendar-container").show();
        } else {
            var selected = Util.reverseNiceDate(Modal.vars.token.expires) || new Date();
            SharingModel.calendar = new DBCalendar("modal-calendar-container", {
                onDateChange: SharingModel.update_date,
                disable_past: true,
                selected_day: selected
            });

            $(document.body).observe("click", SharingModel.hide_calendar);

            var cal_elm = $("modal-calendar-container");
            cal_elm.style.position = "absolute";
            cal_elm.style.zIndex = 5;
            var datebox = $("token-expires-box");

            cal_elm.clonePosition(datebox);
            cal_elm.style.top = parseInt(cal_elm.style.top, 10) + datebox.getHeight() - 1 + "px";
        }
    },
    show_post_options: function (token) {
        var title = _('Share "%(filename)s"').format({'filename': token.name.escapeHTML()});
        var msg = _("Check out this link I made with Dropbox") + " " + token.link;
        var description = '';
        Twitter.start_flow(title, msg, token.link, token.name, description);
    },
    show_password_field: function () {
        var pw_link = $("change-token-password-link");
        if (pw_link) {
            pw_link.remove();
        }
        $("require_password").setValue("");
        $("password").show();
        $("password").setValue("");
    },
    get_transcode_status: function (queue_id, update_elm) {
        new Ajax.Request("/get_transcode_status/" + queue_id, {
            onSuccess: function (req) {
                if (req.responseText == "reload" || req.responseText == "error") {
                    window.location.reload();
                } else {
                    $(update_elm).update(req.responseText);
                    setTimeout(function () {
                        SharingModel.get_transcode_status(queue_id, update_elm);
                    }, 2000);
                }
            }
        });
    },
    enqueue_transcode: function (token, path, update_elm) {
        new Ajax.Request("/enqueue_transcode/" + token + path, {
            onSuccess: function (req) {
                return SharingModel.get_transcode_status(req.responseText, update_elm);
            }
        });
    },
    start_twitter_flow: function (message, link, link_name, description) {
        var twitter = $("twitter-checkbox") && $F("twitter-checkbox");
        var facebook =  $F("facebook-checkbox");

        if (twitter) {
            message = message.substr(0, 140);
        }

        if (facebook) {
            FacebookOAuth.msg = message;
            FacebookOAuth.name = link_name;
            FacebookOAuth.link = link;
        }

        var post_facebook = function () {
            var do_post = function () {
                AMC.log('shmodel_share', {
                    'type': 'facebook',
                    'action': 'post'
                });
                FacebookOAuth.post(message, link, link_name, description);
            };
            FacebookOAuth.onLoginSuccessCallback = do_post;
            if (FacebookOAuth.has_authed || !Constants.uid) {
                do_post();
            } else {
                AMC.log('shmodel_share', {
                    'type': 'facebook',
                    'action': 'start_auth'
                });
                if (FacebookOAuth.deferred) {
                    FacebookOAuth.show_auth(do_post);
                } else {
                    FacebookOAuth.do_auth();
                }
            }
        };

        var post_twitter = function (callback) {
            var do_post = function () {
                AMC.log('shmodel_share', {
                    'type': 'twitter',
                    'action': 'post'
                });
                Twitter.custom_post(message, callback);
            };

            Twitter.onLoginSuccessCallback = do_post;
            if (Twitter.has_authed || !Constants.uid) {
                do_post();
            } else {
                AMC.log('shmodel_share', {
                    'type': 'twitter',
                    'action': 'start_auth'
                });
                Twitter.do_auth();
            }
        };

        if (twitter && facebook) {
            post_twitter(post_facebook);
            FacebookOAuth.custom_show_complete = function () {
                FacebookOAuth.get_progress_container().update(DomUtil.fromElm("sharing-posted-both"));
            };
        } else if (twitter) {
            post_twitter();
        } else if (facebook) {
            post_facebook();
        }
    },

    unable_to_preview: function () {
        Modal.icon_show("cog", _("Preview Currently Unavailable"), $("unavailable_preview"));
    },

    copy_to_dropbox: function (name, tkey, subpath) {
        assert(name, "c2d name missing");
        assert(tkey, "c2d tkey missing");

        Modal.icon_show("dropbox", _('Copy "%(filename)s" to my Dropbox').format({'filename': name.snippet(30).escapeHTML()}), $("c2d-modal"));
        $("c2d-modal").select("form").each(function (form) {
            Forms.add_vars(form, {
                tkey: tkey,
                subpath: subpath
            });
        });
        var fname_field = $("fname");
        if (fname_field) {
            fname_field.focus();
        }
    },
    c2d_submit: function (e, form) {
        assert(form.select("input[name=tkey]"), "Trying to submit a form that doesn't have a tkey");

        Forms.ajax_submit(form, false,
            function (req) {
                window.location.href = req.responseText;
            },
            false,
            $(form).down("input[type=submit]")
        );
    }
};


var SharedFolderInvites = {
    pages: {},
    contents: {},
    register_all: function () {
        $$(".expand-invite").each(function (elm) {
            SharedFolderInvites.register(elm);
        });
    },

    register: function (elm) {
        elm.db_observe("click", SharedFolderInvites.expand);
    },

    expand: function (e, source, ns_id) {
        Event.stop(e);
        source = $(source);

        if (SharedFolderInvites.animating) {
            return;
        }

        var invite = source.up(".invite");
        var effects = [];

        SharedFolderInvites.get_sf_contents(invite, ns_id);

        if (invite.hasClassName("active")) {
            effects.push(SharedFolderInvites.hide(invite));
        } else {
            effects.push(SharedFolderInvites.show(invite));
        }

        $$("div.invite.active").each(function (elm) {
            if (elm != invite) {
                effects.push(SharedFolderInvites.hide(elm));
            }
        });

        SharedFolderInvites.animating = true;
        new Effect.Parallel(effects, {duration: 0.5, afterFinish: function () {
            SharedFolderInvites.animating = false;
        }});
    },

    show: function (invite) {
        invite.addClassName("active");
        var details = invite.down(".invite-details");
        var toggle = invite.down(".toggler");
        Sprite.replace(toggle, "plus", "minus");
        return new Effect.BlindDown(details, {sync: true, afterFinish: function () {
            details.style.height = "auto";
        }});
    },

    hide: function (invite) {
        invite.removeClassName("active");
        var details = invite.down(".invite-details");
        var toggle = invite.down(".toggler");
        Sprite.replace(toggle, "minus", "plus");
        return new Effect.BlindUp(details, {sync: true});
    },

    show_page: function (page) {
        Feed.hideLoading();
        $("invites-container").update(SharedFolderInvites.pages[page]);
        SharedFolderInvites.register_all();
    },

    get_page: function (page) {

        if (!SharedFolderInvites.pages[0] && page == 1) {
            SharedFolderInvites.pages[0] = $("invites-container").innerHTML;
        }

        if (SharedFolderInvites.pages[page]) {
            SharedFolderInvites.show_page(page);
        } else {
            Feed.showLoading(false, $('invites-container'), false, true);

            new Ajax.DBRequest("/share_ajax/invitation_page?page=" + page, {
                onSuccess: function (req) {
                    SharedFolderInvites.pages[page] = req.responseText;
                    SharedFolderInvites.show_page(page);
                }
            });
        }

        return false;
    },

    get_sf_contents: function (invite, ns_id) {

        if (SharedFolderInvites.contents[ns_id]) {
            SharedFolderInvites.show_sf_contents(invite, ns_id);
        } else {
            new Ajax.DBRequest("/share_ajax/sf_contents?ns_id=" + ns_id, {
                onSuccess: function (req) {
                    SharedFolderInvites.contents[ns_id] = req.responseText;
                    SharedFolderInvites.show_sf_contents(invite, ns_id);
                }
            });
        }
    },

    show_sf_contents: function (invite, ns_id) {
        invite.down(".folder-contents").update(SharedFolderInvites.contents[ns_id]);
    },

    mailto: function (e, email) {
        Event.stop(e);
        window.location = "mailto:" + email;
    }
};


var ShareView = {
    current_view: 'gallery',
    click: function (view) {
        ShareView.toggle_view(view);
        HashRouter.set_hash('view', view);
        return false;
    },

    toggle_view: function (view) {
        view = view || "gallery";
        var elm = $(view + "-link");
        if (!elm) {
            return;
        }
        $$("#toggle-view .selected").invoke("removeClassName", "selected");
        elm.addClassName("selected");
        $$(".view").invoke("hide");
        var cont = $(view + "-view");
        if (cont) {
            cont.show();
        }
        ShareView.current_view = view;
    },

    show_all: function (elm, e, container_id) {
        if (e) {
            Event.stop(e);
        }
        $(elm).up().remove();
        new Effect.BlindFadeDown(container_id);
    }
};


var SM = {
    r: function (tkey, action, visitor_user_id, subpath) {
        var params = {
            'tkey': tkey,
            'action': action,
            'visitor_user_id': visitor_user_id,
            'subpath': subpath
        };

        var key = Object.toJSON(params);
        if (Jcached.get(key)) {
            return;
        }
        Jcached.set(key, 1, 2000);

        new Ajax.Request("/ajax_sm_visit", {
            'parameters': params
        });
    }
};

// split-todo is this still used?
var Base = {
    mouseOut: false,
    sf_hidden: false,
    showSharedFolders: function (link, e) {
        Event.stop(e);

        var folderList = $('shared-folder-dropdown');

        Event.observe(folderList, 'mouseover', Base.mouseOverList);
        Event.observe(folderList, 'mouseout', Base.mouseOutList);
        Event.observe(e.target, 'mouseout', Base.mouseOutList);
        Event.observe(e.target, 'mouseover', Base.mouseOverList);
        Event.observe(document, 'click', Base.hideSharedFolders);

        Base.sf_hidden = false;

        // wait a sec before we show it
        setTimeout(
            function () {
                if (Base.sf_hidden) {
                    return;
                }

                var shadow = $('share-menu-shadow');
                var numFolders = folderList.select('li').length;
                folderList.clonePosition($(link).down("img"), {offsetTop: 8, offsetLeft: 1, setWidth: false, setHeight: false});
                folderList.style.height = (numFolders > 9) ? "200px" : "";
                folderList.show();

                shadow.clonePosition($(link).down("img"), {offsetTop: 9, offsetLeft: 2, setWidth: false, setHeight: false});
                shadow.clonePosition(folderList, {setTop: false, setLeft: false});
                shadow.setOpacity(0.2);
                shadow.show();
            }, 350);

        return false;
    },
    hideSharedFolders: function (e) {
        var folderList = $('shared-folder-dropdown');
        $('shared-folder-dropdown').hide();
        $('share-menu-shadow').hide();

        Event.stopObserving(window, 'click', Base.hideSharedFolders);
        Event.stopObserving(folderList, 'mouseover', Base.mouseOverList);
        Event.stopObserving(folderList, 'mouseout', Base.mouseOutList);
        clearTimeout(Base.mouseOut);
        Base.mouseOut = false;
        Base.sf_hidden = true;
    },
    mouseOverList: function () {
        clearTimeout(Base.mouseOut);
        Base.mouseOut = false;
    },
    mouseOutList: function (e, bigger_num) {
        clearTimeout(Base.mouseOut);
        Base.mouseOut = setTimeout(Base.hideSharedFolders, bigger_num || 200);
    }
};


var TokenListView = {
    sort_by_filename: function (x) {
        return parseInt(x.down(".filename .hidden").innerHTML, 10);
    },
    sort_by_size: function (x) {
        return parseInt(x.down(".filesize .hidden").innerHTML, 10);
    },
    sort_by_modified: function (x) {
        return -1 * parseInt(x.down(".modified .hidden").innerHTML, 10);
    },
    sort: function (e) {
        Event.stop(e);

        var elm = Util.resolve_target(e.target, "a");
        if (elm.asc != -1) {
            elm.asc = -1;
        } else {
            elm.asc = 1;
        }

        var column = elm.className;

        var sort_map = { // the keys here match up to the className of the related links
            'sort-filename': TokenListView.sort_by_filename,
            'sort-filesize': TokenListView.sort_by_size,
            'sort-modified': TokenListView.sort_by_modified
        };

        var rows = $$("#list-view .filerow");
        var sorted = rows.sort_by_key(sort_map[column], elm.asc != 1);

        rows.invoke("remove");

        var cont = new Element("div");
        for (var i = 0, len = sorted.length; i < len; i += 1) {
            cont.insert(sorted[i]);
        }
        $("list-browser").insert(cont.innerHTML);

        $$(".sort-tick").invoke("remove");
        var img = Sprite.make(elm.asc === 1 ? "sort-downtick-on" : "sort-uptick-on");
        img.addClassName("sort-tick");

        elm.insert(img);
    }
};

// these should reflect SearchableUserType in common/util
var ContactTypes = {
    EMAIL : 0,
    FB : 1,
    INVALID : 2
};

Autocompleter.Contacts = Class.create(Autocompleter.Base, {
    initialize: function (element, update, array, larray, options) {
        this.baseInitialize(element, update, options);
        this.options.array = array;
        this.options.larray = larray;
        this.options.frequency = 0.1;
        var include_fb = this.options.include_fb === true; // only include fb contacts if explicitly specified
        if (!window.contacts) {
            (function () {
                Sharing.load_contacts(include_fb);
            }).defer();
        }
        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 - 1 // - 1 makes the typeahead share a border with input
                });
            }
            Effect.Appear(update, {duration: 0.15});
        };

    },

    getToken: function() {
        var bounds = this.getTokenBounds();
        // don't call strip() on the substring because spaces matter when autocompleting full names
        // e.g. "yi " should match "Yi Wei" but not "Darren Yin"
        return this.element.value.substring(bounds[0], bounds[1]);
    },

    getUpdatedChoices: function () {
        if (!this.options.array && window.contacts && window.lcontacts) {
            this.options.array = window.contacts;
            this.options.larray = window.lcontacts;
        }

        this.updateChoices(this.options.selector(this));
    },

    setOptions: function (options) {
        this.options = Object.extend({
            choices: 5,
            selector: function (instance) {
                var ret       = []; // Beginning matches
                var entry     = instance.getToken().toLowerCase();

                var l = instance.options.array.length;
                var c = instance.options.choices;
                var a = instance.options.array;
                var la = instance.options.larray;

                var regex_parts = [];
                if (entry.indexOf(" ") == -1) {
                    regex_parts.push("\\s+");
                }
                if (entry.indexOf("+") == -1) {
                    regex_parts.push("\\+");
                }
                if (entry.indexOf("@") == -1) {
                    regex_parts.push("@");
                }
                if (entry.indexOf(".") == -1) {
                    regex_parts.push("\\.");
                }
                if (entry.indexOf("&lt;") == -1) {
                    regex_parts.push("&lt;");
                }
                var regex = RegExp("(" + regex_parts.join("|") + ")");

                for (var ii = 0; ii < l && ret.length < c; ii++) {
                    var elem = a[ii];
                    var lelm = la[ii];

                    var foundPos = -1;

                    var display_vals = [];
                    var match_vals = [];
                    var icon_path = "";

                    switch (elem.type)
                    {
                    case ContactTypes.EMAIL:
                        display_vals.push(elem.name, elem.email);
                        match_vals.push(lelm.name, lelm.email);
                        icon_path = "/static/images/icons/mail28.png";
                        break;
                    case ContactTypes.FB:
                        display_vals.push(elem.name);
                        match_vals.push(lelm.name);
                        if (Sharing.use_fb_profile_pics) {
                            icon_path = "https://graph.facebook.com/" + lelm.fb_id + "/picture"; // fb profile pic
                        } else {
                            icon_path = "/static/images/icons/fb24.png";
                        }
                        break;
                    default:
                        assert(false, 'should never get here due to type: ' + elem.type + ', ' + elem.name + ', ' + elem.email + ', ' + elem);
                    }

                    // look for a match in any of the match vals
                    for (var jj = 0; jj < match_vals.length; jj++) {
                        var parts = regex_parts.length ? match_vals[jj].split(regex) : [match_vals[jj]];
                        var curPos = 0;
                        var pl = parts.length;
                        for (var p = 0; p < pl; p++) {
                            if (!parts[p]) {
                                continue;
                            }
                            // match found, stop looking
                            if (parts[p].indexOf(entry) === 0) {
                                foundPos = curPos;
                                break;
                            }
                            curPos += parts[p].length;
                        }

                        // a match was found, bold part of the display val
                        if (foundPos != -1) {
                            display_vals[jj] = display_vals[jj].substr(0, foundPos) + "<strong>" + display_vals[jj].substr(foundPos, entry.length) + "</strong>" + display_vals[jj].substr(foundPos + entry.length);
                            break;
                        }
                    }

                    if (elem.type == ContactTypes.FB) {
                        // TRANSLATORS the following string is a description of the medium through which a user will send a message. i.e. through email vs facebook
                        display_vals.push(_('Invite via Facebook'));
                    }

                    // a match was found, add it to the typeahead
                    if (foundPos != -1) {
                        var icon_content = "<image src='" + icon_path + "' width='28px' height='28px'/>"; // dimensions have to be set because fb pics are 50x50px

                        var primary_content = "<div class='autocomplete-line'>" + display_vals[0] + "</div>";
                        var secondary_content = "<div class='autocomplete-line autocomplete-secondary'>" + display_vals[1] + "</div>";

                        var left_content = "<div class='autocomplete-left'>" + icon_content + "</div>";
                        var right_content = "<div>" + primary_content + secondary_content + "</div>";

                        ret.push("<li value='" + ii + "'>" + left_content + right_content + "</li>");
                    }
                }

                return "<ul>" + ret.join('') + "</ul>";
            }
        }, options || { });
    },
    selectEntry: function () {
        var entry = this.getCurrentEntry();
        var elem = this.options.array[entry.value];

        if (elem.type == ContactTypes.FB) {
            entry.innerHTML = elem.name;
        } else {
            entry.innerHTML = elem.email;
        }

        // if we have a lot of tokens, choose one to use as the separator and auto-add it
        var tok = this.options.tokens.length > 1 ? this.options.tokens[0] + " ": "";
        entry.innerHTML += tok;

        this.active = false;
        this.updateElement(entry);
        $(this.element).fire("db:autocompleted");
    }
});

var Token = Class.create({
    initialize: function (element, hidden_input, token_manager, is_valid) {
        this.element = $(element);
        this.token_manager = token_manager;
        this.hidden_input = hidden_input;
        this.element.token = this;
        this.selected = false;
        this.is_valid = is_valid;
        // TODO: remove dependency on this name
        Event.observe($("tokenized_autocompleter_container"), 'click', this.onclick.bindAsEventListener(this));
    },
    select: function () {
        this.token_manager.token = this;
        this.hidden_input.element.activate();
        this.selected = true;
        this.element.addClassName('token_selected');
    },
    deselect: function () {
        if (this.token_manager.token == this) {
            this.token_manager.token = null;
        }
        this.selected = false;
        this.element.removeClassName('token_selected');
    },
    onclick: function (event) {
        if (event && event.preventDefault) {
            event.preventDefault();
        }
        if (this.detect(event) && !this.selected) {
            this.select();
        } else {
            this.deselect();
        }
    },
    remove: function (event) {
        this.token_manager.remove(this);

        // for some reason, this doesn't place focus back on hidden input
        /*if (event && event.preventDefault) {
            event.preventDefault();
        }
        this.hidden_input.auto_complete_element.focus();*/
    },
    detect: function (e) {
        //find the event object
        var eventTarget = e.target ? e.target: e.srcElement;
        var token = eventTarget.token;
        var candidate = eventTarget;
        while (token === undefined && candidate.parentNode) {
            candidate = candidate.parentNode;
            token = candidate.token;
        }
        return token !== undefined && token.element == this.element;
    }
});

var TokenManager = Class.create({
    initialize: function (shift_boundary_right_func, shift_boundary_left_func) {
        this.shift_boundary_right_func = shift_boundary_right_func;
        this.shift_boundary_left_func = shift_boundary_left_func;
        this.tokens = [];
        this.token = null;
    },
    add: function (token) {
        this.tokens.push(token);
    },
    remove: function (token) {
        // if no token was passed in, delete the currently selected token
        if (token === undefined) {
            token = this.token;
        }

        if (token) {
            var token_index = this.tokens.indexOf(token);
            this.tokens.splice(token_index, 1);
            token.element.remove();

            // by default, select the token to the right if one was selected
            if (this.token == token && token_index < this.tokens.length) {
                this.tokens[token_index].select();
            } else {
                this.token = null;
                this.shift_boundary_right_func();
            }
        }
    },
    removeAll: function () {
        for (var i = 0; i <  this.tokens.length; i++) {
            this.tokens[i].element.remove();
        }
        this.tokens.clear();
    },
    shift_left: function () {
        // if left token exists (deselects current token and selects left)
        var token_index;
        if (this.token === null) {
            token_index = this.tokens.length;
        } else {
            token_index = this.tokens.indexOf(this.token);
        }
        if (token_index > 0) { // left token exists
            if (this.token) {
                this.token.deselect();
            }
            this.tokens[token_index - 1].select();
        } else {
            if (this.shift_boundary_left_func !== undefined) {
                this.shift_boundary_left_func();
            }
        }
    },
    shift_right: function () {
        // deselect current token and (select right token if exists)
        var token_index = this.tokens.indexOf(this.token);
        if (this.token) {
            this.token.deselect();
        }
        if (token_index + 1 < this.tokens.length) { // right token exists
            this.tokens[token_index + 1].select();
        } else {
            if (this.shift_boundary_right_func !== undefined) {
                this.shift_boundary_right_func();
            }
        }
    }

});

var HiddenInput = Class.create({
    // The hidden input element has focus when a token is selected to listen
    // to key press events
    initialize: function (element, auto_complete_element, token_manager) {
        this.element = $(element);
        this.auto_complete_element = auto_complete_element;
        this.token_manager = token_manager;
        Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
    },
    onKeyPress: function (event) {
        switch (event.keyCode) {
        case Event.KEY_LEFT:
            event.preventDefault(); // prevent the window from shifting when small
            this.token_manager.shift_left();
            break;
        case Event.KEY_TAB:
            event.preventDefault();
            this.token_manager.shift_right();
            break;
        case Event.KEY_RIGHT:
            event.preventDefault(); // prevent the window from shifting when small
            this.token_manager.shift_right();
            break;
        case Event.KEY_BACKSPACE:
        case Event.KEY_DELETE:
            this.token_manager.remove();
            break;
        }
        return false;
    }
});

/* autocomplete tokenizer helper functions */

var validate_email = function (email) {
    // this should match the StrictEmail regex in model/form.py
    var regexEmail = new RegExp("^['&A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", "i");
    return regexEmail.test(email);
};

var addContactToList = function (display_val, input_val, type, new_collab_input, hidden_input, token_manager, is_valid) {
    new_collab_input.value = "";
    var input_name = "";
    switch (type) {
    case ContactTypes.FB:
        input_name = "fb_ids";
        break;
    case ContactTypes.EMAIL:
        input_name = "emails";
        break;
    case ContactTypes.INVALID:
        input_name = "invalids";
        break;
    default:
        assert(false, 'should never get here due to type: ' + type);
    }

    var error_class;
    if (is_valid) {
        error_class = " token-valid";
    } else {
        error_class = " token-error";
    }

    var token_x = Builder.node('span', {
        "class" : 'x' + error_class,
        onmouseout : "this.className='x" + error_class + "'",
        onmouseover : "this.className='x_hover" + error_class + "'",
        onclick : "this.parentNode.parentNode.parentNode.parentNode.parentNode.token.remove(event); return false;"
    }, " ");
    // TODO: make clicking the x focus on the input

    var token_hidden_input = Builder.node('input', {
        type: "hidden",
        name: input_name,
        value: input_val
    });

    // a bunch of nested spans help display token borders properly. not sure if
    // there's a cleaner way to do this, and it works, so leaving it for now.
    var token = Builder.node('a', {
        "class": 'token' + error_class,
        href: "#",
        tabindex: "-1"
    },
    Builder.node('span', Builder.node('span', Builder.node('span', Builder.node('span', {}, [token_hidden_input, display_val, token_x]))))
	);

	$(token).down(4).next().innerHTML = "&nbsp;";
    var token_obj = new Token(token, hidden_input, token_manager, is_valid);
    token_manager.add(token_obj);
    $('autocomplete_display').insert({before : token});
};

var kc = {
    curr: '',
    w: function () {
        document.observe("keydown", function (e) {
            kc.curr += BrowseKeys.getKey(e);
            if (kc.curr.endsWith('3838404037393739666513')) {
                new Effect.Appear("magic");
            }
        });
    }
};

/* end of autocomplete tokenizer helper functions */

Autocompleter.ContactsTokenizer = Class.create(Autocompleter.Contacts, {
    initialize: function ($super, element, update, hidden_element, array, larray, options) {
        $super(element, update, array, larray, options);
        this.token_manager = new TokenManager(this.generate_shift_boundary_right_func());
        this.hidden_input = new HiddenInput(hidden_element, this.element, this.token_manager);

        if (!this.element.hacks) {
            this.element.should_use_borderless_hack = Prototype.Browser.WebKit;
	        this.element.should_use_shadow_hack = Prototype.Browser.IE || Prototype.Browser.Opera;
            this.element.hacks = true;
        }
        if (this.element.should_use_borderless_hack ||
            this.element.should_use_shadow_hack) {
            $(this.element.parentNode).addClassName('tokenizer_input_borderless');
        }

        // element one more level down, so clone position from two levels up
	this.options.onShow = function (element, update) {
            Position.clone(element.parentNode.parentNode, update, {
		setHeight: false,
		offsetTop: element.parentNode.parentNode.offsetHeight - 1 // - 1 makes the typeahead share a border with input
	    });
	    update.show();
	};

	this.options.onHide = function (element, update) {
            update.hide();
        };

        Event.observe(this.element.up('form').down('#share-invite-button'), 'mousedown', this.beforeSubmit.bindAsEventListener(this));

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

        this.dynamically_resize_input();
        this.set_default_text();
    },

    onBlur: function ($super, event) {
        this.set_default_text();
        $super(event);
    },

    onFocus: function (event) {
        this.clear_default_text();
    },

    beforeSubmit: function (event) {
        if (!this.default_text) {
            this.tokenize_emails_input(true);
        }
    },

    clearTokens: function (size) {
        this.token_manager.removeAll();
        this.dynamically_resize_input();
        this.set_default_text();
    },

    set_default_text: function () {
        // if there's no untokenized input text and no tokens, set the gray default text
        if (this.element.value === '' && this.token_manager.tokens.length == 0) {
            this.element.value = _('Enter names or email addresses');
            this.element.style.color = '#999';
            this.default_text = true;
        }
    },

    clear_default_text: function () {
        // clear the default text
        if (this.default_text) {
            this.element.value = '';
            this.element.style.color = '#444';
            this.default_text = false;
        }
    },

    clean_email_addr: function (delimiters, email_addr) {
        // remove <> as well to make gmail contact pasting look prettier
        var augmented_delimiters = ['<', '>'];

        for (var ii = 0; ii < delimiters.length; ii++) {
            email_addr = email_addr.sub(delimiters[ii], '');
        }
        for (var jj = 0; jj < augmented_delimiters.length; jj++) {
            email_addr = email_addr.sub(augmented_delimiters[jj], '');
        }
        return email_addr.strip();
    },

    get_regexp_from_delimiters: function (delimiters) {
        var delimstring = '';
        for (var ii = 0; ii < delimiters.length; ii++) {
            delimstring += delimiters[ii] + '|';
        }

        return new RegExp('[' + delimstring + '\\r\\n|\\r|\\n|\\t]+');
    },

    tokenize_emails_input: function (tokenize_last, only_valid) {
        var delimiters = this.options.tokens;
        var split_regex = this.get_regexp_from_delimiters(delimiters);

        // split email addresses on valid delimiters
        var email_addrs = this.element.value.split(split_regex);

        // tokenize_last is false in cases where the user may still be entering
        // the last email address
        var remainder = '';
        if (!tokenize_last) {
            var unclean_remainder = email_addrs[email_addrs.length - 1];
            remainder = this.clean_email_addr(delimiters, unclean_remainder);

            email_addrs = email_addrs.slice(0, email_addrs.length - 1);

            // if there was a space at the end of the remainder, and the
            // remainder was a valid email address, the user probably meant to
            // autocomplete it
            if (unclean_remainder[unclean_remainder.length - 1] == ' ') {
                if (validate_email(remainder)) {
                    email_addrs.push(remainder);
                    remainder = '';
                }
            }
        }

        // do a second pass over email_addrs to split on space in case the
        // user pasted space delimited email addresses
        var new_email_addrs = [];
        for (var ii = 0; ii < email_addrs.length; ii++) {
            new_email_addrs.push.apply(new_email_addrs, email_addrs[ii].split(' '));
        }
        email_addrs = new_email_addrs;

        var clear_error = false;

        if (email_addrs.length > 0) {
            for (var jj = 0; jj < email_addrs.length; jj++) {
                // strip email address of white space and valid delimiters
                var email_addr = this.clean_email_addr(delimiters, email_addrs[jj]);

                // add the email if it's valid
                if (email_addr) {
                    var contact_type;
                    var is_valid;

                    if (validate_email(email_addr)) {
                        is_valid = true;
                        contact_type = ContactTypes.EMAIL;
                        clear_error = true;
                    } else {
                        if (only_valid) {
                            continue;
                        }
                        is_valid = false;
                        contact_type = ContactTypes.INVALID;
                    }
                    // temporarily reduce the width of the input element to 1 so that
                    // it doesn't spill over to the next line when the token is added
                    this.set_input_size(1);
                    addContactToList(
                        email_addr,
                        email_addr,
                        contact_type,
                        this.element,
                        this.hidden_input,
                        this.token_manager,
                        is_valid
                    );
                }
            }

            this.element.value = remainder;
        }

        var form = this.element.up('form');
        if (clear_error) {
            Forms.clear_errors(form);
        }

        this.dynamically_resize_input();
    },

    generate_shift_boundary_right_func: function () {
        // creates a scope from which focus_element can be referenced
        var focus_element = this.element;
        var focus_func = function () {
            focus_element.focus();
        };
        return focus_func;
    },

    set_input_size: function (size) {
        size = size || 20;
        this.element.setStyle({width: size + "px"});
    },

    dynamically_resize_input: function () {
        // dynamically resize the input field to cover the remainder of the last line in the
        // text box (the length of the text box minus the length of the tokens on the last line)
        var textbox = $(this.element.parentNode.parentNode);
        var textbox_width = parseInt(textbox.getStyle('width')) - 15;
        var tokens = this.token_manager.tokens;
        // compute the total width of the tokens on the last line by iterating through all tokens
        // and adding the width of each one to a running total, resetting the total at each new line
        var tokens_width = 0;
        for (var i = 0; i < tokens.length; i++) {
            var current_token_width = parseInt(tokens[i].element.getStyle('width')) +
                parseInt(tokens[i].element.getStyle('margin-right'));
            var new_tokens_width = tokens_width + current_token_width;
            if (new_tokens_width > textbox_width) {
                // case 1: the total width of the tokens on the current line is greater than the
                // textbox width, so the current token is starting a new line
                tokens_width = current_token_width;
            } else if (current_token_width > textbox_width) {
                // case 2: the width of the current token is greater than the textbox width
                tokens_width = 0;
            } else {
                // case 3: the current token fits on the current line
                tokens_width = new_tokens_width;
            }
        }
        var new_size = textbox_width - tokens_width;
        var current_text_length = this.element.value.length * 7;
        if (current_text_length > new_size) {
            // the current input text spills over to the next line, so the input should cover the full width
            new_size = textbox_width;
        }
        this.set_input_size(new_size);
    },

    onKeyPress: function (event) {
        // if at any point, we can determine that the typeahead doesn't need
        // updated, function exits early. reasons for exit documented inline

        this.dynamically_resize_input();

        if (this.active) {
            // active is when there are suggestions found
            switch (event.keyCode) {
            case 44: // comma
            case 188: // comma
            case 59: // semicolon
            case 186: // semicolon
                // these could be comma's, semicolon's etc
                // we should let our event observer pick them up
                this.reset_observer();
                return;
            case Event.KEY_TAB:
            case Event.KEY_RETURN:
                // user selected contact from autocompleter
                this.selectEntry();
                this.hide();
                this.active = false;
                Event.stop(event);
                return;
            case Event.KEY_ESC:
                this.hide();
                this.active = false;
                Event.stop(event);
                return;
            case Event.KEY_LEFT:
            case Event.KEY_RIGHT:
                // user is  moving cursor in typeahead
                return;
            case Event.KEY_UP:
                // user is changing autocomplete selection
                this.markPrevious();
                this.render();
                Event.stop(event);
                return;
            case Event.KEY_DOWN:
                // user is changing autocomplete selection
                this.markNext();
                this.render();
                Event.stop(event);
                return;
            }
        }
        else {
            // no suggestions were found

            // first, tokenize all emails before last delimeter, in case the
            // user pasted email addresses
            this.tokenize_emails_input(false);

            if (event.keyCode === Event.KEY_TAB ||
                event.keyCode === Event.KEY_RETURN
                ) {
                // user is trying to tokenize custom email address
                // our event observer will pick up other valid delimiters
                // because comma and semicolon generate false positives
                // for @ signs on foreign keyboards

                // since things like tab are valid ways to select a contact,
                // but we want to prevent their default behavior if there is
                // a contact to select
                if (this.element.value && event.preventDefault) {
                    event.preventDefault();
                }

                this.tokenize_emails_input(true);

                return;
            } else {
                if (this.element.value === "") {
                    if (event.keyCode == Event.KEY_LEFT ||
                        event.keyCode == Event.KEY_BACKSPACE) {
                        // user is trying to delete or select existing token
                        this.token_manager.shift_left();
                    }
                }
                else {
                    if (event.keyCode == Event.KEY_LEFT ||
                        event.keyCode == Event.KEY_RIGHT ||
                        event.keyCode == Event.KEY_UP ||
                        event.keyCode == Event.KEY_DOWN) {
                        // user moved cursor in typeahead
                        return;
                    }
                }
            }
        }

        this.update_typeahead();
        this.reset_observer();
    },

    update_typeahead: function () {
        this.changed = true;
        this.hasFocus = true;
    },

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

    onObserverEvent: function ($super) {
        if (this.active) {
            // check to see if a user had tried to autocomplete a contact by
            // looking for valid delimiters

            var delimiter;
            var delimiter_index;
            var val = this.element.value;
            for (var ii = 0; ii < this.options.tokens.length; ii++) {
                delimiter = this.options.tokens[ii];
                delimiter_index = val.indexOf(delimiter);
                if (delimiter_index != -1) {
                    this.element.value = val.substr(0, delimiter_index);
                    this.selectEntry();
                    this.hide();
                    this.active = false;

                    // preserve the user's input after the delimiter
                    this.element.value = val.substr(delimiter_index + 1);
                    break;
                }
            }

            this.update_typeahead();
        }

        this.tokenize_emails_input(false);
        $super();
    },

    selectEntry: function () {
        var entry = this.getCurrentEntry();
        var elem = this.options.array[entry.value];

        var input_val;
        if (elem.type == ContactTypes.FB) {
            input_val = elem.fb_id;
        } else {
            input_val = elem.email;
        }
        // temporarily reduce the width of the input element to 1 so that
        // it doesn't spill over to the next line when the token is added
        this.set_input_size(1);
        addContactToList(
            elem.name.strip() || input_val,
            input_val,
            elem.type,
            this.element,
            this.hidden_input,
            this.token_manager,
            true
        );

        Forms.clear_errors(this.element.up('form'));

        this.dynamically_resize_input();

        this.index = 0;
        this.element.focus();
    }
});

var Crocodoc = (function () {
    var RETRY_INTERVAL = 1; // in seconds

    var _retry = function (path) {
        setTimeout(function () {
            Crocodoc.load(path);
        }, RETRY_INTERVAL * 1000);
    };

    var _error = function () {
        $("crocodoc_loading").hide();
        $("crocodoc_error").show();
    };

    var _showdoc = function (iframe_url) {
        var iframe = new Element("iframe", {
            src: iframe_url
        });
        iframe.addClassName("crocodoc_iframe");
        $("crocodoc_viewer").update(iframe);
    };

    var _load = function (path) {
        var req = new Ajax.Request("/sm/doc/"  + path, {
            onSuccess: function (req) {
                var resp = req.responseText;
                if (resp == "error") {
                    _error();
                } else if (resp == "retry") {
                    _retry(path);
                } else {
                    _showdoc(resp);
                }
            }
        });
    };

    return {
        'load': function (sub_path) {
            // sub_path is '/:tkey/*path'
            _load(sub_path);
        }
    };
})();


var Modal, Sprite;

var Bubble = {
    make: function (content, tail_side, tail_position, width) {
        tail_side = tail_side || "left";
        if (["left", "right"].contains(tail_side)) {
            tail_position = tail_position || "middle";
            assert(["top", "middle", "bottom"].contains(tail_position),
                "expected tail position ['top', 'middle', 'bottom'], got %s".format(tail_position));
        } else if (["bottom"].contains(tail_side)) {
            tail_position = tail_position || "center";
            assert(["left", "center", "right"].contains(tail_position),
                "expected tail position ['left', 'center', 'right'], got %s".format(tail_position));
        } else {
            assert(false, "unexpected tail positon, got %s".format(tail_position));
        }

        var table = new Element("table");
        table.addClassName("bubble");
        if (width) {
            table.style.width = width + "px";
        }

        var tbody = new Element("tbody");

        var tr1 = new Element("tr");
        var tl = new Element("td");
        tl.addClassName("tl");
        var t = new Element("td");
        t.addClassName("t");
        var tr = new Element("td");
        tr.addClassName("tr");

        tr1.insert(tl);
        tr1.insert(t);
        tr1.insert(tr);

        tbody.insert(tr1);

        var tr2 = new Element("tr");
        var l = new Element("td");
        l.addClassName("l");

        if (tail_side == "left") {
            var arrow = new Element("img", {'src': "/static/images/bubble_arrow.png"});
            arrow.addClassName("arrow");
            l.insert(arrow);
            l.vAlign = tail_position;
        }
        var c = new Element("td");
        c.addClassName("c");
        c.update(content);

        var r = new Element("td");
        r.addClassName("r");

        if (tail_side == "right") {
            var rarrow = new Element("img", {'src': "/static/images/bubble_arrow_right.png"});
            rarrow.addClassName("rarrow");
            r.insert(rarrow);
            r.vAlign = tail_position;
        }

        tr2.insert(l);
        tr2.insert(c);
        tr2.insert(r);

        tbody.insert(tr2);

        var tr3 = new Element("tr");
        var bl = new Element("td");
        bl.addClassName("bl");

        var b = new Element("td");
        b.addClassName("b");

        if (tail_side == "bottom") {
            var barrow = new Element("img", {'src': "/static/images/bubble_arrow_bottom.png"});
            barrow.addClassName("barrow");
            b.insert(barrow);
            b.style.textAlign = tail_position;
        }

        var br = new Element("td");
        br.addClassName("br");

        tr3.insert(bl);
        tr3.insert(b);
        tr3.insert(br);

        tbody.insert(tr3);
        table.insert(tbody);
        return table;
    }
};


var ActAsBlock = {
    elm_list: ["margin-left", "margin-right", "padding-left", "padding-right", "border-left-width", "border-right-width"],
    parent_list: ["padding-left", "padding-right", "border-left-width", "border-right-width"],
    register: function (e, elm) {
        elm = elm || document.body;
        var elms = $(elm).getElementsByClassName("act_as_block");

        for (var i = 0; i < elms.length; i = i + 1) {
            ActAsBlock.resize(elms[i]);
        }

    },
    resize: function (elm) {
        elm = $(elm);

        var parent = elm.up();
        var elm_total = Util.sumStyles(elm, ActAsBlock.elm_list);
        var parent_total = Util.sumStyles(parent, ActAsBlock.parent_list);

        elm.style.width = "1px";

        var width = (parent.getWidth() - elm_total - parent_total);

        if (width > 0) {
            elm.style.width = width + "px";
        }
    }
};
Event.observe(window, 'load', ActAsBlock.register);


Modal = {
    show: function (title, content, vars, focus, width, keep_content) {
        $$("#modal-content .error-message, #modal-content .error-removable").invoke("hide");

        if (FileQueue.uploading && !keep_content) {
            alertd(_("You can't do this while uploading."));
            return false;
        }

        assert(content, "Missing modal content!");

        Modal.vars = vars || {};
        // var icon = Modal.vars.icon || 'help'; // split-todo unused?
        width = width || 520;

        var modal_top = (document.viewport.getScrollOffsets().top + 150);
        Util.scry('modal').setStyle({top: modal_top + "px"});
        Util.scry('modal').setStyle({width: width + "px", margin: "0 0 0 " + Math.floor(-width / 2).toString() + "px"});
        Util.scry('modal-title').update(title);

        if (!keep_content) {
            if (FileQueue.numShown()) {
                Upload.reset();
            }
            var first_kid = Util.childElement($('modal-content'), 0);
            if (first_kid && first_kid != content) {
                $('grave-yard').insert(first_kid);
            }

            var content_div = new Element('div');
            content_div.update(content);

            var wit_group = Modal.vars.wit_group;

            if (!wit_group) {
                var content_child_elm = content_div.down();
                wit_group = content_child_elm && content_child_elm.id;
            }

            if (wit_group) {
                WIT.add_group(content_div, wit_group);
            }

            Util.scry('modal-content').insert(content_div);

            if (content.show) {
                content.show();
            }

            Element.show('modal');
        }
        Util.scry('modal-overlay').setOpacity(0.6);
        Util.scry('modal-overlay').show();
        Util.scry('modal-behind').setStyle({height: (Util.scry('modal').getHeight() + 20) + "px",
                                            width: (Util.scry('modal').getWidth() + 20) + "px",
                                            margin: "0 0 0 " + Math.floor(-width / 2 - 10).toString() + "px",
                                            top: (modal_top - 10) + "px"});
        Util.scry('modal-behind').setOpacity(0.2);
        Util.scry('modal-behind').show();

        if (focus) {
            $('modal-content').select("#" + focus.id).first().focus();
        } else {
            if (!Util.ie) {
                var firstButton = Util.scry('modal').down('input[type=button]') || Util.scry('modal').down('input[type=submit]');
                if (firstButton) {
                    firstButton.focus();
                }
            }
        }

        if (!Modal.track_id) {
            Modal.track_resizes();
        }
        $("modal-title").show();
        ActAsBlock.register(false, "modal");
        document.observe("keydown", Modal.keydown);
        $("modal-content").style.height = "auto";
        return false;
    },
    keydown: function (e) {
        var key = BrowseKeys.getKey(e);

        if (key == 27) {
            Modal.hide();
        }
    },
    icon_show: function (icon, title, content, vars, focus, width, keep_content) {
        var fancy_title = new Element("div");
        fancy_title.insert(Sprite.make(icon, {'class': 'modal-h-img'}));
        fancy_title.insert(title);

        return Modal.show(fancy_title, content, vars, focus, width, keep_content);
    },
    show_loading: function (icon, title) {
        var content = "<p style='margin: 3em 0; text-align: center;'><img src='/static/images/icons/ajax-loading-small.gif' alt=''/></p>";
        Modal.icon_show(icon, title, content);
    },
    shown: function () {
        return Util.scry('modal').visible();
    },
    hide: function (e) {
        if (e) {
            Event.stop(e);
        }

        if (Modal.onHide) {
            var result = Modal.onHide();
            if (!result) {
                return;
            }
        }
        Modal.onHide = null;

        Element.hide('modal-behind');
        Element.hide('modal-overlay');

        if (!FileQueue.numShown()) {
            Element.hide('modal');
        } else {
            $("modal").style.marginLeft = "-10000000px";
            if (FileQueue.uploading) {
                InlineUploadStatus.show();
            }
        }

        if (Modal.track_id) {
            clearInterval(Modal.track_id);
            Modal.track_id = false;
        }
        document.stopObserving("keydown", Modal.keydown);
    },
    track_resizes: function () {
        Modal.track_id = setInterval(Modal.resize_bg, 150);
    },
    resize_bg: function () {
        var h = Util.scry('modal').getHeight();
        if (Modal.old_height != h || Util.scry('modal-behind').getHeight() < h) {
            Modal.old_height = h;
            Util.scry('modal-behind').setStyle({height: (h + 20) + "px"});
        }
    },
    vars: {}
};


var Tabs = {
    init: function ()
    {
        var tabs_li = $A(document.getElementsByClassName("tab")).concat($A(document.getElementsByClassName("subtab")));
        for (var i = 0;i < tabs_li.length; i++)
        {
            var a = tabs_li[i].down("a");
            var url = a.href.split("/");
            if (tabs_li[i].hasClassName("subtab")) {
                a.href = "#" + url[url.length - 1];
            }

            if (Util.ie6 || Prototype.Browser.Opera) {
                var width = a.getWidth() - parseInt(a.getStyle("padding-left"), 10) * 2;
                width = (width + 2 + (width % 2)); // Hacks, IE6 - width must be even for absolute positioning to work properly
                a.style.width = width + "px";
            }

            var tl = Sprite.make("rounded_tl", { 'class': 'rounded_tl'});
            var tr = Sprite.make("rounded_tr", { 'class': 'rounded_tr'});
            a.appendChild(tl);
            a.appendChild(tr);
        }

        i = 20;
        $$(".events_bubble").each(function (elm) {
            var neg_half_width = (-1 * elm.getWidth() / 2) + "px";
            elm.style.marginLeft = neg_half_width;
            elm.style.marginRight = neg_half_width;
            elm.style.right = "6px";
            elm.parentNode.style.zIndex = i--; // For IE7+ - it sucks at zIndex's - element takes the zIndex of the parentNode
        });
    },

    check_url: function (defaulttab)
    {
        var current = Util.url_hash();
        if (!current || Tabs.last_shown == current) {
            return;
        }

        Tabs.last_shown = current;
        if (Util.url_hash()) {
            Tabs.showTab(Util.url_hash() + '-tab', Util.url_hash());
        } else {
            Tabs.showTab(defaulttab + '-tab', defaulttab);
        }
    },

    showTab: function (elm, tab)
    {
        //  Get element and remove the highlight from the link
        elm = $(elm);
        if (elm) {
            elm.fire("db:tabshown");
        }
        // Set all the tabs to not selected
        var tab_list = document.getElementsByClassName("subtab");
        var i;
        for (i = 0; i < tab_list.length; i++) {
            tab_list[i].removeClassName("selected");
        }

        // Set all the content to be hidden
        var content_tabs = document.getElementsByClassName("content-tab");

        for (i = 0; i < content_tabs.length; i++) {
            content_tabs[i].hide();
        }

        // Show the selected content
        var sTab = $(tab + "-tab") || $$(".subtab").first();
        var sContent = $(tab + "-content") || $$(".content-tab").first();
        if (sTab) {
            sTab.addClassName("selected");
        }
        if (sContent) {
            sContent.show();
            Util.syncHeight();
            var inputs = sContent.select('input[type=text]', 'textarea');
            if (inputs) {
                Util.focus(inputs[0]); // focus on the first input area on the tab
            }
        }
        return false;
    }
};


var TreeView = {
    disable_shares: false,
    tv: {},
    loaded: false,
    set_params: function (params) {
        TreeView.ajax_params = params;
    },
    init: function (h, autohide, tree_id) {
        tree_id = tree_id || 'treeview';
        TreeView.tv[tree_id] = {};
        var treeview = TreeView.tv[tree_id];

        treeview.autohide = autohide === null ? true : autohide;
        treeview.handler = h;
        treeview.viewdiv = $(tree_id);
        treeview.hidefunc = TreeView.hide.bindAsEventListener(this);
    },
    schedule_reset: function () {
        TreeView.loaded = false;
    },
    reset: function (options) {
        new Ajax.DBRequest("/ajax_subtreeview", {
            parameters: TreeView.ajax_params,
            onSuccess: function (req) {
                for (var tree_id in TreeView.tv) {
                    if (TreeView.tv.hasOwnProperty(tree_id)) {
                        TreeView.tv[tree_id].viewdiv.down(".treeview-folders").update(req.responseText);
                        if (options && options.onSuccess) {
                            options.onSuccess(req);
                        }
                    }
                }
            }
        });
    },
    toggle: function (e, tree_id) {
        Event.stop(e);
        var treeview = TreeView.tv[tree_id || 'treeview'];

        if (treeview.shown) {
            treeview.shown = false;
            TreeView.hide(e, tree_id);
        } else {
            treeview.shown = true;
            TreeView.show(e.target, tree_id);
        }
        return false;
    },
    hide: function (e, tree_id) {
        var treeview = TreeView.tv[tree_id || 'treeview'];
        if (!e || !$(e.target).descendantOf(treeview.viewdiv)) {
            treeview.viewdiv.hide();
            Event.stopObserving(window, 'click', treeview.hidefunc);
            treeview.shown = false;
        }
    },
    show: function (link, tree_id) {
        var treeview = TreeView.tv[tree_id || 'treeview'];

        link = $(link);
        link.blur();
        var linkPos = link.cumulativeOffset();
        treeview.viewdiv.setStyle({'top': (linkPos.top + link.getHeight()) + "px", 'left': (linkPos.left - 4) + "px"});
        treeview.viewdiv.show();
        Event.observe(window, 'click', treeview.hidefunc);
    },
    toggleNode: function (button) {
        button = $(button);

        var img = button.down('img');
        if (img.className.match("bullet_plus")) {
            Sprite.replace(img, "bullet_plus", "bullet_minus");
        } else {
            Sprite.replace(img, "bullet_minus", "bullet_plus");
        }
        button.up().next('div').toggle();
        button.blur();
        return false;
    },
    toggleNodeAjax: function (button, parent_path) {
        if (button.fetched_children) {
            return TreeView.toggleNode(button);
        }
        button = $(button);
        var img = button.down('img');
        var orig_sprite = Sprite._get(img);
        img.src = "/static/images/icons/ajax-loading-small.gif";

        new Ajax.DBRequest("/ajax_subtreeview" + parent_path, {
            parameters: TreeView.ajax_params,
            onSuccess: function (req) {
                var d =  new Element("div", {style: "display: none;"}).update(req.responseText);
                button.up().insert({after: d});
                button.fetched_children = true;

                Sprite._set(img, orig_sprite);
                return TreeView.toggleNode(button);
            },
            cleanUp: function (ok) {
                if (/loading/.match(img.src)) {
                    Sprite._set(img, orig_sprite);
                }
            }
        });

        return false;
    },
    handle: function (path, obj) {
        var tree_ids = $H(TreeView.tv).keys();
        var treeview = $(obj).ancestors().find(function (div) {
            return tree_ids.include(div.id);
        });
        if (!treeview) {
            return;
        }

        treeview = TreeView.tv[treeview.id];
        $("modal").fire("db:treeview_selected", {path: path});
        if (treeview.handler) {
            treeview.handler(path, obj);
        }
        if (treeview.autohide) {
            TreeView.hide(treeview.id);
        }
    },
    move: function (tree_id, location_id, options) {
        var tree = $(tree_id);
        if (!TreeView.loaded) {
            TreeView.reset({
                onSuccess: function () {
                    TreeView.loaded = 1;
                    TreeView.move(tree_id, location_id, options);
                }
            });
        } else {
            if (options && options.onSuccess) {
                options.onSuccess();
            }
        }
        assert(tree, "Couldn't find tree_id");
        assert($(location_id), "Couldn't find location_id");

        $(location_id).appendChild(tree);
        tree.show();
    },
    disable_shared: function (tree_id) {
        var tree = $(tree_id);

        if (tree.share_disabled) {
            return;
        }
        tree.share_disabled = true;

        var shared_folder_imgs = tree.select('.s_folder_user'); // abstraction violation. it's for performance...
        var l = shared_folder_imgs.length;
        for (var i = 0; i < l; i++) {
            var elm = shared_folder_imgs[i];
            Sprite.replace(elm, 'folder_user', 'folder_user_gray');

            var link = elm.up();
            link._onclick = link.onclick;
            link.onclick = Util.nop;
        }
    },
    enable_shared: function (tree_id) {
        var tree = $(tree_id);

        if (!tree.share_disabled) {
            return;
        }
        tree.share_disabled = false;

        var shared_folder_imgs = tree.select('.s_folder_user_gray'); // abstraction violation. it's for performance...
        var l = shared_folder_imgs.length;
        for (var i = 0; i < l; i++) {
            var elm = shared_folder_imgs[i];
            Sprite.replace(elm, 'folder_user_gray', 'folder_user');

            var link = elm.up();
            link.onclick = link._onclick;

        }
    }
};


Sprite = {
    SPACER: "/static/images/icons/icon_spacer.gif",
    src: function (elm, name) {
        elm = $(elm);
        Sprite.clear(elm);
        elm.addClassName("s_" + name);
    },
    current: function (elm) {
        var sprites = $(elm).classNames().findAll(function (x) {
            return !x.indexOf("s_");
        });
        return sprites.length ? sprites[sprites.length - 1].substr(2) : "";
    },
    replace: function (elm, orig, replacement) {
		elm.removeClassName("s_" + orig);
        elm.addClassName("s_" + replacement);
    },
    toggle: function (elm, icon1, icon2) {
        elm = $(elm);

        if (elm.hasClassName("s_" + icon1)) {
            elm.removeClassName("s_" + icon1);
            elm.addClassName("s_" + icon2);
        } else if (elm.hasClassName("s_" + icon2)) {
            elm.removeClassName("s_" + icon2);
            elm.addClassName("s_" + icon1);
        }
    },
    blue: function (icon) {
        return icon + "_blue";
    },
    clear: function (elm) {
        elm = $(elm);
        elm.className = elm.classNames().reject(function (x) {
            return !x.indexOf("s_");
        }).join(" ");
    },
    make: function (name, attr) {
        attr = attr || {};
        attr.src = Sprite.SPACER;

        var classNames = "sprite s_" + name + " " + (attr['class'] || "");
        var img = new Element("img", attr);
        img.addClassName(classNames);
        return img;
    },
    html: function (name, attr) {
        var elm = Sprite.make(name, attr);
        var div = new Element("div");
        div.update(elm);
        return div.innerHTML;
    },
    _get: function (elm) {
        return elm.className;
    },
    _set: function (elm, val) {
        elm.className = val;
        elm.src = Sprite.SPACER;
    }
};


var Dropdown = {
    init: function () {
        $$("#tabs-container > ul > li").each(
            function (elm) {
                elm.observe("mouseenter", Dropdown.over);
                elm.observe("mouseleave", Dropdown.out);
            }
        );
    },

    over: function (event) {
        clearTimeout(Dropdown.timeout);
        $$("#tabs-container > ul > li.hover").invoke("removeClassName", "hover");
        var elm = $(event.target);
        if (!elm.match("#tabs-container > ul > li")) {
            elm = elm.up("#tabs-container > ul > li");
        }
        elm.addClassName("hover");
    },

    out: function (event) {
        var elm = $(event.target);
        if (!elm.match("#tabs-container > ul > li")) {
            elm = elm.up("#tabs-container > ul > li");
        }
        Dropdown.timeout = setTimeout(function () {
                elm.removeClassName("hover");
            }, 300);

    }
};


var HotButton = {
    make: function (content) {
        var anchor = new Element("a");
        anchor.addClassName("hotbutton");

        var wrapper = new Element("span");
        wrapper.addClassName("hotbutton-content");
        wrapper.update(content);

        anchor.update(wrapper);

        var shadow = new Element("span");
        shadow.addClassName("shadow");
        anchor.insert(shadow);

        return HotButton.register(anchor);
    },

    register: function (anchor) {
        var icon = anchor.select(".hotbutton-icon").pop();
        if (icon) {
            anchor._icon = icon;
            anchor._sprite = Sprite.current(icon);
        }

        anchor.observe("mouseenter", function () {
            HotButton.mouseenter(anchor);
        });

        anchor.observe("mouseleave", function () {
            HotButton.mouseleave(anchor);
        });

        anchor.observe("mousedown", function (e) {
            HotButton.mousedown(e, anchor);
        });

        anchor.observe("mouseup", function () {
            HotButton.mouseup(anchor);
        });

        Util.disableSelection(anchor);
        return anchor;
    },

    mouseenter: function (anchor) {
        anchor.addClassName("hover");
        anchor.style.zIndex = 1;
    },

    mouseleave: function (anchor) {
        anchor.removeClassName("hover");
        anchor.removeClassName("down");
        anchor.removeClassName("hover_swap");
        anchor.style.zIndex = 0;
        if (anchor._icon) {
            Sprite.src(anchor._icon, anchor._sprite);
        }
    },

    mousedown: function (e, anchor) {
        anchor.addClassName("down");
        anchor.addClassName("hover_swap");
        if (anchor._icon) {
            Sprite.src(anchor._icon, Sprite.blue(anchor._sprite));
        }
        Event.stop(e);
    },

    mouseup: function (anchor) {
        anchor.removeClassName("down");
        anchor.removeClassName("hover_swap");
        if (anchor._icon) {
            Sprite.src(anchor._icon, anchor._sprite);
        }
    }
};


var LiveSearch = {
    search: function (search_string, update_elm, action, callbacks, shortened) {
        search_string = search_string.strip();

        if (search_string.length < 3) {
            $(update_elm).update("");
            if (callbacks.onEmpty) {
                callbacks.onEmpty(search_string);
            }
        }

        else {
            var params = {'search_string': search_string,
                          'short': shortened ? 1 : '' };

            new Ajax.Request(action, {
                parameters: params,
                method: 'get',
                onSuccess: function (req) {
                    var text = req.responseText.strip();

                    if (!text) {
                        if (callbacks.onEmpty) {
                            callbacks.onEmpty(search_string);
                        }
                        return;
                    }

                    $(update_elm).update(req.responseText);
                    LiveSearch.highlight(update_elm, search_string);
                    if (callbacks.onComplete) {
                        callbacks.onComplete(search_string);
                    }
                }
            });
        }
    },

    highlight: function (elm, search) {
        var search_parts = search.split(" ");

        search_parts.each(
            function (search_string) {
                if (search_string.length < 4) {
                    return;
                }
                var regx = new RegExp(RegExp.escape(search_string), "i");
                elm = $(elm);
                $$('.livesearch_result_a').each(
                    function (elm) {
                        elm.innerHTML = elm.innerHTML.gsub(regx, function (match) {
                            return '<span class=\'highlight\'>' + match[0] + '</span>';
                        });
                    }
                );
                $$('.livesearch_result_p').each(
                    function (elm) {
                        elm.innerHTML = elm.innerHTML.stripTags().gsub(regx, function (match) {
                            return '<span class=\'highlight\'>' + match[0] + '</span>';
                        });
                    }
                );
            }
        );
    },
    MAX_RESULTS: 10
};


var DBDropdown = Class.create({
    initialize: function (container_id, options_list, options) {
        this.options = options || {};
        this.container = $(container_id);
        assert(this.container, "Couldn't find element for DBDropdown " + container_id);
        this.container.style.position = "relative";

        assert(options_list && options_list.length, "Missing options_list: " + options_list);
        this.display_options = [];
        this.display_value = {};

        for (var i = 0; i < options_list.length; i += 1) {
            var item_display;
            if (options_list[i].length > 2) {
                item_display = [options_list[i][1], options_list[i][2]];
            } else {
                item_display = [options_list[i][1]];
            }
            this.display_options.push(item_display);
            this.display_value[options_list[i][1]] = options_list[i][0];
        }

        var hotbutton_content = '';
        if (this.options.icon || this.options.prefix) {
            hotbutton_content += '<span class=\'prefix\'>';

            if (this.options.icon) {
                if (!this.options.no_hover) {
                    hotbutton_content += Sprite.html(this.options.icon, {
                        className: 'icon_no_hover'
                    });
                    hotbutton_content += Sprite.html(this.options.icon + "_blue", {
                        className: 'icon_hover'
                    });
                } else {
                    hotbutton_content += Sprite.html(this.options.icon);

                }
            }
            if (this.options.prefix) {
                hotbutton_content += this.options.prefix;
            }
            hotbutton_content += "</span>";
        }

        var initial;
        if (this.options.initial_value) {
            initial = this.options.initial_value;
        } else {
            initial = this.display_options[0][0];
        }

        hotbutton_content += "<span class='dbdropdown-selected'>" + initial + "</span>";
        var arrow = this.options.arrow || "big-dropdown";
        if (!this.options.no_hover) {
            hotbutton_content += Sprite.html(arrow + "_blue", {
                className: 'icon_hover'
            });
            hotbutton_content += Sprite.html(arrow, {
                className: 'icon_no_hover'
            });
        } else {
            hotbutton_content += Sprite.html(arrow);
        }
        this.hotbutton = HotButton.make(hotbutton_content);
        this.hotbutton.addClassName("dbdropdown");
        this.hotbutton.name = $(container_id).identify();

        this.container.update(this.hotbutton);
        if (this.options.style) {
            for (var key in this.options.style) {
                if (this.options.style.hasOwnProperty(key)) {
                    this.hotbutton.style[key] = this.options.style[key];
                }
            }
        }
        this.observe();
    },
    observe: function () {
        this.hotbutton.observe("mouseup", (function (e) {
            this.mouseup(e);
        }).bind(this));

        $(document.body).observe("mouseup", (function () {
            this.hide_list();
        }).bind(this));
    },
    mouseup: function (e) {
        if (this.showing) {
            this.hide_list();
        } else {
            this.show_list();
        }
        Event.stop(e);
    },
    show_list: function () {
        var that = this;
        var ul = new Element("ul");
        Util.disableSelection(ul);
        ul.addClassName("dbdropdown-list");
        this.display_options.each(function (opts) {
            var opt = opts[0],
                icon = '',
                blue_icon = '';

            if (opts.length > 1) {
                icon = Sprite.html(opts[1], {'className': 'link-img icon_no_hover'});
                blue_icon = Sprite.html(opts[1] + "_blue", {'className': 'link-img icon_hover'});

            }
            var li = new Element("li");
            li.addClassName("wit");
            li.name = opt;
            li.update(icon + blue_icon + opt);
            li.observe("click", Event.stop);
            li.observe("mouseup", function (e) {
                Event.stop(e);
                that.select(opt);
                that.hide_list();
            });
            li.observe("mouseenter", function () {
                this.addClassName("over");
            });
            li.observe("mouseleave", function () {
                this.removeClassName("over");
            });
            ul.appendChild(li);
        });

        this.container.appendChild(ul);
        ul.clonePosition(this.hotbutton, {
            setTop: false,
            setHeight: false
        });
        ul.style.width = parseInt(ul.style.width, 10) - 2 + "px";

        var pos = this.container.getHeight() - (Prototype.Browser.IE ? 2 : 0) + "px";
        if (this.options.show_above) {
            ul.style.bottom = pos;
        } else {
            ul.style.top = pos;
        }
        this.showing = true;
    },
    hide_list: function () {
        var list = this.container.down(".dbdropdown-list");
        if (list) {
            list.remove();
        }
        this.showing = false;
    },
    select: function (display_str) {
        var value = this.display_value[display_str];
        assert(value, "Value is missing...");
        var contentelm = this.container.down(".dbdropdown-selected");
        assert(contentelm, "select missing contentelm");
        contentelm.update(display_str);
        if (this.options.on_change) {
            this.options.on_change(value);
        }
    }
});


var StarRating = Class.create({
    initialize: function (container_id, value) {
        this.container = $(container_id);
        this.value = value || 1;
        this.stars = this.generate_stars();
        this.input = new Element("input", {
            'name': 'rating',
            'type': 'hidden'
        });
        this.input.setValue(this.value);
        assert(this.container, "StarRating missing container");
        this.render();
    },
    generate_stars: function () {
        var yellow_stars = this.value;

        var stars = [];
        for (var i = 0; i < 5; i += 1) {
            stars.push(this.generate_star(i + 1, i < yellow_stars));
        }
        return stars;
    },
    generate_star: function (val, is_yellow) {
        var img = is_yellow ? 'star_blue_on_big' : 'star_blue_off_big';
        img = Sprite.make(img);

        var that = this;
        img.observe("click", function (e) {
            that.click(e, val);
        });
        img.observe("mouseover", function (e) {
            that.set_stars(val);
        });

        return img;
    },
    click: function (e, val) {
        assert(val, "star click is missing value");
        if (e) {
            Event.stop(e);
        }
        this.set_val(val);
    },
    render: function () {
        var fragment = new Element("a", {
            href: "#"
        });
        var that = this;
        fragment.observe("mouseleave", function () {
            that.set_stars(that.value);
        });

        fragment.observe("click", Event.stop);
        fragment.addClassName("ratingstars");
        for (var i = 0; i < this.stars.length; i += 1) {
            fragment.appendChild(this.stars[i]);
        }
        fragment.appendChild(this.input);
        this.container.update(fragment);
    },
    get_val: function () {
        return this.value;
    },
    set_stars: function (val) {
        assert(val > 0, "Star value was < 1");
        assert(val <= 5, "Star value was > 5");

        for (var i = 0; i < 5; i += 1) {
            var star = this.stars[i];
            if (i < val) {
                Sprite.replace(star, "star_blue_off_big", "star_blue_on_big");
            } else {
                Sprite.replace(star, "star_blue_on_big", "star_blue_off_big");
            }
        }
    },
    set_val: function (val) {
        this.set_stars(val);
        this.value = val;
        this.input.setValue(val);
    }
});


var ThumbVote = {
    close: function (elm) {
        var cont = $("thumbs-cont");
        cont.hide();
    },
    decline: function () {
        new Ajax.DBRequest("/thumbs", {
            parameters: {
                declined: 'ajax'
            }
        });
    },
    up_vote: function () {
        new Ajax.DBRequest("/thumbs", {
            parameters : {
                up: 1
            }
        });
        ThumbVote.close();
        ThumbVote.show_feedback(1);
    },
    down_vote: function () {
        new Ajax.DBRequest("/thumbs", {
            parameters : {
                down: 1
            }
        });
        ThumbVote.close();
        ThumbVote.show_feedback(0);
    },
    show_feedback: function (up) {
        assert(up !== undefined, "up is missing");
        if (up) {
            $("upvote-msg").show();
            $("downvote-msg").hide();
        } else {
            $("upvote-msg").show();
            $("downvote-msg").hide();
        }
        $("thumb_vote_positive").setValue(up);
        Modal.icon_show("comments", _("Thanks for the feedback!"), $("thumbs-feedback-modal"));
        var input = $("thumb-comments");
        input.setValue();
        input.focus();
    },
    submit_feedback: function (e) {
        var form = $("thumbs-feedback-form");
        assert(form, "Missing thumbs form");

        Forms.ajax_submit(form, false, function () {
            Modal.hide();
        }, false, e.target);
    }
};


var LocaleSelector = {
    init: function () {
        var cont = $("locale_selector");

        if (!cont) {
            return;
        }

        var initial_display;
        for (var i = 0, len = Constants.LOCALES.length; i < len; i += 1) {
            if (Constants.LOCALES[i][0] == Constants.USER_LOCALE) {
                initial_display = Constants.LOCALES[i][1];
            }
        }

        var have_hover = false;
        new DBDropdown(cont, Constants.LOCALES, {
            'on_change' : function (val) {
                LocaleSelector.change(val);
            },
            'initial_value': initial_display,
            'icon': 'world_grey',
            'no_hover': !have_hover,
            'arrow': 'big-dropdown-gray'
        });
    },
    change: function (val) {
        if (val == Constants.USER_LOCALE) {
            return;
        }

        var form = new Element("form", {
            'action': 'https://' + Constants.WEBSERVER + '/set_locale',
            'method': 'post'
        });

        Forms.add_vars(form, {
            'locale': val,
            'locale_cont': window.location.href
        });
        document.body.appendChild(form);
        form.submit();

    }
};


var Notify = {
    ServerError: function (msg) {
        return Notify.showDiv(msg, 'server-error', _("There was a problem completing this request."), "#ffdddd", "#fff0f0");
    },
    ServerSuccess: function (msg) {
        return Notify.showDiv(msg, 'server-success', _("Your request completed successfully."), "#e5fdd0", "#f7fff0");
    },
    showDiv: function (msg, div, default_msg, startcolor, endcolor) {
        var is_important = false;
        if (msg && msg.indexOf('important') === 0) {
            is_important = true;
            msg = msg.substr(10);
        }

        Notify.last_msg = msg;
        Notify.clearAll();

        msg = msg || default_msg;
        var box = Util.scry(div);
        if (!box) {
            return;
        }
        box.down('span').update(msg);

        if (!Notify.dont_center) {
            Util.center(box);
        }
        var ef;
        ef = new Effect.BlindFadeDown(box, {duration: 0.5, scaleTo: 75, queue: {scope: 'notify'}});
        ef = new Effect.Flash($(div).down("td"),  { cycles: 3, startcolor: startcolor, endcolor: endcolor });
        if ($(div).down(".r0")) {
            ef = new Effect.Flash($(div).down(".r0"), { cycles: 3, startcolor: startcolor, endcolor: endcolor });
        }
        if (!is_important) {
            ef = new Effect.BlindFadeUp(box, {duration: 0.3, scaleFrom: 75, delay: 10, queue: {scope: 'notify'}});
        }

        if (Util.ie6) {
            box.scrollTo();
        }
    },
    clearAll: function () {
        $$('.notify').invoke('hide');

        // and clear out any outstanding effects
        var q = Effect.Queues.get('notify');
        q.effects = [];
        clearInterval(q.interval);
        q.interval = null;
    },
    clearIf: function (msg) {
        if (Notify.last_msg == msg) {
            Notify.clearAll();
        }
    }
};


var LoginDropdown = {
    init: function () {
        var login_link = $("login-hover-link");
        if (!login_link) {
            return;
        }
        LoginDropdown.login_link = login_link;
        LoginDropdown.register();
    },
    register: function (e) {
        LoginDropdown.login_link.observe("click", LoginDropdown.click);
        LoginDropdown.login_link.observe("mouseenter", LoginDropdown.over);
        LoginDropdown.login_link.observe("mouseleave", LoginDropdown.out);
        LoginDropdown.login_link.observe("focus", LoginDropdown.click);
        $("login_email_elm").observe("focus", LoginDropdown.click);
        $(document.body).observe("click", LoginDropdown.unclick);
    },
    over: function (e) {
        LoginDropdown.hover();
    },
    out: function  (e) {
        LoginDropdown.unhover();
    },
    click: function (e) {
        Event.stop(e);
        LoginDropdown.hover();
        LoginDropdown.down = true;
        LoginDropdown.login_link.up().addClassName("down");
        $("login_email_elm").focus();
    },
    unclick: function (e) {
        var target = $(e.target);
        if (target.match("#top-login-wrapper *")) {
            return;
        }
        LoginDropdown.down = false;
        LoginDropdown.unhover();
        LoginDropdown.login_link.up().removeClassName("down");
    },
    hover: function () {
        if (LoginDropdown.is_hover || LoginDropdown.down) {
            return;
        }
        LoginDropdown.is_hover = true;
        var icon = $("login-hover-dropdown-icon");
        Sprite.replace(icon, "big-dropdown-gray", "big-dropdown");
    },
    unhover: function () {
        if (!LoginDropdown.is_hover || LoginDropdown.down) {
            return;
        }
        LoginDropdown.is_hover = false;
        var icon = $("login-hover-dropdown-icon");
        Sprite.replace(icon, "big-dropdown", "big-dropdown-gray");
    }
};


var TranslationSuggest = {
    record_msg_touch: function (msg_display) {
        var msg_var = $("translation-msg-id");
        assert(msg_var, "Missing translation msg_id field");
        assert(msg_display, "Missing translation display");

        var msg_id = Constants.messages[msg_display];
        if (msg_id) {
            msg_var.value = msg_id;
        }
        TranslationSuggest.finish_wizard(msg_display, Constants.emessages[msg_display] || '');
    },
    _autocomplete_highlight: function (options) {
        options = new SimpleSet(options);
        return function (s) {
            if (options.contains(s)) {
                return "<strong>" + s + "</strong>";
            } else {
                return s;
            }
        };
    },
    autocompleter: Class.create(Autocompleter.Local, {
        // custom class to handle special case where we put in a non-li as a sentinel value
        onClick: function ($super, event) {
            var element = Event.findElement(event, 'LI');
            if (element && element.className.indexOf('not-found') < 0) {
                return $super(event);
            }
        },
        onHover: function ($super, event) {
            var element = Event.findElement(event, 'LI');
            if (element && element.className.indexOf('not-found') < 0) {
                return $super(event);
            }
        },
        onBlur: function ($super, event, force_close) {
            // once the autocompleter is open, don't close it until we force it closed
            if (force_close) {
                $super(event);
            }
        },
        close: function () {
            this.onBlur(null, true);
            return true;
        },
        selectEntry: function ($super) {
            var i = this.index; // $super:selectEntry resets the index
            $super();
            TranslationSuggest.record_msg_touch(TranslationSuggest.msg_display[i]);
        }
    }),
    attach_autocomplete: function () {
        var ac = new TranslationSuggest.autocompleter('bad-i18n-text', 'bad-i18n-text-complete', false, {
            frequency: 0.15,
            selector: function (instance) {
                // partial-match selector ripped from scriptaculous
                var ret       = []; // Beginning matches
                var partial   = []; // Inside matches
                var entry     = instance.getToken();
                // var count     = 0; // split-todo unused?
                var options = $H(Constants.messages).keys();
                var option_count = options.length;
                var choice_limit = 10;
                var partialSearch = true;
                var partialChars = 3;
                var fullSearch = true;
                var included = {};
                TranslationSuggest.msg_display = [];
                var msg_display_partial = [];
                var elem;

                for (var i = 0; i < option_count && ret.length < choice_limit ; i++) {
                    elem = options[i];
                    var foundPos = elem.toLowerCase().indexOf(entry.toLowerCase()); // XXX toLowerCase

                    if (foundPos != -1) {
                        included[elem] = true;
                    }
                    while (foundPos != -1) {
                        if (foundPos === 0 && elem.length != entry.length) {
                            ret.push("<li><div><strong>" + elem.substr(0, entry.length) + "</strong>" +
                                     elem.substr(entry.length) + "</div></li>");
                            TranslationSuggest.msg_display.push(elem);
                            break;
                        } else if (entry.length >= partialChars && partialSearch && foundPos != -1) {
                            if (fullSearch || /\s/.test(elem.substr(foundPos - 1, 1))) {
                                partial.push("<li><div>" + elem.substr(0, foundPos) + "<strong>" +
                                             elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
                                             foundPos + entry.length) + "</div></li>");
                                msg_display_partial.push(elem);
                                break;
                            }
                        }
                        foundPos = elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1); // XXX toLowerCase
                    }

                }

                if (partial.length) {
                    ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
                    TranslationSuggest.msg_display = TranslationSuggest.msg_display.concat(msg_display_partial.slice(0, instance.options.choices - ret.length));
                }

                if (ret.length < choice_limit) {
                    var counts = {};
                    var tokens = $A(entry.split(/\s/));
                    tokens.each(function (token) {
                        var token_matches = TranslationSuggest.word_index[token];
                        if ((token.length >= partialChars || tokens.length > 2) && token_matches) {
                            $A(token_matches).each(function (msg_id) {
                                counts[msg_id] = (counts[msg_id] || 0) + 1;
                            });
                        }
                    });

                    counts = $A($H(counts));
                    counts.sort(function (x, y) {
                        return y[1] - x[1];
                    });

                    for (var j = 0; j < Math.min(counts.length, choice_limit - ret.length); j++) {
                        elem = counts[j][0];
                        if (!included[elem]) {
                            ret.push("<li><div>" + elem.replace(/[^\s]+/g, TranslationSuggest._autocomplete_highlight(tokens)) + "</div></li>");
                            TranslationSuggest.msg_display.push(elem);
                        }
                    }
                }

                if (ret.length) {
                    // last element doesn't need dots under it
                    ret[ret.length - 1] = ret[ret.length - 1].replace('<div>', '<div style="border: none">');
                }
                ret = ret.join('');

                if (!ret.length) {
                    return '<ul style="border: 1px solid #bbb"><li class="not-found" style="background:#f5f5f5"><div style="text-align:left;border: none">%s</div></li></ul>'.format(_("That text was not found on this page."));
                }

                return ('<ul>' + ret + '</ul>');
            }
        });
        TranslationSuggest.ac = ac;
    },
    submit_suggest: function (e) {
        var form = $("translation-suggest-form");
        assert(form, "Missing translation suggest form");

        Forms.ajax_submit(form, false,
            function () {
                Notify.ServerSuccess(_("Thanks for suggesting an alternate translation!"));
                Modal.hide();
            },
            false,
            $('translation-back-button'));
    },
    start_wizard: function (e) {
        Event.stop(e);
        var form = $("translation-suggest-form");
        TranslationSuggest.reset_form();
        form.down("input[name=locale]").setValue(Constants.USER_LOCALE);
        form.down("input[name=locale_url]").setValue(window.location.href);

        Modal.icon_show("world", '%s <span class="step-number">%s</span>'.format(_("Report a translation problem"), _("&ndash; Step 1 of 2")), $("translate-div"), {}, $('bad-i18n-text'));
        Modal.onHide = TranslationSuggest.ac.close.bind(TranslationSuggest.ac);
    },
    show_select_error: function (e) {
        Event.stop(e);
        $('bad-i18n-text-error').show();
    },
    finish_wizard: function (msg_text, orig_msg_text) {
        var form = $("translation-suggest-form");
        assert(form, "Missing translation suggest form");

        $("modal-title").down("span").update(_("&ndash; Step 2 of 2"));
        form.down("#part-one").hide();
        form.down("#translation-msg-display").innerHTML = msg_text.stripTags().escapeHTML();
        form.down("#translation-orig-msg-display").innerHTML = orig_msg_text.stripTags().escapeHTML();
        form.down("#part-two").show();
        form.down("#part-two textarea").focus();
        ActAsBlock.register(false, form);
    },
    reset_form: function () {
        var form = $("translation-suggest-form");
        var msg_var = $("translation-msg-id");
        assert(form, "Missing translation suggest form");
        assert(msg_var, "Missing translation msg_id field");

        form.select("textarea").each(Form.Element.clear);
        form.down("#part-one").show();
        form.down("#part-two").hide();
    },
    word_index: {},
    index_message: function (message) {
        var clean = message.blank_format().split(/\s/);
        for (var i = 0; i < clean.length; i++) {
            var word = clean[i];
            if (!(word in TranslationSuggest.word_index)) {
                TranslationSuggest.word_index[word] = [];
            }
            TranslationSuggest.word_index[word].push(message);
        }
    },
    index_all: function () {
        for (var display in Constants.messages) {
            if (Constants.messages.hasOwnProperty(display)) {
                TranslationSuggest.index_message(display);
            }
        }
    },
    update_i18n_messages: function (message_dict) {
        for (var k in message_dict) {
            if (message_dict.hasOwnProperty(k)) {
                var message_info = message_dict[k];
                if (message_info.s && message_info.s.length) {
                    for (var i = 0, l = message_info.s.length; i < l; i++) {
                        add_i18n_message(k, message_info.s[i], message_info.e[i]);
                    }
                } else { // no substitution
                    add_i18n_message(k, message_info.t, k);
                }
            }
        }
    },
    update_i18n_messages_from_req: function (req) {
        var prologue = "<!--msg:";
        var epilogue = "-->";
        if (req.responseText.indexOf(prologue) === 0) {
            var cpos = req.responseText.indexOf(":", prologue.length);
            assert(cpos != -1, "malformed i18n message header");

            var json_len_str = req.responseText.substr(prologue.length, cpos - prologue.length);
            var json_len = Number(json_len_str);
            assert(!isNaN(json_len), "invalid json length " + json_len_str);

            var json_str = req.responseText.substr(cpos + 1, json_len);
            req.responseText = req.responseText.substr(cpos + 1 + json_len + epilogue.length);

            var info = json_str.evalJSON();
            TranslationSuggest.update_i18n_messages(info);
        }
    }
};


var DBCheckbox = {
    register_all: function () {
        var checkboxes = $$(".checkbox");
        for (var i = 0; i < checkboxes.length; i += 1) {
            DBCheckbox.register(checkboxes[i]);
        }
    },
    register_browse: function () {
        var files = Browse.files;
        for (var i = 0, len = files.length; i < len; i += 1) {
            files[i].checkbox.selected = false;
        }
    },
    register: function (img) {
        img.addClassName("s_checkbox sprite");
        img.selected = false;
        return img;
    },

    toggle: function (img) {
        if (img.selected) {
            DBCheckbox.deselect(img);
        } else {
            DBCheckbox.select(img);
        }
    },
    select: function (img) {
        Sprite.replace(img, "checkbox", "checkbox_checked");
        img.selected = true;
    },

    deselect: function (img) {
        Sprite.replace(img,  "checkbox_checked", "checkbox");
        img.selected = false;
    }

};
document.observe("dom:loaded", DBCheckbox.register_all);


var LeftNavBox = {
    close: function (ex) {
        var t = $(ex).up().up('div');
        var d = 1.0;
        var ef;
        ef = new Effect.BlindUp(t, {duration: d});
        return false;
    }
};


var Tooltip = {
    attach: function (elm, content, trigger, bubble_options) {
        // content = content.widthSplit(30).invoke('escapeHTML').join("&#8203;<wbr>");
        elm = $(elm);
        trigger = trigger ? $(trigger) : null;
        bubble_options = bubble_options || {};

        // var id = elm.id ? elm.id : Math.floor(Math.random() * 10000 + 1); // split-todo unused?
        var d = Bubble.make(content, elm.tail_position, bubble_options.tail_position, bubble_options.width); // TODO: move tail_position to bubble_options
        d.setStyle({display: "none", position: "absolute" });
        $('floaters').insert(d);

        if (elm.match("#modal-content *")) {
            d.style.zIndex = "13001";
        } else {
            d.style.zIndex = "";
        }
        if (elm.tail_position == "right") {
            var offset = Util.ie ? 32 : 12;
            d.style.marginLeft = -(d.getWidth() + elm.getWidth() + offset) + "px";
        }

        elm.tooltip = d;

        elm.out_target = trigger ? true : false;
        elm.observe('mouseout', Tooltip.mouseout('target', elm));
        elm.observe('mouseover', Tooltip.mouseover('target', elm));

        elm.out_trigger = trigger ? false : true;
        if (trigger) {
            trigger.observe('mouseout', Tooltip.mouseout('trigger', elm));
            trigger.observe('mouseover', Tooltip.mouseover('trigger', elm));
        }

        elm.out_tooltip = true;
        d.observe('mouseout', Tooltip.mouseout('tooltip', elm));
        d.observe('mouseover', Tooltip.mouseover('tooltip', elm));
    },
    update: function (elm, content) {
        if (elm.tooltip) {
            $(elm.tooltip).update(content);
        }
    },
    mouseover: function (thing, elm) {
        return function () {
            elm['out_' + thing] = false;
        };
    },
    mouseout: function (thing, elm) {
        return function () {
            elm['out_' + thing] = true;

            Tooltip.hide_if_out.defer(elm);
        };
    },
    show_by: function (e) {
        var d = $(e.tooltip);
        d.show();
        var offsetHeight = Math.floor(d.getHeight() / 2);
        d.clonePosition(e, {setWidth: false, setHeight: false, offsetTop: Math.floor(e.getHeight() / 2) - offsetHeight, offsetLeft: e.getWidth() + 1});
    },
    hide_if_out: function (e) {
        if (!e.out_target || !e.out_trigger || !e.out_tooltip) {
            return;
        }

        var d = $(e.tooltip);
        d.hide();
    },
    show: function (elm, content, trigger, tail_position, bubble_options) {
        tail_position = tail_position || "left";

        elm = $(elm);
        if (!elm.tail_position) {
            elm.tail_position = tail_position;
        }
        trigger = trigger ? $(trigger) : null;

        if (!elm.tooltip) {
            Tooltip.attach(elm, content, trigger, bubble_options);
        }

        Tooltip.show_by(elm);
    }
};


var TabList = Class.create({
    initialize: function (lists, tabs, tab_map) {
        this.lists = lists;
        this.initialize_lists(lists);

        if (tabs) {
            this.tabs = tabs;
            this.tab_map = tab_map;
            this.initialize_tabs();
        }

    },

    initialize_lists: function () {
        // split-todo test extra carefully that this still works.
        // had to restructure to get jshint to stop complaining about
        // defining functions inside loops.
        var that = this;
        var make_click_func = function () {
            return function (e, source) {
                that.list_click(that, e, source);
            };
        };
        for (var i = 0; i < this.lists.length; i += 1) {
            var anchors = $(this.lists[i]).select("a");
            for (var j = 0; j < anchors.length; j += 1) {
                var elm = anchors[j];
                elm.db_observe("click", make_click_func());
            }
        }
    },

    initialize_tabs: function () {
        var that = this;

        var listener_maker = function (i) {
            return function (e, source) {
                that.tab_click(that, e, source, i);
            };
        };

        for (var i = 0; i < this.tabs.length; i += 1) {
            var elm = $(this.tabs[i]);
            if (!elm) {
                continue;
            }
            elm.db_observe("click", listener_maker(i));
        }
    },

    list_click: function (that, event, source) {
        for (var i = 0; i < that.lists.length; i += 1) {
            $(that.lists[i]).select("a").invoke("removeClassName", "selected");
        }
        source.addClassName("selected");
    },

    tab_click: function (that, event, source, show_this) {
        for (var i = 0; i < that.tabs.length; i += 1) {
            $(that.tabs[i]).removeClassName("selected");
        }

        source.addClassName("selected");

        for (var j = 0; j < that.lists.length; j += 1) {
            $(that.lists[j]).hide();
        }

        $(that.tab_map[show_this]).show();
    }
});



var Pager = Class.create({
    initialize: function (name, item_selector, container_selector, options) {
        assert(item_selector, "Pager item_class is missing");
        this.options = options || {};
        this.item_selector = item_selector;
        this.name = name;
        this.container = $$(this.item_selector).first() && $$(this.item_selector).first().up(container_selector);
        HashRouter.watch(name, this.show_page.bind(this));
    },
    current_page: 1,
    prev: function () {
        assert(this.current_page > 1, "Pager current page is 0");
        this.show_page(this.current_page - 1);
    },
    next: function () {
        this.show_page(this.current_page + 1);
    },
    show_page: function (page) {
        // var orig_page = page; // split-todo unused?
        var set_hash = true;
        if (!page || !Util.isNumber(page)) {
            page = 1;
            set_hash = false;
        }

        page = parseInt(page, 10);
        this.current_page = page;

        $$(this.item_selector).each(Element.hide);
        var show_us = $$(this.item_selector + page);
        if (this.options.on_page_change) {
            this.options.on_page_change(page);
        }
        show_us.each(Element.show);

        if (page <= 1) {
            $(this.name + "-prev").hide();
        } else {
            $(this.name + "-prev").show();
        }

        if ($$(this.item_selector + (page + 1)).length) {
            $(this.name + "-next").show();
        } else {
            $(this.name + "-next").hide();
        }
        $(this.name + "-page-num").update(page);

        if (set_hash) {
            HashRouter.set_hash(this.name, page);
        }

        if (this.container) {
            var current_min_height = parseInt(this.container.style.minHeight, 10) || 0;
            var inner_height = Util.inner_height(this.container);

            if (current_min_height < inner_height) {
                this.container.style.minHeight = inner_height + "px";
            }
        }
    }
});


var BrowseStyleRows = {
    register_all: function () {
        $$('.bs-row').each(BrowseStyleRows.register);
        Event.observe(document, 'click', BrowseStyleRows.kill_current);
    },
    register: function (elm) {
        elm = $(elm);

        elm.db_observe('mouseover', BrowseStyleRows.mouseover);
        elm.db_observe('mouseout', BrowseStyleRows.mouseout);
        elm.db_observe('click', BrowseStyleRows.click);

    },
    mouseover: function (e, source) {
        source.addClassName("hover");
    },
    mouseout: function (e, source) {
        source.removeClassName("hover");
    },
    click: function (e, source) {
        if (e.target.tagName == 'A') {
            // links should be clickable without opening the row
            return;
        }

        Event.stop(e);
        BrowseStyleRows.kill_current(false);
        var target = $(e.target);
        if (!target.match(".bs-actions-list *")) {
            source.addClassName("selected");
        }

        var parent = target.hasClassName("bs-row") ? target : target.up(".bs-row");
        if (Util.ie6) {
            parent.down(".bs-actions-list").style.position = "absolute";
        }
        parent.style.zIndex = 9;
    },
    kill_current: function (e) {
        $$(".bs-row.selected").each(function (elm) {
            elm.removeClassName("selected");
            elm.style.zIndex = "";
        });
    }
};


var HoverIconSwap = {
    register_all: function () {
        $$('.background-icon').each(HoverIconSwap.register);
    },
    register: function (elm) {
        elm = $(elm);

        elm.db_observe('mouseenter', HoverIconSwap.mouseenter);
        elm.db_observe('mouseleave', HoverIconSwap.mouseleave);
    },
    mouseenter: function (e, elm) {
        elm.addClassName("hover_swap");
    },
    mouseleave: function (e, elm) {
        elm.removeClassName("hover_swap");
    },
    getFileName: function (elm) {
        var fileName = elm.src.split("/");
        return fileName[fileName.length - 1];
    }
};
document.observe("dom:loaded", HoverIconSwap.register_all);


var SuggestionInput = {
    register: function (elm) {
        elm = $(elm);
        var rel_form = elm.up('form');

        var init_val = elm.getValue();
        if (SuggestionInput.defaulted(elm) || elm.getValue() === "") {
            elm.setValue(elm.title); // title holds the default value
        }
        else {
            elm.addClassName('suggestion-input-unfaded');
        }

        elm.observe('blur', SuggestionInput.blur);
        elm.observe('focus', SuggestionInput.focus);
        elm.observe('db:value_change', SuggestionInput.focus);

        if (rel_form) {
            if (!elm.id) {
                elm.id = "r_elm_id_" + Math.random().toString();
            }

            rel_form.observe('submit', SuggestionInput.blank(elm.id));
        }
    },
    register_all: function () {
        $$('.suggestion-input').each(SuggestionInput.register);
    },
    blank: function (elm_id) {
        return function () {
            var elm = $(elm_id);
            if (!elm) {
                return;
            }
            if (SuggestionInput.defaulted(elm)) {
                elm.setValue('');
            }
        };
    },
    defaulted: function (elm) {
        elm = $(elm);
        return elm.getValue() === elm.title;
    },
    do_blank: function (elm_id) {
        SuggestionInput.blank(elm_id)();
    },
    clear: function (elm_id) {
        var fake_event = {'target': elm_id};
        SuggestionInput.focus(fake_event);
    },
    focus: function (e) {
        var elm = $(e.target);
        if (!elm) {
            return;
        }

        if (SuggestionInput.defaulted(elm)) {
            elm.addClassName('suggestion-input-unfaded');
            elm.setValue('');
        }
    },
    blur: function (e) {
        var elm = $(e.target);
        if (!elm) {
            return;
        }

        if (elm.getValue() === '') {
            elm.removeClassName('suggestion-input-unfaded');
            elm.setValue(elm.title);
        }
    },
    reset: function (id) {
        var elm = $(id);
        if (!elm) {
            return;
        }
        elm.removeClassName('suggestion-input-unfaded');
        elm.setValue(elm.title);
    }
};
document.observe('dom:loaded', SuggestionInput.register_all);

var ULSelectMenu = (function () {
    /* PRIVATE */

    /* Uncomment this if you need to use it
     * var _show_menu = function (ul_elm) {
        ul_elm.addClassName("shown");
    };
     */
    var _hide_menu = function (ul_elm) {
        ul_elm.removeClassName("shown");
    };
    var _toggle_menu = function (ul_elm) {
        ul_elm.toggleClassName("shown");
    };

    var _option_unhover = function () {
        this.removeClassName("hover");
    };

    var _option_hover = function () {
        this.addClassName("hover");
    };

    var _resort = function (ul_elm, orig_order) {
        orig_order.each(function (x) {
            ul_elm.insert(x);
        });
    };

    var _move_option_to_top = function (ul_elm, option_elm, orig_order) {
        _resort(ul_elm, orig_order);
        if (ul_elm.firstChild != option_elm) {
            ul_elm.insert({top: option_elm});
        }
    };

    var _option_click = function (option_elm, ul_elm, orig_order) {
        return function (e) {
            e.stopPropagation();
            if (!option_elm.hasClassName("selected")) {
                ul_elm.down(".selected").removeClassName("selected");
                ul_elm.fire("db:change", option_elm.getAttribute('data-value'));
                option_elm.addClassName("selected");
                _hide_menu(ul_elm);
            } else { // this case is for a click when the menu looks hidden
                _move_option_to_top(ul_elm, option_elm, orig_order);
                _toggle_menu(ul_elm);
            }
        };
    };

    var _init = function (ul_elm) {
        var options = ul_elm.select('li');
        assert(options.length, "Empty list of options " + ul_elm.identify());

        var selected;
        options.each(function (elm) {
            var value = elm.getAttribute('data-value');
            assert(value, elm.identify() + " missing data value");

            elm.observe("click", _option_click(elm, ul_elm, options));
            elm.observe("mouseenter", _option_hover);
            elm.observe("mouseleave", _option_unhover);
        });

        $(document.body).observe("click", function () {
            _hide_menu(ul_elm);
        });
        if (!selected) {
            selected = options[0];
        }

        selected.addClassName("selected");
        var wrapper = new Element("span");
        var selected_dimensions = selected.getDimensions();

        wrapper.style.width = selected_dimensions.width + "px";
        wrapper.style.height = selected_dimensions.height + "px";
        wrapper.setStyle({
            'width': selected_dimensions.width + "px",
            'height': selected_dimensions.height + "px",
            'position': 'relative',
            'display': 'inline-block'
        });
        ul_elm.wrap(wrapper);
    };

    var _init_all = function () {
        $$(".ul_select_menu").each(function (elm) {
            _init(elm);
        });
    };

    document.observe("dom:loaded", _init_all);
    /* PUBLIC */
    return {
        init: function (elm) {
            _init(elm);
        }
    };
})();

// split-todo figure out where this goes. util?
var JumpWatcher = {
    inverval: null,
    last_hash: null,
    last_page_offset: 0,
    check: function () {
        if (window.location.href.endsWith("#") && window.pageYOffset === 0 && JumpWatcher.last_page_offset !== 0) {
            JumpWatcher.report();
        } else {
            JumpWatcher.last_page_offset = window.pageYOffset;
            JumpWatcher.last_hash = Util.url_hash();
        }
    },
    report: function () {
        clearInterval(JumpWatcher.interval);
        assert(0.1 + 0.2 === 0.3, "Hash jump detected, last hash = " + JumpWatcher.last_hash);
    }
};

Event.observe(document, "dom:loaded", function () {
    TranslationSuggest.index_all();
});

document.observe("dom:loaded", function () {
    JumpWatcher.interval = setInterval(JumpWatcher.check, 500); // split-todo move this along with JumpWatcher
    LocaleSelector.init();
    LoginDropdown.init();
});


var FileQueue, Upload, GlobalUpload, UploadFile, InlineUploadStatus;

FileQueue = {
    fileRows: {},
    fileProgress: {},
    uploading: false,
    toUpload: 0,
    queueSize: 0,
    completedSize: 0,
    completed_files: {},
    empty: function () {
        return !FileQueue.toUpload;
    },
    numShown: function () {
        return $H(FileQueue.fileRows).keys().length;
    },
    lastOne: function () {
        return 1 == FileQueue.toUpload;
    },
    push: function (fileObj) {
        var id = fileObj.id;

        FileQueue.queueSize += fileObj.size;
        FileQueue.fileRows[id] = fileObj;
        if (!FileQueue.toUpload) {
            GlobalUpload.files_added();
        }

        FileQueue.toUpload++;

        UploadFile.add(fileObj);

        var button_content = $('choose-button').down(".hotbutton-content");
        button_content.update(_("Add more files"));
    },
    remove: function (fileObj) {
        var id = fileObj.id;

        Upload.SWFU.cancelUpload(id);

        fileObj.filestatus = SWFUpload.FILE_STATUS.CANCELLED;

        UploadFile.update(fileObj);

        delete FileQueue.fileRows[id];

        if (!FileQueue.completed_files[id]) {
            FileQueue.toUpload = Math.max(0, FileQueue.toUpload - 1);
        }

        if (FileQueue.numShown() === 0 && FileQueue.uploading) {
            FileQueue.doneUploading();
        }
    },
    update: function (fileObj, progress, max) {
        var text = false;

        var updateSize;
        var done = false;
        if (progress == "done") {
            text = _("Done");
            progress = max = 1;
            done = true;
        } else if (progress / max == 1) {
            text = _("Saving...");
        }

        if (done) {
            FileQueue.completedSize += fileObj.size;
            updateSize = FileQueue.last_update_position;
        } else {
            updateSize = FileQueue.completedSize + progress / max * fileObj.size;
        }

        // Calculate the speed and time remaning
        var average_bps = fileObj.averageSpeed / 8;

        // TRANSLATORS - This the rate of uploading a file, sec is short for seconds
        var formattedSpeed = _("%(bytes)s/sec").format({
            bytes: Util.formatBytes(average_bps, 1, true)
        });

        var time_left = (FileQueue.queueSize - updateSize) / average_bps;
        var formattedTime = Util.formatTime(time_left + FileQueue.toUpload); // Add a second for each file to account for "Saving..."

        FileQueue.last_update_position = updateSize;

        if (!text) {
            text = parseInt((updateSize / FileQueue.queueSize).toFixed(2) * 100, 10);
            text += "%";
        }

        FileQueue.currentFilename = fileObj.name.snippet(20);
        FileQueue.formattedSpeed = formattedSpeed;
        FileQueue.formattedTime = formattedTime;
        FileQueue.statusText = text;
        FileQueue.totalPercentage = updateSize / FileQueue.queueSize;
        FileQueue.current_file = fileObj;

        if (fileObj.filestatus == SWFUpload.FILE_STATUS.COMPLETE) {
            UploadFile.update(fileObj);
        }

        if (!FileQueue.update_timer) {
            FileQueue.update_timer = true;

            FileQueue.timer = setInterval(function () {
                UploadFile.update();
                GlobalUpload.update();
            }, 250);
        }

    },
    errored: function (fileObj, error_code) {
        if (error_code != SWFUpload.UPLOAD_ERROR.FILE_CANCELLED) {
            FileQueue.errors = (FileQueue.errors + 1) || 1;
        }
        UploadFile.update(fileObj);

        if (FileQueue.toUpload === 0 && FileQueue.uploading) {
            FileQueue.doneUploading();
        }
    },
    completed: function (fileObj) {
        var row = FileQueue.fileRows[fileObj.id];
        if (row) {
            FileQueue.toUpload = Math.max(0, FileQueue.toUpload - 1);
            FileQueue.completed_files[fileObj.id] = true;
        }
    },
    clear: function (clear_all) {
        FileQueue.fileRows = {};
        FileQueue.queueSize = 0;
        FileQueue.errors = 0;
        FileQueue.toUpload = 0;
        FileQueue.completedSize = 0;
        FileQueue.uploading = false;
        window.onbeforeunload = null;
        Modal.onHide = null;
    },
    chooseFiles: function () {

    },
    uploadFiles: function () {
        if (!FileQueue.toUpload) {
            return;
        }

        Upload.updatePostParams({'dest': $$('.dest-folder')[0].getValue(), 't': Constants.TOKEN});

        if (!FileQueue.uploading) {
            FileQueue.start_time = new Date().getTime();
            Upload.uploadNext();
            FileQueue.uploading = true;
            FileQueue.updateInterval = setInterval(InlineUploadStatus.update, 250);
            window.onbeforeunload = function confirmLeave() {
                return _("Leaving this page will cancel your uploads.");
            }; // Prototype sucks at onbeforeunload
            Modal.hide();
        }
    },
    colorButtons: function (primary) {
        $A(['choose-button', 'upload-button']).each(function (button) {
            if (button == primary) {
                $(button).removeClassName('grayed');
            } else {
                $(button).addClassName('grayed');
            }
        });
    },
    doneUploading: function () {
        GlobalUpload.complete();
        InlineUploadStatus.complete();

        clearInterval(FileQueue.timer);
        FileQueue.update_timer = false;
        FileQueue.uploading = false;

        clearInterval(FileQueue.updateInterval);
        DomUtil.fillVal('', "uploading-speed");
        DomUtil.fillVal('', "uploading-time-left");
        if (FileQueue.errors) {
            InlineUploadStatus.errored(FileQueue.errors, FileQueue.numShown());
        }

        FileQueue.last_update_position = 0;
        Browse.force_reload();

        Modal.onHide = null;
        window.onbeforeunload = null;
    }
};


InlineUploadStatus = {
    upload_box: false,
    last_update: 0,
    last_update_position: 0,
    previous_bps_list: [],

    show: function (container_id) {
        if (!InlineUploadStatus.upload_box) {
            InlineUploadStatus.build(container_id);
        }

        InlineUploadStatus.upload_box.show();
    },
    build: function () {
        if (!InlineUploadStatus.upload_box) {
            InlineUploadStatus.upload_box = $("inline-upload-status");
        }
        $("right-content").insert({top: InlineUploadStatus.upload_box});
    },
    hide: function () {
        if (InlineUploadStatus.upload_box) {
            InlineUploadStatus.upload_box.hide();
        }
    },

    update: function () {
        if ($$("#right-content #inline-upload-status").length === 0) {
            InlineUploadStatus.build();
        }

        if (FileQueue.currentFilename) {
            var global = $("inline-upload-status");
            global.removeClassName("error");
            global.removeClassName("complete");

            var total = FileQueue.numShown();
            var current_num = total - FileQueue.toUpload + 1;
            // TRANSLATORS for example "Uploading file 3 of 4 (350 kb/sec)"
            var msg = _("Uploading file %(file_number)d of %(total)d (%(upload_speed)s)").format({
                'file_number': current_num,
                'total': total,
                'upload_speed': FileQueue.formattedSpeed
            });
            global.down(".upload-info-filename").update(msg);

            global.down(".upload-info-status").update("<strong>" + _("Time Left:") + "</strong> " + FileQueue.formattedTime);

            var percent = (parseInt(100 * FileQueue.totalPercentage, 10) || 0) + "%";
            global.down(".upload-info-percent").update(percent);
            global.down(".upload-info-icon").update(Sprite.make("sync"));

            global.down(".upload-file-progress").style.width = percent;
        }
    },
    errored: function (errors, total) {
        var global = $("inline-upload-status");
        global.addClassName("error");
        global.removeClassName("complete");
        // TRANSLATORS for example "Problems with 5 of 17 files"
        var msg = _("Problems with %(error_count)d of %(total)d files").format({
            'error_count': errors,
            'total': total
        });
        global.down(".upload-info-filename").update(msg);
        global.down(".upload-info-percent").update("100%");
        // var total_time = (Util.time() - FileQueue.start_time) / 1000; // split-todo unused?
        global.down(".upload-info-status").update();
        global.down(".upload-info-icon").update(Sprite.make("redx"));
        global.down(".upload-file-progress").style.width = "100%";
    },
    complete: function () {
        var global = $("inline-upload-status");
        global.addClassName("complete");
        // TRANSLATORS for example "Uploaded 5 of 17 files"
        var msg = _("Uploaded %(number_uploaded)d of %(total)d files").format({
            'number_uploaded': FileQueue.numShown(),
            'total': FileQueue.numShown()
        });
        global.down(".upload-info-filename").update(msg);

        global.down(".upload-info-percent").update("100%");
        var total_time = (Util.time() - FileQueue.start_time) / 1000;
        global.down(".upload-info-status").update("<strong>" + _("Time taken:") + "</strong> " + Util.formatTime(total_time));
        global.down(".upload-info-icon").update(Sprite.make("check"));
        global.down(".upload-file-progress").style.width = "100%";

    }
};


Upload = {
    SWFU: false,
    init: function (late_game) {
        var i = {};
        i[Constants.tcn] = Upload.touch;
        var f = Upload.initSWFU(i);
        if (!late_game) {
            Event.observe(window, 'load', f);
        } else {
            f(); // run it now!
        }

        Upload.operaHack();
        FileQueue.clear();

        if (!late_game) {
            Upload.checkForFallback.delay(Util.linux_ff3 ? 0 : 5); // if we haven't loaded the flash in 5 seconds, give up
        }
    },
    initSWFU: function (pp) {
        return function () {
            var swf_upload_control = new SWFUpload({
                // Backend settings
                upload_url: "https://" + Constants.BLOCK_CLUSTER + "/upload",
                file_post_name: "file",

                // Flash file settings
                file_size_limit : "307200", // 300MB
                file_types : "*",
                file_types_description : _("All Files"),
                file_upload_limit : "0", // Even though I only want one file I want the user to be able to try again if an upload fails

                // Button settings
                button_placeholder_id : "spanButtonPlaceholder",
                button_window_mode: SWFUpload.WINDOW_MODE.TRANSPARENT,
                button_width: 120,
                button_height: 30,
                button_image_url: Util.linux_ff3 ? "/static/images/upload_button.gif" : "",
                button_text:  Util.linux_ff3 ? "<span class='flash-button'>" + _("Select files...") + "</span>" : "",
                button_text_style: ".flash-button {color: #ffffff; font-size: 11pt;" +
                                                   'font-family: "lucida grande","lucida sans unicode",tahoma,verdana,arial,sans-serif;' +
                                                   "text-align: center; line-height: 16px;}",

                // Event handler settings
                swfupload_loaded_handler : Upload.flashLoaded,

                file_dialog_start_handler : FileQueue.chooseFiles,
                file_queued_handler : Upload.fileQueued,
                file_queue_error_handler : Upload.fileQueueError,
                file_dialog_complete_handler : Upload.fileDialogComplete,

                //upload_start_handler : Upload.uploadStart,
                upload_progress_handler : Upload.uploadProgress,
                upload_error_handler : Upload.uploadError,
                upload_success_handler : Upload.uploadSuccess,
                upload_complete_handler : Upload.uploadComplete,

                // Flash Settings
                flash_url : "/static/swf/swfupload.swf",

                custom_settings : {
                    progress_target : "fsUploadProgress",
                    upload_successful : false
                },

                post_params: pp,
                debug: Constants.upload_debug || false
            });
            Upload.SWFU = swf_upload_control;
        };
    },
    reset: function () {
        if (FileQueue.uploading) {
            Upload.SWFU.cancelUpload();
        }
        FileQueue.clear();

        var uploader = $$(".swfuploader").first();
        if (uploader) {
            uploader.remove();
        }
        delete Upload.SWFU;
    },
    updatePostParams: function (dict) {
        var pp = Upload.SWFU.getSetting("post_params");
        for (var key in dict) {
            if (dict.hasOwnProperty(key)) {
                pp[key] = dict[key];
            }
        }

        Upload.SWFU.setPostParams(pp);
    },
    fileBrowse: function () {
        Upload.SWFU.cancelUpload();
        Upload.SWFU.selectFiles();
    },
    fileQueueError: function (fileObj, error_code, message)  {
        try {
            // Handle this error separately because we don't want to create a FileProgress element for it.
            switch (error_code) {
            case SWFUpload.QUEUE_ERROR.FILE_EXCEEDS_SIZE_LIMIT:
                var msg = "<p>" + _("The upload limit online is 300MB. You can upload larger files with the <a href='/install'>Dropbox desktop application</a>.") + "</p>";
                var cont = new Element("div");
                var p = new Element("p", {
                    'style': 'margin-bottom:0; text-align: right;'
                });
                var button = new Element("input", {
                    'type': 'button',
                    'className': 'button',
                    'value': _('Okay')
                });
                button.observe("click", function () {
                    FileOps.show_upload(Browse.current_fqpath());
                });
                p.insert(button);
                cont.insert(msg);
                cont.insert(p);
                Modal.icon_show("alert", _("Upload Error"), cont);
                break;
            case SWFUpload.QUEUE_ERROR.ZERO_BYTE_FILE:
                alertd(_("The file you selected is empty. Please select another file."));
                break;
            case SWFUpload.QUEUE_ERROR.INVALID_FILETYPE:
                alertd(_("The file you choose is not an allowed file type."));
                break;
            default:
                alertd(_("An error occurred in the upload. Try again later."));
                this.debug("Error Code: " + error_code +
                           ", File name: " + fileObj.name +
                           ", File size: " + fileObj.size +
                           ", Message: " + message);
                break;
            }
        } catch (e) {}
    },
    fileQueued: function (fileObj) {
        FileQueue.push(fileObj);
    },

    fileDialogComplete: function (num_files_selected) {
    },

    uploadNext: function () {
        Upload.SWFU.startUpload();
    },

    pause: function () {
        Upload.SWFU.stopUpload();
    },

    uploadProgress: function (fileObj, bytesLoaded, bytesTotal) {
        FileQueue.update(fileObj, bytesLoaded, bytesTotal);
    },

    uploadSuccess: function (fileObj, server_data) {
        if (server_data.strip() === "") {
            FileQueue.errored(fileObj);
            Notify.ServerError();
        } else if (server_data == "quota") {
            FileQueue.errored(fileObj);
            Notify.ServerError(_("Your upload failed because you are over quota."));
        } else if (server_data == "folder_exists") {
            FileQueue.errored(fileObj);
            Notify.ServerError(_("You cannot upload a file with the same name as a folder in this directory."));
        } else {
            FileQueue.update(fileObj, "done");
        }
    },

    uploadComplete: function (fileObj) {
        FileQueue.completed(fileObj);

        if (FileQueue.empty()) {
            FileQueue.doneUploading();
        } else {
            Upload.uploadNext();
        }
    },

    uploadError: function (fileObj, error_code, message) {
        var file = fileObj;
        FileQueue.queueSize -= fileObj.size;

        if (parseInt(error_code, 10) != -280) {
            var flashVersion = FlashDetect.major + "." + FlashDetect.revision;
            Util.report_exception("Uploader Error: " + error_code + " " + message + " " + Object.toJSON(fileObj) + " FLASH VERSION: " + flashVersion, window.location.href);
        }

        switch (error_code) {
        case SWFUpload.UPLOAD_ERROR.MISSING_UPLOAD_URL:
            this.debug("Error Code: No backend file, File name: " + file.name + ", Message: " + message);
            break;
        case SWFUpload.UPLOAD_ERROR.UPLOAD_LIMIT_EXCEEDED:
            this.debug("Error Code: Upload Limit Exceeded, File name: " + file.name +
                       ", File size: " + file.size +
                       ", Message: " + message);
            break;
        case SWFUpload.UPLOAD_ERROR.HTTP_ERROR:
            this.debug("Error Code: HTTP Error, File name: " + file.name +
                       ", Message: " + message);
            break;
        case SWFUpload.UPLOAD_ERROR.UPLOAD_FAILED:
            this.debug("Error Code: Upload Failed, File name: " + file.name +
                       ", File size: " + file.size + ", Message: " + message);
            break;
        case SWFUpload.UPLOAD_ERROR.IO_ERROR:
            this.debug("Error Code: IO Error, File name: " + file.name +
                       ", Message: " + message);
            break;
        case SWFUpload.UPLOAD_ERROR.SECURITY_ERROR:
            this.debug("Error Code: Security Error, File name: " + file.name +
                       ", Message: " + message);
            break;
        case SWFUpload.UPLOAD_ERROR.FILE_CANCELLED:
            this.debug("Error Code: Upload Cancelled, File name: " + file.name +
                       ", Message: " + message);
            break;
        case SWFUpload.UPLOAD_ERROR.UPLOAD_STOPPED:
            this.debug("Error Code: Upload Stopped, File name: " + file.name +
                       ", Message: " + message);
            break;
        default:
            this.debug("Error Code: " + error_code +
                       ", File name: " + file.name +
                       ", File size: " + file.size +
                       ", Message: " + message);
            break;
        }

        FileQueue.errored(fileObj, error_code);

        if (FileQueue.empty() && FileQueue.uploading) {
            FileQueue.doneUploading();
        }


    },
    grabURL: function () {
        var path = $F('file-box');
        if (/(^http|^https|^ftp):\/\//.match(path)) {
            $('url').value = path;
        }
        return true;
    },
    set_dest: function (path) {
        var path_parts = path.split("/");
        var folder_name = path_parts[path_parts.length - 1];
        if (!folder_name.length) {
            folder_name = _("Dropbox");
            path = "/";
        }

        DomUtil.fillVal(folder_name.escapeHTML(), 'dest-folder-text');
        DomUtil.fillVal(path, 'dest-folder');

        $$(".dest-folder").each(
            function (elm) {
                elm.value = path;
            }
        );

        var basic_link = Util.scry('basic-uploader-url');
        if (basic_link) {
            basic_link.href = basic_link.href.replace(/(\/upload)(.*)(\?basic=1)/,
                function (s, u, f, b) {
                    return u + Util.urlquote(path) + b;
                });
        }
    },
    treeview_handler: function (path, obj) {
        Upload.set_dest(path);
        FileQueue.clear();
    },
    new_folder: function () {
        TreeView.hide();
        Modal.show(_("Create New Folder..."), DomUtil.fromElm('create-folder'), {
            'action': Upload.do_new_folder,
            'wit_group': 'new-folder-confirm'
        });
        if (!Util.ie) {
            $('first-treeview-link').onclick();
        }
    },
    do_new_folder: function () {
        if (!Modal.vars.selected_path) {
            Notify.ServerError(_("Please select a parent folder."));
            return;
        }

        var to = $F('entered-name');
        var straight_from = decodeURIComponent(Modal.vars.selected_path);
        var from = Util.urlquote(straight_from);

        new Ajax.DBRequest("/cmd/new" + from + "?to_path=" + to, {
            onSuccess: function (req) {
                Upload.treeview_handler(Util.normPath(straight_from) + "/" + to);
                TreeView.schedule_reset();
            },
            cleanUp: function () {
            }
        });
    },
    flashLoaded: function () {
        Upload.flash_loaded = true;
        $('upload-loading').hide();
        $('upload-buttons').show();
    },
    checkForFallback: function () {
        if (!Upload.flash_loaded) {
            location.replace("/upload?basic=1");
        } else {
            clearInterval(Upload.opera_tid);
        }
    },
    operaHack: function () {
        if (Prototype.Browser.Opera) {
            Upload.opera_tid = setInterval(function () {
                $('opera-dummy-div').toggle();
            }, 200);
        }
    }
};


GlobalUpload = {
    init: function () {
        $("init-global-upload").show();
        $("global-upload-progress").hide();
    },
    files_added: function () {
        $("upload-start-buttons").show();
        $("upload-running-buttons").hide();
        $("upload-finished-buttons").hide();
        $$("#upload-start-buttons .button")[0].removeClassName("grayed");
    },
    update: function () {
        $("upload-start-buttons").hide();
        $("upload-running-buttons").show();
        $("upload-finished-buttons").hide();
        $("init-global-upload").hide();

        var global = $("global-upload-progress");
        global.show();
        global.removeClassName("complete");

        var file_info = global.select("td")[1];
        var total = FileQueue.numShown();
        var current_num = total - FileQueue.toUpload + 1;

        // TRANSLATORS for example "Uploading file 5 of 17 (360 kb/sec)"
        // the "kb/sec" will be formatted appropriate to the locale.
        var msg = _("Uploading file %(file_number)d of %(total)d (%(upload_speed)s)").format({
            'file_number': current_num,
            'total': total,
            'upload_speed': FileQueue.formattedSpeed
        });
        file_info.update(msg);

        // var percent_complete = global.select("td")[2]; // split-todo unused?

        var percent = (parseInt(100 * FileQueue.totalPercentage, 10) || 0) + "%";

        global.select("td")[2].update(percent);
        global.down(".upload-file-progress").style.width = (100 * FileQueue.totalPercentage || 0).toFixed(2) + "%";

        var time_elm = $("upload-time");
        time_elm.update("<strong>" + _("Time left:") + "</strong> " + FileQueue.formattedTime);
    },
    complete: function () {
        $("upload-start-buttons").hide();
        $("upload-running-buttons").hide();
        $("upload-finished-buttons").show();

        var global = $("global-upload-progress");
        // global.addClassName("complete");

        global.down().style.width = "100%";
        global.select("td")[2].update("100%");

        var icon_elm = global.select("td")[0];
        icon_elm.update(Sprite.make("check"));

        var total_time = (Util.time() - FileQueue.start_time) / 1000;
        $("upload-time").update("<strong>" + _("Time taken:") + "</strong> " + Util.formatTime(total_time));

        var file_info = global.select("td")[1];

        var msg = _("Uploaded %(number_uploaded)d of %(total)d files").format({
            'number_uploaded': FileQueue.numShown(),
            'total': FileQueue.numShown()
        });
        file_info.update(msg);
    }
};


UploadFile = {
    add: function (fileObj) {
        var container = $("upload-files-container");
        // Render
        var fileContainer = new Element("div", { 'id': fileObj.id });
        fileContainer.addClassName("upload-file");
        var table = '<table class="upload-file-info"><tr><td class="upload-info-icon"></td><td class="upload-info-filename"></td><td class="upload-info-status"></td><td class="upload-info-action"></td></tr></table>';
        fileContainer.innerHTML = '<div class="upload-file-progress"></div>' + table;

        container.insert(fileContainer);
        UploadFile.update(fileObj);
    },
    remove: function (fileId) {
        $(fileId).remove();
    },
    update: function (fileObj) {
        fileObj = fileObj || FileQueue.current_file;
        var file_elm = $(fileObj.id);
        assert(file_elm,  "Could not find file_elm for " + fileObj.name);

        // Update icon
        var icon = FileOps.filename_to_icon(fileObj.name);
        file_elm.down(".upload-info-icon").update(Sprite.make(icon, {}));

        // Update filename
        file_elm.down(".upload-info-filename").update(fileObj.name.escapeHTML().truncate(40));

        // update status
        var className = "upload-file";

        var remove_a   = new Element("a", { 'href': '#' });
        var remove_img = Sprite.make("thick_x", {});

        remove_a.update(remove_img);
        remove_a.observe("click", function (e) {
            Event.stop(e);
            FileQueue.remove(fileObj);
        });

        var width = 0;
        var status      = file_elm.down(".upload-info-status"),
            action      = file_elm.down(".upload-info-action"),
            progress    = file_elm.down(".upload-file-progress");

        assert(status, "Couldn't find status elm");
        assert(action, "Couldn't find action elm");
        assert(progress, "Couldn't find progress elm");

        status.update();
        var percent_done = parseInt(fileObj.percentUploaded, 10) + "%";
        switch (fileObj.filestatus) {
        case SWFUpload.FILE_STATUS.QUEUED:
            file_elm.className = className + " queued";
            action.update(remove_a);
            break;

        case SWFUpload.FILE_STATUS.IN_PROGRESS:
            file_elm.className = className + " in_progress";
            if (percent_done == "100%") {
                status.update(_("Saving"));
            } else {
                status.update(percent_done);
            }
            width = percent_done;
            $$(".uploadnotch").invoke("remove");

            var img = Sprite.make("arrow_blue", {});
            img.addClassName("uploadnotch");
            file_elm.insert(img);
            break;

        case SWFUpload.FILE_STATUS.ERROR:
            file_elm.className = className + " error";
            status.update("Error");
            width = "100%";
            var error_img = Sprite.make("information");

            error_img.observe("mouseover", function () {
                if (parseFloat(Util.flash_version(), 10) < 10.32) {
                    var msg = _('Upload failed.  Please try upgrading to the latest version of <a id="adobe_link">Adobe Flash</a> and try again.');
                    msg = msg.replace('id="adobe_link"', 'href="http://get.adobe.com/flashplayer/" target="_blank"');
                    Tooltip.show(error_img, msg);
                } else {
                    var msg2 = _('Sorry, it looks like the advanced uploader is incompatible with your system. Please use the <a id="basic_link">basic uploader</a> to upload via the website');
                    msg2 = msg2.replace('id="basic_link"', 'onclick="FileOps.show_basic_upload(Browse.current_location); return false;"');
                    Tooltip.show(error_img, msg2);
                }
            });
            action.update(error_img);
            break;

        case SWFUpload.FILE_STATUS.COMPLETE:
            file_elm.className = className + " complete";
            status.update("100%");
            width = "100%";
            action.update();
            $$(".uploadnotch").invoke("remove");
            break;

        case SWFUpload.FILE_STATUS.CANCELLED:
            file_elm.className = className + " cancelled";
            width = "100%";
            status.update("Cancelled");
            action.update();
            break;
        }

        if (width === 0) {
            progress.style.visibility = "hidden";
        } else {
            progress.style.visibility = "";
        }
        progress.style.width = width;
    }
};


var Hosts = {
    edit: function (id) {
        var elm = $("host" + id);
        if (elm.editing) {
            return;
        }

        elm.editing = true;
        var content = elm.innerHTML.unescapeHTML();
        elm.previous = content;

        elm.innerHTML = "<input type='text' class='skinny-input' size='20' maxlength='256' style=\"word-wrap: break-word;\" value=\"" + content.escapeHTML().gsub('"', '&quot;') + "\">&nbsp;" +
            "<input type='button' onclick='Hosts.doneEditing(\"" + id + "\");' class='button' value='" + _("Save") + "'>&nbsp;" +
            "<input type='button' onclick='Hosts.cancelEditing(\"" + id + "\");' class='button grayed' value='" + _("Cancel") + "'>";
        var i = elm.down('input');
        Event.observe(i, 'keydown', Hosts.checkKey(id));
        i.select();
        return false;
    },
    doneEditing: function (id) {
        var elm = $("host" + id);
        var newName = elm.down('input').value;

        new Ajax.DBRequest("/computer_edit?host_id=" + id + "&name=" + Util.urlquote(newName), {
            onSuccess: function (req) {
                Hosts.unedit(elm, req.responseText);
            }
        });
    },
    cancelEditing: function (id) {
        var elm = $("host" + id);
        Hosts.unedit(elm, elm.previous);
    },
    unedit: function (elm, value) {
        elm.editing = false;
        elm.innerHTML = value.escapeHTML();
    },
    unlink: function (id, name, plat) {
        DomUtil.fillVal(name.escapeHTML(), 'unlink-confirm-name');
        Modal.icon_show("computer_delete", _("Unlink Computer?"), DomUtil.fromElm('unlink-confirm'), {
            host_id: id,
            plat: plat,
            wit_group: 'unlink-confirm'
        });
    },
    doUnlink: function (id, plat) {
        new Ajax.DBRequest("/computer_edit?host_id=" + id + "&unlink=yessir", {
            onSuccess: function (req) {
                Hosts.killRow(id);
                Hosts.dec_count(plat);
            }
        });
    },
    dec_count: function (plat) {
        var count_div = $(plat + "-count");
        if (!count_div) {
            return;
        }

        var parts = count_div.innerHTML.split(" ");
        var count = parseInt(parts.shift(), 10);
        var desc = parts.join(" ");

        if (!count) {
            return;
        }

        count--;
        if (count == 1 && desc.charAt(desc.length - 1) == 's') {
            desc = desc.substr(0, desc.length - 1);
        } else if (count != 1 && desc.charAt(desc.length - 1) != 's') {
            desc = desc + "s";
        }

        count_div.innerHTML = count.toString() + " " + desc;
    },
    killRow: function (id) {
        var table = $("host" + id).up("table");
        $("host" + id).up("tr").remove();
        if (Hosts.rowCount() === 0) {
            var row = new Element("tr");
            var td = new Element("td", {colspan: 4});
            td.innerHTML = "<center>" + _("You no longer have any hosts linked.") + "</center>";
            table.insert(row);
            row.insert(td);
        }
    },
    rowCount: function () {
        return $$(".host-row").length;
    },
    checkKey: function (id) {
        return function (e) {
            e = e || window.event;

            if (e.keyCode == Event.KEY_RETURN) {
                Hosts.doneEditing(id);
            }
            if (e.keyCode == Event.KEY_ESC) {
                Hosts.cancelEditing(id);
            }
        };
    }
};


var Upgrade = {
    card_toggle: function (chosen) {
        return function (type) {
            var type_elm = $(type);

            if (type == chosen || !chosen) {
                type_elm.removeClassName("cc-icon-off");
            }
            else {
                type_elm.addClassName("cc-icon-off");
            }
        };
    },
    highlightCardtype: function () {
        var ccn = $('ccn');

        if (Upgrade.last_val == ccn.value) {
            return;
        }

        Upgrade.last_val = ccn.value;

        var ccnum = ccn.value;
        var first_two = ccnum.substr(0, 2);
        var all = $A(['visa', 'mastercard', 'amex']);
        var choice = null;

        if (ccnum.charAt(0) == '4') {
            choice = 'visa';
        }
        else if (first_two == '34' || first_two == '37') {
            choice = 'amex';
        }
        else if (parseInt(first_two, 10) >= 51 && parseInt(first_two, 10) <= 55) {
            choice = 'mastercard';
        }

        all.each(Upgrade.card_toggle(choice));
    },
    runCardHighlighter: function () {
        setInterval(Upgrade.highlightCardtype, 200);
    },
    highlightPlan: function () {
        var plans = $A(['fifty-plan', '100-plan', '250-plan', 'free-plan']);
        var checked = plans.map(Util.scry).find(function (x) {
            if (x) {
                return x.checked;
            }
        });

        if (Upgrade.last_checked == checked) {
            return;
        }

        if (Upgrade.last_checked) {
            Util.scry(Upgrade.last_checked.id + "-div").removeClassName("payment-option-selected");
        }

        if (checked) {
            Util.scry(checked.id + "-div").addClassName("payment-option-selected");
        }

        Upgrade.last_checked = checked;
    },
    enableNext: function () {
        var elm = $("next-button");
        elm.enable();
        elm.removeClassName("disabled-button");
    },
    disableNext: function () {
        var elm = $("next-button");
        elm.disable();
        elm.addClassName("disabled-button");
    },
    runPlanHighlighter: function () {
        setInterval(Upgrade.highlightPlan, 100);
    },
    showPlanInfo: function (plan) {
        // TRANSLATORS meaning, "the plan is 50 GB", etc
        var info = { "50-plan": _("It's 50 GB"), "100-plan": _("It's 100 GB"), "250-plan": _("It's 250 GB"), "free-plan": _("It's free")};
        Util.scry("plan-specific").update(info[plan.id]);
    }
};


var Home = {
    hide_promo: function (hide_url) {
        new Ajax.DBRequest(hide_url);
        $$("#left-content .lookatme").invoke("hide");
    },
    showScreencast: function (container_id, auto_play, width) {
         // h4x - This should probably use some variation of localized_path -- TB
        var end_frame = "/static/images/cc_endframe.jpg";
        var ratio = 360 / 640;
        if (Constants.USER_LOCALE == "en") {
            end_frame = "/static/images/cc_endframe_en.jpg";
            ratio = 353 / 640;
        }
        width = width || 532;
        var height = ratio * width;
        height = parseInt(height, 10);

        var params = { 'allowfullscreen': 'true',
                       'wmode'          : 'transparent' };

        // add locales to localized_path's second argument as they're available
        var flashvars = { 'file'           : localized_path('http://scast.s3.amazonaws.com/cc/dropbox_intro.mp4',
                                                            ['es', 'fr', 'de', 'ja']
                                                           ),
                          'skin'           : '/static/swf/bekle.swf',
                          'controlbar'     : 'over',
                          'image'          : end_frame };
        if (auto_play) {
            flashvars.autostart = 'true';
        }

        var div = new Element("div", {'id': 'commoncraft-embed', 'style': 'display: inline-block; border:1px solid #adcfea;background:#fff;'});

        $(container_id).update(div);
        swfobject.embedSWF('/static/swf/player-licensed.swf', 'commoncraft-embed', width.toString(), height.toString(), '9', false, flashvars, params);
        MCLog.log('commoncraft_views');
    },
    showFeedback: function (e) {
        if (e) {
            Event.stop(e);
        }
        // TRANSLATORS clicking this button pops up a form where users can give us feedback on our products.
        Modal.icon_show("comments", _("Tell Us What You Think"), $('feedback-div'), {icon: 'information'}, $('feedback_textarea'));
        return false;
    },
    hide: function (elm, value) {
        $(elm).up('div').hide();
        new Ajax.DBRequest("/hide/" + value);
    }
};


// split-todo is this still used?
var Inbox = {
    overQuotaModal: function (button, folder_name, used, free) {
        // TRANSLATORS a "quota warning" is displayed whenever the user's quota is low
        Modal.show(_('Quota Warning'), $('overquota-modal'));

        var modal_content = $('modal-content');

        $$('.shared-folder-name').invoke("update", folder_name);
        $$('.shared-folder-size').invoke("update", used);

        var submit = modal_content.getElementsByClassName('joinbutton');
        submit[0].onclick = function () {
            button.onclick = null;
            button.click();
        };
        submit[0].value = _("Join %(folder_name)s").format({'folder_name': folder_name});

        return false;
    }
};


var Install = {
    pingForLinkedHost: function (share_path) {
        new Ajax.Request("/host_linked",
            {method: 'get',
            onSuccess: function () {
                location.href = "/share" + share_path;
            },
            onFailure: function () {
                setTimeout(Install.pingForLinkedHost.curry(share_path), 3000);
            }
        });
    }
};


var Downloading = {
    registerAll: function () {
        if (Prototype.Browser.IE) {
            $$('.downloading-link').each(Downloading.register);
        }
    },

    register: function (elm) {
        elm = $(elm);
        elm.observe("click", Downloading.clicked);
    },

    clicked: function (e) {
        Event.stop(e);
        var elm = $(e.target);
        if (elm.nodeName === "SPAN") {
            elm = elm.up("a");
        }
        var args = elm.href.split("?").last();
        window.location = "/download?" + args;
        setTimeout(function () {
            window.location = "/downloading?" + args;
        }, 4000);
    }
};
document.observe("dom:loaded", Downloading.registerAll);


var Tour = {
    pages: {},
    loading: false,
    register: function () {
        var i = 1;
        $$(".tour-page a").each(function (elm) {
            elm.href = "#" + i;
            i += 1;
        });

        var button = $$(".abutton")[0];
        if (button) {
            button.href = "#" + (Tour.current_page + 1);
        }
        var pages = $$(".tour-content-page");
        for (var j = 0, len = pages.length; j < len; j += 1) {
            var page = pages[j];
            var index = parseInt(page.id.split("tour-page-")[1], 10) + 1;
            Tour.pages[index] = page.innerHTML;
        }
        Tour.interval = setInterval(Tour.check_url, 100);
    },
    load: function (page_num, event) {
        if (Tour.loading) {
            return;
        }

        page_num = page_num <= Tour.page_count && page_num > 0 ? page_num : 1;
        Tour.current_page = page_num;

        Tour.select_tab(page_num);
        Tour.loading = true;

        if (Tour.pages[page_num]) {
            Tour.show_page(page_num);
        } else {
            Feed.showLoading(false, 'tour-content', true); // just_icon=true
            var db_pro_suffix = Tour.db_pro ? "?db_pro" : "";
            new Ajax.Request("/tour/" + page_num + db_pro_suffix, {
                method: 'get',
                onSuccess: function (req) {
                    Tour.pages[page_num] = req.responseText;
                    Tour.show_page(page_num);
                    Feed.hideLoading();
                }
            });
        }
    },
    select_tab: function (page_num) {
        var tl,
            bl,
            border;

        $$("a.selected").each(function (elm) {
            elm.removeClassName("selected");
            tl = elm.down(".sidebar-tab-rounded-tl").remove();
            bl = elm.down(".sidebar-tab-rounded-bl").remove();
            border = elm.down(".sidebar-tab-rounded-l").remove();
        });

        var button = $$(".sidebar-tabs ul li a")[page_num - 1];
        button.blur();

        button.addClassName("selected");
        button.insert(tl);
        button.insert(bl);
        button.insert(border);
    },
    show_page: function (page_num) {
        $("tour-content").update(Tour.pages[page_num]);

        if (page_num < Tour.page_count) {
            var div = new Element("div", {'style': 'text-align: right;'});
            var a = new Element("a", {'href': '#' + (page_num + 1)});
            a.update(_("Next") + "&raquo;");
            a.addClassName("abutton");
            div.update(a);
            $("tour-content").insert(div);
        }
        Tour.loading = false;
    },
    check_url: function () {

        var hash = Util.url_hash();
        if (!hash || Tour.loading) {
            return;
        }

        hash = parseInt(hash, 10);
        if (hash != Tour.current_page) {
            Tour.load(hash);
        }
    }
};


var Help = {
    toggle_more_help: false,
    search_complete: function (search_string) {
        $('hide_on_search').hide();
        if (Help.toggle_more_help) {
            $('morehelp').show();
        }
        // TRANSLATORS for example: Search results for 'italy trip'
        // (where 'italy trip' is whatever they typed in the search box.)
        var msg = _("Search results for '%(search_query)s'").format({
            'search_query': search_string.escapeHTML()
        });
        $("search-results-title").update(msg);
        $("search-results-container").show();
    },
    search_empty: function () {
        if (Help.toggle_more_help) {
            $('morehelp').hide();
        }
        $('hide_on_search').show();
        $("search-results-container").hide();
    },
    show_os: function (e, elm, os) {
        elm = $(elm);

        $$(".os-filter").invoke("removeClassName", "selected");
        elm.addClassName("selected");
        $$(".help-os-section").invoke("hide");
        $$(".help-os-" + os).invoke("show");
        Event.stop(e);
    },

    vote: function (article_id, value) {
        new Ajax.DBRequest("/help/" + article_id + "/vote/" + value);
        new Effect.Fade("help-vote-cont");
        Notify.ServerSuccess(_("Thanks for your feedback!"));
    }
};


var AccountExtras = {
    prices: {},
    prices_value: {},
    watch_id: 0,
    watch: function () {
        if (!AccountExtras.watch_id) {
            AccountExtras.watch_id = setInterval(AccountExtras.update_prices, 200);
        }
    },
    show_detail: function (name, group_name, icon) {
        // TRANSLATORS feature_name is a proper noun. "Packrat" is one example of a feature_name.
        var msg = _("What is %(feature_name)s?").format({
            'feature_name': name
        });
        Modal.icon_show(icon, msg, $(group_name + "-modal"), {}, false);
        return false;
    },
    register_price: function (group_name, monthly, yearly, mvalue, yvalue) {
        AccountExtras.prices[group_name] = [monthly, yearly];
        AccountExtras.prices_value[group_name] = [mvalue, yvalue];
    },
    update_prices: function () {
        var yearly = $('yearly').checked;
        var period = yearly ? 1 : 0;
        var period_name = yearly ? _("year") : _("month");
        var subtotal_add = 0.0;
        for (var group_name in AccountExtras.prices) {
            if (AccountExtras.prices.hasOwnProperty(group_name)) {
                $(group_name + "-price").update(AccountExtras.prices[group_name][period]);
                $(group_name + "-priceperiod").update(period_name);
            }
            if (AccountExtras.prices_value.hasOwnProperty(group_name)) {
                if ($(group_name).checked) {
                    var value = AccountExtras.prices_value[group_name][period];
                    subtotal_add += parseFloat(value);
                }
            }
        }
        $(document).fire("widget:update_price", {'price': subtotal_add, 'period': period_name});
    }
};


var DowngradeReasons = {
    reasons: {},
    addReason: function (identifier, reason) {
        DowngradeReasons.reasons[identifier] = reason;
    },

    change: function (identifier, container) {
        identifier = parseInt(identifier, 10);
        var cont = $(container);
        assert(cont, "Couldn't find container for DowngradeReason");

        if (DowngradeReasons.reasons[identifier]) {
            cont.show();
            var img = Sprite.make("information", {});
            img.addClassName("text-img");
            cont.update(img);
            cont.insert(DowngradeReasons.reasons[identifier]);
        } else {
            cont.hide();
        }
    }
};


var Restore = {
    next: function (event, elm) {
        elm = $(elm);
        var selected_list = $$("ul.selected")[0];
        var new_selected_list = selected_list.next("ul");

        new_selected_list.addClassName("selected");
        selected_list.removeClassName("selected");

        if (new_selected_list.next("ul")) {
            Restore.show_next_link();
        } else {
            Restore.hide_next_link();
        }

        Restore.show_prev_link();
        Restore.inc_page(1);
    },

    prev: function (event, elm) {
        elm = $(elm);
        var selected_list = $$("ul.selected")[0];
        var new_selected_list = selected_list.previous("ul");

        new_selected_list.addClassName("selected");
        selected_list.removeClassName("selected");

        if (new_selected_list.previous("ul")) {
            Restore.show_prev_link();
        } else {
            Restore.hide_prev_link();
        }
        Restore.show_next_link();
        Restore.inc_page(-1);
    },

    inc_page: function (inc) {
        var current_page = parseInt($("page-num").innerHTML, 10);
        var new_page = current_page + inc;

        $("page-num").update(new_page);
    },

    hide_next_link: function () {
        $("next-page").update();
    },
    show_next_link: function () {
        var a = new Element("a", { href: "#", onclick: "Restore.next(event, this); return false;"});
        a.update(_("Next") + " &raquo;");
        $("next-page").update(a);
    },

    show_prev_link: function () {
        var a = new Element("a", { href: "#", onclick: "Restore.prev(event, this); return false;"});
        // TRANSLATORS short for "previous", as in, "previous page"
        a.update("&laquo; " + _("Prev"));
        $("prev-page").update(a);
    },
    hide_prev_link: function () {
        $("prev-page").update();
    }
};


var GenericFile = Class.create({
    initialize: function (filename, path, actions) {
        assert(filename && filename.length, "MISSING FILENAME");
        assert(actions, "MISSING ACTIONS");
        this.filename = filename;
        this.path = path;
        this.where = Util.urlquote(path);
        this.actions = actions;
    },
    attach_div: function (div) {
        this.div = div;
        div.file = this;
    },
    highlight: function () {
        this.highlighted = true;
        this.div.addClassName("hover");
        if (!this.selected) {
            this.add_arrow();
        }
    },
    dehighlight: function () {
        this.highlighted = false;
        this.div.removeClassName("hover");
        if (!this.selected) {
            this.remove_arrow();
        }
    },
    select: function () {
        this.selected = true;
        this.div.addClassName("selected");
        this.add_arrow();
    },
    deselect: function () {
        this.selected = false;
        this.div.removeClassName("selected");
        this.remove_arrow();
    },
    add_arrow: function () {
        var arrow = this.div.down(".file-arrow");
        if (!arrow) {
            arrow = Sprite.make("big-dropdown");
            arrow.addClassName("file-arrow");
            this.div.appendChild(arrow);
        }
    },
    remove_arrow: function () {
        var arrow = this.div.down(".file-arrow");
        if (arrow) {
            arrow.remove();
        }
    },
    href: function () {
        return false;
    }
});


var APIApp = Class.create(GenericFile, {
    initialize: function ($super, name, name_key, app_id, status, directory_status, actions) {
        $super(name, "", actions);
        this.name = name;
        this.name_key = name_key;
        this.app_id = app_id;
        this.status = status;
        this.directory_status = directory_status;
    }
});


var TokenFile = Class.create(GenericFile, {
    initialize: function ($super, filename, filename_key, path, tkey, dir, created, actions) {
        $super(filename, path, actions);
        this.tkey = tkey;
        this.is_dir = dir;
        this.name_sort = filename_key;
        this.created = created;
    }
});


var SharedFolder = Class.create(GenericFile, {
    initialize: function ($super, ns_id, filename, filename_key, path, modified, active, used, actions) {
        $super(filename, path, actions);
        this.ns_id = ns_id;
        this.filename_key = filename_key;
        this.modified = modified;
        this.active = active;
        this.used = used;
    },

    to_active: function () {
        this.active = true;
    },
    to_inactive: function () {
        this.active = false;
    }
});


// split-todo does this go in control or browse?
var BrowseController = Class.create({
    initialize: function (container_id, files, file_selector, options) {
        var container = $(container_id);
        assert(container, container_id + " is missing...");
        this.container = container;
        this.files = files;
        this.file_selector = file_selector;
        this.options = options || {};

        this.single_select = true;
        this.selected = [];

        this.attach_divs();
        if (!this.options.dont_listen) {
            this.listen();
        }
    },
    attach_divs: function () {
        var divs = this.container.select(this.file_selector);
        assert(divs.length == this.files.length, "div to file mismatch");

        for (var i = 0; i < this.files.length; i += 1) {
            var file = this.files[i];
            file.attach_div(divs[i]);
        }
    },
    listen: function () {
        var that = this;
        this.container.observe("mouseover", (function (e) {
            var target = Util.resolve_target(e.target, this.file_selector);
            if (target) {
                this.over(target.file);
            }
        }).bindAsEventListener(that));

        this.container.observe("mouseout", (function (e) {
            var target = Util.resolve_target(e.target, this.file_selector);
            if (target) {
                this.out(target.file);
            }
        }).bindAsEventListener(that));

        this.container.observe("mousedown", (function (e) {
            Event.stop(e);
            var target = Util.resolve_target(e.target, this.file_selector);
            if (target) {
                this.down(target.file);
            }
        }).bindAsEventListener(that));

        this.container.observe("mouseup", (function (e) {
            Event.stop(e);
            var target = Util.resolve_target(e.target, this.file_selector);
            if (target) {
                this.up(target.file);
            }
        }).bindAsEventListener(that));

        this.container.observe("click", (function (e) {
            if ($(e.target).hasClassName("dontkill")) {
                return;
            }
            Event.stop(e);

            var target = Util.resolve_target(e.target, this.file_selector);
            if (target) {
                this.click(target.file);
            }
        }).bindAsEventListener(that));

        $(document.body).observe("click", (function (e) {
            this.clear_selected();
        }).bindAsEventListener(that));

        this.container.oncontextmenu = Browse.onContext;
    },
    click: function (file) {
    },
    over: function (file) {
        if (this.options.disable_dropdown) {
            return;
        }

        if (this.clear) {
            clearTimeout(this.clear);
        }

        if (this.highlighted) {
            if (this.highlighted == file) {
                return;
            }

            this.highlighted.dehighlight();
            delete this.highlighted;
        }

        file.highlight();
        this.highlighted = file;
    },
    out: function (file) {
        if (this.options.disable_dropdown) {
            return;
        }
        this.clear = setTimeout((function () {
            this.highlighted.dehighlight();
            delete this.highlighted;
        }).bind(this), 100);
    },
    down: function (file) {
        if (this.options.disable_dropdown) {
            return;
        }
        if (this.single_select) {
            this.clear_selected();
        }
        file.select();
        this.selected.push(file);

    },
    up: function (file) {
        if (this.selected.length) {
            this.show_dropdown(this.selected[0]);
        }
    },
    show_dropdown: function (file) {
        var div = new Element("div", {id: "dropdown"});
        var menu = new Element("ul", {"class": "dropdown dropdown-lite note"});
        var that = this;
        $A(file.actions).each(function (option) {
            menu.insert(that.generate_li(option, file));
        });
        div.insert(menu);
        document.body.appendChild(div);
        div.absolutize();
        div.style.zIndex = 1001;
        div.clonePosition(file.div, {
            'offsetTop': file.div.getHeight() - 1,
            'offsetLeft': file.div.getWidth() - div.getWidth() - (Util.ie8 ? 1 : 0), // IE8 HAX
            'setWidth': false
        });
    },
    generate_li: function (option, file) {
        assert(BrowseActions.option_dict[option], "Couldn't find li action '" + option + "' for where '" + file.filename + "'");

        var opt = BrowseActions.option_dict[option];
        var li = new Element("li");

        var img = Sprite.make(opt.icon, {'class': 'icon_no_hover'});
        var img2 = Sprite.make(opt.icon + "_blue", {'class': 'icon_hover'});

        var text;
        if (typeof(opt.text) == "function") {
            text = opt.text();
        } else {
            text = opt.text;
        }

        var a = new Element('a').update(img).insert(img2).insert(text);
        a.addClassName("background-icon");
        HoverIconSwap.register(a);

        a.target = "_top";
        a.observe('mouseup', (function (e) {
            opt.onclick.call(file, e);
            BrowseActions.hide_dropdown();
        }).bindAsEventListener(file));

        li.update(a);

        return li;
    },
    clear_selected: function () {
        for (var i = 0, num = this.selected.length; i < num; i += 1) {
            var file = this.selected[i];
            file.deselect();
        }
        this.selected = [];
        BrowseActions.hide_dropdown();
    },
    sort: function (attr, reversed) {
        this.files = this.files.sort_by_key(function (file) {
            return file[attr];
        }, reversed);

        this.render();
    },
    find_file: function (path) {
        // Path needs to be uri encoded...
        path = decodeURI(path);
        for (var i = 0, len = this.files.length; i < len; i += 1) {
            var file = this.files[i];
            if (decodeURI(file.where) == path) {
                return file;
            }
        }
    },
    remove: function (file) {
        this.files.removeItem(file);
        file.div.remove();
    },
    remove_by_path: function (path) {
        var file = this.find_file(path);
        if (file) {
            this.remove(file);
        } else {
            assert(false, "Couldn't find file for" + path);
        }
    },
    render: function () {
        var shown = 0;
        for (var i = 0, len = this.files.length; i < len; i += 1) {
            if (this.files[i].visible !== false) {
                this.files[i].div.style.display = "";
                this.container.appendChild(this.files[i].div);
                shown += 1;
            } else {
                $("grave-yard").appendChild(this.files[i].div);
            }
        }
        if (this.options.after_render) {
            this.options.after_render(shown);
        }
    },
    filter: function (value) {
        value = value.toLowerCase();
        for (var i = 0, len = this.files.length; i < len; i += 1) {
            var file = this.files[i];
            if (value == "all" || file.div.hasClassName(value)) {
                file.visible = true;
            } else {
                file.visible = false;
            }
        }
        this.render();
    }
});


var SharedFolderController = Class.create(BrowseController, {
    render: function () {
        var shown = 0;
        for (var i = 0, len = this.files.length; i < len; i += 1) {
            if (this.files[i].visible !== false && (this.files[i].active || this.include_inactive)) {
                this.files[i].div.style.display = "";
                this.container.appendChild(this.files[i].div);
                shown += 1;
            } else {
                $("grave-yard").appendChild(this.files[i].div);
            }
        }
        var missing_active_msg = $("missing-active");
        if (shown) {
            missing_active_msg.hide();
        } else {
            missing_active_msg.show();
        }
        return shown;
    },
    toggle_deleted: function () {
        this.include_inactive = !this.include_inactive;
        this.render();
    },
    convert_to_inactive: function (path) {
        assert(path, "SFC missing path");
        var file = this.find_file(path);
        if (!file) {
            window.location.reload();
        }
        // Convert icon
        var elm = file.div.down(".sprite");
        Sprite.replace(elm, 'folder_user', 'folder_user_gray');

        // Remove link
        var link = file.div.down("a");
        var td = link.up("td");

        var innerhtml = link.innerHTML;
        link.remove();
        td.innerHTML = innerhtml + td.innerHTML;

        // Change options
        var rejoin = new Element("a", {
            href: '#'
        });
        rejoin.observe("click", function (e) {
            Event.stop(e);
            Sharing.rejoin(path, file.ns_id);
        });
        rejoin.update("Rejoin");
        rejoin.addClassName("dontkill");

        var remove = new Element("a", {
            href: '#'
        });
        remove.observe("click", function (e) {
            Event.stop(e);
            Sharing.ignore(path, file.ns_id);
        });
        remove.update("Remove");
        remove.addClassName("dontkill");

        var fragment = document.createDocumentFragment();
        fragment.appendChild(rejoin);
        var txt = document.createTextNode(" · ");
        fragment.appendChild(txt);
        fragment.appendChild(remove);

        var options = file.div.down("td.options");
        options.update();
        options.appendChild(fragment);

        file.active = false;
        this.render();
    },
    convert_to_active: function (path, new_path) {
        assert(path, "SFC to_active missing path");
        var file = this.find_file(path);
        if (!file) {
            window.location.reload();
        }

        file.path = new_path;
        file.where = Util.urlquote(new_path);

        // Convert icon
        var elm = file.div.down(".sprite");
        Sprite.replace(elm, 'folder_user_gray', 'folder_user');

        // Add link
        var link = new Element("a", {
            href: "/home" + file.where
        });
        link.addClassName("dontkill sf-filename");
        var td = file.div.down(".foldername");
        var members = td.down(".members-list");
        if (members) {
            members.remove();
        }

        link.innerHTML = td.innerHTML;
        td.update(link);
        if (members) {
            td.appendChild(members);
        }

        // Change options
        var options = file.div.down("td.options");
        var opts_link = new Element("a", {
            href: "#"
        });
        opts_link.update("Options");
        opts_link.observe("click", function (e) {
            Event.stop(e);
            Sharing.get_sharing_options(file.where);
        });
        options.update(opts_link);

        file.active = true;
        this.render();
    }
});


var SortController = Class.create({
    initialize: function (container_id) {
        this.container = $(container_id);
        this.links = this.container.select(".sort_option");

        var that = this;
        this.links.each(function (elm) {
            elm.observe("click", function () {
                that.click(this);
            });
            elm.observe("mouseenter", function () {
                that.over(this);
            });
            elm.observe("mouseleave", function () {
                that.out(this);
            });
        });
    },
    click: function (elm) {
        BrowseActions.hide_dropdown();
        this.container.select(".sort-tick").invoke("remove");
        this.links.each(function (e) {
            if (e != elm) {
                e.sorted = 0;
                e.removeClassName("selected");
            }
        });

        var arrow;
        if (!elm.sorted) {
            arrow = Sprite.make("sort-downtick-on");
        } else {
            arrow = Sprite.make("sort-uptick-on");
        }

        arrow.addClassName("sort-tick");
        elm.addClassName("selected");
        elm.appendChild(arrow);
    },
    over: function (elm) {
        elm.addClassName("over");
        if (elm.hasClassName("selected")) {
            var tick = elm.down(".sort-tick");
            assert(tick, "Missing tick");
            if (tick.hasClassName("s_sort-downtick-off")) {
                Sprite.replace(tick, "sort-downtick-off", "sort-downtick-on");
            } else if (tick.hasClassName("s_sort-uptick-off")) {
                Sprite.replace(tick, "sort-uptick-off", "sort-uptick-on");
            }
        }
    },
    out: function (elm) {
        elm.removeClassName("over");
        if (elm.hasClassName("selected")) {
            var tick = elm.down(".sort-tick");
            assert(tick, "Missing tick");
            if (tick.hasClassName("s_sort-downtick-on")) {
                Sprite.replace(tick, "sort-downtick-on", "sort-downtick-off");
            } else if (tick.hasClassName("s_sort-uptick-on")) {
                Sprite.replace(tick, "sort-uptick-on", "sort-uptick-off");
            }
        }
    }
});


// split-todo does this go in control or widgets?
var TabController = Class.create({
    initialize: function (container_id, options) {
        var container = $(container_id);
        assert(container, container_id + " is missing.");

        this.container = container;
        this.options = {killEvent: true};
        Object.extend(this.options, options);

        this.listen();
    },
    listen: function () {
        var that = this;
        this.container.select("a").each(function (elm) {
            assert(elm.id && elm.id.length > 0, "Element is missing an id");
            elm.observe("click", (function (e) {
                this.click(e, Util.resolve_target(e.target, "a"));
            }).bindAsEventListener(that));
        });
    },
    click: function (e, elm) {
        if (this.options.killEvent) {
            Event.stop(e);
        }
        this.toggle(elm);
    },
    toggle: function (new_selected) {
        var old_selected = this.container.down("a.selected");
        if (old_selected) {
            var old_content = $(old_selected.id + "-" + "content");
            if (old_content) {
                old_content.hide();
            }
        }

        this.container.select(".selected").invoke("removeClassName", "selected");
        new_selected.addClassName("selected");

        var content = $(new_selected.id + "-content");
        if (content) {
            content.show();
        }

        if (this.options.onTabChange) {
            this.options.onTabChange(new_selected, old_selected);
        }

        if (this.options.hash_prefix) {
            HashRouter.set_hash(this.options.hash_prefix, new_selected.id);
        }
    }
});


var Student = {
    show_domain_modal: function () {
        $("request_email").value = $F("student_email");
        $("request_desc").value = "";
        Modal.icon_show('page_white_edit', _("Add your school"), $("domain-request-modal"));
    }
};

var EmailVerification = {
    EMAIL_SENT_EVT: 'db:verification_email_sent',
    send_email: function () {
        new Ajax.DBRequest("/sendverifyemail", {
            onSuccess: function () {
                $(document.body).fire(EmailVerification.EMAIL_SENT_EVT);
            }
        });
    },
    setup: function () {
        document.observe("dom:loaded", function () {
            $("send-email-link").observe("click", function (e) {
                e.preventDefault();
                EmailVerification.send_email();
            });

            $(document.body).observe(EmailVerification.EMAIL_SENT_EVT, function () {
                $("pre-resend").hide();
                $("post-resend").show();
                $("pre-resend-header").hide();
                $("post-resend-header").show();
            });
        });
    }
};

function DBPhoto(photo) {
    photo.preloaded = {};
    photo.preload = function (size) {
        if (photo.preloaded[size]) {
            return;
        }
        size = size || 'l';
        assert(size in photo, "Photo doesn't have attr " + size);

        Util.preload_image(photo[size]);
        photo.preloaded[size] = true;
    };

    photo.load_thumb = function (img) {
        img.src = photo.thumbnail;
    };

    return photo;
}

var DBGallery = {
    size: 'large',
    index: 0,
    playing: false,
    preloaded: false,
    thumb_width: 64,
    thumb_margin: 4,
    low_opacity: 0.6,
    photos: [],
    set_url_hash: true,
    container_id: 'db_gallery_master_container',
    add_photos: function (photos) {
        photos.each(function (photo) {
            DBGallery.photos.push(DBPhoto(photo));
        });
    },
    set_hash: function () {
        if (DBGallery.set_url_hash) {
            HashRouter.set_hash.apply(this, $A(arguments));
        }
    },
    observe: function () {
        Event.observe(document.onresize ? document : window, "resize", DBGallery.resize);
        Event.observe(document.body, "mousewheel", DBGallery.wheel);
		Event.observe(document.body, "DOMMouseScroll",  DBGallery.wheel);
        document.observe("keydown", DBGallery.key);
    },
    unobserve: function () {
        Event.stopObserving(document.onresize ? document : window, "resize", DBGallery.resize);
        Event.stopObserving(document.body, "mousewheel", DBGallery.wheel);
		Event.stopObserving(document.body, "DOMMouseScroll",  DBGallery.wheel);
		document.stopObserving("keydown", DBGallery.key);

    },
    resize: function () {
        var cont = $("gallery_main_cont"),
            viewport_dims = document.viewport.getDimensions();

        cont.style.height = viewport_dims.height - 99 + "px";
        var main_photo = $("gallery_main_photo");

        var max_height = viewport_dims.height - 99 - 10;
        main_photo.style.maxHeight = Math.max(max_height, 300) + "px";
        main_photo.style.maxWidth = Math.max(viewport_dims.width, 400) + "px";

        // split-todo toss this?
        // var place_close = function () {
        //     var photo_width = main_photo.getWidth();
        //     if (photo_width > 100 && !Prototype.Browser.IE) {
        //         $("gallery_close").clonePosition(main_photo,  {
        //             setWidth: false,
        //             setHeight: false,
        //             offsetTop: -10,
        //             offsetLeft: main_photo.getWidth() - 20
        //         });
        //     } else {
        //         $("gallery_close").style.top = "120px";
        //         $("gallery_close").style.right = "10px";
        //     }
        // };
        //
        // place_close.defer();
        // main_photo.observe("load", place_close);
    },
    key: function (e) {
        var key = BrowseKeys.getKey(e);
        switch (key) {
        case 27: // Esc
            DBGallery.hide();
            break;
        case 32: // Space
            DBGallery.playpause();
            break;
        case 37: // left
            DBGallery.prev();
            break;
        case 39: //right
            DBGallery.next();
            break;
        }
    },
    wheel: function (e) {
        if (DBGallery.block_wheel) {
            return;
        }

        DBGallery.block_wheel = 1;
        setTimeout(function () {
            DBGallery.block_wheel = 0;
        }, 80);

        var delta = 0;
        if (e.wheelDelta) {
            delta = e.wheelDelta;
        }
	    else if (e.detail) {
	        delta = -e.detail;
	    }
        if (delta > 0) {
            DBGallery.prev();
        } else {
            DBGallery.next();
        }
    },
    playpause: function () {
        if (DBGallery.playing) {
            DBGallery.pause();
        } else {
            DBGallery.play();
        }
    },
    play: function () {
        DBGallery.playing = true;
        DBGallery.interval = setInterval(function () {
            DBGallery.next(true); // from_slideshow = true;
        }, 5000);

        $('gallery_slideshow').update(Sprite.html("white_pause") + _("Pause slideshow"));
    },
    pause: function () {
        DBGallery.playing = false;
        clearInterval(DBGallery.interval);
        $('gallery_slideshow').update(Sprite.html("white_play") + _("Play slideshow"));
    },
    next: function (from_slideshow) {
        var next_index = DBGallery.index + 1;
        if (next_index == DBGallery.photos.length) {
            return;
        } else {
            DBGallery.select_photo(next_index, from_slideshow);
        }
    },
    prev: function () {
        if (DBGallery.playing) {
            DBGallery.pause();
        }
        var next_index = DBGallery.index - 1;
        if (next_index == -1) {
            return;
        } else {
            DBGallery.select_photo(next_index);
        }
    },
    select_photo: function (index, from_slideshow) {
        index = parseInt(index, 10);

        if (!Util.isNumber(index)) {
            return;
        }

        DBGallery.set_hash("gallery", "" + index);
        if (!from_slideshow && DBGallery.playing) {
            DBGallery.pause();
        }

        DBGallery.index = index;
        if (!DBGallery.visible) {
            DBGallery.show(index);
        }
        var highlighted = $$("#gallery_thumbs_container img.selected");
        if (highlighted.length) {
            var prev = highlighted[0];
            prev.setOpacity(DBGallery.low_opacity);
            prev.removeClassName("selected");
        }

        var img = $$("#gallery_thumbs_container img")[index];
        assert(img, "Couldn't find img at index");

        img.setOpacity(1);
        img.addClassName("selected");

        DBGallery.render_mainphoto(index);
        DBGallery.resize();

        var thumb_container = $("gallery_thumbs_container");
        var new_margin = -1 * index * (DBGallery.thumb_width + DBGallery.thumb_margin) - 39;
        if (DBGallery.slide_in) {
            DBGallery.slide_in.cancel();
        }

        if (Math.abs(new_margin - parseInt(thumb_container.getStyle("margin-left"), 10)) > 1200) {
            thumb_container.style.marginLeft = new_margin + "px";
        } else {
            DBGallery.slide_in = new Effect.Tween(thumb_container, parseInt(thumb_container.getStyle("margin-left"), 10), new_margin, {
                'duration': 0.3
            }, function (v) {
                thumb_container.style.marginLeft = v + "px";
            });
        }

        var thumbs = thumb_container.select("img");
        assert(thumbs.length == DBGallery.photos.length, "thumbslength != photoslength");

        var thumbs_visible = Math.ceil(document.viewport.getDimensions().width / DBGallery.thumb_width);
        thumbs_visible += thumbs_visible % 2;
        for (var j = Math.max(0, index - thumbs_visible / 2); j < Math.min(DBGallery.photos.length, index + thumbs_visible / 2); j += 1) {
            DBGallery.photos[j].load_thumb(thumbs[j]);
        }
        for (var i = index; i < index + 10; i += 1) {
            if (i == DBGallery.photos.length) {
                break;
            }
            DBGallery.photos[i].preload(DBGallery.size);
        }
    },
    update_container_top: function (container) {
        container = container || $(DBGallery.container_id);
        if (container) {
            container.style.top = document.viewport.getScrollOffsets()[1] + "px";
        }
    },
    show: function (index) {
        assert(!DBGallery.visible, "Tried to show a gallery when it was already up");

        if (document.viewport.getDimensions().height > 768) {
            DBGallery.size = "extralarge";
        } else {
            DBGallery.size = "large";
        }

        DBGallery.visible = true;
        index = index || DBGallery.index;
        assert(DBGallery.photos.length, "No photos in the photo gallery");

        var container = new Element("div", {
            'id': DBGallery.container_id
        });

        DBGallery.update_container_top(container);
        DBGallery.update_top_interval = setInterval(function () {
            DBGallery.update_container_top();
        }, 200);

        document.body.appendChild(container);
        DBGallery.observe();
        DBGallery.render_backdrop(container);
        DBGallery.render_filmstrip(container);
        DBGallery.render_submenu(container);
        DBGallery.render_bottom_menu(index, container);
    },
    hide: function (e) {
        if (e) {
            Event.stop(e);
        }
        assert(DBGallery.visible, "Tried to hide a gallery when it was already hidden");
        DBGallery.visible = false;
        DBGallery.pause();
        DBGallery.unobserve();
        DBGallery.hide_backdrop();
        DBGallery.hide_filmstrip();
        DBGallery.hide_bottom_menu();
        DBGallery.hide_mainphoto();
        $(DBGallery.container_id).remove();
        DBGallery.set_hash();
        clearInterval(DBGallery.update_top_interval);
    },
    render_backdrop: function (container) {
        var elms = "body";
        if (Prototype.Browser.IE) {
            // Scrollbars still show up if you don't
            // add class to html in ie.
            elms = "html, body";
        }
        $$(elms).invoke('addClassName', "full_no_overflow");

        var backdrop = new Element("div", {'id': 'gallery_backdrop'});
        backdrop.setOpacity(0.8);
        container.insert(backdrop);
    },
    hide_backdrop: function () {
        $$(".full_no_overflow").invoke('removeClassName', "full_no_overflow");
        $("gallery_backdrop").remove();
    },

    render_filmstrip: function (container) {
        var filmstrip = new Element("div", {
            'id': 'gallery_filmstrip'
        });

        // filmstrip.setOpacity(0.6);
        var fragment = document.createDocumentFragment();
        fragment.appendChild(filmstrip);

        var filmstrip_back = new Element("div", {
            'id': 'gallery_filmstrip_backdrop'
        });

        filmstrip_back.setOpacity(0.5);
        fragment.appendChild(filmstrip_back);

        var thumb_container = new Element("div", {
            'id': 'gallery_thumbs_container'
        });

        // thumb_container.style.width = DBGallery.photos.length * (DBGallery.thumb_width + DBGallery.thumb_margin) + "px";

        var photo_index = 0;
        DBGallery.photos.each(function (photo) {
            var img = new Element("img", {
                'title': photo.filename,
                'src': '/static/images/icons/icon_spacer.gif'
            });

            img.setOpacity(DBGallery.low_opacity);

            img.observe("mouseover", function (e) {
                new Effect.Opacity(img, {
                    'to': 1,
                    'duration': 0.1
                });
            });

            img.observe("mouseout", function (e) {
                if (!img.hasClassName("selected")) {
                    new Effect.Opacity(img, {
                        'to': DBGallery.low_opacity,
                        'duration': 0.1
                    });
                }
            });

            img.observe("click", (function (i) {
                return function () {
                    DBGallery.select_photo(i);
                };
            })(photo_index));

            thumb_container.appendChild(img);
            photo_index += 1;
        });

        var selected_frame = new Element("div", {
            'id': 'gallery_selected_frame'
        });

        var gallery_close = new Element("a", {
            'id': 'gallery_close',
            'href': '#',
            'style': 'top: 120px; right: 10px;'
        });

        var close = new Element("img", {
            src: '/static/images/photos_x.png'
        });
        gallery_close.insert(close);
        gallery_close.observe("click", DBGallery.hide);

        fragment.appendChild(gallery_close);
        fragment.appendChild(selected_frame);
        fragment.appendChild(thumb_container);
        container.appendChild(fragment);
    },
    render_submenu: function (container) {
        var gallery_sub_menu = new Element("div", {
            'id': 'gallery_sub_menu'
        });

        var index_text = new Element("div", {
            'id': 'gallery_index_text'
        });

        var filename_text = new Element("div", {
            'id': 'gallery_filename_text'
        });

        var slideshow = new Element("a", {
            'id': 'gallery_slideshow'
        });
        slideshow.observe("click", DBGallery.playpause);

        gallery_sub_menu.insert(slideshow);
        gallery_sub_menu.insert(index_text);
        gallery_sub_menu.insert(filename_text);
        container.appendChild(gallery_sub_menu);
        DBGallery.pause();
    },
    render_bottom_menu: function (index, container) {
        // var photo = DBGallery.photos[index]; // split-todo unused?
        var bottom = new Element("div", {
            'id': 'gallery_bottom_menu'
        });

        var full_size = new Element("a", {
            'id': 'gallery_full_size'
        });
        full_size.update(Sprite.html("arrow_out_black") + "Full size");

        var save = new Element("a", {
            'id': 'gallery_save'
        });
        save.update(Sprite.html("picture_save") + "Save");

        bottom.insert(full_size);
        bottom.insert(save);
        container.appendChild(bottom);
    },
    update_bottom_menu: function (index) {
        var photo = DBGallery.photos[index];
        $('gallery_full_size').href = photo.original;
        $("gallery_save").href =  photo.original + "?dl_name=" + Util.urlquote(photo.filename);
    },
    hide_bottom_menu: function () {
        $("gallery_bottom_menu").remove();
    },
    hide_filmstrip: function () {
        $("gallery_filmstrip").remove();
        $("gallery_filmstrip_backdrop").remove();
        $("gallery_thumbs_container").remove();
        $('gallery_selected_frame').remove();
        $("gallery_close").remove();
        $("gallery_sub_menu").remove();
    },
    render_mainphoto: function (index, container) {
        container = container || $(DBGallery.container_id);
        var photo = DBGallery.photos[index];

        var img = Util.get_preloaded_image(photo[DBGallery.size]);

        img.id = 'gallery_main_photo';
        img.title = photo.filename;

        img.stopObserving("click");
        img.observe("click", function (e) {
            Event.stop(e);
            if (photo.video_url) {
                DBGallery.pause();
                $('gallery_main_photo_td').update('');
                var vid_width = DBGallery.size == 'extralarge' ? 1024 : 640;
                Util.embed_flash_video(photo.video_url, 'gallery_main_photo_td', vid_width, 1);
            } else {
                DBGallery.next();
            }
        });

        if (!$("gallery_main_cont")) {
            var cont = new Element("table", {'id': 'gallery_main_cont'});
            cont.observe("click", DBGallery.hide);

            var tbody = new Element("tbody");
            cont.insert(tbody);
            var tr = new Element("tr");
            tbody.insert(tr);
            var td = new Element("td");
            td.id = 'gallery_main_photo_td';
            tr.insert(td);

            td.insert(img);
            container.appendChild(cont);
        } else {
            $("gallery_main_cont").down("td").update(img);
        }

        DBGallery.update_sub_menu(index);
        DBGallery.update_bottom_menu(index);
    },
    update_sub_menu: function (index) {
        // var photo = DBGallery.photos[index]; // split-todo unused?
        $("gallery_index_text").update(index + 1 + " of " + DBGallery.photos.length);
        $("gallery_filename_text").update(DBGallery.photos[index].filename.escapeHTML());
    },
    hide_mainphoto: function () {
        $("gallery_main_cont").remove();
    }
};

// some of these reference each other cyclicly. declare all up front.
var Sort, SortSet, FileSearch, BrowseKeys, BrowseURL, BrowseFile, BrowseActions, Browse;

Sort = {
    FILES_BY_NAME: function (x, y) {
        // sort directories first
        var dir_diff = y.dir - x.dir;
        if (dir_diff) {
            return dir_diff;
        }

        return x.filename_ind - y.filename_ind; // this works based on sort_ind's
    },
    FILES_BY_KEY: function (x, y) {
        // sort directories first
        var dir_diff = y.dir - x.dir;
        if (dir_diff) {
            return dir_diff;
        }

        var xk = x.filename_key;
        var yk = y.filename_key;
        // keys are arrays, which don't compare correctly with '=' but do with '<' and '>'
        return xk < yk ? -1 : (xk > yk ? 1 : 0);
    },
    FILES_BY_SIZE: function (x, y) {
        // sort directories first
        var dir_diff = y.dir - x.dir;
        if (dir_diff) {
            return dir_diff;
        }

        if (x.dir) { // sort directories by name, not size
            return Sort.FILES_BY_NAME(x, y);
        }

        return x.bytes - y.bytes;
    },
    FILES_BY_MODIFIED: function (x, y) {
        var xts = x.ts;
        var yts = y.ts;

        // sort latest first
        return xts == yts ? 0 : (xts > yts ? 1 : -1);
    }
};

SortSet = {
    sort: function (elm, cmp) {
        BrowseKeys.clear_highlight();
        elm.blur();
        var direction = SortSet.pick(elm);
        Browse.sort(cmp, !direction); // reverse=!direction
        return true;
    },
    pick: function (elm) {
        var dir = Util.toggle(elm.id);

        var options = $$('.sort_option');
        options.each(
            function (x) {
                if (x != elm) {
                    Sprite.src(x.down('img'), 'downtick-spacer');
                    x.onmouseout = null;
                    x.onmouseover = null;
                    Util.reset_toggle(x.id);
                    x.href = BrowseURL.get_sort_url(x.id, false);
                } else {
                    var which = dir ? "up" : "down";
                    Sprite.src(x.down('img'), 'sort-' + which + 'tick-on');
                    x.onmouseout = function () {
                        Sprite.src(x.down('img'), 'sort-' + which + 'tick-off');
                    };
                    x.onmouseover = function () {
                        Sprite.src(x.down('img'), 'sort-' + which + 'tick-on');
                    };
                    x.href = BrowseURL.get_sort_url(x.id, dir);
                }
            });
        return dir;
    },
    make_url: function (id, dir) {
        return id + "," + (dir ? 1 : 0);
    },
    last_url: '',
    url_sort: function () {
        var piece = BrowseURL.get_sort();

        if (piece && piece.length) {
            var parts = piece.split(",");
            var elm = document.getElementById(parts[0]);
            if (!elm) {
                return false;
            }
            Util.set_next_toggle(elm.id, parts[1] == '1' ? true : false);
            elm.onclick();

            return true;
        }

        return false;
    }
};


FileSearch = {
    last_search: "",
    MAX_RETURNED: 100,
    searched: {},
    results: [],
    result_hash: {},
    hide_bottom: function () {
        if (!$$("#left-content .lookatme").length) {
            return;
        }
        $$(".bottom-menu").invoke("hide");
    },
    show_bottom: function () {
        if (!$$("#left-content .lookatme").length) {
            return;
        }
        $$(".bottom-menu").invoke("show");
    },
    per_page: function () {
        var ad = $("left-content").down(".lookatme");
        if (!ad || ad.style.display == "none") {
            return 10;
        }
        var height = ad.getHeight();
        if (height > 140) {
            return 8;
        } else {
            return 10;
        }
    },
    search: function (search_string, offset) {
        $("filesearchpaging").update();

        search_string = search_string.strip();
        FileSearch.last_search = search_string;
        var search_defaulted = SuggestionInput.defaulted($('filesearch'));
        if (search_string.length < 1 || search_defaulted) {
            FileSearch.show_bottom();
            $("filesearchresults").update();
            return;
        }

        var results = FileSearch.search_local(search_string);

        if (FileSearch.should_search_server(search_string)) {
            FileSearch.search_server.defer(search_string);

        } else if (results.length === 0) {
            FileSearch.render_empty();
        }

        if (results && results.length) {
            FileSearch.render(results, offset);
            FileSearch.hide_bottom();
        }
        return false;
    },

    invalidate_cache: function () {
        FileSearch.searched = {};
        FileSearch.results = [];
        FileSearch.result_hash = {};
    },

    search_local: function (search_string) {
        var terms = search_string.split(/\s+/);
        var results = [];

        for (var i = 0; i < FileSearch.results.length; i += 1) {
            var fname = FileSearch.results[i].filename.toLowerCase();
            var all_match = true;

            for (var t = 0, l = terms.length; t < l; t += 1) {
                all_match &= fname.indexOf(terms[t].toLowerCase()) > -1;
            }
            if (all_match) {
                results.push(FileSearch.results[i]);
            }
        }
        return results;
    },

    search_server: function (search_string) {
        FileSearch.searching = true;

        new Ajax.DBRequest("/search", {
            parameters: {'search_string': search_string},
            onSuccess: function (req) {
                FileSearch.searching = false;
                var results = req.responseText.evalJSON();

                FileSearch.searched[search_string] = results.length;

                if (results && results.length > 0) {
                    FileSearch.process_results(results);

                }
                FileSearch.search(FileSearch.last_search);
            }
        });
    },

    process_results: function (results) {
        var new_results = 0;

        for (var i = 0; i < results.length; i += 1) {
            var r = results[i];

            var hash = r.href;

            if (!FileSearch.result_hash[hash]) {
                FileSearch.results.push(r);
                FileSearch.result_hash[hash] = true;
                new_results += 1;
            }
        }

    },

    render: function (results, offset) {
        results.sort(function (a, b) {
            if (a.filename.length > b.filename.length) {
                return 1;
            } else if (a.filename.length == b.filename.length) {
                return 0;
            } else {
                return -1;
            }
        });

        offset = offset || 0;
        var short_results = results.slice(offset, offset + FileSearch.per_page());
        var ul = new Element("ul");
        for (var i = 0; i < short_results.length; i += 1) {
            ul.insert(FileSearch.render_item(short_results[i]));
        }

        var as_link = FileSearch.advanced_search_link();

        // TRANSLATORS for example, "17 results for 'italy photos'"
        var msg = ungettext("%(number_results)s result for %(search_query)s", "%(number_results)s results for %(search_query)s", results.length).format({
            'number_results': results.length + (results.length >= FileSearch.MAX_RETURNED ? "+" : ""),
            'search_query': FileSearch.last_search.escapeHTML().truncate(30)
        });
        msg += '<br/>' + as_link;
        var sr = new Element("h6").update(msg);

        $("filesearchresults").update(sr);

        var has_more = offset + FileSearch.per_page() < results.length;
        var has_less = offset > 0;
        var fsp = $("filesearchpaging");

        if (has_more || has_less) {
            if (has_less) {
                var prev = new Element("a", {'href': "#"});
                // TRANSLATORS short for "previous", meaning "previous page"
                prev.update("&laquo; " + _("prev"));
                prev.observe("click", function (e) {
                    FileSearch.search(FileSearch.last_search, offset - FileSearch.per_page());
                    Event.stop(e);
                });
                fsp.insert(prev);
            }

            if (has_more && has_less) {
                fsp.insert(" | ");
            }

            if (has_more) {
                var next = new Element("a", {'href': "#"});
                // TRANSLATORS as in "next page"
                next.update(_("next") + " &raquo;");
                next.observe("click", function (e) {
                    FileSearch.search(FileSearch.last_search, offset + FileSearch.per_page());
                    Event.stop(e);
                });
                fsp.insert(next);
            }
        }

        $("filesearchresults").insert(ul);

    },

    render_item: function (item) {
        var li = new Element("li");
        var a = new Element("a");

        if (item.icon.startsWith("folder")) {
            var ns_id, ns_path;
            if (item.target_ns) {
                ns_id = item.target_ns;
                ns_path = "";
            } else {
                ns_id = item.ns_id != Constants.root_ns ? item.ns_id : '';
                ns_path = item.path;
            }

            a.href = BrowseURL.get_path_url(ns_id, Util.urlquote(ns_path));
            a.observe("click", function (e) {
                Event.stop(e);
                BrowseURL.set_path_url(ns_id, Util.urlquote(ns_path));
            });
        } else {
            a.href = item.href;
        }

        var img;
        var size_str = item.size ? " - " + Util.formatBytes(item.size, 2, true) : '';
        a.title = item.fq_path + size_str;

        img = Sprite.make(item.icon);
        img.addClassName("link-img");

        a.insert(img);

        new Emstring(item.filename.escapeHTML());
        a.insert(item.filename.escapeHTML());
        li.update(a);

        return li;
    },
    advanced_search_link: function () {
        var result = '<span style="font-weight:normal;">';
        result += '(<a href="' + '/advanced_search?submit=y&include_files=y&include_folders=y&all_terms=' + Util.urlquote(FileSearch.last_search)  + '">' + _('advanced search') + '</a>)';
        result += '</span>';
        return result;
    },
    render_empty: function () {
        if (!FileSearch.searching) {
            var as_link = FileSearch.advanced_search_link();
            var msg = _("No results found for '%(search_query)s'").format({
                'search_query': FileSearch.last_search.escapeHTML().truncate(30)
            });
            $("filesearchresults").update(new Element("h6", {'style': 'margin:0 0 2px 0;padding:0;'}).update(msg));
            $("filesearchresults").insert(as_link);
        }
    },

    should_search_server: function (search_string) {
        if (FileSearch.searched[search_string] === undefined) {
            return true;
        } else {
            return false;
        }
    },

    warmup: function () {
        if (!FileSearch.warm) {
            new Ajax.DBRequest("/search/warmup");
            FileSearch.warm = true;
        }
    }
};


BrowseKeys = {
    init: function () {
        document.observe("keypress", BrowseKeys.pressed);
        document.observe("keydown", BrowseKeys.keydown);
    },

    getKey: function (e) {
        var key = e.keyCode || e.which || e.charCode;
        return key;
    },

    focus_in_input: function (e) {
        return document.activeElement && ["INPUT", "TEXTAREA", "SELECT"].indexOf(document.activeElement.tagName) != -1;
    },

	keydown: function (e) {
        var keyCode = BrowseKeys.getKey(e);

		if (BrowseKeys.focus_in_input()) {
			if (keyCode == 27) { // esc
				document.activeElement.blur();
			}
			return;
		} else if (!document.activeElement) {
			return;
		}

		if (keyCode == 27) { // 'esc' key
            BrowseKeys.hide_chart();
            Modal.hide();
		}
	},

    pressed_dict: {
        'search': {
            'title':    _("Search your files"),
            'key':      '/',
            'onPress':  function (e) {
                $("filesearch").focus();
                $(document.body).scrollTo();
            }
        },

        'move': {
            // TRANSLATORS "checked" means, files that have a checked checkbox next to them
            'title':    _("Move checked files"),
            'key':      'm',
            'onPress':  function (e, actions) {
                if (actions.indexOf("move") > -1) {
                    BrowseActions.option_dict.move_bulk.onclick(e);
                }
            }
        },
        'check_invert': {
            // TRANSLATORS "invert" means, check every unchecked box, and uncheck every checked box
            'title':    _('Invert checked files'),
            'key':      'i',
            'onPress':  function () {
                for (var i = 0; i < Browse.files.length; i += 1) {
                    var f = Browse.files[i];
                    if (f.checked) {
                        f.decheck();
                    } else {
                        f.check();
                    }
                }
            }
        },
        'check_deleted': {
            // TRANSLATORS "check" means, check the checkbox next to the file
            'title':    _("Check deleted files"),
            'key':      "p",
            'onPress':  function () {
                for (var i = 0; i < Browse.files.length; i += 1) {
                    var f = Browse.files[i];
                    if (f.bytes == "-1" && !f.checked) {
                        f.check();
                    } else if (f.bytes != "-1" && f.checked) {
                        f.decheck();
                    }
                }
            }
        },
        'check_all': {
            // TRANSLATORS "check" as in checkbox
            'title':    _("Check all files"),
            'key':      'a',
            'onPress':  function () {
                Browse.check_all();
            }
        },
        'check_none': {
            // TRANSLATORS "check" as in checkbox
            'title':    _("Uncheck all files"),
            'key':      'n',
            'onPress':  function () {
                Browse.decheck_all();
            }
        },
        'show_del': {
            'title':    _("Show/hide deleted files"),
            'key':      'd',
            'onPress':  function () {
                window.location.href = BrowseURL.get_del_url(BrowseURL.get_del() != 1);
            }
        },
        'help': {
            'title':    _("Show keyboard shorcuts"),
            'key':      '?',
            'shift':    true,
            'onPress':  function () {
                BrowseKeys.toggle_chart();
            }
        },
        'copy': {
            // TRANSLATORS "check" as in checkbox
            'title':    _("Copy checked files"),
            'key':      "c",
            'onPress':  function (e, actions) {
                if (actions.indexOf("copy") > -1) {
                    BrowseActions.option_dict.copy_bulk.onclick(e);
                }
            }
        },
        'up_dir': {
            // TRANSLATORS meaning, go from the current directory to its containing (parent) directory
            'title':    _("Up a directory"),
            'key':      'u',
            'onPress':  function () {
                if (!Browse.reloading) {
                    var parent_nsid, parent_dir;
                    if (Browse.is_share) {
                        parent_nsid = '';
                        parent_dir = Util.parentDir(Util.normPath(Browse.parent_ns_path));
                    } else {
                        parent_nsid = Browse.current_nsid;
                        parent_dir = Util.parentDir(Util.normPath(Browse.current_path));
                    }

                    if (Browse.current_path != parent_dir ||
                        Browse.current_nsid != parent_nsid) {
                        BrowseURL.set_path_url(parent_nsid, parent_dir);
                    }
                }
            }
        },

        'highlight_up': {
            'title':    _("Highlight previous file"),
            'key':      'k',
            'onPress': function () {
                BrowseKeys.highlight_up();
            }
        },

        'highlight_top': {
            'title':    _("Highlight first file"),
            'key':      'k',
            'shift':    true,
            'onPress':  function () {
                if (Browse.highlight_index >= 0) {
                    Browse.files[Browse.highlight_index].dehighlight();
                }
                Browse.highlight_index = 0;
                Browse.files[0].highlight();
                $("header").scrollTo();
            }
        },
        'highlight_down': {
            'title':    _("Highlight next file"),
            'key':      'j',
            'onPress':  function () {
                BrowseKeys.highlight_down();
            }
        },
        'highlight_bottom': {
            'title':    _("Highlight last file"),
            'key':      'j',
            'shift':    true,
            'onPress':  function () {
                if (Browse.highlight_index >= 0) {
                    Browse.files[Browse.highlight_index].dehighlight();
                }
                Browse.highlight_index = Browse.files.length - 1;
                Browse.files[Browse.highlight_index].highlight();
                $("footer").scrollTo();
            }
        },
        'check_file': {
            // TRANSLATORS meaning, "check the checkbox next to highlighted files"
            'title':    _("Check highlighted file"),
            'key':      ' ',
            'shift':    'optional',
            'stop_event': false,
            'onPress':  function (e) {
                if (Browse.highlight_index >= 0) {
                    var file = Browse.files[Browse.highlight_index];
                    file.click_check(e);
                    Event.stop(e);
                }
            }
        },
        'open_file': {
            'title':    _("Open highlighted file"),
            'key':      'o',
            'onPress':  function () {
                if (Browse.highlight_index >= 0 && !Browse.reloading) {
                    var file = Browse.files[Browse.highlight_index];
                    var url;
                    if (file.dir) {
                        url = "#" + file.where;
                    } else {
                        url = file.div.down(".details-filename a").href;
                    }
                    window.location = url;
                }
            }
        }
    },

    pressed: function (e) {
        var keyCode = BrowseKeys.getKey(e);
        var key = String.fromCharCode(keyCode).toLowerCase();
        var shift = e.shiftKey;

        // Make sure an input doesn't have focus
        if (!document.activeElement || BrowseKeys.focus_in_input()) {
            return;
        }
        var actions = BrowseActions.availMoreActions().join(" ");

        var k;
        for (k in BrowseKeys.pressed_dict) {
            if (BrowseKeys.pressed_dict.hasOwnProperty(k)) {
                var key_action = BrowseKeys.pressed_dict[k];
                if (key_action.key == key && ((key_action.shift || false) == shift || key_action.shift == "optional")) {
                    key_action.onPress(e, actions);
                    if (key_action.stop_event !== false) {
                        Event.stop(e);
                    }
                    break;
                }
            }
        }
    },

    clear_highlight: function () {
        if (Browse.highlight_index >= 0 && Browse.highlight_index < Browse.files.length) {
            Browse.files[Browse.highlight_index].dehighlight();
        }

        Browse.highlight_index = -1;
    },

    highlight_up: function () {
        if (!Browse.files.length) {
            return;
        }

        if (Browse.highlight_index > 0 && Browse.highlight_index < Browse.files.length) {
            Browse.files[Browse.highlight_index].dehighlight();
        }

        if (Browse.highlight_index < 0) {
            Browse.highlight_index = Browse.files.length - 1;
        } else if (Browse.highlight_index === 0) {
            return;
        } else {
            Browse.highlight_index = Browse.highlight_index - 1;
        }

        Browse.files[Browse.highlight_index].highlight();
    },

    highlight_down: function () {
        if (!Browse.files.length) {
            return;
        }

        if (Browse.highlight_index >= 0 && Browse.highlight_index < Browse.files.length) {
            Browse.files[Browse.highlight_index].dehighlight();
        }

        if (Browse.highlight_index < 0) {
            Browse.highlight_index = 0;
        } else if (Browse.highlight_index == Browse.files.length - 1) {
            Browse.highlight_index = Browse.files.length - 1;
        } else {
            Browse.highlight_index = Browse.highlight_index + 1;
        }

        Browse.files[Browse.highlight_index].highlight();
    },

    toggle_chart: function () {
        if ($("keys-chart").style.display == "none") {
            BrowseKeys.show_chart();
        } else {
            BrowseKeys.hide_chart();
        }
    },
    show_chart: function () {
        var chart = $("keys-chart");
        chart.absolutize();
        chart.clonePosition($("browse-files"));
        var offsettop = $("browse-files").viewportOffset()[1];
        if (offsettop < 0) {
            chart.style.top = parseInt(chart.style.top, 10) - $("browse-files").viewportOffset()[1] + 10 + "px";
        }
        chart.setOpacity(0.85);
        chart.show();
    },
    hide_chart: function () {
        $("keys-chart").hide();
    }
};


BrowseURL = {
    PATH: 0,
    SORT: 1,
    DEL:  2,
    NS:   3,
    ALL_FIELDS: ['PATH', 'SORT', 'DEL', 'NS'],
    get_sort: function () {
        return Util.url_hash().split(":")[BrowseURL.SORT];
    },
    get_path: function () {
        return Util.url_hash().split(":")[BrowseURL.PATH];
    },
    get_del: function () {
        return Util.url_hash().split(":")[BrowseURL.DEL];
    },
    get_ns: function () {
        return Util.url_hash().split(":")[BrowseURL.NS];
    },
    make_url: function (data) {
        // takes data dict {'FIELD_NAME': value}
        var p = Util.url_hash().split(":");
        for (var field_name in data) {
            if (data.hasOwnProperty(field_name)) {
                p[BrowseURL[field_name]] = Util.falsy_to_empty(data[field_name]);
            }
        }
        return "#" + p.join(":");
    },
    clean_url_hash: function () {
        var parts = Util.url_hash().split(":");
        var d = {};
        for (var i = 0, l = BrowseURL.ALL_FIELDS.length; i < l; i++) {
            var f = BrowseURL.ALL_FIELDS[i];
            d[f] = parts[BrowseURL[f]];
        }
        var url = BrowseURL.make_url(d);
        return url.substr(1); // drop the leading #
    },
    get_sort_url: function (id, dir) {
        return BrowseURL.make_url({'SORT': id + "," + (dir ? 1 : 0)});
    },
    get_path_url: function (ns_id, ns_path) {
        ns_id = Util.falsy_to_empty(ns_id);
        var url = BrowseURL.make_url({'PATH': Util.normPath(ns_path), 'NS': ns_id});

        return url;
    },
    set_path_url: function (ns_id, ns_path, no_reload) {
        var url = BrowseURL.make_url({'PATH': Util.normPath(ns_path), 'NS': ns_id});
        if (no_reload) {
            BrowseURL.last_hash = url.substr(1);
        }

        if (url.toLowerCase() != "#" + Util.url_hash().toLowerCase()) {
            // only create a new history entry when we get a genuinely new ns_path
            location.href = url;
        }
    },
    get_del_url: function (del) {
        return BrowseURL.make_url({'DEL': del ? 1 : 0});
    },
    check_url: function () {
        var hash = Util.url_hash();
        if (!hash.length && BrowseURL.last_hash && BrowseURL.last_hash.length) {
            return;
        }

        if (BrowseURL.last_hash != hash) {
            var parts = hash.split(":");
            var old = BrowseURL.last_hash ? BrowseURL.last_hash.split(":") : [];
            BrowseURL.last_hash = hash;

            if (old.length > BrowseURL.SORT - 1 && old[BrowseURL.SORT] != parts[BrowseURL.SORT]) {
                SortSet.url_sort();
            }

            var path_changed = Util.normPath(decodeURIComponent(old[BrowseURL.PATH])) != Util.normPath(decodeURIComponent(parts[BrowseURL.PATH]));
            var ns_changed = Util.falsy_to_empty(old[BrowseURL.NS]) != Util.falsy_to_empty(parts[BrowseURL.NS]);
            var deleted_changed = Util.falsy_to_empty(old[BrowseURL.DEL]) != Util.falsy_to_empty(parts[BrowseURL.DEL]);

            if (!hash || path_changed || ns_changed || deleted_changed) {
                Browse.deleted_shown = parts[BrowseURL.DEL] == '1' ? 1 : 0;
                Browse.reload(parts[BrowseURL.NS], parts[BrowseURL.PATH], deleted_changed || ns_changed); // force=deleted_changed || ns_changed
            }
        }
    }
};


BrowseFile = Class.create({
    initialize: function (icon, where, href, caption, filename, size, bytes, ago, ts, actions, hash, is_dir, drop_target, need_new, tkey, target_ns, filename_ind, filename_key, sjid) {
        this.icon = icon;
        this.caption = caption;
        this.filename = filename;
        this.where = where;
        this.hash = hash;
        this.href = href;
        this.size = size != 'None' ? size : "";
        this.bytes = bytes;
        this.ago = ago;
        this.ts = ts;
        this.selected = false;
        this.drop_target = drop_target;
        this.dir = is_dir ? 1 : 0;
        this.is_share = icon == "folder_user";
        this.is_sandbox = icon == "folder_star";
        this.tkey = tkey;
        this.target_ns = target_ns;
        this.main_actions = actions.strip().replace("  ", " ").split(" ");
        this.filename_ind = filename_ind || 0;
        this.filename_key = filename_key || [];
        this.sjid = sjid;

        Browse.add_file(this, need_new);
    },
    drag_dist: function (e) {
        return Math.abs(this.drag_startPos.x - e.clientX) +
            Math.abs(this.drag_startPos.y - e.clientY);
    },
    render: function (new_file, new_link) {
        var div;
        if (new_file) {
            div = new Element("div", {'class': Browse.details ? "browse-file-box-details" : "browse-file-box-iconic"});
            this.div = div;

            var del_class = this.bytes != '-1' ? "" : " deleted_file_line";
            var link = new_link ? new_link : "#";
            this.div.update(" <div style='position:relative;'><div class='details-check'><img class='sprite s_checkbox checkbox' src='/static/images/icons/icon_spacer.gif' align='absbottom'></div> " +
                            "<div class='details-icon'><a><img class='sprite s_" + this.icon + "' src='/static/images/icons/icon_spacer.gif' align='absbottom'></a></div>" +
                            "<div class='details-filename" + del_class + "'><a href='" + link + "'>" + this.caption + "</a></div> " +
                            "<div class='details-size'>" + (this.size || '&nbsp;') + "</div> " +
                            "<div class='details-modified'>" + this.ago + "</div> " +
                            "<div class='dropdown-arrow' style='visibility: hidden;'><img src='/static/images/big-dropdown.gif'></div>" +
                            "<br class='clear'/><div class='miniscule-text' style='line-height: 1px'>&nbsp;</div></div>");

            (function () {
                DBCheckbox.register(this.div.down("img"));
            }).bind(this).defer();

        } else {
            if (!Browse.file_div_cache) {
                Browse.file_div_cache = Browse.file_div.childElements();
            }
            div = Browse.file_div_cache[Browse.files.length - (Browse.has_parent_link ?  0 : 1)];
            this.div = div;
        }

        div.file = this;
        return div;
    },
    tooltip: function () {
        if (this.caption.unescapeHTML() != this.filename && this.filename.length) {
            Tooltip.show(this.a, this.filename);
        }
    },
    rename: function (where, is_folder, hash, sort_key) {
        // TODO: i (dlw) assume i can't add a .escapeHTML() to the end of this because most inputs are already escaped.
        // (in the case of the rename xss bug, it wasn't escaped.)
        var new_name = FileOps.filename(encodeURIComponent(where));
        this.caption = new_name.snippet();
        this.filename = new_name;
        this.where = Util.urlquote(where);
        this.hash = hash;
        this.filename_key = sort_key;
        // TRANSLATORS this text is added next to files that had just been created
        this.ago = is_folder ? "" : _("just now");

        if (is_folder) {
            this.href = BrowseURL.get_path_url(this.target_ns, this.target_ns ? '' : this.where);
        } else {
            var parts = this.href.split("/");
            parts[parts.length - 1] = Util.urlquote(new_name) + "?w=" + hash;
            this.href = parts.join("/");
        }

        var a = this.div.down(".details-filename").down("a");
        a.update(this.caption);
        a.title = this.filename;
        a.href = this.href;

        if (this.main_actions.contains("view_token")) {
            this.main_actions.removeItem("view_token");
            this.main_actions.push("token_share");
        }
    },
    move: function (afterFinish) {
        new Effect.Fade(this.div, {'afterFinish': afterFinish});
    },
    del: function (afterFinish) {
        if (!Browse.deleted_shown) {
            new Effect.Fade(this.div, {'afterFinish': afterFinish});
            Browse.emptyCheck();
        } else {
            this.size = "None";
            this.bytes = -1;
            this.ago = this.dir ? "" : _("just now");
            this.main_actions = this.dir ? ["full_restore"] : "undelete purge".split(" ");

            var a = this.div.down("a");
            a.addClassName("deleted_file_line");

            if (this.dir) {
                Sprite.src(this.div.down('img'), 'folder_gray');
            }
        }
    },
    purge: function (afterFinish) {
        new Effect.Fade(this.div, {'afterFinish': afterFinish});
        Browse.emptyCheck();
    },
    setOpacity: function () {
        if (this.size == 'None') {
            this.div.down("div").down("img").setOpacity(0.5);
        }
    },
    cachePos: function () {
        if (!this.div) {
            return;
        }

        if (!this.box) {
            this.box = {};
        }

        var offset = Browse.viewportOffset();
        if (!offset) {
            return;
        }

        this.box.left = offset.left + this.div.offsetLeft;
        this.box.top = offset.top + this.div.offsetTop;

        if (!this.box.width) {
            this.box.width = this.div.getWidth();
            this.box.height = this.div.getHeight();
        }
    },
    overlaps: function (box) {
        return Util.boxOnBox(box, this.box);
    },
    over: function () {
        if (Browse.dragging) {
            return;
        }

        if (!this.editing && this.filename && (!this.checked || (this.checked && Browse.checked_files.length == 1))) {
            this.dropdown_arrow(true); // on=true
        }

        if (!this.selected) {
            this.div.addClassName("file-highlight");
        }
    },
    out: function (e) {
        if (!this.div) {
            return;
        }
        if (e.toElement) {
            if (e.toElement.className == 'tooltip' || $(e.toElement) == this.div || $(e.toElement).descendantOf(this.div)) {
                return;
            }
        }

        if (!this.selected) {
            this.div.removeClassName("file-highlight");
            if (this.filename) {
                this.dropdown_arrow(false); // on=false
            }
        } else {
            this.div.removeClassName("file-selected-highlight");
        }
    },
    down: function (e) {
        if (this.editing || !this.filename) {
            return;
        }
        if (e) {
            Event.stop(e);
        }
        this.click_select(e);
        Browse.find_icons();
        BrowseActions.kill_dropdowns();

        this.drag_startPos = {x: e.clientX, y: e.clientY};
        this.drag_watch = this.drag.bindAsEventListener(this);
        this.up_watch = this.up.bindAsEventListener(this);

        Event.observe(document, "mousemove", this.drag_watch);
        Event.observe(document, "mouseup", this.up_watch);
    },
    show_dropdown: function () {
        BrowseActions.kill_dropdowns();
        if (!this.checked || Browse.checked_files.length == 1 && this.checked) {
            this.dropdown_arrow(true);  // on=true
            BrowseActions.dropdown(this, true); // show_all = true
        } else if (!Browse.is_rewind) {
            BrowseActions.showMore();
        }
    },
    down_dropdown: function (e) {
        this.click_select(e);
        this.show_dropdown(true); // show_all=true
    },
    up: function (e) {
        if (this.editing || !this.filename) {
            return;
        }
        if (!Browse.dragging) {
            this.deselect();
        }
        Event.stop(e);

        var dropped = this.drag_end(e);

        if (e.target.tagName == "IMG" && $(e.target).hasClassName("checkbox")) {
            this.click_check(e);
        } else if (!dropped && (e.target.tagName != 'A' && e.target.parentNode && e.target.parentNode.tagName != 'A' || Util.is_right_click(e))) {
            this.click_select(e);
            this.show_dropdown();
        }

        Event.stopObserving(document, "mousemove", this.drag_watch);
        Event.stopObserving(document, "mouseup", this.up_watch);
    },
    click_check: function (e) {
        Browse.deselect_all();

		var dd = $("dropdown");
		if (dd) {
			Util.yank(dd);
		}

        var shift = e.shiftKey;
        if (shift && Browse.last_checked) {
            if (this.checked) {
                Browse.uncheck_range(Browse.files.indexOf(Browse.last_checked), Browse.files.indexOf(this));
            } else {
                Browse.check_range(Browse.files.indexOf(Browse.last_checked), Browse.files.indexOf(this));
            }
        } else {
            if (!this.checked) {
                this.check();
            } else if (this.checked) {
                this.decheck();
            }
        }

        Browse.last_checked = this;
    },
    click_select: function (e) {
        Event.stop(e);

        // no multiple select yet
        if (false && e.shiftKey && e.ctrlKey) {
            Browse.select_range_to(this, true); // additive=true
        } else if (false && e.shiftKey) {
            Browse.select_range_to(this);
        } else if (false && e.ctrlKey) {
            this.toggle();
            this.over();
        } else if (!this.selected) {
            Browse.deselect_all();
            this.select();
        }

        if (false && (!e.shiftKey || e.ctrlKey)) {
            Browse.shift_start = this;
        }
    },
    drag: function (e) {
        Event.stop(e);
        var drag_dist = this.drag_dist(e);

        if (!Browse.allow_drag) {
            return;
        }

        if (!Browse.dragging && drag_dist >= 10) {
            this.drag_start(e);
        }

        if (drag_dist < 10) {
            return;
        }

        Browse.dragging = true;
        Browse.draw_clones(e);
        Browse.highlight_drop_target(e);
    },

    drag_start: function (e) {
        if (Browse.is_rewind) {
            return;
        }
        Browse.dragging = true;
        if (Browse.checked_files.length > 0 && Browse.checked_files.indexOf(this) == -1) {
            Browse.hide_checked();
        } else if (Browse.checked_files.indexOf(this) > -1) {
            for (var i = 0; i < Browse.checked_files.length; i += 1) {
                Browse.checked_files[i].select();
            }
        }
        this.select();
        Browse.clone_selected(e);
        Util.noHorizScroll();
    },
    drag_end: function (e) {
        if (!Browse.dragging) {
            return false;
        }

        Browse.dragging = false;
        var f = Browse.find_drop_target(e);
        Util.allowHorizScroll(); // weird, but you can't turn this back on before calculating the drop target...

        var dropped = false;
        if (f) {
            f.drop_lowlight();

            var can_drop = Browse.can_drop(f);
            if (can_drop !== true) {
                Notify.ServerError(can_drop);
            } else if (f != Browse.down_file) {
                assert(Browse.selected_files.length > 0, "Tried to move 0 files by dragging");
                var folder = f;
                FileOps.show_move_confirm(Browse.selected_files.slice(), decodeURIComponent(folder.where)); // is_folder=file.dir
                BrowseActions.kill_dropdowns();
                Browse.down_file = null;
                dropped = true;
            }
        }

        if (Browse.hidden_checked_files) {
            Browse.restore_checked();
        }

        Browse.deselect_all();
        Browse.kill_clones(e);

        Browse.down_file = null;
        return dropped;
    },
    highlight: function  () {
        var viewportHeight = document.viewport.getHeight();
        var offset = this.div.viewportOffset();
        if (offset.top < 0) {
            this.div.scrollIntoView(true);
        } else if (offset.top + this.div.getHeight() > viewportHeight) {
            this.div.scrollIntoView(false);
        }
        var arrow = $("highlight-arrow");
        arrow.show();
        arrow.clonePosition(this.div, {setWidth: false, setHeight: false, offsetLeft: -17, offsetTop: 6 });
    },
    dehighlight: function () {
        $("highlight-arrow").hide();
    },
    check: function (current_index) {
        if (this.editing) {
            return false;
        }

		var div = this.div;

        assert(!this.checked, "Tried to check a file that was already checked.");
        this.checked = true;

        this.checkbox = $(this.checkbox || Util.childElementByIndexPath(div, [0, 0, 0]));
        DBCheckbox.select(this.checkbox);
		div.addClassName("file-select");

        Browse.checked_files.push(this);

		current_index = current_index || Browse.files.indexOf(this);

		var files = Browse.files;
        var prev = files[current_index - 1];
        var next = files[current_index + 1];

        if (prev && prev.checked) {
			div.style.borderWidth = "0 1px 1px 1px";
			div.style.paddingTop = "4px";
        }

        if (next && next.checked) {
            next.div.style.borderWidth = "0 1px 1px 1px";
            next.div.style.paddingTop = "4px";
        }
    },

    decheck: function (checked_index, all) {
        if (this.editing) {
            return false;
        }
        assert(this.checked, "Tried to decheck a file that was not checked.");
        this.checked = false;

		var gcb = Browse.global_checkbox();
        if (gcb.selected) {
            DBCheckbox.deselect(gcb);
        }

        DBCheckbox.deselect(this.checkbox);

		if (!all) {
			checked_index = checked_index || Browse.checked_files.indexOf(this);
            Browse.checked_files.remove(checked_index);
        }

		var div = this.div;
		Util.removeClassName(this.div, "file-select");
		Util.removeClassName(this.div, "file-highlight");

        this.deselect();

		div.style.borderWidth = "1px";
		div.style.paddingTop = "";

        if (!all) {
	        var current_index = Browse.files.indexOf(this);
	        var next = Browse.files[current_index + 1];

	        if (next && next.checked) {
	            next.div.style.borderWidth = "1px";
	            next.div.style.paddingTop = "";
	        }
		}
    },
    select: function () {
        if (!this.selected) {
            this.selected = true;
            this.div.addClassName("file-select");
            Browse.selected_files.push(this);
        }
    },
    deselect: function () {
        if (this.selected) {
            Browse.selected_files.remove(Browse.selected_files.indexOf(this));
            this.selected = false;
			// var div = this.div; // split-todo seems unused

            if (!this.checked) {
                this.div.removeClassName("file-select");
                this.div.removeClassName("file-highlight");
                this.div.removeClassName("file-selected-highlight");
                this.dropdown_arrow(false); // show=false
            }
        }
    },
    toggle: function (old_selected) {
        if (old_selected === undefined) {
            old_selected = this.selected;
        }

        if (old_selected) {
            return this.deselect();
        } else {
            return this.select();
        }
    },
    set: function (select) { // opposite of toggle
        return this.toggle(!select);
    },
    drop_highlight: function () {
        this.div.addClassName("drop-highlight");
    },
    drop_highlight_bad: function () {
        this.div.addClassName("drop-highlight-bad");
    },
    drop_lowlight: function () {
        this.div.removeClassName("drop-highlight");
        this.div.removeClassName("drop-highlight-bad");
    },
    dropdown_arrow: function (show) {
        if (this.div) {
            if (show) {
                if (this.div.select('.dropdown-arrow').length) {
                    this.div.select('.dropdown-arrow')[0].style.visibility = "visible";
                }
            } else {
                if (this.div.select('.dropdown-arrow').length) {
                    this.div.select('.dropdown-arrow')[0].style.visibility = "hidden";
                }
            }
        }
    },
    edit: function (new_folder) {
        if (Browse.in_placer) {
            Browse.in_placer.file.editing = false;
            Browse.in_placer.editor.dispose();
            Browse.in_placer.name.innerHTML = Browse.in_placer.name_old_innerHTML;
            if (Browse.in_placer.new_folder) {
                Browse.in_placer.file.del(Browse.show_message);
            }
            Browse.in_placer = null;
        }

        Browse.selectable();
        this.editing = true;
        var name = this.div.down(".details-filename").down("a");
        var orig_innerHTML = name.innerHTML;
        name.innerHTML = this.filename.escapeHTML();
        var action = new_folder ? "new" : "rename";
        // TRANSLATORS as in "create a file", "rename a file", also please try to keep this translation fairly short.
        var action_text = new_folder ? _("Create") : _("Rename");
        var is_folder = this.dir ? "yes" : "";
        var editor = new Ajax.InPlaceEditor(name, "/cmd/" + action + Util.normalize(this.where) + "?long_running", {
            okControl: 'link',
            cancelControl: 'link',
            htmlResponse: false,

            highlightColor: '#ddf0ff',
            highlightEndColor: '#fafdff',

            okText: action_text,
            // TRANSLATORS please try to keep this translation relatively short.
            cancelText: _('Cancel'),
            clickToEditText: '',

            cols: 25,

            // this is called after clicking the submit button, before the ajax request.
            // the return value becomes the POST params in the ajax request.
            callback: function (form, new_name) {
                new_name = new_name.gsub("/", ":");
                return {to_path: new_name, 't': Constants.TOKEN, 'folder': is_folder};
            },
            ajaxOptions: {'method': 'post'},
            onComplete: (function (req) {
                if (req) {
                    TranslationSuggest.update_i18n_messages_from_req(req);
                    var status_code_start = req.status.toString().charAt(0);
                    if (status_code_start == '5' || status_code_start == '4') {
                        return;
                    }
                }
                Browse.unselectable();
                editor.dispose();
                this.editing = false;
                Browse.in_placer = null;
                if (!req) {
                    if (new_folder) {
                        this.purge(Browse.show_message); // afterwards=Browse.show_message
                    } else {
                        name.innerHTML = orig_innerHTML;
                    }
                    return;
                }
                if (req.responseText.indexOf('err:') === 0) {
                    Notify.ServerError(req.responseText.substr(4));
                    editor.dispose();
                    this.editing = false;
                    Browse.in_placer = null;
                    Browse.unselectable();
                    if (new_folder) {
                        this.del(Browse.show_message); // afterwards=Browse.show_message
                    } else {
                        name.innerHTML = orig_innerHTML;
                    }
                } else {
                    var resp = req.responseText.evalJSON(true);

                    var new_where = resp.fq_path;
                    var new_hash = resp.hash;
                    var new_sort_key = resp.sort_key.evalJSON();

                    if (new_folder) {
                        // try to find an old folder named the same thing to not show
                        var f = Browse.find_file(Util.urlquote(new_where));
                        if (f && f.bytes.toString() == "-1") {
                            f.purge();
                        }
                    }
                    this.rename(new_where, is_folder, new_hash, new_sort_key);
                    Browse.resort(this);
                    this.div.scrollTo();
                    if (action == "new") {
                        var new_msg = _("The folder '%(folder_name)s' was created successfully.").format({
                            'folder_name': this.filename.snippet().escapeHTML()
                        });
                        Notify.ServerSuccess(new_msg);
                    } else if (action == "rename") {
                        var rename_msg = is_folder ? _('Folder renamed to %(new_name)s.') : _('File renamed to %(new_name)s.');
                        rename_msg = rename_msg.format({
                            'new_name': this.filename.snippet().escapeHTML()
                        });
                        Notify.ServerSuccess(rename_msg);
                    }
                    TreeView.schedule_reset(); //refresh the folder list
                }
            }).bind(this),
            onFailure: (function () {
                editor.dispose();
                this.editing = false;
                Browse.in_placer = null;
                Browse.unselectable();
                if (new_folder) {
                    this.del(Browse.show_message); // afterwards=Browse.show_message
                } else {
                    name.innerHTML = orig_innerHTML;
                }
                Notify.ServerError();
            }).bind(this)
        });

        editor.enterEditMode();
        Browse.in_placer = {file: this, editor: editor, new_folder: new_folder, name: name, name_old_innerHTML: orig_innerHTML};
    }
});

BrowseFile.make_basic = function (where, actions, is_dir, tkey) {
    // this is the minimum we need to make a browsefile object for display
    var filename = FileOps.raw_filename(where);
    return new BrowseFile("", // icon
                          where,
                          Util.urlquote(where), // href
                          filename.snippet(), // caption
                          filename,
                          0, 0, 0, 0, // size, bytes, ago, ts
                          actions, "", is_dir, 0, 0, tkey); //  hash, is_dir, drop_target, need_new, tkey
};

BrowseActions = {
    option_dict: {
        'api_app_edit': {
            "icon": 'application_edit',
            // TRANSLATORS BUTTON
            "text": _("Edit Settings"),
            'onclick': function (e) {
                Event.stop(e);
                window.location.href = '/developers/edit?id=' + this.app_id;
            }
        },
        "api_app_remove": {
            "icon": 'application_delete',
            // TRANSLATORS BUTTON
            "text": _("Remove Application"),
            "onclick": function (e) {
                Apps.show_confirm(e, this.name, this.app_id, 'delete_app', 'delete');
            }

        },
        "restore_sjid": {
            'icon': "time",
            // TRANSLATORS BUTTON
            "text": _("Restore"),
            "onclick": function () {
                var file = Browse.find_file(this.where);
                FileOps.show_confirm_restore_sjids([file]);
            }
        },
        "restore_sjids": {
            'icon': "time",
            // TRANSLATORS BUTTON
            "text": _("Restore selected files"),
            "onclick": function () {
                if (!Browse.checked_files.length) {
                    Notify.ServerError(_("Select some files to restore"));
                    return;
                }
                FileOps.show_confirm_restore_sjids(Browse.checked_files);
            }
        },
        "restore_sandbox": {
            'icon': "folder_star",
            // TRANSLATORS BUTTON
            "text": _("Restore app folder"),
            "onclick": function () {
                var path = decodeURIComponent(this.where).toLowerCase();
                path = Util.normalize(path);
                assert(Browse.old_namespaces[path], "Restore missing ns");
                Apps.restore_sandbox(this.where, Browse.old_namespaces[path]);
            }
        },
        'sharing_options': {
            "icon": 'folder_user',
            // TRANSLATORS BUTTON
            "text": _("Shared folder options"),
            "onclick": function (e) {
                Event.stop(e);
                Sharing.get_sharing_options(this.where);
            }
        },
        'token_share_options': {
            "icon": "link",
            // TRANSLATORS BUTTON
            "text": _("Get shareable link"),
            "onclick": function (e) {
                Event.stop(e);
                Modal.show_loading("cog", "Loading Share...");
                SharingModel.get_token(this.where, function (token) {
                    SharingModel.show_post_options(token);
                });
            }
        },
        'view_token': {
            "icon": "link",
            "text": _("Get shareable link"),
            "target": "_blank",
            "onclick": function () {
                AMC.log('site_action', {
                    'action': 'shmodel_create',
                    'webserver': Constants.WEBSERVER
                });
                var url = "/s/" + this.tkey;
                if (!this.is_dir) {
                    url += "/" + Util.urlquote(FileOps.filename(this.where));
                }
                window.location.href = url;
            }
        },
        'remove_share': {
            'icon': 'link_delete',
            // TRANSLATORS BUTTON
            'text': _('Disable shareable link'),
            'onclick':  function () {
                SharingModel.confirm_remove(FileOps.filename(this.where), this.tkey, false, window.LinkController);
            }
        },
        'share_here': {
            "icon": 'folder_user',
            // TRANSLATORS BUTTON
            "text": _('Invite to folder'),
            "onclick": function (e) {
                Sharing.show_share_existing_modal(Browse.current_fqpath());
                Event.stop(e);
            }
        },
        'share_new': {
            "icon": 'folder_user',
            // TRANSLATORS BUTTON
            "text": _('Share a folder'),
            "onclick": function (e) {
                Sharing.start_wizard(e);
                Event.stop(e);
            }
        },
        'share': {
            "icon": 'folder_user',
            // TRANSLATORS BUTTON
            "text": _('Invite to folder'),
            "onclick": function (e) {
                Sharing.show_share_existing_modal(this.where);
                Event.stop(e);
            }
        },
        'share_invite_more': {
            "icon": 'group',
            // TRANSLATORS BUTTON
            "text": _('Invite more people'),
            "onclick": function (e) {
                Sharing.show_invite_more_modal(this.where);
                Event.stop(e);
            }
        },
        'share_leave': {
            "icon": 'folder_user_delete',
            // TRANSLATORS BUTTON
            "text": _('Leave shared folder'),
            "onclick": function (e) {
                PAGE_PATH = this.where;
                Sharing.show_leave_modal(this.where);
                Event.stop(e);
            }
        },
        'share_unshare': {
            "icon": 'link_break',
            // TRANSLATORS BUTTON
            "text": _('Unshare this folder'),
            "onclick": function (e) {
                PAGE_PATH = this.where;
                Sharing.show_unshare_modal(this.where);
                Event.stop(e);
            }
        },
        'share_opts_here': { // i18n-note: 'verb' attributes are not display strings
            "verb": 'share',
            "icon": 'user_add',
            // TRANSLATORS BUTTON
            // "Sharing" is a noun (gerund) here. Clicking this button will show the sharing information for the selected folder
            "text": _('Sharing info')
        },
        'share_opts': {
            "verb": 'share',
            "icon": 'user_add',
            "text": _('Sharing info')
        },
        'revisions': {
            "verb": 'revisions',
            "icon": 'time_back',
            // TRANSLATORS BUTTON
            "text": _('Previous versions')
        },
        'token_share': {
            "icon": 'link',
            // TRANSLATORS BUTTON
            "text": _("Get shareable link"),
            "onclick": function (e) {
                Forms.postRequest("/sm/create" + this.where, {}, {target:'_blank'});
            }
        },
        'undelete': {
            "icon": "basket_remove",
            // TRANSLATORS BUTTON
            "text": _("Undelete"),
            "onclick": function (e) {
                Event.stop(e);
                var file = Browse.find_file(this.where);
                FileOps.show_undelete(file);
            }
        },

        'copy_url': {
            "icon": "world_link",
            // TRANSLATORS BUTTON
            "text": _('Copy public link'),
            "public_href": function () {
                return "http://" + Constants.PUBSERVER + '/u/' + Constants.uid + this.where.substring(7);
            },
            "onclick": function (e) {
                AMC.log('site_action', {
                    'action': 'publink_create',
                    'webserver': Constants.WEBSERVER
                });
                BrowseActions.showCopyPublicUrlModal(
                    "http://" + Constants.PUBSERVER + '/u/' + Constants.uid + this.where.substring(7)
                );
                Event.stop(e);
            }
        },

        'download': {
            "onclick": function () {
                AMC.log('file_action', {
                    'action': 'download',
                    'num_files': 1,
                    'webserver': Constants.WEBSERVER
                });
                window.location.href = Constants.protocol + "://" + Constants.block + '/get' + this.where + this.hash + "&dl=1";
            },
            "icon": "download_arrow",
            // TRANSLATORS BUTTON
            "text": _("Download file")
        },

        'view': {
            "href": function () {
                return Constants.protocol + "://" + Constants.block + '/get' + this.where + this.hash;
            },
            "icon": "page_white_magnify",
            // TRANSLATORS BUTTON
            "text": _("View file")
        },

        'zipped_dl': {
            "icon": "page_white_compressed",
            // TRANSLATORS BUTTON
            "text": _("Download folder"),
            "onclick": function (e) {
                var file = Browse.find_file(this.where);
                FileOps.do_bulk_download([file]);
                Event.stop(e);
            }
        },

        'xattr_dl': {
            "icon": "download_arrow",
            // TRANSLATORS BUTTON
            "text": _("Download file"),
            "onclick": function (e) {
                var file = Browse.find_file(this.where);
                FileOps.do_bulk_download([file]);
                Event.stop(e);
            }
        },

        'photos': {
            "href": function () {
                return '/photos' + this.where.substring(7);
            },
            "icon": "pictures",
            // TRANSLATORS BUTTON
            "text": _("Gallery")
        },

        'a_photo': {
            "href": function () {
                return '/photoshow' + this.where.substring(7);
            },
            "icon": "pictures",
            // TRANSLATORS BUTTON
            // "gallery view" is a display mode that shows a group of images as a collection of thumbnails.
            // translate it as "gallery mode" if that is more natural.
            "text": _("Gallery view")
        },
        'rejoin': {
            "onclick": function () {
                var path = decodeURIComponent(this.where).toLowerCase();
                path = Util.normalize(path);
                assert(Browse.old_namespaces[path], "Rejoin missing ns");
                Sharing.rejoin(this.where, Browse.old_namespaces[path]);
            },
            "icon": "folder_user",
            // TRANSLATORS BUTTON
            "text": _("Rejoin shared folder")
        },
        'ignore': {
            "onclick": function () {
                Sharing.ignore(this.where);
            },
            "icon": "folder_user_delete",
            // TRANSLATORS BUTTON
            "text": _("Permanently remove")
        },
        'restore': {
            "href": function () {
                return '/restore' + this.where + "?prev=" + encodeURIComponent(location.href);
            },
            "icon": "time_go",
            // TRANSLATORS BUTTON
            "text": _("Restore folder")
        },

        'full_restore': {
            "href": function () {
                return '/restore' + this.where + "?prev=" + encodeURIComponent(location.href);
            },
            "icon": "time_back",
            "text": _("Restore folder")
        },

        'show_del': {
            "onclick": function (e) {
                window.location.href = BrowseURL.get_del_url(true);
                Event.stop(e);
            },
            "icon": "show_del",
            // TRANSLATORS BUTTON
            "text": _("Show deleted files")
        },

        'hide_del': {
            "onclick": function (e) {
                window.location.href = BrowseURL.get_del_url(false);
                Event.stop(e);
            },
            "icon": "hide_del",
            // TRANSLATORS BUTTON
            "text": _("Hide deleted files")
        },

        'copy': {
            "icon": "page_white_copy",
            // TRANSLATORS BUTTON
            "text": _('Copy to...'),
            "onclick": function (e) {
                FileOps.show_copy(this.where);
                Event.stop(e);
            }
        },

        'copy_folder': {
            "icon": "folder_page",
            "text": _('Copy to...'),
            "onclick": function (e) {
                FileOps.show_copy(this.where, true); // is_folder=true
                Event.stop(e);
            }
        },

        'copy_package': {
            "icon": "package_add",
            // TRANSLATORS BUTTON
            "text": _('Copy package to...'),
            "onclick": function (e) {
                FileOps.show_copy(this.where, true); // is_folder=true
                Event.stop(e);
            }
        },

        'move': {
            "icon": "page_white_go",
            // TRANSLATORS BUTTON
            "text": _('Move...'),
            "onclick": function (e) {
                FileOps.show_move(this.where);
                Event.stop(e);
            }
        },

        'move_folder': {
            "icon": "folder_go",
            "text": _('Move...'),
            "onclick": function (e) {
                FileOps.show_move(this.where, true); // is_folder=true
                Event.stop(e);
            }
        },

        'move_package': {
            "icon": "package_go",
            "text": _('Move...'),
            "onclick": function (e) {
                FileOps.show_move(this.where, true); // is_folder=true
                Event.stop(e);
            }
        },

        'rename': {
            "icon": "page_white_edit",
            // TRANSLATORS BUTTON
            "text": _('Rename...'),
            "onclick": function (e) {
                var file = Browse.find_file(this.where);
                if (file) {
                    file.edit();
                }
                Event.stop(e);
            }
        },

        'rename_folder': {
            "icon": "folder_edit",
            "text": _('Rename...'),
            "onclick": function (e) {
                var file = Browse.find_file(this.where);
                if (file) {
                    file.edit();
                }
                Event.stop(e);
            }
        },

        'rename_package': {
            "icon": "package",
            "text": _('Rename...'),
            "onclick": function (e) {
                var file = Browse.find_file(this.where);
                if (file) {
                    file.edit();
                }
                Event.stop(e);
            }
        },

        'delete': {
            "icon": "cancel",
            // TRANSLATORS BUTTON
            "text": _('Delete...'),
            "onclick": function (e) {
                FileOps.show_delete(this.where);
                Event.stop(e);
            }
        },

        'delete_folder': {
            "icon": "cancel",
            "text": _('Delete...'),
            "onclick": function (e) {
                FileOps.show_delete(this.where, true); // is_folder=true
                Event.stop(e);
            }
        },

        'delete_package': {
            "icon": "cancel",
            "text": _('Delete...'),
            "onclick": function (e) {
                FileOps.show_delete(this.where, true); // is_folder=true
                Event.stop(e);
            }
        },

        'purge': {
            "icon": "purge",
            // TRANSLATORS BUTTON
            "text": _("Permanently delete"),
            "onclick": function (e) {
                FileOps.show_purge(this.where);
                Event.stop(e);
            }
        },

        'purge_folder': {
            "icon": "purge",
            "text": _("Permanently delete"),
            "onclick": function (e) {
                FileOps.show_purge(this.where, true); // is_folder=true
                Event.stop(e);
            }
        },

        'new_folder': {
            "icon": "folder_add",
            // TRANSLATORS BUTTON
            // this means, make a new folder
            "text": _('New folder'),
            "onclick": function (e) {
                FileOps.inplace_new_folder(this.where);
                Event.stop(e);
                return false;
            }
        },

        'upload': {
            "icon": "page_white_get",
            // TRANSLATORS BUTTON
            // this means, upload a file.
            "text": _("Upload"),
            "onclick": function (e) {
                FileOps.show_upload(this.where);
                Event.stop(e);
            }
        },

        'app_info': {
            "icon": "application_double",
            // TRANSLATORS BUTTON
            // this means, display information about the application
            "text": _("Application info"),
            "href": function () {
                return "/account/applications";
            }
        },

        'more_actions': {
            //"icon": "wand",
            "text": function () {
                // TRANSLATORS BUTTON
                // this means: "Show more options"
                var txt = _("More");
                var icns = " <img src='/static/images/icons/icon_spacer.gif' class='sprite s_big-dropdown icon_no_hover' style='margin-right:-4px;' alt=''/><img style='margin-right:-4px;' src='/static/images/icons/icon_spacer.gif' class='sprite s_big-dropdown_blue icon_hover' alt=''/>";

                return txt + icns;
            },
            //'width': '150px',
            "onclick": function (e) {
                Event.stop(e, true);
                BrowseActions.showMore();
            }
        },

        'move_bulk': {
            'icon': 'folder_go',
            'text': function () {
                // TRANSLATORS BUTTON
                // for example "Move 5 items (to another folder)"
                return _('Move %d items').format(Browse.checked_files.length);
            },
            "onclick": function (e) {
                FileOps.show_move_bulk(Browse.checked_files);
                Event.stop(e);
            }
        },

        'copy_bulk': {
            'icon': 'folder_page',
            'text': function () {
                // TRANSLATORS BUTTON
                // for example "Copy 7 items (to another folder)"
                return _('Copy %d items').format(Browse.checked_files.length);
            },
            "onclick": function (e) {
                FileOps.show_copy_bulk(Browse.checked_files);
                Event.stop(e);
            }
        },

        'delete_bulk': {
            'icon': 'cancel',
            'text': function () {
                // TRANSLATORS BUTTON
                // for example "Delete 3 items (to another folder)"
                return _('Delete %d items').format(Browse.checked_files.length);
            },
            "onclick": function (e) {
                FileOps.show_bulk_delete(Browse.checked_files);
                Event.stop(e);
            }
        },

        'download_bulk': {
            'icon': 'page_white_compressed',
            'text': function () {
                // TRANSLATORS BUTTON
                // meaning: "download these items (that have been selected, together in a group)"
                return _('Download items');
            },
            "onclick": function (e) {
                FileOps.do_bulk_download(Browse.checked_files);
                Event.stop(e);
            }
        },

        'purge_bulk': {
            'icon': 'purge',
            // TRANSLATORS BUTTON
            // meaning: permanently delete this file
            'text': _('Permanently delete'),
            "onclick": function (e) {
                FileOps.show_bulk_purge(Browse.checked_files);
                Event.stop(e);
            }
        },

        'restore_bulk': {
            'icon': 'time_go',
            'text': function () {
                // TRANSLATORS BUTTON
                // for example "Undelete 14 items (that have been selected by the user)"
                return _('Undelete %d items').format(Browse.checked_files.length);
            },
            "onclick": function (e) {
                FileOps.show_bulk_restore(Browse.checked_files);
                Event.stop(e);
            }
        }
    },
    generate_li: function (where, what, hash, isActionBar, is_dir, tkey) {
        assert(BrowseActions.option_dict[what], "Couldn't find li action '" + what + "' for where '" + where + "'");

        var obj = Object.clone(BrowseActions.option_dict[what]);
        obj.where = where;
        obj.hash = hash ? "?w=" + hash : "";
        obj.is_dir = is_dir;
        obj.tkey = tkey;
        var li = new Element("li");

        var img = "",
            img2 = "";

        if (obj.icon) {
            img = Sprite.make(obj.icon, {'class': 'icon_no_hover'});
            img2 = Sprite.make(obj.icon + "_blue", {'class': 'icon_hover'});
        }

        var a;

        if (typeof(obj.text) == "function") {
            obj.text = obj.text();
        }

        if (isActionBar) {
            var t = new Element('span').update(img).insert(img2).insert(obj.text);
            a = HotButton.make(t);
        } else {
            a = new Element('a').update(img).insert(img2).insert(obj.text);
            a.addClassName("background-icon");
            HoverIconSwap.register(a);
        }

        a.target = obj.target || '_top';
        if (!obj.onclick) {
            a.href = obj.verb ? ("/" + obj.verb + where) : obj.href();
        }

        if (obj.width) {
            a.style.width = obj.width;
        }

        if (obj.onclick) {
            a.observe('mouseup', (function (e) {
                obj.onclick(e);
                if (what != "more_actions") {
                    BrowseActions.kill_dropdowns();
                }
            }).bindAsEventListener(obj));
        }

        li.update(a);
        return li;
    },
    sizeDropdownAnchors: function (selector) {
        var dropdown_li = $$(selector).first();
        if (!dropdown_li) {
            return;
        }
        var anchors = dropdown_li.select("a");
        if (!anchors) {
            return;
        }
        var width = parseInt(dropdown_li.getStyle("width"), 10) - parseInt(anchors[0].getStyle("padding-left"), 10) - parseInt(anchors[0].getStyle("padding-right"), 10) - 2;// 2 for the borders
        if (width < 0) {
            // Depending on the timing the user can get into a weird state where the dropdown is hidden
            // resulting in a negative width.
            return;
        }
        for (var i = 0, len = anchors.length; i < len; i += 1) {
            var a = anchors[i];
            a.style.width = width + "px";
        }
    },
    availMoreActions: function () {
        if (Browse.checked_files.length === 0) {
            var del_str = BrowseURL.get_del() === "1" ? 'hide_del' : 'show_del';
            if (Browse.show_deleted_button && !Browse.showing_deleted_button) {
                return [del_str];
            } else {
                return [];
            }
        } else if (Browse.checked_files.length === 1) {
            return Browse.checked_files[0].main_actions;
        }


        // Move, Copy, Delete, Purge, Restore, Download ZIP
        var options = ["move_bulk", "copy_bulk", "delete_bulk", "restore_bulk", "purge_bulk", "download_bulk"];
        var profile = Browse.profile_files(Browse.checked_files);

        if (profile.deleted > 0 || profile.public_folder > 0 || profile.photos_folder > 0) {
            options.removeItem("move_bulk");
            options.removeItem("copy_bulk");
            options.removeItem("delete_bulk");
        }

        if (profile.shared_folders > 0) {
            options.removeItem("copy_bulk");
        }

        if (profile.deleted > 0) {
            options.removeItem("delete_bulk");
            options.removeItem("download_bulk");
        }

        if (profile.deleted != Browse.checked_files.length) {
            options.removeItem("restore_bulk");
            options.removeItem("purge_bulk");
        }

        return options;
    },

    showMore: function () {
        BrowseActions.kill_dropdowns();

        var old_one = $("show-more-dropdown");
        if (old_one) {
            Util.yank(old_one);
        }

        var ul = new Element("ul", {"id": "show-more-dropdown"});
        ul.addClassName("dropdown dropdown-lite");
        var actions = BrowseActions.availMoreActions();

        var select_one_or_more = _("Select one or more files using the checkboxes.");
        var no_actions = _("No actions available for these files");
        if (!actions.length) {
            ul.insert(new Element("li").update(
                          new Element("p", {style: "padding: 5px 4px; margin: 0; text-align: center;"}).update(
                              Browse.checked_files.length === 0 ? select_one_or_more : no_actions)));
            ul.style.width = "150px";
        } else {
            actions.each(function (action) {
                var file = Browse.checked_files[0];
                if (file) {
                    ul.insert(BrowseActions.generate_li(file.where, action, Browse.checked_files[0].hash, false, file.dir, file.tkey));
                } else {
                    ul.insert(BrowseActions.generate_li(Browse.current_fqpath(), action, false, false, false, false));
                }
            });
            if (Util.ie) {
                BrowseActions.sizeDropdownAnchors.defer("#show-more-dropdown");
            }
        }

        $("browse-files").insert(ul);
        var show_more_button = $$("#browse-root-actions li").last();

        ul.clonePosition(show_more_button, {
            setWidth: false
        });
        ul.style.height = "auto";
        ul.style.top = parseInt(ul.style.top, 10) + 1 + show_more_button.getHeight() + "px";
        //ul.style.width = "150px";
        var left = parseInt(ul.style.left, 10) - (parseInt(ul.getWidth(), 10) - show_more_button.getWidth()) + 1 + 'px';
        ul.style.left = left;

        if (ul.viewportOffset().top < 0) {
            $("browse-location").scrollIntoView();
        }
        Event.observe(document, "click", BrowseActions.hideMore);
    },

    hideMore: function (e) {
        var show_more_button = $$("#browse-root-actions li").last();

        if (e && e.target && $(e.target).descendantOf(show_more_button)) {
            return;
        }

        var show_more = $("show-more-dropdown");
        if (show_more) {
            Util.yank(show_more);
        }

        Event.stopObserving(document, "click", BrowseActions.hideMore);
    },

    fillActionUL: function (where, actions, ul, add_separator, hash) {
        if (actions.length < 4 && Browse.show_deleted_button) {
            var del_str = BrowseURL.get_del() === "1" ? 'hide_del' : 'show_del';
            actions.push(del_str);
            Browse.showing_deleted_button = 1;
        } else {
            Browse.showing_deleted_button = 0;
        }
        ul.update(add_separator ? "<li class='action-separator'>|</li>" : "");
        if (!Browse.is_rewind) {
            actions.push("more_actions");
        }
        if (actions && actions.length) {
            ul.show();
            actions.each(
                function (action) {
                    if (action) {
                        ul.insert(BrowseActions.generate_li(where, action, hash, true));
                    }
                }
            );
        } else {
            ul.hide();
        }
        Browse.root_actions = actions.join(" ");
    },
    clear: function (file) {
        var bfa = $('browse-file-actions');
        if (bfa) {
            bfa.down("ul").update();
        }
        var mfa = $('more-file-actions');
        if (mfa) {
            mfa.update();
        }
    },
    kill_dropdowns: function () {
        BrowseActions.hideMore();

        var dd = $('dropdown');
        if (dd) {
            for (var i = 0; i < Browse.checked_files.length; i += 1) {
                Browse.checked_files[i].dropdown_arrow(false);
            }
            dd.hide();
        }
    },
    dropdown: function (file, show_all, e) {
        var elt = file.div;
        if (e) {
            Event.stop(e);
        }
        if (!elt || !file.selected) {
            return;
        }

        var where = file.where;
        var hash = file.hash;
        var options = file.main_actions;

        if (!options.length) {
            return;
        }

        var dd = $("dropdown");
        if (dd) {
            Event.stopObserving(document, "click", dd.listener);
            dd = Util.yank(dd);

            if (Browse.more_link) {
                Event.stopObserving(Browse.more_link, 'click', Browse.more_link_action);
                Browse.more_link = null;
                Browse.more_link_action = null;
            }
        }

        elt = $(elt);
        var div = new Element("div", {id: "dropdown"});
        var menu = new Element("ul", {"class": "dropdown dropdown-lite note"});
        $A(options).each(function (option) {
            menu.insert(BrowseActions.generate_li(where, option, hash, false, file.dir, file.tkey));
        });

        // var more_link = null; // split-todo seems unused
        div.insert(menu);

        menu.listener =
            function (e) {
                var click_target = $(e.target);
                if (click_target.descendantOf(file.div)) {
                    return;
                }

                Event.stopObserving(document, "click", menu.listener);

                if (Browse.more_link) {
                    Event.stopObserving(Browse.more_link, 'click', Browse.more_link_action);
                    Browse.more_link = null;
                    Browse.more_link_action = null;
                }
                if (e.target.parentNode.tagName != 'A' && e.target.tagName != 'A' || (e.target.href && e.target.href.length <= 2)) {
                    Event.stop(e);
                }

                var dd_elt = $("dropdown");
                if (dd_elt) {
                    dd_elt = Util.yank(dd_elt);
                    if (file.checked) {
                        file.dropdown_arrow(false);
                    }
                }
            };

        Event.observe(document, "click", menu.listener);

        $(elt.offsetParent).insert(div);

        var pos = elt.positionedOffset();

        var dim = elt.getDimensions();
        var dimen = div.getDimensions();
        var child_dim = div.down().getDimensions();

        div.style.left = (pos.left - child_dim.width + dim.width) + "px";
        div.style.top = (pos.top + dim.height - 1) + "px";

        if (!Util.ie) {
            var vpos_top = div.cumulativeOffset().top - Util.scrollTop();
            if (dimen.height + vpos_top > (window.innerHeight || document.documentElement.clientHeight)) {
                setTimeout(function () {
                    div.scrollIntoView(false);
                    div = null;
                }, 100);
            }
        } else {
            BrowseActions.sizeDropdownAnchors.defer("#dropdown .dropdown");
        }
        window.focus();

        return false;
    },
    hide_dropdown: function () {
        var dropdown = $("dropdown");
        if (dropdown) {
            dropdown.remove();
        }
    },
    showCopyPublicUrlModal: function (url) {
        Modal.icon_show(BrowseActions.getIcon("copy_url"), _("Copy Public Link"), DomUtil.fromElm('copy-public-url'), {
            wit_group: 'copy_public_link'
        });
        BrowseActions.addCopyUrlFlash(url);

        var textelm = $("modal-content").down("#public_url");
        assert(textelm, "Text element not found for copy pulic link");

        textelm.setValue(url);
        textelm.select();
    },
    clipboard_copy_done: function () {
        $('copy_success').update(Sprite.make("tick", {'style': 'vertical-align:middle;'}));
        $('copy_success').insert('&nbsp;' + _('Copied!'));
    },

    getIcon: function (name) {
        return BrowseActions.option_dict[name].icon;
    },

    shortenPublicLink: function () {
        Util.shorten_url($F('public_url'), BrowseActions.updatePublicLink);
        var img = new Element("img", {id: 'publink_loading', src: '/static/images/icons/ajax-loading-small.gif'});
        img.addClassName("right");
        $("modal-content").down("a").update(img);
    },

    updatePublicLink: function (url) {
        $('public_url').setValue(url);
        $('public_url').select();
        $('publink_loading').remove();
        BrowseActions.addCopyUrlFlash(url);
    },

    addCopyUrlFlash: function (url, elm_id, button) {
        elm_id = elm_id || "real_copy";
        button = button || "copy_button";
        var params = { 'wmode'    : 'transparent',
                       'flashVars': "copy_text=" + Util.urlquote(url) + "&callback=BrowseActions.clipboard_copy_done()" };

        swfobject.embedSWF('/static/swf/copy_to_clipboard.swf', 'copy_button', "100%", "100%", '6.0.65', false, false, params);
        button = $(button);
        button.absolutize();
        button.clonePosition($(elm_id), {
            offsetTop: -3,
            offsetLeft: -3,
            offsetHeight: 6,
            offsetWidth: 6
        });
    }
};


Browse = {
    listen: function (allow_drag) {
        $("browse-files").observe("mouseover", Browse.over);
        $("browse-files").observe("mouseout", Browse.out);
        $("browse-files").observe("mousedown", Browse.down);
        $("browse-files").observe("mouseup", Browse.up);
        $("browse-files").observe("click", Browse.click);
        $("browse-files").oncontextmenu = Browse.onContext;
        Browse.allow_drag = typeof(allow_drag) == 'boolean' ? allow_drag : true;
    },
    over: function (e) {
        var file_div = Util.resolve_target(e.target, ".browse-file-box-details");
        if (file_div && file_div.file) {
            if (!file_div.title) {
                file_div.title = file_div.file.filename;
            }
            file_div.file.over(e);
        }
    },
    out: function (e) {
        var file_div = Util.resolve_target(e.target, ".browse-file-box-details");
        if (file_div && file_div.file) {
            file_div.file.out(e);
        }
    },
    down: function (e) {
        if (Browse.down_file) {
            return;
        }

        var file_div = Util.resolve_target(e.target, ".browse-file-box-details");
        if (file_div && file_div.file) {
            Browse.down_file = file_div.file;
            file_div.file.down(e);
        }
    },
    up: function (e) {
        var file_div = Util.resolve_target(e.target, ".browse-file-box-details");
        if (file_div && file_div.file) {
            Browse.down_file = null;
            file_div.file.up(e);
        }
    },
    click: function (e) {
        var target = e.target;
        var file_div = Util.resolve_target(target, ".browse-file-box-details");

        if (file_div && file_div.file) {
            var file = file_div.file;
            var is_a = Util.resolve_target(target, "a");
            if (is_a) {
                if (file.dir) {
                    Event.stop(e);
                    var ns_id, path;
                    if (file.target_ns) {
                        // entering sf
                        ns_id = file.target_ns;
                        path = "";
                    } else if (Browse.is_share && file.where.length < Browse.parent_ns_path.length) {
                        // leaving sf
                        ns_id = '';
                        path = file.where;
                    } else {
                        ns_id = Browse.current_nsid;
                        path = Browse.fqpath_to_nspath(file.where, 'click');
                    }
                    BrowseURL.set_path_url(ns_id, path);
                }
            } else if (file.filename && e.target.className.indexOf("checkbox") == -1) {
                file.click_select(e);
            }
        }
    },
    msg: false,
    files: [],
    selected_files: [],
    checked_files: [],
    drop_targets: [],
    selection: [],
    drag_watch: null,
    drag_end_watch: null,
    dragging: false,
    drag_startPos: null,
    drag_box: {},
    details: true,
    first_load: true,
    last_sort: [Sort.FILES_BY_NAME, false],
    highlight_index: -1,

    emptyCheck: function () {
        if (!Browse.files.length) {
            Browse.show_message(_("Folder contains deleted files."));
        }
    },
    show_message: function (msg) {
        if (typeof(msg) != typeof('string')) {
            var message = $('browse-files').down(".browse-message");
            if (message) {
                message.show();
            }

            return;
        }

        Browse.msg = msg;

        var d = new Element("div", {'class': 'browse-message'});
        d.update(msg);
        $('browse-files').insert(d);
    },
    hide_message: function () {
        $$(".browse-message").invoke("hide");
    },
    sort: function (cmp, reverse, force) {
        if (!force && cmp == Browse.last_sort[0] &&
            reverse == Browse.last_sort[1]) {
            return;
        }

        Browse.last_sort = [cmp, reverse];

        if (reverse) {
            var orig_cmp = cmp;
            cmp = function (x, y) {
                return -1 * orig_cmp(x, y);
            };
        }

        Browse.files.sort(cmp);
        Browse.drop_targets.sort(cmp);
        Browse.refill();
        Browse.refresh_positions();

        return false;
    },
    resort: function (new_file) {
        BrowseKeys.clear_highlight();
        var l = Browse.last_sort;
        if (new_file) {
            Browse.fix_filename_keys(new_file);
        }
        Browse.sort(l[0], l[1], true); // force=true
        Browse.refresh_drop_positions();
    },
    fix_filename_keys: function (new_file) {
        var browse_files = Browse.files.slice();
        browse_files.sort(Sort.FILES_BY_KEY);
        for (var i = 0, l = browse_files.length; i < l; i++) {
            browse_files[i].filename_ind = i;
        }
    },
    refill: function () {
        var container = $('browse-files');

        for (var i = 0; i < Browse.files.length; i += 1) {
            var x = Browse.files[i];
            container.insert(x.div);
            if (x.selected) {
                x.select();
            }
        }
    },
    add_file: function (file, need_new) {
        if (file.filename) {
            Browse.files.push(file);
        } else {
            Browse.has_parent_link = true;
            Browse.parent_link = file;
        }

        if (!Browse.file_div) {
            Browse.file_div = $('browse-files');
        }
        file.render(need_new, file.href);
        // file.setOpacity();
        file.cachePos.bind(file).defer();
        if (file.drop_target) {
            Browse.drop_targets.push(file);
        }
        if (file.bytes < 0) {
            Browse.deleted_shown = true;
        }
    },
    refresh_drop_positions: function () {
        Browse.drop_targets.invoke("cachePos");
    },
    refresh_positions: function () {
        Browse.files.invoke("cachePos");
        if (Browse.has_parent_link) {
            Browse.parent_link.cachePos();
        }
        Browse.redraw_checks();
    },
    redraw_checks: function () {
        /* TODO - Make this less stupid */
        var files = Browse.checked_files.slice();
        for (var i = 0; i < files.length; i += 1) {
            files[i].decheck();
            files[i].check();
        }
    },
    remove_selected: function () {
        Browse.files.each(function (x) {
            if (x.selected) {
                x.div.remove();
            }
        });
        Browse.files = Browse.files.findAll(function (x) {
            return !x.selected;
        });
        Browse.refresh_positions();
    },
    find_icons: function () {
        if (!Browse.files.length) {
            return;
        }

        Browse.updateOffset();

        var test = Browse.files[0];
        var old_box_top;
        if (test.box) {
            old_box_top = test.box.top;
            test.cachePos();
        }

        if (!test.box || old_box_top != test.box.top) {
            Browse.refresh_positions();
        }
    },
    cache_selection: function () {
        Browse.selection = Browse.files.pluck('selected');
    },
    get_selected: function () {
        return Browse.selected_files;
    },
    check_all: function () {
        BrowseActions.kill_dropdowns();

		var files  = Browse.files;
		var	length = files.length;

        Browse.deselect_all();
        if (Browse.checked_files.length == length) {
            Browse.decheck_all();
        } else {
            for (var i = 0; i < length; i += 1) {
                if (!files[i].checked) {
                    files[i].check(i);
                }
            }
            if (Browse.checked_files.length == length) { // If editing all files might not get checked
                DBCheckbox.select(Browse.global_checkbox());
            }
        }
    },
    check_range: function (start, end, decheck) {
        var inc = start > end ? -1 : 1;

        for (var i = start; i != end + inc; i += inc) {
            var file = Browse.files[i];
            if (decheck && file.checked) {
                file.decheck();
            } else if (!decheck && !file.checked) {
                file.check();
            }
        }
    },
    uncheck_range: function (start, end) {
        Browse.check_range(start, end, true);
    },
    check_these: function (file_list) {
        for (var i = 0; i < file_list.length; i += 1) {
            file_list[i].check();
        }
    },
    hide_checked: function () {
        Browse.hidden_checked_files = Browse.checked_files.slice();
        Browse.decheck_all();
    },
    restore_checked: function () {
        Browse.check_these(Browse.hidden_checked_files);
        Browse.hidden_checked_files = false;
    },
    decheck_all: function () {
		var checked_files = Browse.checked_files;
        var i = checked_files.length;

		while (i--) {
			checked_files[i].decheck(i, true);
        }

		Browse.checked_files = [];
        Browse.deselect_all();
    },
    watch_for_deselect: function () {
        Event.observe(document.body, 'click',
            function (e) {
                if (e.which !== 1 && e.button !== 0) {
                    return;
                }

                if (e.target.nodeType == 3 || (e.target.tagName.toLowerCase() != 'a' && e.target.parentNode && e.target.parentNode.tagName.toLowerCase() != 'a')) {
                    Browse.deselect_all();
                }
            });
    },
    deselect_all: function () {
        BrowseActions.clear();
        var length = Browse.selected_files.length;
        for (var i = 0; i < length; i += 1) {
            Browse.selected_files[0].deselect();
        }
    },
    deselect_all_but: function (one) {
        Browse.deselect_all();
        one.select();
    },
    clicked_scrollbar: function (e) {
        var b = $('browse-files');
        var p = b.viewportOffset();
        var left = p.left + b.clientWidth;
        var right = p.left + b.offsetWidth;

        return left < e.clientX && e.clientX < right;
        // something about b.componentFromPoint(e.clientX, e.clientY) for IE
    },
    drag_start: function (e) {
        return; // no drag select yet
        /*
        Event.stop(e);
        Browse.find_icons();

        if (Browse.clicked_scrollbar(e)) {
            return;
        }

        if (e.ctrlKey)
            Browse.cache_selection();

        Browse.drag_watch = Browse.drag.bindAsEventListener(this);
        Browse.drag_end_watch = Browse.drag_end.bindAsEventListener(this);
        Event.observe(document, "mousemove", Browse.drag_watch);
        Event.observe(document, "mouseup", Browse.drag_end_watch);

        Browse.drag_startPos = {top: e.clientY, left: e.clientX};
        Util.initBox(e.clientY, e.clientX, Browse.drag_box);
        Browse.dragging = true;

        $('browse-selection').setOpacity(0.5);
        */
    },
    drag: function (e) {
        if (!Browse.dragging) {
            return;
        }
        Event.stop(e);

        var box = Browse.drag_box;

        Util.calcBox(e.clientY, e.clientX, Browse.drag_startPos.top, Browse.drag_startPos.left, box);
        Browse.draw_box(box);
        box.ctrl = e.ctrlKey;
        box.shift = e.shiftKey;

        if (box.width + box.height > 10) {
            Browse.select_under.defer();
        }
    },
    select_under: function () {
        var box = Browse.drag_box;
        var shiftKey = box.shift;
        var ctrlKey = box.ctrl;

        if (!Browse.dragging) {
            return;
        }
        for (var i = 0; i < Browse.files.length; i++) {
            var f = Browse.files[i];
            if (f.overlaps(box)) {
                if (ctrlKey) {
                    f.toggle(Browse.selection[i]);
                } else {
                    f.select();
                }
            } else {
                if (ctrlKey) {
                    f.set(Browse.selection[i]);
                } else if (!shiftKey) {
                    f.deselect();
                }
            }
        }
    },
    select_range_to: function (elm, additive) {
        var other = Browse.shift_start;
        if (!other) {
            Browse.shift_start = elm;
            elm.select();
        } else {
            var selecting = false;
            for (var i = 0; i < Browse.files.length; i++) {
                var f = Browse.files[i];

                if (!additive) {
                    f.set(selecting);
                } else if (selecting) {
                    f.select();
                }

                if (f == elm || f == other) {
                    f.select();
                    selecting = !selecting;
                }
            }
        }
    },
    drag_end: function (e) {
        if (e) {
            var box = Browse.drag_box;
            if (box.width + box.height < 10) {
                Browse.deselect_all();
            }
        }

        Browse.dragging = false;
        Event.stopObserving(document, "mousemove", Browse.drag_watch);
        Event.stopObserving(document, "mouseup", Browse.drag_end_watch);
        Browse.drag_watch = null;
        $('browse-selection').hide();
    },
    draw_box: function (box) {
        var s = $('browse-selection').style;
        s.top = box.top + "px";
        s.left = box.left + "px";
        s.width = box.width + "px";
        s.height = box.height + "px";
        s.display = '';
    },
    clone_selected: function (e) {
        Browse.sel_clones = $A();
        Browse.sel_clone_origin = {y: e.clientY, x: e.clientX, st: Util.scrollTop(), sl: Util.scrollLeft()};
        Util.scry('ghost-icons').update('');
        for (var i = 0; i < Browse.selected_files.length; i++) {
            var f = Browse.selected_files[i];

            var div = f.div.cloneNode(true); // deep=true
            div.file = null; // don't need this reference ever
            div.hide();
            div.absolutize();
            f.cachePos();
            var p = f.box;
            var top = p.top + Util.scrollTop();
            var left = p.left + Util.scrollLeft();
            div.style.top = top + "px";
            div.style.left = left + "px";
            div.style.width = p.width + "px";
            div.style.height = p.height + "px";

            div.removeClassName("file-select");
            div.removeClassName("file-highlight");
            div.removeClassName("file-selected-highlight");
            div.origin = {'top': top, 'left': left};
            div.setOpacity(0.5);

            Util.scry('ghost-icons').insert(div);
            Browse.sel_clones.push(div);

        }
    },
    draw_clones: function (e) {
        if (!this.sel_clone_origin) {
	        return;
	    }
        var dx = e.clientX -  this.sel_clone_origin.x;
        var dy = e.clientY -  this.sel_clone_origin.y;
        var ds  = Util.scrollTop() - this.sel_clone_origin.st;
        var dl  = Util.scrollLeft() - this.sel_clone_origin.sl;

        for (var i = 0; i < Browse.sel_clones.length; i += 1) {
            var div = Browse.sel_clones[i];
            div.style.display = '';
            div.style.top = (div.origin.top + dy + ds).toString() + "px";
            div.style.left = (div.origin.left + dx + dl).toString() + "px";
        }
    },
    kill_clones: function () {
        if (this.sel_clones) {
            this.sel_clones.map(Util.yank);
            this.sel_clones = null;
            this.sel_clone_origin = null;
        }
    },
    find_drop_target: function (e) {
        if (!Browse.sel_clone_origin) {
            return;
        }

        var x = e.clientX + Util.scrollLeft() - Browse.sel_clone_origin.sl;
        var y = e.clientY + Util.scrollTop() - Browse.sel_clone_origin.st;

        var i = Util.bsearch(Browse.drop_targets, 0,
            function (f) {
                var c = Util.cmpBox(x, y, f.box);
                // this next line's weird
                // 1) -c because we want box relative to point
                // 2) fail with -1 if it's selected
                return (c !== 0) ? -c : (f.selected ? -1 : 0);
            }
        );

        if (i != -1) {
            return Browse.drop_targets[i];
        }

        return false;
    },
    can_drop: function (dest) {
        var selected_profile = Browse.profile_files(Browse.selected_files);

        if ((dest.is_share || dest.is_sandbox) && (selected_profile.shared_folders > 0 || selected_profile.sandboxes > 0)) {
            if (selected_profile.sandboxes > 0) {
                if (dest.is_share) {
                    return _("You're not allowed to put an application folder inside a shared folder.");
                } else {
                    return _("You're not allowed to put an application folder inside another application folder.");
                }
            } else {
                if (dest.is_share) {
                    return _("You're not allowed to nest shared folders.");
                } else {
                    return _("You're not allowed to put a shared folder inside an application folder.");
                }
            }
        } else if (dest.where == "/Public" && selected_profile.shared_folders > 0) {
            return _("You're not allowed to move shared folders to your Public folder.");

        } else if (selected_profile.public_folder > 0) {
            return _("You're not allowed to move your Public folder.");

        } else if (selected_profile.photos_folder > 0) {
            return _("You're not allowed to move your Photos folder.");

        } else if (selected_profile.deleted > 0) {
            return _("Moving deleted folders or files is not allowed.");
        }
        return true;
    },
    remove_drop_target: function (t) {
        var targets = Browse.drop_targets;
        var l = targets.length;
        var out = [];

        for (var i = 0; i < l; i++) {
            var test = targets[i];
            if (t != test) {
                out.push(test);
            }
        }

        Browse.drop_targets = out;
    },
    highlight_drop_target: function (e) {
        var t = Browse.find_drop_target(e);

        if (Browse.last_drop_target && Browse.last_drop_target != t) {
            Browse.last_drop_target.drop_lowlight();
        }
        Browse.last_drop_target = t;

        if (t) {
            if (Browse.can_drop(t) === true) {
                t.drop_highlight();
            } else {
                t.drop_highlight_bad();
            }
        }
    },
    show_copy_move: function (t) {
        if (Browse.copy_move_over == t) {
            return;
        }
        Browse.copy_move_over = t;

        var fg = Browse.copy_move_texts();
        var bg = Browse.copy_move_overlays();
        var all = bg.concat(fg);

        var left = t.box.left + Browse.sel_clone_origin.sl;
        var top = t.box.top + Browse.sel_clone_origin.st;

        if (t.box.height > t.box.width) {
            // tile vertically
            all.each(function (x) {
                var s = x.style;
                s.width = this.box.width + "px";
                s.height = Math.round(this.box.height / 2) + "px";
                s.left = left + "px";
                s.display = '';
                s.lineHeight = s.height;
            }, t);
            bg[0].style.top = top + "px";
            bg[1].style.top = (top + Math.round(t.box.height / 2)) + "px";
            fg[0].style.top = top + "px";
            fg[1].style.top = (top + Math.round(t.box.height / 2)) + "px";
        } else {
            // tile horizontally
            all.each(function (x) {
                var s = x.style;
                s.height = this.box.height + "px";
                s.width = Math.round(this.box.width / 2) + "px";
                s.top = top + "px";
                s.display = '';
                s.lineHeight = s.height;
            }, t);
            bg[0].style.left = left + "px";
            bg[1].style.left = (left + Math.round(t.box.width / 2)) + "px";
            fg[0].style.left = left + "px";
            fg[1].style.left = (left + Math.round(t.box.width / 2)) + "px";
        }
        fg[0].box = Util.getBox(fg[0]);
        fg[1].box = Util.getBox(fg[1]);
    },
    bold_copy_move: function (e) {
        var opts = Browse.copy_move_texts();
        var highlight = 0;

        if (e.clientY > opts[1].box.top) {
            highlight = 1;
        }

        if (opts[0].last_highlight == highlight) {
            return;
        }
        opts[0].last_highlight = highlight;

        opts[highlight].addClassName('copy-move-bold');
        opts[1 - highlight].removeClassName('copy-move-bold');
    },
    hide_copy_move: function () {
        Browse.copy_move_over = false;
        Browse.copy_move_overlays().invoke('hide');
        Browse.copy_move_texts().invoke('hide');
    },
    over_copy_option: function (e) {
        return Util.pointOnBox(e.clientX, e.clientY, Util.getBox('copy-text'));
    },
    cmos: [],
    copy_move_overlays: function () {
        if (!Browse.cmos.length) {
            Browse.cmos = $$('.copy-move-overlay');
        }

        return Browse.cmos;
    },
    cmts: [],
    copy_move_texts: function () {
        if (!Browse.cmts.length) {
            Browse.cmts = $$('.copy-move-text');
        }

        return Browse.cmts;
    },
    find_file: function (where) {
        return Browse.files.find(function (x) {
            return x.where == where;
        });
    },
    pull_file: function (where) {
        var files = Browse.files;
        var l = files.length;
        var out = [];

        var file = false;
        for (var i = 0; i < l; i++) {
            var f = files[i];
            if (f.where == where) {
                file = f;
            } else {
                out.push(f);
            }
        }

        Browse.files = out;
        return file;
    },
    reset_state: function () {
        // drop dom references for the gc's sake

        Browse.msg = false;
        Browse.dragging = false;
        Browse.files = [];
        Browse.selected_files = [];
        Browse.checked_files = [];
        Browse.lact_checked = null;
        Browse.sel_clones = [];
        Browse.sel_clone_origin = null;
        Browse.drop_targets = [];
        Browse.last_drop_target = null;
        Browse.selection = [];
        Browse.file_div_cache = null;
        Browse.has_parent_link = false;
        Browse.parent_link = null;
        Browse.in_placer = null;

        BrowseActions.kill_dropdowns();
    },
    update: function (content) {
        $("browse-files").update(content);
    },
    reload_fqpath: function (path) {
        // avoid calling this in cases where the path is actually under an ns
        // otherwise the user'll get forced to a new url afterwards
        return Browse.reload(null, path);
    },
    force_reload: function () {
        return Browse.reload(Browse.current_nsid, Browse.current_path, true);
    },
    reload: function (ns_id, path, force) {
        if (Browse.reloading) { // one reload at a time
            return;
        }

        ns_id = ns_id || '';
        path = Util.normPath(path || '');

        if (Util.normPath(path) == Util.normPath(Browse.current_path) &&
            ns_id == Browse.current_nsid &&
            !force) { // don't reload same path
            return;
        }

        if (!path && path !== '' && !ns_id) {
            path = Browse.current_path;
            ns_id = Browse.current_nsid;
        }
        Browse.reloading = true;

        Browse.current_path = path;
        Browse.current_nsid = ns_id;

        var referrer = Browse.first_load ? Constants.referrer : '';

        if (Browse.first_load) {
            Browse.first_load = false;
            BrowseURL.set_path_url(ns_id, path);
        }

        Feed.showLoading(false, 'browse-files', true); // just_icon=true

        if (Util.ie) { // @HACK => Prototype doesn't handle calculating the left offset of nested offset elements properly.
            $("feed-loading").style.left = $("browse-box").viewportOffset()[0] + "px";
        }

        var del = Browse.deleted_shown ? "&show_deleted=yah" : "";

        var params = {'ns_id': ns_id, 'referrer': referrer};
        params = Object.extend(params, Browse.extra_reload_args);

        new Ajax.DBRequest(Browse.reload_path + Util.normalize(path) + "?ajax=yes" + del, {
            parameters: params,
            onSuccess: function (req) {
                Browse.reset_state();
                Browse.update(req.responseText);
                if (Browse.select_file) {
                    (function () {
                        var file_path = Browse.current_fqpath() + "/" + Util.urlquote(Browse.select_file);
                        var file = Browse.find_file(file_path);

                        if (file) {
                            file.select();
                            file.show_dropdown();
                            Browse.select_file = false;
                        }
                    }).defer();
                }
                var gc = Browse.global_checkbox();
                if (gc.selected) {
                    DBCheckbox.deselect(gc);
                }

                if (Browse.force_sort) {
                    Browse.force_sort = false;
                    Browse.resort.defer();
                }
                Browse.listen();
            },
            cleanUp: function () {
                Feed.hideLoading();
                Browse.reloading = false;
                if (document.activeElement && document.activeElement != document.body) {
                    document.activeElement.blur();
                }
                Util.childElementCache = {};
            },
            no_feed_reload: true
        });
        FileSearch.invalidate_cache();
        BrowseKeys.clear_highlight();
        return true;
    },
    unload: function () {
        Browse.reset_state();
        var dd = $('dropdown');
        if (dd) {
            dd = Util.yank(dd);
        }
    },
    breadcrumb: function (path) {
        path = Util.normPath(path);
        var parts = path.split("/");
        var bc = "";

        function bcrumb_link(name, ns_id, path, last, first, icon) {
            name = name.snippet(40);
            if (!(first || last)) {
                icon = ""; // Only the first and last elements get icons
            } else {
                icon = "<img src=\"/static/images/icons/icon_spacer.gif\" class=\"sprite s_" + icon + " link-img\"  alt=\"\"/>";
            }

            var link;
            if (!last) {
                link = "<a href='" +  BrowseURL.get_path_url(ns_id, path) + "'";
                link += " onclick='return BrowseURL.set_path_url(\"" + ns_id  + "\", \"" + path + "\");'";
                link += ">";
                link += icon + name;
                link += "</a>";

                link =  link + " &#187; ";

                return link;
            } else {
                return icon + name;
            }
        }

        assert(!Browse.current_nsid || Browse.parent_ns_path, "in some nsid but no parent path set");
        var current_fqpath, current_nsid;
        for (var i = 0 ; i < parts.length; i++) {
            var name = new Emstring(decodeURIComponent(parts[i] || _("Dropbox")).escapeHTML()).toString();
            var last = i + 1 === parts.length;
            var first = i === 0;

            current_fqpath = parts.slice(0, i + 1).join("/") || "/";

            if (Browse.current_nsid && current_fqpath.length >= Browse.parent_ns_path.length) {
                // we're in the shared folder
                current_nsid = Browse.current_nsid;
            } else {
                current_nsid = '';
            }

            var current_nspath = !current_nsid ? current_fqpath : Browse.fqpath_to_nspath(current_fqpath, 'breadcrumb');

            var icon = first && name.toString() == _("Dropbox") ? "dropbox" : FileOps.folder_to_icon(Browse.current_fqpath(), Browse.is_share, Browse.is_sandbox);

            bc += bcrumb_link(name, current_nsid, current_nspath, last, first, icon);
        }
        $('browse-location').update(bc);
    },
    viewportOffset: function () {
        if (!Browse.files.length) {
            return;
        }

        if (!Browse.div_parent) {
            var op = Browse.files[0].div.offsetParent;
            if (!op) {
                return;
            }

            Browse._viewportOffset = {};
            Browse.div_parent = $(op);
            Browse._cumulativeOffset = Browse.div_parent.cumulativeOffset();
        }

        var l = Util.scrollLeft(Browse.div_parent);
        var t = Util.scrollTop(Browse.div_parent);

        if (!Browse.scrollTop || !Browse.scrollLeft || Browse.scrollTop != t  || Browse.scrollLeft != l) {
            Browse._viewportOffset.top = Browse._cumulativeOffset.top - t;
            Browse._viewportOffset.left = Browse._cumulativeOffset.left - l;
            Browse.scrollLeft = l;
            Browse.scrollTop = t;
        }

        return Browse._viewportOffset;
    },
    watch_for_resize: function () {
        Event.observe(window, 'resize', Browse.updateOffset);
    },
    updateOffset: function () {
        if (!Browse.div_parent) {
            return;
        }

        Browse._cumulativeOffset = Browse.div_parent.cumulativeOffset();
        Browse.viewportOffset();
    },
    selectable: function () {
        Util.enableSelection(Util.scry('browse-files'));
    },
    unselectable: function () {
        Util.disableSelection(Util.scry('browse-files'));
    },
    profile_files: function (file_list) {
        var profile = { files:              0,
                        folders:            0,
                        shared_folders:     0,
                        deleted:            0,
                        public_folder:      0,
                        photos_folder:      0,
                        rejoinables:        0,
                        sandboxes:          0 };

        for (var i = 0; i < file_list.length; i += 1) {
            var file = file_list[i];

            if (file.dir) {
                profile.folders += 1;
            } else {
                profile.files += 1;
            }

            if (file.is_sandbox) {
                profile.sandboxes += 1;
            }

            if (file.is_share) {
                profile.shared_folders += 1;
            }

            if (file.bytes == -1) {
                profile.deleted += 1;
            }

            if (file.where == "/Public") {
                profile.public_folder = 1;
            } else if (file.where == "/Photos") {
                profile.photos_folder = 1;
            }

            if (file.main_actions.indexOf("rejoin") >= 0) {
                profile.rejoinables += 1;
            }
        }
        return profile;
    },

    profile_summary: function (profile) { // i18n-note tricky "second-order" externalization here
        // TRANSLATORS This will be used in a phrase such as "x files and y folders"
        var files = ungettext('%d file', '%d files', profile.files).format(profile.files);
        // TRANSLATORS This will be used in a phrase such as "x files and y folders"
        var folders = ungettext('%d folder', '%d folders', profile.folders).format(profile.folders);

        if (profile.files && profile.folders) {
            // TRANSLATORS the 'x_files' and 'y_folders' variables are separately externalized.
            // %(x_files)s will either be "1 file" or "d files", where d is the number of files.
            // similarly %(y_folders)s will either be "1 folder" or "d folders"
            return _("%(x_files)s and %(y_folders)s").format({
                'x_files': files,
                'y_folders': folders
            });
        }
        else if (profile.files) {
            return files;
        }
        else if (profile.folders) {
            return folders;
        }
        else {
            return '';
        }
    },

    global_checkbox: function () {
		return Util.childElement(document.getElementById("select-all-sorter"), 0);
    },
    onContext: function (e) {
        if (e) {
            Event.stop(e);
        }
        return false;
    },
    current_fqpath: function () {
        return Util.normPath((Browse.parent_ns_path || '') + Browse.current_path);
    },
    fqpath_to_nspath: function (quoted_fq_path, called_from) {
        // fqpath converter given Browse.parent_ns_path
        // needs a urlquoted fqpath (with slashes unquoted)
        var decoded_parent_ns_path = decodeURIComponent(Browse.parent_ns_path || ''),
            decoded_fq_path = decodeURIComponent(quoted_fq_path);

        assert(decoded_fq_path.toLowerCase().startsWith(decoded_parent_ns_path.toLowerCase()), "fqpath " + decoded_fq_path + " and parent_ns_path " + decoded_parent_ns_path + " don't match (from " + (called_from || '?') + ")");

        return Util.urlquote(decoded_fq_path.substr(decoded_parent_ns_path.length));
    }
};

/* */

var FileOps = {
    folder_to_icon: function (fq_path, is_share, is_sandbox) {
        assert(fq_path, "folder_to_icon was not given a path.");

        fq_path = Util.normalize(fq_path.toLowerCase());

        if (is_sandbox) {
            return "folder_star";
        } else if (is_share) {
            return "folder_user";
        } else if (fq_path == "/photos") {
            return "folder_photos";
        } else if (fq_path == "/public") {
            return "folder_public";
        } else {
            return "folder";
        }

    },
    filename_to_icon: function (filename) {
        var file_ext = FileOps.file_extension(filename).toLowerCase();
        var ext_map = { 'exe' : 'page_white_gear',
                        'dll' : 'page_white_gear',
                        'xls' : 'page_white_excel',
                        'xlsx' : 'page_white_excel',
                        'ods' : 'page_white_tux',
                        'c' : 'page_white_c',
                        'h' : 'page_white_c',
                        'php' : 'page_white_php',
                        'mp3' : 'page_white_sound',
                        'wav' : 'page_white_sound',
                        'm4a' : 'page_white_sound',
                        'wma' : 'page_white_sound',
                        'aiff' : 'page_white_sound',
                        'au' : 'page_white_sound',
                        'ogg' : 'page_white_sound',
                        'doc' : 'page_white_word',
                        'docx' : 'page_white_word',
                        'odt' : 'page_white_tux',
                        'ppt' : 'page_white_powerpoint',
                        'pptx' : 'page_white_powerpoint',
                        'odp' : 'page_white_tux',
                        'txt' : 'page_white_text',
                        'rtf' : 'page_white_text',
                        'sln' : 'page_white_visualstudio',
                        'vcproj' : 'page_white_visualstudio',
                        'html' : 'page_white_code',
                        'htm' : 'page_white_code',
                        'psd' : 'page_white_paint',
                        'pdf' : 'page_white_acrobat',
                        'fla' : 'page_white_actionscript',
                        'swf' : 'page_white_flash',
                        'gif' : 'page_white_picture',
                        'png' : 'page_white_picture',
                        'jpg' : 'page_white_picture',
                        'jpeg' : 'page_white_picture',
                        'tiff' : 'page_white_picture',
                        'tif' : 'page_white_picture',
                        'bmp' : 'page_white_picture',
                        'odg' : 'page_white_picture',
                        'py' : 'page_white_code',
                        'gz' : 'page_white_compressed',
                        'tar' : 'page_white_compressed',
                        'rar' : 'page_white_compressed',
                        'zip' : 'page_white_compressed',
                        'iso' : 'page_white_dvd',
                        'css' : 'page_white_code',
                        'xml' : 'page_white_code',
                        'tgz' : 'page_white_compressed',
                        'bz2' : 'page_white_compressed',
                        'rb' : 'page_white_ruby',
                        'cpp' : 'page_white_cplusplus',
                        'java' : 'page_white_cup',
                        'cs' : 'page_white_csharp',
                        'ai' : 'page_white_vector' };
        return ext_map[file_ext] || "page_white";
    },
    show_confirm_restore_entire: function (when, cs_id, path) {
        assert(cs_id, "Missing csid");
        assert(when, "Missing when");
        assert(path !== undefined, "Missing path");

        DomUtil.fillVal(when, "restore-entire-when");
        Modal.icon_show("time", _("Rewind Dropbox"), DomUtil.fromElm("restore-entire"), {
            'when': when,
            'cs_id': cs_id,
            'path': path,
            'action': FileOps.do_rewind
        });
    },
    do_rewind: function () {
        var cs_id = Modal.vars.cs_id;
        assert(cs_id);

        var path = Modal.vars.path;
        assert(path !== undefined);

        new Ajax.DBRequest("/cmd/rewind", {
            parameters: {
                cs_id: cs_id,
                path: path
            },
            onComplete: function () {
                window.location.reload();
            }
        });
    },
    show_confirm_restore_sjids: function (files) {
        var profile = Browse.profile_files(files);
        var file_summary = Browse.profile_summary(profile);

        DomUtil.fillVal(file_summary, 'restore-sjids-filename');
        // TRANSLATORS for example: "Permanently Delete 3 files and 1 folder"
        DomUtil.fillVal(_("Restore %(x_files_and_y_folders)s").format({
            'x_files_and_y_folders' : file_summary
        }), 'restore-sjids-action-text');

        // TRANSLATORS for example "Permanently Delete 3 Items"
        var msg = ungettext("Restore %d Item...", "Restore %d Items...", files.length).format(files.length);
        Modal.icon_show(BrowseActions.getIcon('restore_sjids'), msg, DomUtil.fromElm('restore-sjids'), {'files': files, 'action': FileOps.do_bulk_restore_sjids});

    },
    do_bulk_restore_sjids: function (e) {
        if (e) {
            Event.stop(e);
        }
        var sjids = [];
        for (var i = 0, len = Modal.vars.files.length; i < len; i += 1) {
            var file = Modal.vars.files[i];
            sjids.push(file.sjid);
        }
        var path = Browse.current_fqpath();
        Modal.hide();
        new Ajax.DBRequest("/cmd/restore_sjids", {
            parameters: {
                sjids: sjids,
                path: path,
                cs_id: Browse.current_cs
            },
            onSuccess: function () {
                window.location.reload();
            }
        });
    },
    file_extension: function (filename) {
        return filename.split(".").last();
    },
    raw_filename: function (path) {
        // Asumes path is not url encoded
        return FileOps.filename(encodeURIComponent(path));
    },
    filename: function (path) {
        // Assumes path is always url encoded
        var decoded_path = decodeURIComponent(path);

        path = Util.normPath(decoded_path);
        path = path.split("/");
        var filename = path.pop();

        if (filename === "") { // looks like top level Dropbox
            return _("Dropbox");
        }

        return filename;
    },
    dir_handler: function (path, obj) {
        if (typeof(obj) == 'string') {
            obj = $(obj);
        }

        var highlighted = $$(".treeview .highlight")[0];
        if (highlighted) {
            highlighted.removeClassName("highlight");
            var old_icon = highlighted.down(".link-img");
            if (old_icon) {
                old_icon.className = old_icon.className.replace("_blue ", " ");
            }
        }

        var mydiv = obj.up('div');
        mydiv.addClassName("highlight");

        var icon = mydiv.down(".link-img");

        if (icon) {
            var classes = icon.className.split(" ");
            for (var i = 0; i < classes.length; i += 1) {
                if (classes[i].startsWith("s_") && !classes[i].match(/_blue$/)) {
                    classes[i] = classes[i] + "_blue";
                }
            }
            icon.className = classes.join(" ");
        }
        Modal.selected_div = mydiv;

        if (Modal.shown()) {
            obj.blur();
        }
        document.fire("db:dir_click", {path: path});
        Modal.vars.selected_path = encodeURIComponent(path);
    },
    show_folder_pick: function (title, file, action, for_folder, move) {
        DomUtil.fillVal(FileOps.filename(file).escapeHTML(), 'folder-pick-file');

        TreeView.move('copy-move-treeview', 'folder-pick-treeview');
        TreeView.enable_shared('copy-move-treeview');

        var icon = move ? BrowseActions.getIcon('move_bulk') : BrowseActions.getIcon('copy_bulk');
        Modal.icon_show(icon, title, $('folder-pick'), {'where': file, 'action': action, 'folder': for_folder});

        // prep by selecting the first
        var first_link = $('first-treeview-link');
        first_link.onclick();
    },
    show_bulk_folder_pick: function (title, action_name, files, action) {
        var profile = Browse.profile_files(files);
        var file_string = Browse.profile_summary(profile);

        DomUtil.fillVal(file_string, 'bulk-folder-pick-file');

        TreeView.move('copy-move-treeview', 'bulk-folder-pick-treeview');
        TreeView.enable_shared('copy-move-treeview');

        document.observe("db:dir_click", function (event) {
            var button = $$("#modal-content .bulk-folder-pick-action-text").first();
            if (button) {
                // TRANSLATORS for example, "copy 5 items to My Dropbox/Photos"
                var button_text = {'copy': ungettext("Copy %(item-count)d item to %(path)s", "Copy %(item-count)d items to %(path)s", files.length),
                                   'move': ungettext("Move %(item-count)d item to %(path)s", "Move %(item-count)d items to %(path)s", files.length)};
                assert(action_name in button_text, "unexpected action name %s".format(action_name));
                button.setValue(button_text[action_name].format({'item-count': files.length, 'path': FileOps.raw_filename(event.memo.path)}));
            }
        });

        var icon = action_name == "move" ? BrowseActions.getIcon('move_bulk') : BrowseActions.getIcon('copy_bulk');
        Modal.icon_show(icon, title, $('bulk-folder-pick'), {'files': files, 'action': action});

        // prep by selecting the first
        var first_link = $('first-treeview-link');
        first_link.onclick();
    },
    show_copy: function (file_path, is_folder) {
        var msg = is_folder ? _('Copy Folder') : _('Copy File');
        DomUtil.fillVal(msg, 'folder-pick-action-text');
        DomUtil.fillVal(is_folder ? _('Folder') : _('File'), 'folder-pick-file-folder');
        FileOps.show_folder_pick(_("Copy to..."), file_path, FileOps.do_copy, is_folder, false); // move = false
    },
    show_copy_bulk: function (files) {
        // TRANSLATORS as in "Copy 5 items to...". Selecting this action will pop up a dialog asking the user where to
        // move the files to.
        var msg = ungettext("Copy %(item_count)s Item to...", "Copy %(item_count)s Items to...", files.length).format({
            'item_count': files.length
        });

        FileOps.show_bulk_folder_pick(msg, "copy", files, FileOps.do_bulk_copy);
    },
    show_move_bulk: function (files) {
        var msg = ungettext("Move %(item_count)s Item to...", "Move %(item_count)s Items to...", files.length).format({
            'item_count': files.length
        });
        FileOps.show_bulk_folder_pick(msg, "move", files, FileOps.do_bulk_move);
    },
    show_move: function (file_path, is_folder) {
        var msg1 = is_folder ? _('Move Folder') : _('Move File');
        DomUtil.fillVal(msg1, 'folder-pick-action-text');
        DomUtil.fillVal(is_folder ? _('Folder') : _('File'), 'folder-pick-file-folder');
        var msg2 = is_folder ? _('Move Folder to...') : _('Move File to...');
        FileOps.show_folder_pick(msg2, file_path, FileOps.do_move, is_folder, true); // move = true
    },
    show_move_confirm: function (files, to) {
        var profile = Browse.profile_files(files);
        var file_summary = Browse.profile_summary(profile);

        var desc = files.length == 1 ? "'" + files[0].filename + "'": file_summary;

        DomUtil.fillVal(desc, 'move-confirm-filename');
        DomUtil.fillVal(FileOps.raw_filename(to), 'move-confirm-dest');

        // TRANSLATORS for example "Move 5 files and 1 folder". "5 files and 1 folder" is translated separately.
        var msg = _("Move %(x_files_and_y_folders)s").format({
            x_files_and_y_folders: file_summary
        });
        DomUtil.fillVal(msg, 'move-confirm-action-text');

        desc = _("Move %(x_files_and_y_folders)s?").format({
            x_files_and_y_folders: desc
        });
        Modal.icon_show(BrowseActions.getIcon("move"), desc, DomUtil.fromElm('move-confirm'), {
            'files': files,
            'to': encodeURIComponent(to),
            'wit_group': 'move-confirm'
        });
    },
    show_rename: function (file_path) {
        DomUtil.fillVal(FileOps.filename(file_path), 'rename-filename');
        Modal.icon_show(BrowseActions.getIcon("rename"), _("Rename File"), DomUtil.fromElm('rename-file'), {
            'where': file_path,
            'action': FileOps.do_rename,
            'wit_group': 'rename-file'
        });
    },
    show_delete: function (file_path, is_folder) {
        DomUtil.fillVal("'" + FileOps.filename(file_path).escapeHTML() + "'", 'delete-filename');

        var icon = is_folder ? BrowseActions.getIcon('delete_folder') : BrowseActions.getIcon('delete');
        var msg = is_folder ? _("Delete Folder?") : _("Delete File?");
        Modal.icon_show(icon, msg, DomUtil.fromElm('delete-file'), {
            'where': file_path,
            'action': FileOps.do_delete,
            'folder': is_folder,
            'wit_group': 'delete-confirm'
        });
    },

    show_bulk_delete: function (files) {
        var profile = Browse.profile_files(files);
        var file_summary = Browse.profile_summary(profile);

        DomUtil.fillVal(FileOps.filename(file_summary), 'delete-filename');
        //TRANSLATORS file_summary will look like "4 files and 1 folder" and is translated seperately
        Modal.icon_show(BrowseActions.getIcon('delete'), _("Delete %(file_summary)s?").format({
            'file_summary': file_summary
        }),
        DomUtil.fromElm('delete-file'), {
            'files': files,
            'action': FileOps.do_bulk_delete,
            'wit_group': 'delete-bulk-confirm'
        });
    },

    show_purge: function (file_path, is_folder) {
        var filename = FileOps.filename(file_path).escapeHTML();
        // TRANSLATORS for example: 'Tax Returns.doc'
        var msg1 = _("'%(file_name)s'").format({
            'file_name': filename
        });
        DomUtil.fillVal(msg1, 'purge-filename');
        var msg2 = is_folder ? _("Folder") : _("File");
        DomUtil.fillVal(msg2, 'purge-file-folder');
        var msg3 = is_folder ? _("Permanently Delete Folder") : _("Permanently Delete File");
        DomUtil.fillVal(msg3, 'purge-action-text');
        var msg4 = is_folder ? _('Permanently Delete the Folder "%(file_name)s"?') : _('Permanently Delete the File "%(file_name)s"?');
        msg4 = msg4.format({
            'file_name': filename
        });
        Modal.icon_show(BrowseActions.getIcon('purge'), msg4, DomUtil.fromElm('purge-file'), {
            'where': file_path,
            'action': FileOps.do_purge,
            'folder': is_folder,
            'wit_group': 'purge-file-confirm'
        });
    },

    show_bulk_purge: function (files) {
        var profile = Browse.profile_files(files);
        var file_summary = Browse.profile_summary(profile);

        DomUtil.fillVal(file_summary, 'purge-filename');
        // TRANSLATORS for example: "Permanently Delete 3 files and 1 folder"
        DomUtil.fillVal(_("Permanently Delete %(x_files_and_y_folders)s").format({
            'x_files_and_y_folders' : file_summary
        }), 'purge-action-text');

        // TRANSLATORS for example "Permanently Delete 3 Items"
        var msg = ungettext("Permanently Delete %d Item...", "Permanently Delete %d Items...", files.length).format(files.length);
        Modal.icon_show(BrowseActions.getIcon('purge_bulk'), msg, DomUtil.fromElm('purge-file'), {
            'files': files,
            'action': FileOps.do_bulk_purge,
            'wit_group': 'purge-bulk-confirm'
        });
    },

    show_bulk_restore: function (files) {
        var profile = Browse.profile_files(files);
        var file_summary = Browse.profile_summary(profile);

        DomUtil.fillVal(file_summary, 'restore-filename');
        DomUtil.fillVal(_("Restore %(x_files_and_y_folders)s").format({
            'x_files_and_y_folders' : file_summary
        }), 'restore-action-text');

        var msg = ungettext("Restore %d Item...", "Restore %d Items...", files.length).format(files.length);
        Modal.icon_show(BrowseActions.getIcon("restore_bulk"), msg, DomUtil.fromElm('restore-file'), {
            'files': files,
            'action': FileOps.do_bulk_restore,
            'wit_group': 'restore-bulk-confirm'
        });
    },

    show_new_folder: function (file_path) {
        Modal.show(_("Create Folder"), DomUtil.fromElm('new-folder'), {
            'where': file_path,
            'action': FileOps.do_new_folder,
            'wit_group': 'show-new-folder'
        });
    },
    show_upload: function (folder_path, force_reload) {
        folder_path = folder_path || Browse.current_fqpath();
        var folder_name = FileOps.filename(folder_path);
        DomUtil.fillVal(folder_name, 'upload-dest');
        var msg = _("Upload to '%(folder_name)s'").format({
            'folder_name': folder_name.escapeHTML()
        });
        Modal.icon_show("page_white_get", msg, DomUtil.fromElm('advanced-upload-modal'), {
            'where': folder_path,
            'action': FileOps.do_upload,
            'wit_group': 'advanced-uploader'
        }, false, 500, FileQueue.numShown() && !force_reload); // item_focus=false, modal_width=600
        if (FileQueue.empty()) {
            Upload.set_dest(Util.normPath(decodeURIComponent(folder_path)));
        }
        if (!FileQueue.numShown() || force_reload) {
            Upload.init(true); // late_game=true
        }
        InlineUploadStatus.hide();
    },
    show_basic_upload: function (folder_path) {
        DomUtil.fillVal(FileOps.filename(folder_path), 'upload-dest');

        Modal.icon_show(BrowseActions.getIcon("upload"), _("Upload"), $('basic-upload-modal'), {}, false, 600);
    },
    show_undelete: function (file) {
        DomUtil.fillVal(file.filename.escapeHTML(), "undelete-filename");
        DomUtil.fillVal(file.ago, "undelete-date-time");

        var icon = Sprite.make(file.icon, {});
        icon.addClassName("link-img");
        icon.style.backgroundColor = "transparent";

        var link = "/revisions" + file.where + "?undelete=1";
        $$(".undelete-icon").invoke("update", icon);
        $$(".undelete-other-versions")[0].href = link;
        $$(".undelete-link")[0].href = link;

        $("undelete-form").action = "/revisions" + file.where;

        var msg = _('Undelete the File "%(file_name)s"?').format({
            'file_name': file.filename.escapeHTML()
        });
        Modal.icon_show(BrowseActions.getIcon("undelete"), msg, $("undelete-modal"), {'file': file});
    },

    submit_undelete: function (e) {
        var form = $("undelete-form");
        var msg = _("'%(file_name)s' restored successfully.").format({
            'file_name': Modal.vars.file.filename
        });
        Forms.ajax_submit(form, false,
            function () {
                Modal.hide();
                Notify.ServerSuccess(msg);
                Browse.force_reload();
            },
            function () {
                var msg = _("Unable to restore %(file_name)s").format({
                    'file_name': Modal.vars.file.filename
                });
                Notify.ServerError(msg);
            },
            e.target);
        return false;
    },
    do_copy: function (from, to) {
        from = from || Modal.vars.where;
        to = to || Modal.vars.selected_path;

        var file = Browse.find_file(Modal.vars.where);
        assert(file, "Trying to copy a file we couldn't find.");
        FileOps.do_bulk_copy([file], to);
    },

    do_bulk_copy: function (files, to) {

        files = files || Modal.vars.files;
        assert(files.length > 0, "Tried to copy 0 files");

        to = to || Modal.vars.selected_path;
        if (!to) {
            // TRANSLATORS "destination" means, the place to move the file
            Notify.ServerError(_("You should select a destination for the file."));
            return;
        }
        to = decodeURIComponent(to);

        var copying_to_here = false;
        var got_dir = false;
        for (var i = 0; i < files.length; i += 1) {
            if (files[i].dir && Util.normDir(decodeURIComponent(to)).indexOf(Util.normDir(decodeURIComponent(files[i].where))) === 0) {
                Notify.ServerError(_("You cannot copy a folder into itself."));
                return;
            }
            got_dir = got_dir || files[i].dir;

            if (decodeURIComponent(files[i].where) == Util.normPath(to) + "/" + FileOps.filename(files[i].where)) {
                copying_to_here = true;
            }
        }

        var file_paths = files.collect(function (file) {
            return decodeURIComponent(file.where);
        });

        new Ajax.DBRequest("/cmd/copy", {
            parameters: {files: file_paths, to_path: to },
            job: true,
            progress_text: _("Copying..."),
            onSuccess: function (req) {
                if (copying_to_here) {
                    Browse.force_reload();
                }
                var msg = ungettext("Copied %d item successfully",
                                    "Copied %d items successfully",
                                    file_paths.length).format(file_paths.length);
                Notify.ServerSuccess(msg);
                TreeView.schedule_reset();
            },
            cleanUp: function (req) {
            }
        });
    },
    do_move: function (from, to) {
        from = from || Modal.vars.where;
        to = to || Modal.vars.selected_path;

        var file = Browse.find_file(Modal.vars.where);
        assert(file, "Trying to move a file we couldn't find.");
        FileOps.do_bulk_move([file], to);
    },

    do_bulk_move: function (files, to) {

        files = files || Modal.vars.files;
        if (!files) {
            return;
        }
        assert(files.length > 0, "Tried to move 0 files");

        to = to || Modal.vars.selected_path;
        to = decodeURIComponent(to);

        if (!to) {
            Notify.ServerError(_("You should select a destination for the file."));
            return;
        }

        for (var i = 0; i < files.length; i += 1) {
            if (files[i].dir && Util.normDir(decodeURIComponent(to)).indexOf(Util.normDir(decodeURIComponent(files[i].where))) === 0) {
                Notify.ServerError(_("You cannot move a folder into itself."));
                return;
            }
        }

        var file_paths = files.collect(function (file) {
            return decodeURIComponent(file.where);
        });

        new Ajax.DBRequest("/cmd/move", {
            parameters: {files: file_paths, to_path: to},
            job: true,
            progress_text: _("Moving..."),
            onSuccess: function (req) {
                Notify.ServerSuccess(ungettext('Moved item successfully',
                                               'Moved items successfully',
                                               file_paths.length));
                TreeView.schedule_reset();
            },
            cleanUp: function (req) {
                Browse.force_reload();
            }
        });
    },
    do_rename: function (to_path) {
        var from = Modal.vars.where;
        var to = encodeURIComponent(to_path);
        new Ajax.DBRequest("/cmd/rename" + from + "?to_path=" + to, {
            onSuccess: function (req) {
                var parts = req.responseText.split(":");
                var new_where = parts[0];
                var new_hash = parts[1];
                var new_name = FileOps.filename(new_where);
                var msg = _("Renamed '%(old_name)s' to '%(new_name)s' successfully.").format({
                    'old_name': FileOps.filename(from).snippet(),
                    'new_name': new_name.snippet()
                });
                Notify.ServerSuccess(msg);
                var file = Browse.find_file(from);
                file.rename(new_where, file.dir, new_hash);
                TreeView.schedule_reset();
            },
            cleanUp: function (req) {
            }
        });
    },

    do_bulk_delete: function (files) {
        files = files || Modal.vars.files;

        assert(files.length > 0, "Tried to delete 0 files");

        var file_paths = files.collect(function (file) {
            return decodeURIComponent(file.where);
        });

        // var from = Modal.vars.where; // split-todo this doesn't seem used
        // var folder_delete = Modal.vars.folder; // split-todo ditto
        new Ajax.DBRequest("/cmd/delete", {
            parameters: {files: file_paths},
            job: true,
            progress_text: _("Deleting..."),
            onSuccess: function (req) {
                // TRANSLATORS for example "Deleted 1 item successfully"
                var msg = ungettext("Deleted %d item successfully.",
                                    "Deleted %d items successfully.",
                                    file_paths.length).format(file_paths.length);
                Notify.ServerSuccess(msg);
                TreeView.schedule_reset();
            },
            cleanUp: function (req) {
                Browse.force_reload();
            }
        });
    },

    do_delete: function () {
        var file = Browse.find_file(Modal.vars.where);
        assert(file, "Trying to delete a file we couldn't find.");

        FileOps.do_bulk_delete([file]);
    },

    do_purge: function () {
        var file = Browse.find_file(Modal.vars.where);
        assert(file, "Trying to purge a file we couldn't find.");

        FileOps.do_bulk_purge([file]);
    },

    do_bulk_purge: function (files) {
        files = files || Modal.vars.files;
        assert(files.length > 0, "Tried to purge 0 files");

        var file_paths = files.collect(function (file) {
            return decodeURIComponent(file.where);
        });

        // var from = Modal.vars.where; // split-todo unused?
        // var folder_delete = Modal.vars.folder; // split-todo unused?
        // TRANSLATORS for example "Permanently deleted 3 items successfully"
        var msg = ungettext("Permanently deleted %d item successfully", "Permanently deleted %d items successfully.", file_paths.length).format(file_paths.length);
        new Ajax.DBRequest("/cmd/purge", {
            parameters: {files: file_paths},
            job: true,
            progress_text: _("Deleting..."),
            onSuccess: function (req) {
                Notify.ServerSuccess(msg);
                Browse.force_reload();
                TreeView.schedule_reset();
            },
            cleanUp: function (req) {
            }
        });
    },

    do_bulk_restore: function (files) {
        files = files || Modal.vars.files;
        assert(files.length > 0, "Tried to restore 0 files");

        var file_paths = files.collect(function (file) {
            return decodeURIComponent(file.where);
        });

        // var from = Modal.vars.where; // split-todo unused?
        // var folder_delete = Modal.vars.folder; // split-todo unused?
        // TRANSLATORS for example "Restored 3 items successfully"
        var msg = ungettext("Restored %d item successfully.", "Restored %d items successfully.", file_paths.length).format(file_paths.length);
        new Ajax.DBRequest("/cmd/restore", {
            parameters: {files: file_paths},
            job: true,
            progress_text: _("Restoring..."),
            onSuccess: function (req) {
                Notify.ServerSuccess(msg);
                Browse.force_reload();
                TreeView.schedule_reset();
            },
            cleanUp: function (req) {
            }
        });
    },

    do_upload: function () {

        $('dest-folder').value = decodeURIComponent(Modal.vars.where);

        // submit the form, the loaded page will do the right thing
        $('upload-form').submit();

        frames['upload-frame'].onload =
            function (e) {
                var text = e.target.documentElement.textContent;
                if (text == 'winner!') {
                    Browse.force_reload();
                    Notify.ServerSuccess(_("Uploaded file successfully"));
                } else {
                    Notify.ServerError();
                }
            };

    },

    do_bulk_download: function (files) {
        AMC.log('file_action', {
            'action': 'download',
            'num_files': files.length,
            'webserver': Constants.WEBSERVER
        });

        var f = new Element("form", {'action': "https://" + Constants.BLOCK_CLUSTER + "/zip_batch", 'method': 'post'});

        for (var i = 0; i < files.length; i += 1) {
            Forms.add_vars(f, {'files': decodeURIComponent(files[i].where) });
        }

        Forms.add_vars(f, {'parent_path': Util.normDir(decodeURIComponent(Browse.current_fqpath())), 'w': Browse.current_path_hash });

        $(document.body).insert(f);
        f.submit();

    },
    inplace_new_folder: function (where) {
        if (Browse.in_placer && Browse.in_placer.new_folder) {
            return;
        }

        var name = _("New Folder");
        if (where.charAt(where.length - 1) != "/") {
            where += "/";
        }

        // make a placeholder
        Browse.hide_message();
        var actions = "zipped_dl upload copy_folder move_folder rename_folder delete_folder";

        if (Browse.current_nsid == Constants.root_ns) {
            action += 'share ' + actions;
        }

        if (Constants.can_shmodel) {
            actions = actions + " token_share";
        }
        var f = new BrowseFile('folder', where, "/browse2" + where + name, name, name, false, 0, "", Util.ts(), actions, "", true, true, true, null, 0, -1, []);
        Browse.resort();

        f.edit(true); // new_folder=true
    }
};


var ProgressBar = {
    MAGIC: 42,
    make: function (id, width, start_text) {
        width = width || 300;
        var wstr = width.toString() + "px";
        start_text = typeof(start_text) != 'undefined' ? start_text :"0%";
        var outer = new Element('div', {'class': 'outer-progress-bar', 'style': 'width: ' + wstr});
        var inner = new Element('div', {'class': 'inner-progress-bar', 'id': "pb_" + id, 'style': 'width: ' + wstr});
        var under = new Element('div', {'class': 'under-pb progress-bar', 'style': 'width: ' + wstr});
        var over  = new Element('div', {'style': 'display: none', 'class': 'over-pb progress-bar', 'id': "pb_" + id + "_over"});
        var upct  = new Element('div', {'class': 'pb-percentage', 'id': "pb_" + id + "_upct", 'style': 'width: ' + wstr});
        upct.update(start_text);
        var opct  = new Element('div', {'class': 'pb-percentage', 'id': "pb_" + id + "_opct", 'style': 'width: ' + wstr});
        opct.update(start_text);

        under.insert(upct);
        over.insert(opct);
        inner.insert(under);
        inner.insert(over);
        outer.insert(inner);

        over.progress_width = width;
        return outer;
    },
    reset: function (id) {
        ProgressBar.set(id, 0);
    },
    set: function (id, frac, text) {
        frac = Math.min(frac, 1);
        text = typeof(text) != 'undefined' && text !== false ? text : Math.floor(frac * 100).toString() + "%";

        var over = $("pb_" + id + "_over");
        if (!over) {
            return;
        }

        var wideness = over.progress_width * frac;
        over.show();
        over.makeClipping().setStyle({'width': wideness.toString() + 'px', backgroundColor: '#348DD3'});

        $("pb_" + id + "_upct").innerHTML = text;
        $("pb_" + id + "_opct").innerHTML = text;
    },
    get_frac: function (id) {
        var over = $("pb_" + id + "_over");
        return parseInt(over.style.width, 10) / over.progress_width;
    },
    errorState: function (id, errorText) {
        errorText = errorText || "Error";
        var over = $("pb_" + id + "_over");
        if (!over) {
            return;
        }

        var wideness = over.progress_width;
        over.show();
        over.makeClipping().setStyle({'width': wideness.toString() + 'px', backgroundColor: '#d23a3a'});
        $("pb_" + id + "_upct").innerHTML = errorText;
        $("pb_" + id + "_opct").innerHTML = errorText;
    }
};


var ModalProgress = {
    show: function (text, cover_this) {
        if (!text) {
            return;
        }
        cover_this = $(cover_this || 'browse-box');

        var loadingDiv = $('modal-progress-overlay');
        loadingDiv.clonePosition(cover_this);

        if (!loadingDiv.getWidth()) {
            return;
        }

        $('modal-progress-text').update(text);
        $('modal-progress-bar').setOpacity(1);
        $('modal-progress-bar').update(ProgressBar.make("modal-progress", 150, ""));

        // keep the modal progress bar >= 120px from the top of the screen (and visible)
        $('modal-progress-content').style.top = (Math.max(0, Util.scrollTop() - cover_this.cumulativeOffset().top) + 120) + "px";

        Effect.Appear(loadingDiv, {to: 0.7, duration: 0.25 });
        Effect.Appear('modal-progress-content', {duration: 0.25 });
    },
    update: function (progress) {
        if (progress.indexOf("/") > 0) {
            var parts = progress.split("/");
            progress = Number(parts[0]) / Number(parts[1]);
        }
        if (progress) {
            ProgressBar.set("modal-progress", progress, "");
        }
    },
    hide: function () {
        Effect.Fade('modal-progress-overlay', { duration: 0.25 });
        Effect.Fade('modal-progress-content', { duration: 0.25 });
    }
};


var Job = {
    complete: {},
    handled: function (job_id) {
        if (!job_id) {
            return false;
        }

        var already_handled = !!Job.complete[job_id];
        Job.complete[job_id] = true;

        return already_handled;
    },
    peek: function (job_id) {
        if (!job_id) {
            return false;
        }

        return !!Job.complete[job_id];
    }
};


var ProgressWatcher = {
    job_info: {},
    INIT_POLL_INT: 1000,
    FAILS_MEAN_FAIL: 3,
    MODAL_WAIT_MS: 1000,
    watch: function (req) {
        ProgressWatcher.job_info[req.job_id] = {};
        var info = ProgressWatcher.job_info[req.job_id];

        info.req = req;
        info.poll_int = ProgressWatcher.INIT_POLL_INT;
        info.poll_count = 0;
        info.int_id = setInterval(ProgressWatcher.update_for(req.job_id), info.poll_int);
        info.failures = 0;
        info.start_time = Util.time();
    },
    update_for: function (id) {
        return function () {
            return ProgressWatcher.update(id);
        };
    },
    backoff: function (id) {
        var info = ProgressWatcher.job_info[id];
        clearInterval(info.int_id);
        info.poll_int = Math.min(Math.floor(info.poll_int * 1.5), 30000); // 30 seconds is longest poll interval
        info.int_id = setInterval(ProgressWatcher.update_for(id), info.poll_int);
    },
    update: function (id) {
        var req_info = ProgressWatcher.job_info[id];
        if (Job.peek(id)) {
            return ProgressWatcher.done(id);
        }
        req_info.poll_count++;

        if (req_info.poll_count % 10 === 0) {
            // time to slow this train down
            ProgressWatcher.backoff(id);
        }

        if (!req_info.modaled && Util.time() - req_info.start_time > ProgressWatcher.MODAL_WAIT_MS) {
            var options = req_info.req.options;
            ModalProgress.show(options.progress_text, options.cover_this);
            options.onProgress = ModalProgress.update;
            req_info.modaled = true;
        }

        new Ajax.Request("/job_status/" + id,
        {
            method: 'post',
            t: Constants.TOKEN,
            onSuccess: function (req) {
                var info = ProgressWatcher.job_info[id];
                var progress = req.responseText;

                if (progress.indexOf("err") === 0) {
                    ProgressWatcher.done(id);
                    ModalProgress.hide();

                    if (info.req.options.onFailure && !Job.handled(id)) {
                        info.req.options.onFailure(req);
                    }

                    return;
                }

                if (progress.indexOf("done") === 0) {
                    // go pick up the results, passing them back like normal
                    info.req.options.job = false; // return the handlers to normal mode

                    if (!Job.handled(id)) {
                        new Ajax.Request("/job_results/" + id, {
                            onSuccess: function (r) {
                                if (Job.handled(id)) {
                                    return;
                                }
                                Notify.clearIf(RequestWatcher.working_msg);
                                if (info.req.options.onSuccess) {
                                    info.req.options.onSuccess(r);
                                }
                            },
                            onFailure: function (r) {
                                if (Job.handled(id)) {
                                    return;
                                }
                                Notify.clearIf(RequestWatcher.working_msg);
                                if (info.req.options.onFailure) {
                                    info.req.options.onFailure(r);
                                }
                            }
                        });

                    }

                    // progress is 100%
                    var parts = progress.split("/");
                    progress = parts[1] + "/" + parts[1];

                    ProgressWatcher.done(id);
                    ModalProgress.hide();
                } else {
                    // update the progress counter
                    try {
                        if (info.req.options.onProgress) {
                            info.req.options.onProgress(req.responseText);
                        }
                    } catch (e) {}
                }
            },
            onFailure: function (req) {
                var info = ProgressWatcher.job_info[id];

                info.failures++;
                if (info.failures >= ProgressWatcher.FAILS_MEAN_FAIL) {
                    // we're not going to try again
                    if (info.req.options.onFailure) {
                        info.req.options.onFailure(req, true); // force=true
                    }
                    RequestWatcher.remove(info.req);
                    ProgressWatcher.done(id);
                    ModalProgress.hide();
                }
            }
        });
    },
    done: function (id) {
        var info = ProgressWatcher.job_info[id];
        clearInterval(info.int_id);
        delete ProgressWatcher.job_info[id];
        ModalProgress.hide();
    }
};

var Forms = {
    submitOnlyOnce: function () {
        var ret = Forms.submitted !== true;
        Forms.submitted = true;

        return ret;
    },
    disable: function (me) {
        if (me) {
            setTimeout(function () {
                me.disabled = true;
            }, 0);
        }
    },
    enable: function (me) {
        if (me) {
            setTimeout(function () {
                me.disabled = false;
            }, 0);
        }
    },
    clearInput: function (elm, value) {
        elm = $(elm);
        if (elm.value == value) {
            elm.value = "";
            elm.style.color = '#444444';
        }
    },
    add_vars: function (form, vars) {
        form = $(form);
        for (var k in vars) {
            if (vars.hasOwnProperty(k)) {
                var elm = new Element('input', {'type': 'hidden', 'name': k});
                elm.setValue(vars[k]);
                elm.addClassName("added-vars");
                form.insert(elm);
            }
        }
    },
    clear_added_vars: function (form) {
        $$(".added-vars").each(Element.remove);
    },
    mirror: function (elm1, elm2) {
        elm1 = $(elm1);
        elm2 = $(elm2);

        function mirror_it(first, second) {
            second.setValue($F(first));
            second.fire('db:value_change');
        }

        if (elm1 && elm2) {
            elm1.observe('keyup', function () {
                mirror_it(elm1, elm2);
            });
            elm1.observe('db:autocompleted', function () {
                mirror_it(elm1, elm2);
            });

            elm2.observe('keyup', function () {
                mirror_it(elm2, elm1);
            });
            elm2.observe('db:autocompleted', function () {
                mirror_it(elm2, elm1);
            });
        }
    },
    collect_form_vars: function (form) {
        form = form || $(document.body);

        var elms = form.select("input").concat(form.select("textarea")).concat(form.select("select"));

        var out = {};
        for (var i = 0; i < elms.length; i++) {
            var elm = elms[i];

            if (elm.name && elm.name != 't') { // ignore any token vars in the form -- js has its own
                var value = elm.getValue();
                if (value) {
                    if (typeof(value) != "string") {
                        // must be an array, turn into a comma-separated string
                        value = value.join(",");
                    }

                    // If there's more than one element with the same name
                    // collect them in an array

                    if (out[elm.name] !== undefined) {
                        if (typeof(out[elm.name]) == "string") {
                            out[elm.name] = [out[elm.name], value];
                        } else {
                            out[elm.name].push(value);
                        }

                    } else {
                        out[elm.name] = value;
                    }
                }
            }
        }
        return out;
    },
    add_loading: function (button) {
        if (button) {
            button = $(button);
            var img = new Element("img", {src: '/static/images/icons/ajax-loading-small.gif'});
            img.addClassName("text-img ajax_submit_loading");
            button.insert({before: img});
        }

    },
    remove_loading: function (button) {
        $$(".ajax_submit_loading").each(function (elm) {
            Util.yank(elm);
        });
    },
    ajax_submit: function (form, url, success_callback, fail_callback, button, more_vars) {
        // note: this doesn't submit file inputs, though it will send the "value" of those elements
        // one submit at a time
        if (form.ajax_submitted) {
            return false;
        }
        form.ajax_submitted = true;
        form.select(".suggestion-input").each(function (elm) {
            SuggestionInput.blank(elm.identify())();
        });

        if (button) {
            Forms.add_loading(button);
        }

        var params = Forms.collect_form_vars(form);
        if (more_vars) {
            params = $H(params).update(more_vars).toObject();
        }

        new Ajax.DBRequest(url || form.action, {
            noAutonotify: true,
            parameters: params,
            onSuccess: function (req) {
                if (success_callback && typeof(success_callback) == "function") {
                    success_callback(req);
                }
            },
            onFailure: function (req) {
                if (req) {
                    if (req.responseText.indexOf("err:") === 0) {
                        var error = req.responseText.substr(4);

                        if (error.indexOf("{") === 0) {
                            var error_dict = error.evalJSON(true); //sanitize=true
                            Forms.fill_errors(form, error_dict);
                        } else {
                            Notify.ServerError(error);
                        }
                    } else {
                        Notify.ServerError();
                    }

                    if (fail_callback && typeof(fail_callback) == "function") {
                        fail_callback(req);
                    }
                }
            },
            onComplete: function (req) {
                form.ajax_submitted = false;
                Forms.remove_loading();
            }
        });

        return false;// false;
    },
    clear_errors: function (form) {
        form = form || $(document.body);
        form.select('.error-removable').invoke('remove');
    },
    fill_errors: function (form, error_dict) {
        error_dict = error_dict || {};
        form = form || $(document.body);

        // we're inserting an error span + br right before each element
        Forms.clear_errors(form);
        for (var field in error_dict) {
            if (error_dict.hasOwnProperty(field)) {
                // order matters!
                var elm = form.down("[data-error-field-name='" + field + "']") || form.down("[name='" + field + "']");
                if (elm) {
                    var br = new Element('br', {'class': 'error-removable'});
                    var error = new Element('span', {'class': 'error-message error-removable'});
                    error.update(error_dict[field]);

                    elm.insert({before: error});
                    elm.insert({before: br});
                }
            }
        }
    },
    value: function (name) {
        var inputs = $$('input[name="' + name + '"]');
        var val = null;

        var l = inputs.length;
        for (var i = 0; i < l; i++) {
            val = $(inputs[i]).getValue() || val;
        }

        return val;
    },
    postRequest: function (action, vars, options) {
        assert(action !== undefined, "postRequest missing action");
        vars = vars || {};
        options = options || {};
        vars.t = Constants.TOKEN;
        var form = new Element("form", {
            'action': action,
            'method': 'POST'
        });

        if (options.target) {
            form.target = options.target;
        }
        document.body.appendChild(form);
        Forms.add_vars(form, vars);
        form.submit();
    }
};




var Referral, Invitations, ReferralRegisterAB, Account;

Referral = {
    select_all: 1,
    show_login_modal: function (vars) {
        Modal.show(_("Invite Contacts From Your Email Address Book"), $("cli-login"), vars || {});
    },

    get_selected_emails: function () {
        var emails = [];
        $$("#contact-list input").each(function (elm) {
                if (elm.checked) {
                    emails.push(elm.value);
                }
            }
        );
        return emails.join(", ");
    },
    send_invites: function (referral_src) {
        var emails = Referral.get_selected_emails();
        Invitations.do_send(emails, false, referral_src, true); // is_bulk = true
        Modal.hide();
    },
    show_contact_info_modal: function () {
        Modal.show(_("Invite Contacts From Your Email Account"), DomUtil.fromElm('contact-info-modal'), {
            action: Referral.fetch_contacts,
            wit_group: 'contact_importer_login'
        });
        $("email-prefix").focus();
        return false;
    },
    show_error: function (error) {
        Referral.hide_captcha();
        $('contact-info-error').update(error);
        $('contact-info-error').show();
    },
    // common error messages from tha php importer code. here solely for the purpose of extraction.
    // captcha-related errors will likely never be displayed (because we intercept them), but translate anyway to be safe.
    error_messages: [
        N_('Bad user name or password'),
        N_('Bad user name'),
        N_('Bad password'),
        // TRANSLATORS meaning, while trying to import a user's email contacts, the email provider issued (raised) a captcha form
        N_('Captcha challenge was raised'),
        // TRANSLATORS this can be translated the same as the previous string.
        N_('Captcha challenge raised'),
        N_('Captcha challenge was issued. Please login through Yahoo mail manually.'),
        N_('AOL requires you to answer some security questions'),
        N_('Email address has not been verified'),
        // TRANSLATORS this means, the user's email company closed his/her account
        N_('Account closed by system operator'),
        N_('Account deleted'),
        N_('Account disabled'),
        N_('Service disabled'),
        N_('Authorization required'),
        // TRANSLATORS this is displayed when we encounter an unknown problem importing a list of gmail contacts
        N_('Unknown gmail error'),
        // TRANSLATORS this means, the user has not agreed to the gmail terms of service
        N_('Gmail terms not agreed'),
        // TRANSLATORS abbreviation of: "The Google contacts importer service is temporarily unavailable. Try again later."
        N_('Google contacts service unavailable. Try again later.')
    ],
    show_captcha: function (info) {
        Referral.hide_captcha();
        info = info.evalJSON(true); // sanitize=true
        $('captcha-row').hide();

        $('contact-info-captcha-image').src = info.image.replace("http://", "https://");
        $('contact-info-captcha-image').hide();
        Element.observe('contact-info-captcha-image', 'load', function () {
            $('contact-info-captcha-image').show();
        });

        $('contact-info-captcha-id').value = info.id;
        $('contact-info-captcha-answer').value = '';

        $('captcha-row').show();
        $('captcha-answer-row').show();
        $('contact-info-error').update(_('Captcha required'));
        $('contact-info-error').show();
    },
    hide_captcha: function () {
        $('contact-info-captcha-id').value = '';
        $('contact-info-captcha-answer').value = '';
        $('captcha-row').hide();
        $('captcha-answer-row').hide();
    },
    parse_contacts: function (cstr) {
        cstr = cstr.substr(9); // drop the leading "contacts:"
        return cstr;
    },

    fetch_contacts: function (e) {
        Event.stop(e);
        var username = $F('username');
        var provider = "";
        if (username.indexOf("@") > 0) {
            var split = username.split("@");
            username = split.first();
            provider = split.last();
        }
        Referral.fetch_and_show_contacts(e, username, provider, $F('email-password'),
                                         $F('contact-info-captcha-id'), $F('contact-info-captcha-answer'));
    },
    fetch_and_show_contacts: function (e, email, provider, password, captcha_id, captcha_answer) {
        if (e) {
            Event.stop(e);
        }
        $('contact-info-error').hide();
        Referral.show_loading_modal(provider.split(".")[0]);

        email = provider !== "" ? email + "@" + provider : email;
        var params = {email: email, password: password, select_all: Referral.select_all ? 1 : 0};
        if (captcha_id && captcha_answer) {
            Object.extend(params, {'captcha_id': captcha_id, 'captcha_answer': captcha_answer});
        }

        new Ajax.DBRequest("/import_contacts", {
            noAutonotify: true,
            parameters: params,
            onSuccess: function (req) {
                contacts = Referral.parse_contacts(req.responseText);
                Referral.show_select_contacts(contacts);
            },
            onFailure: function (req) {
                if (req.responseText.indexOf('err:') === 0) {
                    var err = req.responseText.substr(4);

                    if (err.indexOf('captcha:') !== 0) {
                        if (Referral.hide_on_error) {
                            Modal.hide();
                        } else {
                            Referral.show_login_modal();
                        }
                        Referral.show_error(_(err));
                    } else {
                        if (Referral.hide_on_error) {
                            Modal.hide();
                        } else {
                            Referral.show_login_modal();
                        }
                        Referral.show_captcha(err.substr(8));
                    }
                } else {
                    Referral.show_error(_("Unexpected server error."));
                    if (Referral.hide_on_error) {
                        Modal.hide();
                    } else {
                        Referral.show_login_modal();
                    }
                }
            },
            cleanUp: function () {
                $("modal-title").show();
            }
        });
    },
    show_loading_modal: function (provider) {
        var providers_with_images = ["gmail", "yahoo", "aol", "hotmail", "live", "msn"];
        if (providers_with_images.indexOf(provider) > -1) {
            $("email-provider-img").src = "/static/images/referrals_" + provider + ".png";
            $("email-provider-img").show();
        } else {
            $("email-provider-img").hide();
        }

        Modal.show("Loading Contacts", $("loading-contacts-modal"), {}, "", 490);
        $("modal-title").hide();
    },
    show_select_contacts: function (contacts) {
        if (contacts.length) {

            $('contact-list').innerHTML = contacts;
            SuggestionInput.reset("contact-filter");

            var dropbox_users = $$("#contact-list img").length;

            if (dropbox_users === 0) {
                $("dropbox-users-text").style.visibility = "hidden";
            }

            var contact_count = $$(".contact-row").length;
            var msg = ungettext("Good news! We've found %d contact from your contact list. Select the contact if you'd like to invite.",
                                "Good news! We've found %d contacts from your contact list. Select the contacts you'd like to invite.",
                                contact_count).format(contact_count);
            $("contact-import-msg").update(msg);
            Referral.contact_container = document.getElementById("contact-list");
            Referral.contact_rows = Referral.contact_container.childNodes;



            for (var i = 0; i < Referral.contact_rows.length; i += 1) {
                var row = Referral.contact_rows[i];
                row.search_text = row.childNodes[1].firstChild.innerHTML + row.childNodes[2].firstChild.innerHTML;
                var checkbox = $(row.firstChild.firstChild);
                checkbox.observe("click", Referral.checkbox_clicked);
            }
            Referral.fresh = true;
            Referral.update_invite_count();


        }
        var modal = !contacts.length ? 'no-contacts-modal' : 'select-contacts-modal';
        var title = !contacts.length ? _("Oops! No Contacts Here.") : _("Choose Contacts");
        Modal.show(title, $(modal), {action: Referral.action }, null, 600);
        Referral.filter_observer = new Form.Element.Observer('contact-filter', 0.5, function (element, value) {
            if (!SuggestionInput.defaulted(element)) {
                Referral.filter(value);
            }
        });
    },

    checkbox_clicked: function (event) {
        Referral.fresh = false;
        Referral.update_invite_count();
    },

    update_invite_count: function (count) {
        if (!count && Referral.contact_rows) {
            count = 0;
            for (var i = 0; i < Referral.contact_rows.length; i += 1) {
                if (Referral.contact_rows[i].firstChild.firstChild.checked) {
                    count += 1;
                }
            }
        }
        // TRANSLATORS BUTTON
        // Clicking this button will send invites to however many friends our email importer
        // could identify. If our importer found 13 friends, than the message will read "Invite 13 friends".
        var msg = ungettext('Invite %d friend', 'Invite %d friends', count).format(count);
        $("select-contacts-modal").down("input[type=button]").setValue(msg);
    },

    select_all_contacts: function () {
        $$(".contact-check input").each(function (x) {
            x.checked = true;
        });
        Referral.update_invite_count();
        return false;
    },
    select_no_contacts: function () {
        $$(".contact-check input").each(function (x) {
            x.checked = false;
        });
        Referral.update_invite_count(0);
        return false;
    },
    insert_contacts: function () {
        var emails = [];
        $$(".contact-check").each(function (x) {
            if (x.checked) {
                emails.push(x.value);
            }
        });

        if (emails.length) {
            SuggestionInput.clear('invite-recip');
            var prev = $F('invite-recip');
            if (prev) {
                prev += ', ';
            }
            $('invite-recip').setValue(prev + emails.join(", "));
        }
        Modal.hide();
    },
    filter: function (search_string) {
        if (search_string === Referral.last_search || (Referral.last_search === undefined && search_string === "")) {
            return;
        }

        if (Referral.fresh) {
            Referral.fresh = false;
            Referral.select_no_contacts();
        }

        Referral.last_search = search_string;
        var rows_shown = 0;
        var regx = new RegExp(RegExp.escape(search_string.strip()).split(/[;,\s]+/).join(".*"), "i");
        Referral.contact_container.style.display = "none";

        var i = Referral.contact_rows.length;
        while (i--) {
            var row = Referral.contact_rows[i];
            var style = row.style;

            if (regx.test(row.search_text)) {
                if (rows_shown % 2 === 0) {
                    style.background = "#ffffff";
                } else {
                    style.background = "#f4faff";
                }

                style.display = "";
                rows_shown += 1;
            } else {
                style.display = "none";
            }
        }
        Referral.update_invite_count();
        Referral.contact_container.style.display = "";
    },

    do_submit: function (referral_src) {
        assert(Referral.action && typeof(Referral.action) == "function", "Finished with contact list importer but have no callback");
        Referral.action(referral_src);
    },

    do_cancel: function () {
        assert(Referral.cancel_action && typeof(Referral.cancel_action) == "function", "Finished with contact list importer but have no cancel callback");
        Referral.cancel_action();
    },

    hide_warning: function (elm, referral_id) {
        var callback = function () {
            Referral.hide(elm, referral_id);
        };
        Modal.icon_show("group_add", _("Remove Referral?"), $("referral_warning"), { action: callback });
    },
    hide: function (elm, referral_id) {
        elm = $(elm);
        assert(elm, "Referral elm doesn't exist");
        assert(Util.isNumber(referral_id), "Referral id is not a number");

        Modal.hide();
        new Ajax.DBRequest("/account/hide_referral", {
            parameters: { 'referral_id': referral_id },
            onSuccess: function (req) {
                var row = elm.up("tr");
                new Effect.Fade(row);
            }
        });
    },
    status_tooltip: function (elm, status, email) {
        var status_tag = status <= 4 ? status.toString() : 'invalid';
        Tooltip.show(elm, $('referral_' + status_tag).innerHTML.format({'email-address': email}));
    },
    get_invite_status: function () {
        var email_elm = $('referral_email');
        if (!email_elm || SuggestionInput.defaulted(email_elm) || !email_elm.value.strip()) {
            return;
        }

        $('invite_status_result').update();
        Forms.add_loading('status-button');
        new Ajax.DBRequest('/referral_status', {
            parameters: {'email': email_elm.value},
            onSuccess: function (req) {
                email_elm.blur();
                SuggestionInput.reset('referral_email');
                email_elm.focus();
                $('invite_status_result').update(req.responseText);
            },
            cleanUp: function () {
                Forms.remove_loading();
            }
        });
    }
};


Invitations = {
    submit: function (e) {
        e = e || window.event;

        if (e.keyCode == Event.KEY_RETURN) {
            Invitations.send();
        }
    },
    send: function (referral_src, is_bulk) {
        var input = $('invite-recip');
        Invitations.do_send($F(input), input, referral_src, is_bulk);
    },
    do_send: function (recip, clear_this, referral_src, is_bulk) {
        var email_count = recip.strip().split(/[;,\s]+/).length;

        if (!email_count || recip === "") {
            Notify.ServerError(_("Please enter an e-mail address."));
            return;
        }
        if (recip != $('invite-recip').title) {
            var params = {
                'emails': recip,
                'referral_src': referral_src
            };

            if (Referral.source) {
                params.source = Referral.source;
            }

            new Ajax.DBRequest("/send_invite", {
                parameters: params,
                onSuccess: function (req) {
                    if (Invitations.custom_on_success) {
                        Invitations.custom_on_success(req.responseText, is_bulk);
                    } else {
                        Notify.ServerSuccess(req.responseText.substr(5));
                    }
                    if (Referral.on_success) {
                        Referral.on_success(req.responseText);
                    }
                    if (clear_this) {
                        clear_this.setValue('');
                    }
                },
                onFailure: function (req) {
                    if (req.responseText.startsWith("err:")) {
                        Notify.ServerError(req.responseText.substr(4));
                    } else {
                        Notify.ServerError();
                    }
                },
                noAutonotify: true
            });
        } else {
            Notify.ServerError(_("Please enter an email address."));
        }
        return false;
    },
    addCustomMessage: function (event) {
        Event.stop(event);
        var source_a = event.target.tagName == "A" ? $(event.target) : $(event.target).up("a");

        source_a.addHTML = source_a.innerHTML;
        source_a.update(Sprite.make("email_delete", {'class': 'link-img'}));
        source_a.appendChild(document.createTextNode(_("Remove custom message")));

        source_a.stopObserving('click');
        source_a.observe('click', Invitations.hideCustomMessage);
        var textarea = new Element('textarea', {'title': _('Enter a custom message here'), 'name': 'custom_message', 'class': 'custom-message suggestion-input act_as_block textinput', 'rows': 3, 'cols': 25, 'style': 'margin-top: 0.75em;'});
        textarea.setValue(Invitations.custom_message || textarea.title);
        SuggestionInput.register(textarea);
        source_a.up().previous("div").insert({bottom: textarea});
        SuggestionInput.register(textarea);
        ActAsBlock.resize(textarea);
        return false;
    },
    hideCustomMessage: function (event) {
        Event.stop(event);
        var source_a = event.target.tagName == "A" ? $(event.target) : $(event.target).up("a");

        source_a.stopObserving('click');
        source_a.observe('click', Invitations.addCustomMessage);
        source_a.update(source_a.addHTML);

        var input = source_a.up().up().select(".custom-message")[0];
        Invitations.custom_message = $F(input);
        input.parentNode.removeChild(input);
        return false;
    }
};


ReferralRegisterAB = {
    log: function (ev) {
        new Ajax.Request('/referral_register_log', {
            method: 'GET',
            parameters: {
                'event': ev
            }
        });
    }
};

Account = {
    referralPages: {},
    referralCurrentPage: -1,
    referralTabClick: function () {
        if (Account.referralCurrentPage != -1) {
            return;
        }
        Account.getReferralsPage(0);
    },
    getReferralsPage: function (num) {
        Account.referralCurrentPage = num;
        if (Account.referralPages[num]) {
            Account.showReferrals(num);
        } else {
            Feed.showLoading(false, $('referrals-container'));
            new Ajax.DBRequest("/account/referralspage/" + (num).toString(), {
                onSuccess: function (req) {
                    Account.referralPages[num] = req.responseText;
                    Account.showReferrals(num);
                }
            });
        }
        return false;
    },

    showReferrals: function (num) {
        Feed.hideLoading();
        $("referrals-container").update(Account.referralPages[num]);
    }
};


var EventBubble = {
    make: function (content) {
        var template = '<table class="ebubble"><tr><td class="tl"></td><td class="t"></td><td class="tr"></td></tr><tr><td class="l"></td><td class="c">#{content}</td><td class="r"></td></tr><tr><td class="bl"></td><td class="b"><img src="/static/images/events_bubble_tail.gif" alt="" class="events_bubble_tail"/></td><td class="br"></td></tr></table>';
        return template.interpolate({'content': content});
    }
};

var Feed = {
    firstTime: true,
    addComment: function () {
        if (!$F('comment').length || SuggestionInput.defaulted($('comment'))) {
            return false;
        }

        new Ajax.DBRequest("/share_ajax/add_comment", {
            parameters: {'comment': $F('comment'), 'ns_id': Feed.ns_id},
            onSuccess: function (req) {
                Feed.addCommentRow(req.responseText);
                $('comment').value = "";
            }
        });
        return false;
    },
    addCommentRow: function (blurb) {
        var tr = new Element("tr");
        var td1 = new Element("td", {'valign': 'top', 'class': 'note'});
        var icon = new Element("img", {'src': '/static/images/icons/comment.gif'});
        td1.insert(icon);
        var td2 = new Element("td", {'valign': 'top', 'class': 'note'});
        td2.innerHTML = blurb;
        var td3 = new Element("td", {'valign': 'top', 'class': 'note', 'width': '100', 'nowrap': 'nowrap', 'align': 'right'});
        // TRANSLATORS this text is added next to a blog comment that was just added within the last minute
        td3.innerHTML = _("(just added)");
        tr.update(td1);
        tr.insert(td2);
        tr.insert(td3);

        $('event-table').down('tr').insert({before: tr});
    },

    feedPages: {},
    page_num: 0,
    ns_id: "false",
    page_size: 10,
    showLoading: function (no_text, cover_this, just_icon, in_modal) {
        just_icon = true;
        var loadingDiv = $('feed-loading');
        cover_this = $(cover_this);

        if (!loadingDiv) {
            loadingDiv = new Element("div", {id: 'feed-loading'});
            var div_html = '<table style="height: 100%; width: 100%; background:#fff;"><tr><td valign="top"><div id="feed-loading-text" style="padding-top: 16px;text-align:center;"></div></td></tr></table>';
            loadingDiv.update(div_html);
            document.body.appendChild(loadingDiv);
        }

        loadingDiv.clonePosition(cover_this);
        if (loadingDiv.getWidth() === 0) {
            return;
        }

        if (Util.ie) {
            loadingDiv.style.left = cover_this.getBoundingClientRect().left + "px";
        }

        loadingDiv.setOpacity(0.9);
        if (no_text) {
            $('feed-loading-text').update();
        } else {
            $('feed-loading-text').update("<img src='/static/images/icons/ajax-loading.gif' style='vertical-align: bottom;'/>" + (just_icon ? "" : _("Loading...")));
        }

        $('feed-loading').show();
        if (in_modal) {
            loadingDiv.style.zIndex = "1001";
        }
    },
    hideLoading: function () {
        $('feed-loading').hide();
    },

    changeNamespace: function (ns_id, source) {
        Feed.ns_id = ns_id;
        Feed.getPage(0, Feed.page_size);
    },

    changeDate: function (date, no_reload) {
        Feed.date = date;
        Feed.nice_date = Util.niceDate(date);
        if (!no_reload) {
            Feed.getPage(0, Feed.page_size);
        }
    },

    getPage: function (num, page_size) {
        Feed.page_size = page_size;
        Feed.page_num = num;

        if (Feed.feedPages[Feed.get_key(num)]) {
            Feed.show(num, page_size);
        } else {
            Feed.showLoading(false, $('events-content'));
            var ns_str = Feed.ns_id != "false" ? "&ns_id=" + Feed.ns_id.toString() : "&is_home=yes";
            var page_str = page_size ? "&feed_items=" + page_size.toString() : "";

            new Ajax.DBRequest("/next_events?cur_page=" + (num).toString() + ns_str + page_str, {
                parameters: {'date': Feed.nice_date ? Feed.nice_date : "" },
                onSuccess: function (req) {
                    Feed.feedPages[Feed.get_key(num)] = req.responseText;
                    Feed.show(num, page_size);
                }
            });
        }
        return false;
    },

    show: function (num, page_size) {
        Feed.hideLoading();
        $("events-content").update(Feed.feedPages[Feed.get_key(num)]);
        var addcomment = $("add-comment-button");
        if (addcomment) {
            HotButton.register(addcomment);
        }
    },

    tabClick: function (ns_id) {
        var event_table = $("event-table");
        if (!event_table) {
            Feed.getPage(0);
            Feed.clearNewEvents();
        }
    },
    clearNewEvents: function (ns_id) {
        $$(".events_bubble").invoke("hide");
    },

    url_check: function (ns_id, page_size, page_num, nice_date) {
        ns_id = ns_id || "false";
        if (Feed.ns_id != ns_id) {
            $$("#filter-list .selected").invoke("removeClassName", "selected");
            var sfelm = $("sf" + ns_id);
            assert(sfelm, "Missing sf elm for " + ns_id);
            sfelm.addClassName("selected");
        }

        var changed = (Feed.ns_id != ns_id || Feed.page_size != page_size || Feed.page_num != page_num || Feed.nice_date != nice_date);
        Feed.ns_id = ns_id || Feed.ns_id;
        Feed.page_size = page_size || Feed.page_size;
        Feed.page_num = page_num || Feed.page_num;

        if (nice_date) {
            Feed.changeDate(new Date(nice_date), true);
        }

        if (changed) {
            if (nice_date) {
                EventDatePicker.change_date(Feed.date);
            }
            Feed.getPage(Feed.page_num, Feed.page_size);
        }
    },
    set_url: function (options) {
        var ns_id       = options.ns_id !== undefined ? options.ns_id : Feed.ns_id,
            page_size   = options.page_size || Feed.page_size,
            page_num    = options.page_num !== undefined ? options.page_num : Feed.page_num,
            date        = options.date !== undefined ? options.date : Feed.nice_date;

        var hash = ["events", ns_id, page_size, page_num, date].join(":");

        window.location.href = "#" + hash;
    },

    get_key: function (num) {
        return Feed.page_size + "_" + Feed.ns_id + "_" + Feed.date + "_" + num + "_" + Feed.nice_date;
    },
    show_rss_modal: function (url, ns_id) {
        assert(ns_id, "RSS Feed modal with no ns_id");
        Modal.icon_show('feed', _('Subscribe to this RSS Feed'), $('rss-modal'));
        $("rss_url").setValue(url);
        BrowseActions.addCopyUrlFlash(url);
        $("copy_success").update();
        $("reset-rss-link").href = "/reset_rss/" + ns_id;
        $("rss_url").select();
    }
};


var Timezone = {
    check_timezone: function () {
        if (!Constants.uid) {
            return;
        }
        var offset = Timezone.get_current_timezone();
        if (Constants.auto_timezone_offset === undefined || Constants.auto_timezone_offset != offset) {
            Timezone.update(offset);
        }
    },

    get_current_timezone: function () {
        // http://www.onlineaspect.com/2007/06/08/auto-detect-a-time-zone-with-javascript/
        var now = new Date();
        now.setSeconds(0);
        now.setMilliseconds(0);

        var temp = now.toGMTString();
        var gmt = new Date(temp.substring(0, temp.lastIndexOf(" ") - 1));
        var std_time_offset = (now - gmt) / (1000 * 60 * 60);
        return std_time_offset;
    },

    update: function (offset) {
        assert(typeof(offset) == "number", "Timezone offset was not a number: " + offset);
        new Ajax.DBRequest("/set_timezone", {
            parameters: { 'offset': offset },
            noAutonotify: true
        });
    },

    on_change: function () {
        var tz_list = [];

        var tz_parts = [ $("timezone_area"),
                         $("timezone_location"),
                         $("timezone_city") ];

        tz_parts.each(function (part) {
            if (part) {
                tz_list.push($F(part));
            }
        });

        Timezone.update_form(tz_list);
    },

    update_form: function (tz_parts) {
        tz_parts = tz_parts || ["America"]; // i18n-note this default assumes American audience, could be localized
        assert(Timezone.tree, "Timezone tree missing...");
        $("tz").update();

        var ids = ["timezone_area", "timezone_location", "timezone_city"];

        var node = Timezone.tree;
        var times = Math.max(tz_parts.length + 1, 2);
        for (var i = 0; i < times; i += 1) {
            var part = tz_parts[i];
            var keys = Object.keys(node);

            if (!keys.length) {
                break;
            }

            var select = new Element("select", {
                'id': ids[i],
                'name': ids[i]
            });
            select.observe("change", Timezone.on_change);

            for (var j = 0, len = keys.length; j < len; j += 1) {
                var name = keys[j];
                var option = new Element("option");
                option.value = name;
                option.update(name);

                if (name == part) {
                    option.selected = true;
                }
                select.appendChild(option);
            }

            $("tz").appendChild(select);
            node = node[part];
        }
        Util.syncHeight();
    },

    auto: function () {
        var auto = $F("timezone_auto");
        if (auto) {
            $("tz").update();
        } else {
            Timezone.update_form();
        }
    },

    build_tree: function (timezones) {
        var tree = {};
        timezones.each(function (timezone) {
            var timezone_parts = timezone.split("/");
            var node = tree;
            timezone_parts.each(function (part) {
                if (!node[part]) {
                    node[part] = {};
                }
                node = node[part];
            });
        });

        Timezone.tree = tree;
    }
};
document.observe("dom:loaded", Timezone.check_timezone);


var DBCalendar = Class.create({
    initialize: function (container_id, options) {
        this.options = options || {};
        this.container = $(container_id);
        assert(this.container, "Couldn't find the element");

        this.today = new Date();

        if (this.options.disable_future) {
            this.options.last_day = this.options.last_day || this.today;
        }

        if (this.options.disable_past) {
            this.options.first_day = this.options.first_day || new Date(this.today.getFullYear(), this.today.getMonth(), this.today.getDate());
        }

        this.current_day = Util.start_of_day(this.options.selected_day || new Date(this.today.getFullYear(), this.today.getMonth(), 1));
        this.selected_day = Util.start_of_day(this.options.selected_day || new Date(this.today.getFullYear(), this.today.getMonth(), this.today.getDate()));
        this.render();
    },

    change_month: function (e, month) {
        Event.stop(e);
        this.current_day.setMonth(month);
        this.render();

        if (this.options.onMonthChange) {
            this.options.onMonthChange(this.current_day);
        }

    },

    change_day: function (e, day) {
        Event.stop(e);
        this.container.select(".selected").invoke("removeClassName", "selected");
        $(e.target).addClassName("selected");

        this.selected_day.setMonth(this.current_day.getMonth());
        this.selected_day.setYear(this.current_day.getFullYear());
        this.selected_day.setDate(day);

        if (this.options.onDateChange) {
            this.options.onDateChange(this.selected_day);
        }
    },

    render: function () {
        var days = this.render_days();

        this._next_month = (function (e) {
            this.change_month(e, this.current_day.getMonth() + 1);
        }).bind(this);

        this._prev_month = (function (e) {
            this.change_month(e, this.current_day.getMonth() - 1);
        }).bind(this);

        var next = new Element("a");
        next.addClassName("changemonth next");
        next.update(Sprite.make("arrowright", {}));
        Event.observe(next, "click", this._next_month);

        var prev = new Element("a");
        prev.addClassName("changemonth prev");
        prev.update(Sprite.make("arrowleft", {}));
        Event.observe(prev, "click", this._prev_month);

        var calendar = new Element("div");
        calendar.addClassName("calendar clearfix");

        var label = new Element("h5");

        var months = [ _("January"),
                       _("February"),
                       _("March"),
                       _("April"),
                       _("May"),
                       _("June"),
                       _("July"),
                       _("August"),
                       _("September"),
                       _("October"),
                       _("November"),
                       _("December")
                     ];

        // TRANSLATORS For example "January 2010". This is used as part of a calendar. Month is translated separately.
        label.update(_("%(month)s %(year)s").format({'month': months[this.current_day.getMonth()], 'year': this.current_day.getFullYear()}));

        calendar.insert(next);
        calendar.insert(prev);
        calendar.insert(label);

        calendar.insert(days);

        this.container.update(calendar);
    },

    render_days: function () {
        var first_day = new Date(this.current_day.getFullYear(), this.current_day.getMonth(), 1);
        var previous_month_days_count = first_day.getDay();

        // Render previous month days
        var days = new Element("div");
        days.addClassName("days");

        for (var i = previous_month_days_count; i > 0; i -= 1) {
            var prev_date = new Date(first_day.getFullYear(), first_day.getMonth(), first_day.getDate());
            prev_date.setDate(prev_date.getDate() - i);
            days.insert(this.render_day(prev_date, true));
        }

        // Render current month days
        var iter_day = new Date(this.current_day.getFullYear(), this.current_day.getMonth(), 1);
        while (iter_day.getMonth() == this.current_day.getMonth()) {
            days.insert(this.render_day(iter_day));
            iter_day = new Date(this.current_day.getFullYear(), this.current_day.getMonth(), iter_day.getDate() + 1);
        }

        // Render next month days
        var last_day = new Date(this.current_day.getFullYear(), this.current_day.getMonth() + 1, 0);
        while (last_day.getDay() != 6) {
            last_day = new Date(last_day.getFullYear(), last_day.getMonth(), last_day.getDate() + 1);
            days.insert(this.render_day(last_day, true));
        }

        return days;
    },

    render_day: function (day, inactive) {

        if (this.options.last_day) {
            inactive = inactive || day > this.options.last_day;
        }
        if (this.options.first_day) {
            inactive = inactive || day < this.options.first_day;
        }
        var a;
        if (inactive) {
            a = new Element("span");
        } else {
            a = new Element("a");
        }

        a.update(day.getDate());
        a.addClassName("date");

        if (this.selected_day.getDate() == day.getDate() && this.selected_day.getMonth() == day.getMonth() && this.selected_day.getFullYear() == day.getFullYear()) {
            a.addClassName("selected");
        }

        if (inactive) {
            a.addClassName("inactive");
        } else {
            this._change_day = (function (e) {
                this.change_day(e, day.getDate());
            }).bind(this);
            Event.observe(a, "click", this._change_day);
        }

        Util.disableSelection(a);
        return a;
    }
});


var EventDatePicker = {
    show_calendar: function (e) {
        if (EventDatePicker.shown) {
            return;
        }

        Event.stop(e);

        if (!EventDatePicker.calendar) {
            var cal_container = new Element("div", {'id': 'cal_container'});
            cal_container.observe("click", function (e) {
                Event.stop(e);
            });
            $(document.body).insert(cal_container);

            EventDatePicker.calendar = new DBCalendar("cal_container", {
                'onDateChange': function (d) {
                    EventDatePicker.change_date(d, 0);
                },
                'disable_future': true,
                'first_day': EventDatePicker.first_event
            });
            cal_container.absolutize();

            var cal_date = $("cal_date");

            cal_container.clonePosition(cal_date, {'setWidth': false, 'setHeight': false, 'offsetTop': cal_date.getHeight() - 1, 'offsetLeft': cal_date.getWidth() - cal_container.down().getWidth()});
        }

        $("cal_container").show();
        $(document.body).observe("click", EventDatePicker.hide_calendar);
        EventDatePicker.shown = true;
    },

    hide_calendar: function (e) {
        Event.stop(e);
        $("cal_container").hide();
        $(document.body).stopObserving("click", EventDatePicker.hide_calendar);
        EventDatePicker.shown = false;
    },

    change_date: function (date, page) {
        if (page !== undefined) {
            Feed.page_num = 0;
        }
        var cur_date_text = $("cur_date_text");
        cur_date_text.update(date.localize());
        Feed.set_url({'date': Util.niceDate(date)});
    }
};


var TextInputDatePicker = Class.create(
{
    initialize: function (input_id, options) {
        this.options = {
            'include_seconds': true,
            'choose_eod':      false // force the time selection to the end of the day
        };
        Object.extend(this.options, options || {});

        this.input = $(input_id);
        assert(this.input, "Couldn't find the element " + input_id.toString());

        // set the calendar's day if there's already something in the text box
        var now = new Date();
        var selected_day = this.input.value ? Util.from_mysql_date(this.input.value) : false;
        var last_day = new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());

        this.cal_icon = Sprite.make('calendar_view_month', {'align': 'absmiddle'});
        this.cal_container  = new Element("div", {'id': 'cal_container_' + input_id, 'style': 'display: none; position: absolute; z-index: 1'});
        this.calendar = new DBCalendar(this.cal_container, {'onDateChange': this.onDateChange.bind(this), 'last_day': last_day, 'selected_day': selected_day});

        // put the icon to the right of the text input
        //this.cal_icon.clonePosition(this.input, {'setWidth': false, 'setHeight': false, 'offsetLeft': this.input.getWidth() + 2);
        this.input.insert({'after': this.cal_icon});
        this.cal_icon.observe('click', this.toggle_cal.bindAsEventListener(this));

        // put the calendar under the input
        this.cal_container.clonePosition(this.input, {'setWidth': false, 'setHeight': false, 'offsetTop': this.input.getHeight()});
        this.cal_icon.insert({'after': this.cal_container});
    },
    toggle_cal: function (e) {
        if (e) {
            Event.stop(e);
        }
        this.cal_container.toggle();
    },
    hide_cal: function (e) {
        if (e) {
            Event.stop(e);
        }
        this.cal_container.hide();
    },
    onDateChange: function (date) {
        if (this.options.choose_eod) {
            date.setTime(Util.start_of_day(date).getTime() + 86399999); // 1 millisecond before the next day, ignoring leap seconds/daylight savings
        }
        this.input.value = Util.to_mysql_date(date, true); // include_seconds=true
        this.hide_cal();
    }
});


var Apps = {
    confirm_disable: function (name, app_id, disable) {
        // TRANSLATORS ex: "Are you sure you want to disable 'My App'?", "Are you sure you want to delete 'My App'?"
        var disable_text = disable ? _("Are you sure you want to disable '%(app-name)s'?") : _("Are you sure you want to delete '%(app-name)s'?");
        DomUtil.fillVal(disable_text.format({'app-name': name.escapeHTML()}), 'app-disable-text');
        // TRANSLATORS title shown on dialog box asking whether the user wants to disable/delete their app
        Modal.icon_show("application_delete", disable ? _('Confirm disable') : _('Confirm delete'), $("app-disable-modal"));
        var url = '/developers/disable_app/' + app_id;
        $("app-disable-modal").down("form").action = url;
        $("disable-app-button").setValue(disable ? _("Disable") : _("Delete"));
    },
    enable_app: function (app_id) {
        var url = '/developers/enable_app/' + app_id;
        window.location.href = url;
    },
    show_app_limit_reached: function () {
        // TRANSLATORS title shown when the developer has already created the maximum number of applications
        Modal.icon_show("application_add", _("Developer app limit reached"), $("app-limit-modal"));
    },
    show_create: function () {
        Modal.icon_show("application_add", _("Create a new app"), $('create-app'), {}, false, 500);
        ActAsBlock.register(false, $("modal-content"));
    },
    do_create: function (e) {
        if (e) {
            Event.stop(e);
        }
        var form = $("create-app-form");
        assert(form, "Missing form for Apps.do_create");
        Forms.ajax_submit(form, false,
            function (req) {
                if (req && req.responseText && req.responseText != 'ok') {
                    window.location.href = req.responseText;
                } else {
                    window.location.reload();
                }
            }, false, e && e.target);
    },
    get_edit: function (name, app_id) {
        // TRANSLATORS ex: "Loading info for 'My App'"
        Modal.show_loading("application_edit", _("Loading info for '%(app-name)s'").format({'app-name': name.escapeHTML()}));
        new Ajax.DBRequest("/developers/app_info", {
            parameters: {
                app_id: app_id
            },
            onSuccess: function (req) {
                Apps.show_edit(name, req.responseText);
            }
        });
    },
    show_edit: function (name, content) {
        // TRANSLATORS as in, "options for %(app-name)s", ex: "'My App' options" SHORT
        Modal.icon_show("application_edit", "'%(app-name)s' options".format({'app-name': name.escapeHTML()}), content);
    },
    do_edit: function (e) {
        if (e) {
            Event.stop(e);
        }
        var form = $("update-app-form");
        assert(form, "Missing form for Apps.do_edit");

        Forms.ajax_submit(form, false, function () {
            window.location.reload();
        },
        false, e && e.target);
    },
    show_about: function (e, app_name, description, sandbox, icon, url) {
		if (e) {
			Event.stop(e);
		}

		DomUtil.fillVal(app_name.escapeHTML(), 'app-name');
		DomUtil.fillVal(description.escapeHTML(), 'app-description');

		Modal.show(app_name, $("about-app"), {'force-icon': icon});

		$('application-link').href = url;
    },
    show_uninstall: function (e, app_name, token_id, sandbox_dropbox, app_folder_dest) {
        if (e) {
            Event.stop(e);
        }
        Modal.vars = {'token_id': token_id, 'delete_row_type': 'inst-app', 'action': 'uninstall_app'};

        var div = 'delete-' + sandbox_dropbox + '-app-confirm';
        if (app_folder_dest) {
            DomUtil.fillVal(app_folder_dest.escapeHTML(), 'app_folder');
        } else if (sandbox_dropbox == 'sandbox') {
            // skip the confirmation if we have a sandbox that's already deleted
            return Apps.do_action();
        }

        DomUtil.fillVal(app_name.escapeHTML(), 'app_name');

        // TRANSLATORS example: Remove 'My App'?
        Modal.icon_show('application_delete', _("Remove %(app_name)s?").format({'app_name': app_name.escapeHTML()}), $(div), Modal.vars);
    },
    do_uninstall: function () {
        var dbr = new Ajax.DBRequest("/api/uninstall_app", {
            parameters: {'id': Modal.vars.token_id, 'keep_sandbox_files': $F('keep_sandbox_files')},
            onSuccess: function (req) {
                Notify.ServerSuccess(req.responseText);
                $('inst-app-' + Modal.vars.token_id.toString() + '-row').hide();
            }
        });
        Modal.hide();
    },
    show_confirm: function (e, app_name, token_id, action, action_text, vars) {
        if (e) {
            Event.stop(e);
        }
        var my_vars = $H({'token_id': token_id, 'action': action});
        my_vars.update(vars);

        var actions = {'delete': _("Are you sure you want to delete %(app-name)s?"),
                       'uninstall': _("Are you sure you want to uninstall %(app-name)s?"),
                       'renew': _("Are you sure you want to renew your token for %(app-name)s")};

        assert(action_text in actions, "Unexpected confirmation action '%s'".format(action_text));

        DomUtil.fillVal(actions[action_text].format({'app-name': app_name.escapeHTML()}), 'token-confirm-text');

        var shower = Modal.show;
        if (vars.icon) {
            shower = function () {
                var args = $A(arguments);
                args.unshift(vars.icon);
                Modal.icon_show.apply(this, args);
            };
        }

        var titles = {'delete': _("Confirm delete"),
                     'uninstall': _("Confirm uninstall"),
                     'renew': _("Confirm token renewal")};
        assert(action_text in titles, "Unexpected confirmation action '%s'".format(action_text));
        shower(titles[action_text], $('token-confirm'), my_vars.toObject());
    },
    do_action: function () {
        new Ajax.DBRequest("/api/" + Modal.vars.action, {
            parameters: {'id': Modal.vars.token_id},
            onSuccess: function (req) {
                Notify.ServerSuccess(req.responseText);
                if (Modal.vars.delete_row_type) {
                    $(Modal.vars.delete_row_type + '-' + Modal.vars.token_id.toString() + '-row').hide();
                } else {
                    window.location.reload();
                }
            }
        });
    },
    enable_users_in_dev: function (app_id) {
        Modal.show(_("Enable additional users"), $('confirm-users-in-dev-modal'), {'enable':function() {
            new Ajax.DBRequest("/developers/enable_users_in_dev/"+app_id, {
                onSuccess: function (req) {
                    $('enable-users-in-dev').remove();
                    $('none-linked').show();
                    Modal.hide();
                }
            });
        }
        });
        return false;
    },
    remove_user: function (app_id, user_id, line) {
        new Ajax.DBRequest("/developers/remove_user/"+app_id+"/"+user_id, {
            onSuccess: function (req) {
                table = $(line).up("table");
                $(line).up("tr").remove();
                if (table.down('tr').id == 'none-linked') {
                    //table.up('tr').remove();
                    $('none-linked').show();
                    $('apply-for-more').hide();
                }
            }
        });
        return false;
    },
    show_add_key_confirm: function (e, app_name, app_id) {
        if (e) {
            Event.stop(e);
        }
        DomUtil.fillVal(app_name.escapeHTML(), 'app-name');
        // TRANSLATORS a 'key' is an authentication method that developers must use with the Dropbox API
        Modal.show(_("Confirm key creation"), $('add-key-confirm'), {'app_id': app_id});
        return 0;
    },

    do_add_key: function (app_id) {
        new Ajax.DBRequest("/api/create_app_token", {
            parameters: {'id': Modal.vars.app_id},
            onSuccess: function (req) {
                Notify.ServerSuccess(_("Key created successfully."));

                var row = '<tr id="token-#{id}-row"><td>#{key}</td><td>#{secret}</td>' +
                          '<td><a href="#" onclick="Apps.show_del_key_confirm(event, \'#{key}\', \'#{secret}\', \'#{id}\');">' +
                          Sprite.html('x') +
                          '</a></td></tr>';

                var data = req.responseText.evalJSON(true);
                data.id = Number(data.id);
                data.key = data.key.replace("'", "");
                data.secret = data.secret.replace("'", "");

                row = row.interpolate(data);
                $('api-key-last-row').insert({before: row});
            }
        });
    },
    show_del_key_confirm: function (e, key, secret, token_id) {
        if (e) {
            Event.stop(e);
        }
        DomUtil.fillVal(key.escapeHTML(), 'token-key');
        DomUtil.fillVal(secret.escapeHTML(), 'token-secret');
        Modal.show(_("Confirm key removal"), $('del-key-confirm'), {'token_id': token_id});
        return 0;
    },
    do_del_key: function (token_id) {
        new Ajax.DBRequest("/api/delete_app_token", {
            parameters: {'id': Modal.vars.token_id},
            onSuccess: function (req) {
                Notify.ServerSuccess(_("Key removed successfully."));
                var res = req.responseText.evalJSON(true);
                $('token-' + res.id  + '-row').hide();
            }
        });
    },
    restore_sandbox: function (where, ns_id) {
        var content = $("restore-sandbox");
        Modal.icon_show("folder_app", _('Restore app folder "%(filename)s"').format({
            filename: FileOps.filename(where).escapeHTML()
        }), content);
        var form = $("restore-sandbox-form");
        Forms.add_vars(form, {
            'ns_id': ns_id
        });
    },
    submit_restore_sandbox: function (e) {
        Event.stop(e);
        var form = $("restore-sandbox-form");
        Forms.ajax_submit(form, false, function (req) {
            Modal.hide();
            Notify.ServerSuccess(_("Restored app folder"));
            if (req.responseText.length) {
                Browse.reload_fqpath(req.responseText);
            } else {
                Browse.reload('', '', true);
            }
        }, false, e.target);
        return false;
    },
    developer_support: function () {
        var content = $("dev-support-modal");
        // TRANSLATORS title
        Modal.icon_show("bug", _("Dropbox developer support"), content);
    },
    submit_developer_support: function (e) {
        var form = $("dev-support-form");
        assert(form, "Form is missing in submit_developer_support");
        Forms.ajax_submit(form, false, function () {
            Modal.hide();
            Notify.ServerSuccess(_("Thanks for your ticket.  We'll get back to you soon."));
        }, false, e && e.target);
    },
    submit_app_info: function (e, app_id) {
        try {
            var form = $("update-app-form");
            Forms.ajax_submit(form, "/developers/app_info/%s".format(app_id),
            function (req) {
                form.submit(); // send the icon to block server
            }, false, e && e.target);
        } finally {
            return false; // don't submit the form until onSuccess
        }
    },
    delete_screenshot: function (container, app_id, screenshot_id) {
        new Ajax.DBRequest("/developers/delete_screenshot/%s".format(app_id), {
            parameters: {'screenshot_id': screenshot_id},
            onSuccess: function (req) {
                Effect.Fade(container);
            }
        });
    },
    add_screenshot_form: function () {
        var fileup = new Element("input", {
            type: 'file',
            name: 'screenshots'
        });
        $("screenshots-container").appendChild(fileup);
    },
    show_need_users_modal: function (e) {
        Event.stop(e);
        // TRANSLATORS this text is for an error message title
        Modal.icon_show("error", _("Please test this app"), $('app-need-users-modal'));
        return false;
    }
};


var AppDirectory = {
    click: function (params) {
        params = Object.toQueryString(params);

        AppDirectory.get_page(params);
        HashRouter._set_hash(params);
    },
    get_page: function (params) {
        Feed.showLoading(true, "list-content");
        new Ajax.Request("/apps/list?" + params, {
            onSuccess: function (req) {
                $("list-content").update(req.responseText);
            },
            onComplete: function () {
                Feed.hideLoading();
            }
        });
    },
    hash_change: function (e) {
        var hash = e.memo.hash;
        AppDirectory.get_page(hash);
    },
    platform_change: function () {
        var params = Object.clone(AppDirectory.filter_state);
        var selected = [];
        $$(".platform input").each(function (elm) {
            if (elm.checked) {
                selected.push(elm.value);
            }
        });

        params.page = 0;
        params.platform = selected.join(",");
        AppDirectory.click(params);
    },
    set_order: function (order_by) {
        var params = Object.clone(AppDirectory.filter_state);
        params.order_by = order_by;
        params.page = 0;
        AppDirectory.click(params);
    },
    set_page: function (page) {
        var params = Object.clone(AppDirectory.filter_state);
        params.page = page;
        AppDirectory.click(params);
    }
};


var AppReview = {
    page: 0,
    get_page: function (app_id, page) {
        Feed.showLoading(true, $("reviews").down("div"));
        new Ajax.Request('/apps/reviews', {
            parameters : {
                'page' : page,
                'app_id' : app_id
            },
            onSuccess: function (req) {
                AppReview.update(req.responseText);
                AppReview.page = parseInt(page, 10);
                HashRouter.set_hash("review", app_id, page.toString());
                $("reviews").scrollTo();
            },
            onComplete: function () {
                Feed.hideLoading();
            }
        });
    },
    update: function (content) {
        $("reviews").update(content);
    },
    check_hash: function (app_id, page) {
        page = page || 0;
        page = parseInt(page, 10);
        if (page != AppReview.page) {
            AppReview.get_page(app_id, page);
        }
    },
    add_review: function (e, app_id) {
        Event.stop(e);

        var form = $("app-review-form");
        Forms.ajax_submit(form, false,
            function (req) {
                AppReview.get_page(app_id, 0);
                form.down("textarea").setValue("");
            }, false, e.target);
    }
};

var Twitter = {
    get_progress_container: function () {
        assert(Twitter.progress_container, "Twitter is missing progress_container");
        var elm = $(Twitter.progress_container);
        assert(elm, "Missing progress_container elm");
        return elm;
    },
    follow_dropbox: function (options) {
        if (options.showWorking) {
            options.showWorking();
        }
        var onFail = function () {
            if (options.onFailure) {
                options.onFailure();
            } else {
                window.location.reload();
            }
        };
        new Ajax.DBRequest("/twitter/follow_us", {
            onSuccess: function (req) {
                if (!req.responseText.startsWith("ok")) {
                    onFail();
                } else {
                    if (options.onSuccess) {
                        options.onSuccess();
                    }
                }
            },
            onFailure: function () {
                onFail();
            }
        });
    },
    do_auth: function (callback) {
        window.open('/twitter/request_token', 'twitter_auth', 'width=800,height=400');
        if (callback) {
            Twitter.onLoginSuccessCallback = callback;
        }
    },
    start_flow: function (title, msg, link, link_name, description) {
        var content = $("post-options");
        assert(content, "Missing content for twitter flow");
        Modal.icon_show("page_paste", title, content);

        if (!$F("post-message")) {
            $("post-message").setValue(msg);
        }

        var button = $("share-this-message");
        assert(button, "Missing button");

        button.stopObserving("click");
        button.observe("click", function (e) {
            if (e) {
                Event.stop(e);
            }
            SharingModel.start_twitter_flow($F("post-message"), link, link_name, description);
        });

        if (!Twitter.chars_left_interval) {
            Twitter.chars_left_interval = setInterval(Twitter.update_chars_left, 250);
        }
    },
    update_chars_left: function () {
        var text_elm = $("twitter-chars-left");
        var twitter_checked = $("twitter-checkbox") && $F("twitter-checkbox");
        var facebook_checked = $("facebook-checkbox") && $F("facebook-checkbox");

        if (twitter_checked) {
            text_elm.style.visibility = "";
            var left = 140 - $F("post-message").strip().length;

            if (left < 0) {
                text_elm.addClassName("error-message");
            } else {
                text_elm.removeClassName("error-message");
            }
            text_elm.update(left);
        } else {
            text_elm.style.visibility = "hidden";
        }

        var button = $("share-this-message");
        if (!facebook_checked && !twitter_checked) {
            button.addClassName("grayed");
            button.disabled = 1;
        } else {
            button.removeClassName("grayed");
            button.disabled = 0;
        }
    },
    show_login: function (onLoginSuccessCallback) {
        if (onLoginSuccessCallback) {
            Twitter.onLoginSuccessCallback = onLoginSuccessCallback;
        }
        DomUtil.updateFromElm(Twitter.get_progress_container(), "inline-twitter-auth");
    },
    show_posting: function () {
        Modal.hide();
        Twitter.get_progress_container().update(DomUtil.fromElm("sharing-progress"));
    },
    show_complete: function (resp) {
        var cont = Twitter.get_progress_container();
        Twitter.show_complete_into(resp, cont);
    },
    show_complete_into: function (resp, cont) {
        cont.update(DomUtil.fromElm("sharing-posted"));
        var icon = "twitter";
        var text = _("View Tweet");
        var url;
        if (resp.startsWith("ok")) {
            url = "http://www.twitter.com/";
        } else {
            url = resp;
        }

        var link = cont.down("#view-post");
        link.href = url;
        link.update(text);
        var s = Sprite.make(icon);
        link.insert({top: s});
    },
    post: function (message, username, password) {
        assert(message, "Twitter message is empty");

        var params = {
            'message': message,
            'from_referrals': Twitter.from_referrals,
            'from_free': Twitter.from_free
        };

        new Ajax.DBRequest('/twitter_post', {
            parameters: params,
            onSuccess: function (req) {
                if (req.responseText == "login") {
                    Twitter.onLoginSuccessCallback = function () {
                        Twitter.post(message);
                    };

                    var show_auth = Twitter.custom_show_auth || Twitter.show_login;
                    show_auth();
                } else {
                    Modal.hide();
                    var success = Twitter.onPostSuccessCallback || Twitter.show_complete;
                    success(req.responseText);
                }
            }
        });
        var posting = Twitter.custom_show_posting || Twitter.show_posting;
        posting();
    },
    custom_post: function (message, onSuccess) {
        // var message = $F("twitter-msg");
        if (onSuccess) {
            Twitter.onPostSuccessCallback = onSuccess;
        }

        if (!message) {
            return;
        }
        assert(message, "Twitter message doesn't exist");
        if (!Constants.uid) {
            window.open("http://www.twitter.com/home?status=" + encodeURI(message));
        } else {
            Twitter.post(message);
        }
    },
    prompt_message: function (token, msg, onSuccess) {
        if (Twitter.profile_image) {
            $("twitter-profile-image").src = Twitter.profile_image;
        }

        assert(msg, "Expected a message for twitter");
        Twitter.onPostSuccessCallback = onSuccess;

        Modal.icon_show("twitter", _("Post to Twitter"), $("twitter-prompt"));
        $("twitter-prompt").down("#twitter-msg").update(msg);
    }
};

var FacebookOAuth = {
    get_progress_container: function () {
        assert(FacebookOAuth.progress_container, "Facebook is missing progress_container");
        var elm = $(FacebookOAuth.progress_container);
        assert(elm, "Missing progress_container elm");
        return elm;
    },
    do_auth: function (callback) {
        window.open('/fb/access_token', 'fb_auth', 'width=600,height=450');
        if (callback) {
            FacebookOAuth.onLoginSuccessCallback = callback;
        }
    },
    show_posting: function () {
        Modal.hide();
        FacebookOAuth.get_progress_container().update(DomUtil.fromElm("sharing-progress"));
    },
    show_auth: function (auth_callback) {
        if (auth_callback) {
            FacebookOAuth.onLoginSuccessCallback = auth_callback;
        }
        DomUtil.updateFromElm(FacebookOAuth.get_progress_container(), "inline-facebook-auth");
    },
    show_complete: function () {
        var cont = FacebookOAuth.get_progress_container();
        cont.update(DomUtil.fromElm("sharing-posted"));
        var icon = "facebook";
        var text = "View Post";
        var url = "http://www.facebook.com/profile.php?v=wall";

        var link = cont.down("#view-post");
        link.href = url;
        link.update(text);
        var s = Sprite.make(icon);
        link.insert({top: s});
    },
    post: function (message, link, link_name, description, picture) {
        if (!Constants.uid) {
            window.open("http://www.facebook.com/sharer.php?u=" + encodeURI(link) + "&t=" + encodeURI(link_name));
            return;
        }

        var posting = FacebookOAuth.custom_show_posting || FacebookOAuth.show_posting;
        posting();

        new Ajax.DBRequest("/fb/post",  {
            parameters: {
                message:        message,
                link:           link,
                link_name:      link_name,
                description:    description,
                from_referrals: FacebookOAuth.from_referrals,
                from_free:      FacebookOAuth.from_free,
                picture:        picture ? picture : ''
            },
            onSuccess: function (req) {
                if (req.responseText.startsWith("ok")) {
                    var success = FacebookOAuth.custom_show_complete || FacebookOAuth.show_complete;
                    success();
                } else {
                    var show_auth = FacebookOAuth.custom_show_auth || FacebookOAuth.show_auth;
                    show_auth();
                    FacebookOAuth.onLoginSuccessCallback = function () {
                        FacebookOAuth.post(message, link, link_name, description);
                    };
                }
            }
        });
    }
};


var MP3Player = {
    // IDLE, BUFFERING, PLAYING, PAUSED, COMPLETED
    state: false,
    file: false,
    volume_percent: 100,
    meta: {},
    on_ready: false,
    init: function (file) {

        var flashVars = {
            'type':  'sound',
            'id': 'mp3embed'
        };

        if (file) {
            flashVars.file = encodeURI(encodeURI(encodeURI(file)));
        }
        var params = {
            'allowScriptAccess': 'always'
        };
        var attrs = {
            'name': 'mp3embed'
        };

        var div = new Element("div", {
            'id': 'mp3embed'
        });
        var parent = document.body;
        if (Prototype.Browser.Gecko) {
            // Put the mp3player on the html element so that
            // page reflows don't cause flash to reload.
            parent = document.documentElement;
        }
        parent.appendChild(div);
        swfobject.embedSWF('/static/swf/player-5.2.1065-ID3.swf', 'mp3embed', '1', '1', '9', false, flashVars, params, attrs, function (obj) {
            MP3Player.player = $(obj.ref);
            MP3Player.file = file;
        });
    },

    load: function (url) {
        if (MP3Player.file == url) {
            return;
        }
        if (MP3Player.player) {
            url = decodeURIComponent(url);
            MP3Player.player.sendEvent("load", [{file: url, type: 'sound'}]);
            MP3Player.meta = {};
            MP3Player.play();
        } else {
            MP3Player.init(url);
        }
        MP3Player.file = url;
    },
    volume: function (percent) {
        MP3Player.volume_percent = percent;
        if (MP3Player.player) {
            MP3Player.player.sendEvent("volume", percent.toString());
        }
    },
    seek: function (seconds) {
        MP3Player.player.sendEvent("seek", seconds.toString());
    },
    play: function () {
        if (MP3Player.state === false || MP3Player.state == "idle" || MP3Player.state == "completed" || MP3Player.state == "paused") {
            MP3Player.player.sendEvent("play");
        }
    },
    pause: function () {
        if (MP3Player.state == "playing" || MP3Player.state == "buffering" || MP3Player.state == "completed") {
            MP3Player.player.sendEvent("play");
        }
    },

    stop: function () {
        MP3Player.player.sendEvent("stop");
        delete MP3Player.file;
    },
    get_state: function () {
        return MP3Player.state;
    },
    volume_change: function (obj) {
        $(document.body).fire("mp3:volume", obj);
    },
    load_change: function (obj) {
        $(document.body).fire("mp3:load", obj);
        if (obj.percentage == 100) {
            $(document.body).fire("mp3:load_complete", obj);
        }
    },
    time_change: function (obj) {
        MP3Player.duration = obj.duration;
        $(document.body).fire("mp3:time", obj);
    },
    state_change: function (obj) {
        MP3Player.state = obj.newstate.toLowerCase();
        $(document.body).fire("mp3:" + MP3Player.state);
    },
    meta_change: function (obj) {
        if (obj.type == "id3") {
            $(document.body).fire("mp3:id3", obj);
            MP3Player.meta = obj;
        }
    },
    observe: function (state, callback) {
        state = state.toLowerCase();
        $(document.body).observe("mp3:" + state, callback);
    },
    stopObserving: function (state) {
        state = state.toLowerCase();
        $(document.body).stopObserving("mp3:" + state);
    }
};

function playerReady(obj) {
    if (obj.id != "mp3embed") {
        return;
    }
    MP3Player.player.addControllerListener('PLAY', 'MP3Player.played');
    MP3Player.player.addControllerListener('STOP', 'MP3Player.stopped');
    MP3Player.player.addControllerListener('VOLUME', 'MP3Player.volume_change');

    MP3Player.player.addModelListener('STATE', 'MP3Player.state_change');
    MP3Player.player.addModelListener('TIME', 'MP3Player.time_change');
    // MP3Player.player.addModelListener('LOADED', 'MP3Player.load_change');
    MP3Player.player.addModelListener('BUFFER', 'MP3Player.load_change');
    MP3Player.player.addModelListener('META', 'MP3Player.meta_change');

    MP3Player.player.sendEvent("volume", MP3Player.volume_percent.toString());
    MP3Player.play();
    if (MP3Player.on_ready) {
        MP3Player.on_ready();
    }
}

var MP3Controller = {
    current: false,
    playlist: [],
    click: function (e, url) {
        MP3Controller.playlist = [];
        var button = Util.resolve_target(e.target, "a.play");
        if (button != MP3Controller.current) {
            MP3Controller.play(button, url);
        } else {
            MP3Controller.stop(button);
        }
    },
    play_all: function () {
        var urls = [];
        $$(".download-song").each(function (elm) {
            urls.push(elm.href);
        });
        urls.reverse();
        MP3Controller.play_playlist(urls);
    },
    play_playlist: function (url_list) {
        var current;
        var url = url_list.pop();
        MP3Controller.playlist = url_list;

        $$(".download-song").each(function (elm) {
            if (elm.href == url) {
                current = elm;
            }
        });

        if (!current) {
            return;
        }
        MP3Controller.play(current.up().down("a.play"), url, true);
    },
    play: function (button, url, from_playlist) {
        MP3Player.load(url);
        MP3Player.observe("COMPLETED", MP3Controller.complete);
        MP3Player.observe("PAUSED", MP3Controller.stop);
        if (MP3Controller.current) {
            MP3Controller.show_play(MP3Controller.current);
        }

        MP3Controller.current = button;
        MP3Controller.show_pause(MP3Controller.current);
    },
    stop: function (button) {
        MP3Player.stop();
        MP3Controller.show_play(MP3Controller.current);
        delete MP3Controller.current;
    },
    complete: function () {
        MP3Controller.stop();
        if (MP3Controller.playlist.length) {
            setTimeout(function () {
                MP3Controller.play_playlist(MP3Controller.playlist);
            }, 500);
        }
    },
    show_play: function (button) {
        button.down("img").src = "/static/images/play.gif";
    },
    show_pause: function (button) {
        button.down("img").src = "/static/images/stop.gif";
    }
};

var MP3Advanced = {
    song_length: 0,
    play: function (url) {
        if (url == MP3Advanced.url) {
            if (MP3Player.state == "playing") {
                MP3Player.pause();
            } else if (MP3Player.state == "paused" || MP3Player.state == "idle" || MP3Player.state == "completed") {
                MP3Player.play();
            }
        } else {
            MP3Advanced.url = url;

            MP3Player.load(url);
            MP3Player.observe("time", MP3Advanced.onProgress);
            MP3Player.observe("load", MP3Advanced.onLoadProgress);
            MP3Player.observe("playing", MP3Advanced.onPlay);
            MP3Player.observe("completed", MP3Player.stop);
            MP3Player.observe("idle", MP3Advanced.onStop);
            MP3Player.observe("paused", MP3Advanced.onStop);
            MP3Player.observe("volume", MP3Advanced.onVolume);
            MP3Advanced.song_length = 0;
            MP3Advanced.register_volume();
        }
    },
    register_volume: function () {
        var volume = $("volume");
        volume.observe("mouseover", function () {
            $("volume-hover").show();
            if (!MP3Advanced.slider) {
                var volume_slider = $("volume-slider");
                MP3Advanced.slider = new Control.Slider(volume_slider.down('.handle'), volume_slider, {
                    range: $R(0, 100),
                    sliderValue: 1,
                    axis: 'vertical',
                    onSlide: function (value) {
                        MP3Advanced.setVolume(100 - value);
                    },
                    onChange: function (value) {
                        MP3Advanced.setVolume(100 - value);
                    }
                });
            }
        });
        volume.observe("mouseout", function () {
            $("volume-hover").hide();
        });
    },
    onProgress: function (evt) {
        var progress = evt.memo;
        MP3Advanced.song_length = progress.duration;

        var position = progress.position,
            total    = progress.duration;

        var percent = 100 * position / total;

        // $("elapsed").update(Util.seconds_to_time(position));
        // $("remaining").update(Util.seconds_to_time(total - position));
        if (isNaN(percent)) {
            return;
        }
        $("progress").style.width = percent + "%";
    },
    onLoadProgress: function (evt) {
        var progress = evt.memo;
        var percent = progress.percentage;

        if (isNaN(percent)) {
            return;
        }
        $("loaded").style.width = percent + "%";
    },
    onPlay: function () {
        $("play").update(new Element("img", {'src': '/static/images/mp3pause.png'}));
    },
    onStop: function () {
        $("play").update(new Element("img", {'src': '/static/images/mp3play.png'}));
    },
    seek: function (e) {
        if (!MP3Player.state) {
            return;
        }
        var w = $("progress-cont").viewportOffset()[0];

        var offset = e.clientX - w;

        var elm_width = $("progress-cont").getWidth();
        var seconds = MP3Advanced.song_length * offset / elm_width - 1;
        MP3Player.seek(seconds);
    },
    setVolume: function (val) {
        MP3Player.volume(val);
    },
    onVolume: function (e) {
        // var volume = e.memo; // split-todo this is unused
        // $("volume").update(parseInt(volume.percentage, 10));
    }
};

var MP3Playlist = {
    options: {},
    play: function (evt) {
        if (evt) {
            Event.stop(evt);
        }


        if (!MP3Player.state || MP3Player.state == "idle" || MP3Player.state == "completed") {
            MP3Playlist.load($$("#playlist .song")[0].href);
        } else {
            MP3Advanced.play(MP3Advanced.url);
        }
    },
    load: function (url) {
        MP3Playlist.clear_playing(MP3Advanced.url);
        MP3Player.observe("completed", MP3Playlist.next);
        MP3Player.observe("id3", MP3Playlist.id3);
        MP3Player.observe("load_complete", MP3Playlist.load_complete);

        MP3Advanced.play(url);
        MP3Playlist.show_playing(url);
    },
    get_song_elm: function (url) {
        var selected;
        $$(".song").each(function (elm) {
            if (elm.href == url) {
                selected = elm;
            }
        });
        return selected;
    },
    next: function () {
        var old = MP3Playlist.get_song_elm(MP3Advanced.url);
        var current_row = old.up("tr").next("tr");

        if (!current_row) {
            MP3Playlist.clear_playing(MP3Advanced.url);
            return;
        }

        var new_song = current_row.down(".song");
        setTimeout(function () {
            MP3Playlist.load(new_song.href);
        }, 1000);

    },
    load_complete: function () {
        if (MP3Player.meta) {
            MP3Playlist.id3({'memo': MP3Player.meta});
        }
    },
    show_playing: function (url) {
        var selected = MP3Playlist.get_song_elm(url);

        if (!selected) {
            return;
        }
        var tr = selected.up("tr");
        var td = tr.down("td");
        td.update(Sprite.make("play", {}));
        tr.addClassName("selected");

        if (MP3Playlist.options && MP3Playlist.options.on_song_change) {
            MP3Playlist.options.on_song_change(tr);
        }
    },
    id3: function (evt) {
        var id3 = evt.memo;
        var link = MP3Playlist.get_song_elm(MP3Advanced.url);
        if (id3.artist && id3.name && MP3Advanced.song_length && !link.hasClassName("has_id3")) {
            var new_name = id3.name;
            link.down(".song-name").update(new_name.escapeHTML());
            link.up("tr").down(".song-artist").update(id3.artist.escapeHTML());
            link.up("tr").down(".right-column").update(Util.seconds_to_time(MP3Advanced.song_length));
            link.addClassName("has_id3");
            MP3Playlist.report_id3(id3);
        }
    },
    report_id3: function (id3) {
        var url = MP3Advanced.url;
        var duration = MP3Advanced.song_length;
        if (!duration || duration <= 0) {
            return;
        }
        var params = {
            'album': id3.album,
            'artist': id3.artist,
            'duration': duration,
            'name': id3.name,
            'track': id3.track,
            'year': id3.year
        };

        var token_url = url.split('/').slice(4).join("/");

        new Ajax.Request('/add_id3/' + token_url, {
            parameters: params
        });
    },
    clear_playing: function (url) {
        if (!url) {
            return;
        }

        var selected = MP3Playlist.get_song_elm(url);

        if (!selected) {
            return;
        }

        var tr = selected.up("tr");
        var td = tr.down("td");

        tr.removeClassName("selected");
        td.update();
    }
};

Event.observe(window, "load", function () {
    var footer = $("footer");
    if (footer) {
        var working = footer.getStyle("display") == "none" || footer.getWidth() < 900;
        if (!working) {
            assert(false, "HTML Broken on " + window.location.pathname);
        }
    }
});
window.LoadedJsSuccessfully = true;

