/*
 * Gloabl javascript file which will be included on
 * all pages across the entire site. It should include all site-wide
 * required javascript libraries (prototype.js for example) and any
 * other javascript code which is required on all pages
 *
 * Custom javascript code will be at the top of this file. Javascript
 * libraries will be at the bottom of the file.
 */

function cookiesEnabled() {
  document.cookie = "chkcookie=foo;"
  return document.cookie.indexOf("chkcookie",0) != -1;
}


var FlashInterface = {
  observings: {},

  observe: function(event,fn) {
    if( !this.observings[event] )
      this.observings[event] = []
    this.observings[event].push(fn);
  },

  unobserve: function(event,fn) {
    if( !this.observings[event] ) return;
    this.observings[event] = this.observings[event].without(fn)
  },

  fire: function(event,data) {
    if( !this.observings[event] ) return;
    this.observings[event].each(function(fn) {
      try {
        fn(data);
      } catch(e) {
        FlashInterface.trace(e);
      }
    })
  },

  DEBUG: false,
  trace: function(o) {
    if( typeof(console) != "undefined" && this.DEBUG )
      console.info(o);
  }
};

FlashInterface.Audio = {
  PLAY: "audio:play",   // => {id,position (milliseconds)}
  PAUSE: "audio:pause", // => {id,position (milliseconds)}
  SEEK: "audio:seek",   // => {id,position (milliseconds)}

  play: function(id,to) {
    if( this.embedObj(id) )
      this.embedObj(id).playSong(to);
  },

  pause: function(id,to) {
    if( this.embedObj(id) )
      this.embedObj(id).pause(to);
  },

  seek: function(id,to) {
    if( this.embedObj(id) )
      this.embedObj(id).seek(to);
  },

  embedObj: function(id) {
    return $("audioPlayer_"+id);
  }
};

FlashInterface.Video = {
  PLAY: "video:play",   // => {id}
  PAUSE: "video:pause", // => {id}
  SEEK: "video:seek",   // => {id,position}

  play: function(id,to) {
    if( this.embedObj(id) )
      this.embedObj(id).playVideo(to);
  },

  pause: function(id,to) {
    if( this.embedObj(id) )
      this.embedObj(id).pause(to);
  },

  seek: function(id,to) {
    if( this.embedObj(id) )
      this.embedObj(id).seek(to);
  },

  embedObj: function(id) {
    return $("videoPlayer_"+id);
  }
}

function inHex(x) {
  var digits = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"];
  return digits[parseInt(x/16)] + digits[(x-16*parseInt(x/16))];
}

function getCssClass(className) {

	for (var s = document.styleSheets.length-1; s >= 0; s--)
	{
		if (document.styleSheets[s].href == null || !(document.styleSheets[s].href.split("://")[1] != undefined && document.styleSheets[s].href.split("/")[2] != window.location.href.split("/")[2])) {
			if(document.styleSheets[s].rules) {
				for (var r = 0; r < document.styleSheets[s].rules.length; r++) {
					if (document.styleSheets[s].rules[r].selectorText == className) {
						return document.styleSheets[s].rules[r];
					}
				}
			}

			else if(document.styleSheets[s].cssRules) {
				for (var r = 0; r < document.styleSheets[s].cssRules.length; r++) {
					if (document.styleSheets[s].cssRules[r].selectorText == className) {
						return document.styleSheets[s].cssRules[r];
					}
				}
			}

		}
	}

}

function watch_limit(ta,limit) {
  if(ta.value.length + 1 > limit)
    ta.value = ta.value.substring(0,limit-1);
}

function rand (n) {
    return (Math.floor(Math.random()*n+1));
}

function randomString(string_length) {
    var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
    var randomstring = '';
    for (var i=0; i<string_length; i++) {
	var rnum = Math.floor(Math.random() * chars.length);
	randomstring += chars.substring(rnum,rnum+1);
    }
    return randomstring;
}

function watchHTMLUpload(form_id,callback) {
  if( $$("#"+form_id+" input.referer").first() )
    $$("#"+form_id+" input.referer").first().value = top.location

  if( $("upload_div") == null ) {
    var d = new Element("div",{id:"upload_div"});
    d.innerHTML = '<iframe id="upload_frame" name="upload_frame" style="width:0px;height:0px;border:0px"></iframe>';
    $(form_id).insert(d);
    $(form_id).target = "upload_frame";
  }
  HTMLUploadResponse.callback = callback;
  return true;
}

var HTMLUploadResponse = {
  callback: null,

  HTMLUploadCallback: function(res) {
    try {
        res = res.replace("#","");
        HTMLUploadResponse.callback(eval("("+res+")"));
    } catch(e) {
        HTMLUploadResponse.callback({result:false,message:"An unknown error occurred"})
    }
  }
};

function onGetBuildInfo(version) {
  var v = new Element("p",{id:"FLASH_UPLOADER_VERSION",style:"display:none"}).update(version);;
  $("fileUploader").insert({after:v})
}


/*  Prototype JavaScript framework, version 1.6.0.2
 *  (c) 2005-2008 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.0.2',

  Browser: {
    IE:     !!(window.attachEvent && !window.opera),
    Opera:  !!window.opera,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  },

  BrowserFeatures: {
    XPath: !!document.evaluate,
    ElementExtensions: !!window.HTMLElement,
    SpecificElementExtensions:
      document.createElement('div').__proto__ &&
      document.createElement('div').__proto__ !==
        document.createElement('form').__proto__
  },

  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;


/* Based on Alex Arnell's inheritance implementation. */
var Class = {
  create: function() {
    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) {
      var subclass = function() { };
      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;
  }
};

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

    if (!Object.keys({ toString: true }).length)
      properties.push("toString", "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 = Object.extend((function(m) {
          return function() { return ancestor[m].apply(this, arguments) };
        })(property).wrap(method), {
          valueOf:  function() { return method },
          toString: function() { return method.toString() }
        });
      }
      this.prototype[property] = value;
    }

    return this;
  }
};

var Abstract = { };

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

Object.extend(Object, {
  inspect: function(object) {
    try {
      if (Object.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;
    }
  },

  toJSON: function(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 (Object.isElement(object)) return;

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

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

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

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

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

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

  clone: function(object) {
    return Object.extend({ }, object);
  },

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

  isArray: function(object) {
    return object != null && typeof object == "object" &&
      'splice' in object && 'join' in object;
  },

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

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

  isString: function(object) {
    return typeof object == "string";
  },

  isNumber: function(object) {
    return typeof object == "number";
  },

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

Object.extend(Function.prototype, {
  argumentNames: function() {
    var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");
    return names.length == 1 && !names[0] ? [] : names;
  },

  bind: function() {
    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
    var __method = this, args = $A(arguments), object = args.shift();
    return function() {
      return __method.apply(object, args.concat($A(arguments)));
    }
  },

  bindAsEventListener: function() {
    var __method = this, args = $A(arguments), object = args.shift();
    return function(event) {
      return __method.apply(object, [event || window.event].concat(args));
    }
  },

  curry: function() {
    if (!arguments.length) return this;
    var __method = this, args = $A(arguments);
    return function() {
      return __method.apply(this, args.concat($A(arguments)));
    }
  },

  delay: function() {
    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  },

  wrap: function(wrapper) {
    var __method = this;
    return function() {
      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
    }
  },

  methodize: function() {
    if (this._methodized) return this._methodized;
    var __method = this;
    return this._methodized = function() {
      return __method.apply(null, [this].concat($A(arguments)));
    };
  }
});

Function.prototype.defer = Function.prototype.delay.curry(0.01);

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"';
};

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

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();
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
});
Object.extend(String, {
  interpret: function(value) {
    return value == null ? '' : String(value);
  },
  specialChar: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '\\': '\\\\'
  }
});

Object.extend(String.prototype, {
  gsub: function(pattern, replacement) {
    var result = '', source = this, match;
    replacement = arguments.callee.prepareReplacement(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;
  },

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

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

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

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

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

  stripTags: function() {
    return this.replace(/<\/?[^>]+>/gi, '');
  },

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

  extractScripts: function() {
    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];
    });
  },

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

  escapeHTML: function() {
    var self = arguments.callee;
    self.text.data = this;
    return self.div.innerHTML;
  },

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

  toQueryParams: function(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;
    });
  },

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

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

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

  camelize: function() {
    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;
  },

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

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

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

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

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

  unfilterJSON: function(filter) {
    return this.sub(filter || Prototype.JSONFilter, '#{1}');
  },

  isJSON: function() {
    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);
  },

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

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

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

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

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

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

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

if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
  escapeHTML: function() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  },
  unescapeHTML: function() {
    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
  }
});

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

String.prototype.parseQuery = String.prototype.toQueryParams;

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

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

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

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

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

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

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

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

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

var $break = { };

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

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

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

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

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

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

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

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

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

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

  include: function(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;
  },

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

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

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

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

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

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

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

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

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

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

  zip: function() {
    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));
    });
  },

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

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

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  filter:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray,
  every:   Enumerable.all,
  some:    Enumerable.any
});
function $A(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

if (Prototype.Browser.WebKit) {
  $A = function(iterable) {
    if (!iterable) return [];
    if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
        iterable.toArray) return iterable.toArray();
    var length = iterable.length || 0, results = new Array(length);
    while (length--) results[length] = iterable[length];
    return results;
  };
}

Array.from = $A;

Object.extend(Array.prototype, Enumerable);

if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  },

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

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

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

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

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

  without: function() {
    var values = $A(arguments);
    return this.select(function(value) {
      return !values.include(value);
    });
  },

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

  reduce: function() {
    return this.length > 1 ? this : this[0];
  },

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

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

  clone: function() {
    return [].concat(this);
  },

  size: function() {
    return this.length;
  },

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

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

if (Object.isFunction(Array.prototype.forEach))
  Array.prototype._each = Array.prototype.forEach;

if (!Array.prototype.indexOf) Array.prototype.indexOf = function(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;
};

if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(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;
};

Array.prototype.toArray = Array.prototype.clone;

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

if (Prototype.Browser.Opera){
  Array.prototype.concat = function() {
    var array = [];
    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
    for (var i = 0, length = arguments.length; i < length; i++) {
      if (Object.isArray(arguments[i])) {
        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
          array.push(arguments[i][j]);
      } else {
        array.push(arguments[i]);
      }
    }
    return array;
  };
}
Object.extend(Number.prototype, {
  toColorPart: function() {
    return this.toPaddedString(2, 16);
  },

  succ: function() {
    return this + 1;
  },

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

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

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

$w('abs round ceil floor').each(function(method){
  Number.prototype[method] = Math[method].methodize();
});
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {

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

  return {
    initialize: function(object) {
      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
    },

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

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

    get: function(key) {
      return this._object[key];
    },

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

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

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

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

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

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

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

    toQueryString: function() {
      return this.map(function(pair) {
        var key = encodeURIComponent(pair.key), values = pair.value;

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

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

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

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

Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
Hash.from = $H;
var ObjectRange = Class.create(Enumerable, {
  initialize: function(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  },

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

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

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

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() {
  var element = this.Element;
  this.Element = function(tagName, attributes) {
    attributes = attributes || { };
    tagName = tagName.toLowerCase();
    var cache = Element.cache;
    if (Prototype.Browser.IE && 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(this.Element, element || { });
}).call(window);

Element.cache = { };

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).style.display = 'none';
    return element;
  },

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

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

  update: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);
    content = Object.toHTML(content);
    element.innerHTML = content.stripScripts();
    content.evalScripts.bind(content).defer();
    return element;
  },

  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('parentNode');
  },

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

  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('previousSibling');
  },

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

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

  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();
    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();
    return Object.isNumber(expression) ? element.descendants()[expression] :
      element.select(expression)[index || 0];
  },

  previous: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
    var previousSiblings = element.previousSiblings();
    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();
    return Object.isNumber(expression) ? nextSiblings[expression] :
      Selector.findElement(nextSiblings, expression, index);
  },

  select: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element, args);
  },

  adjacent: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element.parentNode, args).without(element);
  },

  identify: function(element) {
    element = $(element);
    var id = element.readAttribute('id'), self = arguments.callee;
    if (id) return id;
    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
    element.writeAttribute('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().height;
  },

  getWidth: function(element) {
    return $(element).getDimensions().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(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(className) ?
      'removeClassName' : 'addClassName'](className);
  },

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

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

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

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

    if (element.sourceIndex && !Prototype.Browser.Opera) {
      var e = element.sourceIndex, a = ancestor.sourceIndex,
       nextAncestor = ancestor.nextSibling;
      if (!nextAncestor) {
        do { ancestor = ancestor.parentNode; }
        while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
      }
      if (nextAncestor && nextAncestor.sourceIndex)
       return (e > a && e < nextAncestor.sourceIndex);
    }

    while (element = element.parentNode)
      if (element == originalAncestor) return true;
    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = element.cumulativeOffset();
    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) {
      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('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';
    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 (window.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 == '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('position') == 'absolute') return;

    var offsets = element.positionedOffset();
    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('position') == 'relative') return;

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

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

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

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

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

    return $(document.body);
  },

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

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

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

    } while (element = element.offsetParent);

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

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

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

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

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

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

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

Element.Methods.identify.counter = 1;

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);
      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);
        var position = element.getStyle('position');
        if (position !== 'static') return proceed(element);
        var offsetParent = element.getOffsetParent();
        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
          offsetParent.setStyle({ zoom: 1 });
        element.setStyle({ position: 'relative' });
        var value = proceed(element);
        element.setStyle({ position: position });
        return value;
      }
    );
  });

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

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

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

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

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

  Element._attributeTranslations = {
    read: {
      names: {
        'class': 'className',
        'for':   'htmlFor'
      },
      values: {
        _getAttr: function(element, attribute) {
          return element.getAttribute(attribute, 2);
        },
        _getAttrNode: function(element, attribute) {
          var node = element.getAttributeNode(attribute);
          return node ? node.value : "";
        },
        _getEv: function(element, attribute) {
          attribute = element.getAttribute(attribute);
          return attribute ? attribute.toString().slice(23, -2) : null;
        },
        _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').each(function(attr) {
    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
  });

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

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 == '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 (Prototype.Browser.IE || Prototype.Browser.Opera) {
  Element.Methods.update = function(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 in Element._insertionTranslations.tags) {
      $A(element.childNodes).each(function(node) { element.removeChild(node) });
      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
        .each(function(node) { element.appendChild(node) });
    }
    else element.innerHTML = content.stripScripts();

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

if ('outerHTML' in document.createElement('div')) {
  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() {
  Object.extend(this.tags, {
    THEAD: this.tags.TBODY,
    TFOOT: this.tags.TBODY,
    TH:    this.tags.TD
  });
}).call(Element._insertionTranslations);

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

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

Element.extend = (function() {
  if (Prototype.BrowserFeatures.SpecificElementExtensions)
    return Prototype.K;

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

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

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

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

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

    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];

    window[klass] = { };
    window[klass].prototype = document.createElement(tagName).__proto__;
    return window[klass];
  }

  if (F.ElementExtensions) {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Element.Methods.Simulated, HTMLElement.prototype, 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() {
    var dimensions = { };
    var B = Prototype.Browser;
    $w('width height').each(function(d) {
      var D = d.capitalize();
      dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] :
        (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D];
    });
    return dimensions;
  },

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

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

  getScrollOffsets: function() {
    return Element._returnOffset(
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
  }
};
/* 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();
    this.compileMatcher();
  },

  shouldUseXPath: 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(this.expression))
      return false;

    return true;
  },

  compileMatcher: function() {
    if (this.shouldUseXPath())
      return this.compileXPathMatcher();

    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
        c = Selector.criteria, le, p, m;

    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 in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
    	      new Template(c[i]).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;

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

    this.matcher = ['.//*'];
    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        if (m = e.match(ps[i])) {
          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
            new Template(x[i]).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;
    if (this.xpath) return document._getElementsByXPath(this.xpath, root);
    return this.matcher(root);
  },

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

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

    while (e && le !== e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          if (as[i]) {
            this.tokens.push([i, 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() + ">";
  }
});

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 or translate(text(), ' \t\r\n', '') = '')]",
      'checked':     "[@checked]",
      'disabled':    "[@disabled]",
      'enabled':     "[not(@disabled)]",
      'not': function(m) {
        var e = m[6], p = Selector.patterns,
            x = Selector.xpath, le, v;

        var exclusion = [];
        while (e && le != e && (/\S/).test(e)) {
          le = e;
          for (var i in p) {
            if (m = e.match(p[i])) {
              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).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: {
    laterSibling: /^\s*~\s*/,
    child:        /^\s*>\s*/,
    adjacent:     /^\s*\+\s*/,
    descendant:   /^\s/,

    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
    id:           /^#([\w\-\*]+)(\b|$)/,
    className:    /^\.([\w\-\*]+)(\b|$)/,
    pseudo:
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
    attrPresence: /^\[([\w]+)\]/,
    attr:         /\[((?:[\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(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = undefined;
      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 (!(n = nodes[i])._countedByPrototype) {
          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 (!targetNode) return [];
      if (!nodes && root == document) return [targetNode];
      if (nodes) {
        if (combinator) {
          if (combinator == 'child') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (targetNode.parentNode == node) return [targetNode];
          } else if (combinator == 'descendant') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Element.descendantOf(targetNode, node)) return [targetNode];
          } else if (combinator == 'adjacent') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Selector.handlers.previousElementSibling(targetNode) == node)
                return [targetNode];
          } else nodes = h[combinator](nodes);
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node == targetNode) return [targetNode];
        return [];
      }
      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
    },

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

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

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

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

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

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

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

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

    'empty': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) 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) 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.startsWith(v); },
    '$=': function(nv, v) { return nv.endsWith(v); },
    '*=': function(nv, v) { return 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;
    },

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

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}
var Form = {
  reset: function(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 != '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) {
    return $A($(form).getElementsByTagName('*')).inject([],
      function(elements, child) {
        if (Form.Element.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'].include(element.tagName.toLowerCase());
    });
  },

  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'].include(element.type)))
        element.select();
    } catch (e) { }
    return element;
  },

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

  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);
  }
});
if (!window.Event) var Event = { };

Object.extend(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: { },

  relatedTarget: function(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);
  }
});

Event.Methods = (function() {
  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);
    };
  }

  return {
    isLeftClick:   function(event) { return isButton(event, 0) },
    isMiddleClick: function(event) { return isButton(event, 1) },
    isRightClick:  function(event) { return isButton(event, 2) },

    element: function(event) {
      var node = Event.extend(event).target;
      return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);
    },

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

    pointer: function(event) {
      return {
        x: event.pageX || (event.clientX +
          (document.documentElement.scrollLeft || document.body.scrollLeft)),
        y: event.pageY || (event.clientY +
          (document.documentElement.scrollTop || document.body.scrollTop))
      };
    },

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

    stop: function(event) {
      Event.extend(event);
      event.preventDefault();
      event.stopPropagation();
      event.stopped = true;
    }
  };
})();

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

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

    return function(event) {
      if (!event) return false;
      if (event._extendedByPrototype) return event;

      event._extendedByPrototype = Prototype.emptyFunction;
      var pointer = Event.pointer(event);
      Object.extend(event, {
        target: event.srcElement,
        relatedTarget: Event.relatedTarget(event),
        pageX:  pointer.x,
        pageY:  pointer.y
      });
      return Object.extend(event, methods);
    };

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

Object.extend(Event, (function() {
  var cache = Event.cache;

  function getEventID(element) {
    if (element._prototypeEventID) return element._prototypeEventID[0];
    arguments.callee.id = arguments.callee.id || 1;
    return element._prototypeEventID = [++arguments.callee.id];
  }

  function getDOMEventName(eventName) {
    if (eventName && eventName.include(':')) return "dataavailable";
    return eventName;
  }

  function getCacheForID(id) {
    return cache[id] = cache[id] || { };
  }

  function getWrappersForEventName(id, eventName) {
    var c = getCacheForID(id);
    return c[eventName] = c[eventName] || [];
  }

  function createWrapper(element, eventName, handler) {
    var id = getEventID(element);
    var c = getWrappersForEventName(id, eventName);
    if (c.pluck("handler").include(handler)) return false;

    var wrapper = function(event) {
      if (!Event || !Event.extend ||
        (event.eventName && event.eventName != eventName))
          return false;

      Event.extend(event);
      handler.call(element, event);
    };

    wrapper.handler = handler;
    c.push(wrapper);
    return wrapper;
  }

  function findWrapper(id, eventName, handler) {
    var c = getWrappersForEventName(id, eventName);
    return c.find(function(wrapper) { return wrapper.handler == handler });
  }

  function destroyWrapper(id, eventName, handler) {
    var c = getCacheForID(id);
    if (!c[eventName]) return false;
    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
  }

  function destroyCache() {
    for (var id in cache)
      for (var eventName in cache[id])
        cache[id][eventName] = null;
  }

  if (window.attachEvent) {
    window.attachEvent("onunload", destroyCache);
  }

  return {
    observe: function(element, eventName, handler) {
      element = $(element);
      var name = getDOMEventName(eventName);

      var wrapper = createWrapper(element, eventName, handler);
      if (!wrapper) return element;

      if (element.addEventListener) {
        element.addEventListener(name, wrapper, false);
      } else {
        element.attachEvent("on" + name, wrapper);
      }

      return element;
    },

    stopObserving: function(element, eventName, handler) {
      element = $(element);
      var id = getEventID(element), name = getDOMEventName(eventName);

      if (!handler && eventName) {
        getWrappersForEventName(id, eventName).each(function(wrapper) {
          element.stopObserving(eventName, wrapper.handler);
        });
        return element;

      } else if (!eventName) {
        Object.keys(getCacheForID(id)).each(function(eventName) {
          element.stopObserving(eventName);
        });
        return element;
      }

      var wrapper = findWrapper(id, eventName, handler);
      if (!wrapper) return element;

      if (element.removeEventListener) {
        element.removeEventListener(name, wrapper, false);
      } else {
        element.detachEvent("on" + name, wrapper);
      }

      destroyWrapper(id, eventName, handler);

      return element;
    },

    fire: function(element, eventName, memo) {
      element = $(element);
      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 = "ondataavailable";
      }

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

Element.addMethods({
  fire:          Event.fire,
  observe:       Event.observe,
  stopObserving: Event.stopObserving
});

Object.extend(document, {
  fire:          Element.Methods.fire.methodize(),
  observe:       Element.Methods.observe.methodize(),
  stopObserving: Element.Methods.stopObserving.methodize(),
  loaded:        false
});

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

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

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

Element.addMethods();





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) + 0.5;
    },
    reverse: function(pos) {
      return 1-pos;
    },
    flicker: function(pos) {
      var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
      return pos > 1 ? 1 : pos;
    },
    wobble: function(pos) {
      return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
    },
    pulse: function(pos, pulses) {
      pulses = pulses || 5;
      return (
        ((pos % (1/pulses)) * pulses).round() == 0 ?
              ((pos * pulses * 2) - (pos * pulses * 2).floor()) :
          1 - ((pos * pulses * 2) - (pos * pulses * 2).floor())
        );
    },
    spring: function(pos) {
      return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
    },
    none: function(pos) {
      return 0;
    },
    full: function(pos) {
      return 1;
    }
  },
  DefaultOptions: {
    duration:   1.0,   // seconds
    fps:        100,   // 100= assume 66fps max.
    sync:       false, // true for combining
    from:       0.0,
    to:         1.0,
    delay:      0.0,
    queue:      'parallel'
  },
  tagifyText: function(element) {
    var tagifyStyle = 'position:relative';
    if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';

    element = $(element);
    $A(element.childNodes).each( function(child) {
      if (child.nodeType==3) {
        child.nodeValue.toArray().each( function(character) {
          element.insertBefore(
            new Element('span', {style: tagifyStyle}).update(
              character == ' ' ? String.fromCharCode(160) : character),
              child);
        });
        Element.remove(child);
      }
    });
  },
  multiple: function(element, effect) {
    var elements;
    if (((typeof element == 'object') ||
        Object.isFunction(element)) &&
       (element.length))
      elements = element;
    else
      elements = $(element).childNodes;

    var options = Object.extend({
      speed: 0.1,
      delay: 0.0
    }, arguments[2] || { });
    var masterDelay = options.delay;

    $A(elements).each( function(element, index) {
      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
    });
  },
  PAIRS: {
    'slide':  ['SlideDown','SlideUp'],
    'blind':  ['BlindDown','BlindUp'],
    'appear': ['Appear','Fade']
  },
  toggle: function(element, effect) {
    element = $(element);
    effect = (effect || 'appear').toLowerCase();
    var options = Object.extend({
      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
    }, arguments[2] || { });
    Effect[element.visible() ?
      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
  }
};

Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;

/* ------------- core effects ------------- */

Effect.ScopedQueue = Class.create(Enumerable, {
  initialize: function() {
    this.effects  = [];
    this.interval = null;
  },
  _each: function(iterator) {
    this.effects._each(iterator);
  },
  add: function(effect) {
    var timestamp = new Date().getTime();

    var position = Object.isString(effect.options.queue) ?
      effect.options.queue : effect.options.queue.position;

    switch(position) {
      case 'front':
        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':
        timestamp = this.effects.pluck('finishOn').max() || timestamp;
        break;
    }

    effect.startOn  += timestamp;
    effect.finishOn += timestamp;

    if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
      this.effects.push(effect);

    if (!this.interval)
      this.interval = setInterval(this.loop.bind(this), 15);
  },
  remove: function(effect) {
    this.effects = this.effects.reject(function(e) { return e==effect });
    if (this.effects.length == 0) {
      clearInterval(this.interval);
      this.interval = null;
    }
  },
  loop: function() {
    var timePos = new Date().getTime();
    for(var i=0, len=this.effects.length;i<len;i++)
      this.effects[i] && this.effects[i].loop(timePos);
  }
});

Effect.Queues = {
  instances: $H(),
  get: function(queueName) {
    if (!Object.isString(queueName)) return queueName;

    return this.instances.get(queueName) ||
      this.instances.set(queueName, new Effect.ScopedQueue());
  }
};
Effect.Queue = Effect.Queues.get('global');

Effect.Base = Class.create({
  position: null,
  start: function(options) {
    function codeForEvent(options,eventName){
      return (
        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
      );
    }
    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
    this.currentFrame = 0;
    this.state        = 'idle';
    this.startOn      = this.options.delay*1000;
    this.finishOn     = this.startOn+(this.options.duration*1000);
    this.fromToDelta  = this.options.to-this.options.from;
    this.totalTime    = this.finishOn-this.startOn;
    this.totalFrames  = this.options.fps*this.options.duration;

    eval('this.render = function(pos){ '+
      'if (this.state=="idle"){this.state="running";'+
      codeForEvent(this.options,'beforeSetup')+
      (this.setup ? 'this.setup();':'')+
      codeForEvent(this.options,'afterSetup')+
      '};if (this.state=="running"){'+
      'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+
      'this.position=pos;'+
      codeForEvent(this.options,'beforeUpdate')+
      (this.update ? 'this.update(pos);':'')+
      codeForEvent(this.options,'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);
    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'
    });
  }
});

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() {
    if (this.element.getStyle('display')=='none') { this.cancel(); return; }
    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');
    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(),
    max = (window.height || document.body.scrollHeight) - document.viewport.getHeight();

  if (options.offset) elementOffsets[1] += options.offset;

  return new Effect.Tween(null,
    scrollOffsets.top,
    elementOffsets[1] > max ? max : 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,
  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();
  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] || { })
  );
};

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] || { };
  var oldOpacity = element.getInlineOpacity();
  var transition = options.transition || Effect.Transitions.sinoidal;
  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
  reverser.bind(transition);
  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);

/**
 * WYSIHAT sanitize method
 */
 Object.extend(String.prototype, {
   sanitize: function(options) {
     return Element("div").update(this).sanitize(options).innerHTML;
   }
 });

 Element.addMethods({
   sanitize: function(element, options) {
     element = $(element);
     options = $H(options);
     var allowed_tags = $A(options.get('tags') || []);
     var allowed_attributes = $A(options.get('attributes') || []);
     var sanitized = Element(element.nodeName);

     $A(element.childNodes).each(function(child) {
       if (child.nodeType == 1) {
         var children = $(child).sanitize(options).childNodes;

         if (allowed_tags.include(child.nodeName.toLowerCase())) {
           var new_child = Element(child.nodeName);
           allowed_attributes.each(function(attribute) {
             if ((value = child.readAttribute(attribute)))
               new_child.writeAttribute(attribute, value);
           });
           sanitized.appendChild(new_child);

           $A(children).each(function(grandchild) { new_child.appendChild(grandchild); });
         } else {
           $A(children).each(function(grandchild) { sanitized.appendChild(grandchild); });
         }
       } else if (child.nodeType == 3) {
         sanitized.appendChild(child);
       }
     });
     return sanitized;
   }
 });


function displayNotifications() {
  var queue_options = {position:'end', scope:'notifications'};
  if (!$('systemNotification').empty()) {
	$('systemNotification').appear({queue:queue_options}).fade({queue:queue_options, delay:3});
  }
  $('systemStickyNotification').appear({queue:queue_options});
}

/**
 * Helptip helper functions
 */
var Helptip = Class.create({
  initialize: function(tip, targetId) {
    this.tipElement = tip;
    this.targetElementId = targetId;

    Event.observe($(this.targetElementId), 'mouseover', function(e) {
  		verticalOffset = Position.cumulativeOffset($(this.targetElementId))[1] - this.tipElement.getHeight() - 5;
  		leftOffset = (Position.cumulativeOffset($(this.targetElementId))[0] + $(this.targetElementId).getWidth() / 2) - (this.tipElement.getWidth() / 2);
      this.tipElement.style.top = verticalOffset + "px";
      this.tipElement.style.left = leftOffset + "px";

      Effect.Appear(this.tipElement, {duration:0.3, to:0.87});
    }.bind(this));

    Event.observe($(this.targetElementId), 'mouseout', function(e) {
      Effect.Fade(this.tipElement, {duration:0.3});
    }.bind(this));
  }
})

function generateHelptips() {
  $$('.helptip').each(function(element) {
    tipTargetId = element.readAttribute("helptipTarget");
    new Helptip(element, tipTargetId);

  });
}

/**
* Form related JS that occurs in multiple controllers
*/
function prefillPasswordField(passwordFieldId, passwordFormId) {
	passwordField = $(passwordFieldId);
	if (passwordField && passwordField.readAttribute("pre_filled")) {
		passwordField.writeAttribute({value:"â€¢â€¢â€¢â€¢â€¢â€¢â€¢â€¢â€¢"});
		passwordField.writeAttribute({valueChanged:false});

		Event.observe(passwordFieldId,"focus", function(){
			$(passwordFieldId).writeAttribute({value:""});
			$(passwordFieldId).writeAttribute({valueChanged:true});
		});

		Event.observe(passwordFormId,"submit",function(){
			if (!$(passwordFieldId).readAttribute("valueChanged")) {
				$(passwordFieldId).writeAttribute({name:""});
			}
		});
	}
}

(function(){
  var methods = {
    defaultValueActsAsHint: function(element){
      element = $(element);
      element._default = element.value;
      element._is_password = (element.readAttribute('type') == 'password')

      return element.observe('focus', function(){

        if(element._default != element.value) return;
        if(element._is_password) element.writeAttribute('type','password');
        element.removeClassName('hint').value = '';

      }).observe('blur', function(){

        if(element.value.strip() != '') return;
        element.addClassName('hint').writeAttribute('type',null).value = element._default;

      }).addClassName('hint').writeAttribute('type',null);
    }
  };

  $w('input textarea').each(function(tag){ Element.addMethods(tag, methods) });
})();


/*
Class: LazyLoad

LazyLoad makes it easy and painless to lazily load one or more JavaScript
files on demand after a web page has been rendered.

Supported browsers include Firefox 2.x, Firefox 3.x, Internet Explorer 6.x,
Internet Explorer 7.x, Safari 3.x (including iPhone), and Opera 9.x. Other
browsers may or may not work and are not officially supported.

Author:
  Ryan Grove (ryan@wonko.com)

Copyright:
  Copyright (c) 2008 Ryan Grove (ryan@wonko.com). All rights reserved.

License:
  BSD License (http://www.opensource.org/licenses/bsd-license.html)

URL:
  http://wonko.com/post/painless_javascript_lazy_loading_with_lazyload

Version:
  1.0.4 (2008-07-24)
*/
var LazyLoad = function () {


  /*
  Object: d
  Shorthand reference to the browser's *document* object.
  */
  var d = document,

  /*
  Object: pending
  Pending request object, or null if no request is in progress.
  */
  pending = null,

  /*
  Array: queue
  Array of queued load requests.
  */
  queue = [],

  /*
  Object: ua
  User agent information.
  */
  ua;


  /*
  Method: getUserAgent
  Populates the *ua* variable with user agent information. Uses a paraphrased
  version of the YUI user agent detection code.
  */
  function getUserAgent() {
    if (ua) {
      return;
    }

    var nua = navigator.userAgent, m;

    ua = {
      gecko : 0,
      ie    : 0,
      webkit: 0
    };

    m = nua.match(/AppleWebKit\/(\S*)/);

    if (m && m[1]) {
      ua.webkit = parseFloat(m[1]);
    } else {
      m = nua.match(/MSIE\s([^;]*)/);

      if (m && m[1]) {
        ua.ie = parseFloat(m[1]);
      } else if ((/Gecko\/(\S*)/).test(nua)) {
        ua.gecko = 1;

        m = nua.match(/rv:([^\s\)]*)/);

        if (m && m[1]) {
          ua.gecko = parseFloat(m[1]);
        }
      }
    }
  }

  return {

    /*
    Method: load
    Loads the specified script(s) and runs the specified callback function
    when all scripts have been completely loaded.

    Parameters:
      urls     - URL or array of URLs of scripts to load
      callback - function to call when loading is complete
      obj      - (optional) object to pass to the callback function
      scope    - (optional) if true, *callback* will be executed in the scope
                 of *obj* instead of receiving *obj* as an argument.
    */
    load: function (urls, callback, obj, scope) {
      var head = d.getElementsByTagName('head')[0],
          i, script;

      if (urls) {
        urls = urls.constructor === Array ? urls : [urls];

        for (i = 0; i < urls.length; ++i) {
          queue.push({
            'url'     : urls[i],
            'callback': i === urls.length - 1 ? callback : null,
            'obj'     : obj,
            'scope'   : scope
          });
        }
      }

      if (pending || !(pending = queue.shift())) {
        return;
      }

      getUserAgent();

      script = d.createElement('script');
      script.src = pending.url;

      if (ua.ie) {
        script.onreadystatechange = function () {
          if (this.readyState === 'loaded' ||
              this.readyState === 'complete') {
            LazyLoad.requestComplete();
          }
        };
      } else if (ua.gecko || ua.webkit >= 420) {
        script.onload  = LazyLoad.requestComplete;
        script.onerror = LazyLoad.requestComplete;
      }

      head.appendChild(script);

      if (!ua.ie && !ua.gecko && !(ua.webkit >= 420)) {
        script = d.createElement('script');
        script.appendChild(d.createTextNode('LazyLoad.requestComplete();'));
        head.appendChild(script);
      }
    },

    /*
    Method: loadOnce
    Loads the specified script(s) only if they haven't already been loaded
    and runs the specified callback function when loading is complete. If all
    of the specified scripts have already been loaded, the callback function
    will not be executed unless the *force* parameter is set to true.

    Parameters:
      urls     - URL or array of URLs of scripts to load
      callback - function to call when loading is complete
      obj      - (optional) object to pass to the callback function
      scope    - (optional) if true, *callback* will be executed in the scope
                 of *obj* instead of receiving *obj* as an argument
      force    - (optional) if true, *callback* will always be executed, even if
                 all specified scripts have already been loaded
    */
    loadOnce: function (urls, callback, obj, scope, force) {
      var newUrls = [],
          scripts = d.getElementsByTagName('script'),
          i, j, loaded, url;

      urls = urls.constructor === Array ? urls : [urls];

      for (i = 0; i < urls.length; ++i) {
        loaded = false;
        url    = urls[i];

        for (j = 0; j < scripts.length; ++j) {
          if (url === scripts[j].src) {
            loaded = true;
            break;
          }
        }

        if (!loaded) {
          newUrls.push(url);
        }
      }

      if (newUrls.length > 0) {
        LazyLoad.load(newUrls, callback, obj, scope);
      } else if (force) {
        if (obj) {
          if (scope) {
            callback.call(obj);
          } else {
            callback.call(window, obj);
          }
        } else {
          callback.call();
        }
      }
    },

    /*
    Method: requestComplete
    Handles callback execution and cleanup after a request is completed. This
    method should not be called manually.
    */
    requestComplete: function () {
      if (pending.callback) {
        if (pending.obj) {
          if (pending.scope) {
            pending.callback.call(pending.obj);
          } else {
            pending.callback.call(window, pending.obj);
          }
        } else {
          pending.callback.call();
        }
      }

      pending = null;

      if (queue.length) {
        LazyLoad.load();
      }
    }
  };
}();


function loadAjaxlessInPlaceEditor() {
  if( typeof(AjaxlessInPlaceEditor) == "undefined" ) {
    AjaxlessInPlaceEditor = Class.create(Ajax.InPlaceEditor, {
      handleFormSubmission: function(e) {
          var form = this._form;
          var value = $F(this._controls.editor);
          this.options.callback(form, value);
          this.leaveEditMode();
          this.element.update(value);
          if(e) {
            e.stop();
          }
      }
    });
  }
}


/*
window.onerror = function(msg, url, line) {
  new Ajax.Request("/e", {
    method: 'post',
    parameters: {
      language: "javascript",
      exception_class: "Javascript Error",
      exception_message: msg,
      exception_backtrace: url+":"+line,
      url: window.location,
      controller_name: url.split('/').slice(-1)
    }
  });
};
*/

var HelpBubble = Class.create({
  initialize: function(options) {
    if (typeof(options.fromElement) != 'undefined') {
      this.bubbleElement = options.fromElement
      this.target = $(this.bubbleElement.getAttribute('targetElement'))

      this.bubbleElement.show()
      this.offset = {}
      this.offset['x'] = this.bubbleElement.getStyle('left') ? parseInt(this.bubbleElement.getStyle('left').gsub('px','')) : 0;
      this.offset['y'] = this.bubbleElement.getStyle('top') ? parseInt(this.bubbleElement.getStyle('top').gsub('px','')) : 0;
      this.bubbleElement.hide()

      $(['left','right','top','bottom']).each(function(direction){
        if(this.bubbleElement.hasClassName(direction))
          this.position = direction;
      }.bind(this));
    } else {
      this.target = $(options.target);
      this.position = options.position;
      this.offset = options.offset ? options.offset : {x:0,y:0};

      this.bubbleElement = (new Element('div')).insert($('helpBubbleTemplate').innerHTML.interpolate({body:options.content})).down()
      this.bubbleElement.addClassName(options.position);

      $$('body')[0].insert({top:this.bubbleElement})
    }


    this.hide();
  },

  show: function() {
      this.bubbleElement.show();

      this.setPosition();
    	Event.observe(window,'resize',function(){
    	  this.setPosition();
    	}.bind(this))
   },

  setPosition: function() {
    if (['right','left'].include(this.position)) {
      if (this.position == 'left') {
        this.bubbleElement.style.left = this.offset.x + this.target.cumulativeOffset().left - this.bubbleElement.getWidth() - 32 + "px";
      } else {
        this.bubbleElement.style.left = this.offset.x + this.target.cumulativeOffset().left + this.target.getWidth() + 32 + "px";
      }
      this.bubbleElement.style.top = this.offset.y + this.target.cumulativeOffset().top - (this.bubbleElement.getHeight() / 2) + (this.target.getHeight() / 2) + "px";
    } else {
      if (this.position == 'top') {
        this.bubbleElement.style.top = this.offset.y + this.target.cumulativeOffset().top - this.bubbleElement.getHeight() - 32 + "px";
      } else {
        this.bubbleElement.style.top = this.offset.y + this.target.cumulativeOffset().top + this.target.getHeight() + 32 + "px";
      }
      this.bubbleElement.style.left = this.offset.x + this.target.cumulativeOffset().left - (this.bubbleElement.getWidth() / 2) + (this.target.getWidth() / 2) + "px";
    }
  },

  hide: function() {
    this.bubbleElement.hide();
  }
});

function setupHelpButtons() {
  $$('.helpButtonTrigger').each(function(helpButton){
    bubble = new HelpBubble({
      position:helpButton.getAttribute('position') ? helpButton.getAttribute('position') : 'right',
      target:helpButton.getAttribute('target') ? helpButton.getAttribute('target') : helpButton,
      content:helpButton.getAttribute('bubble_content'),
      offset:{x:helpButton.getAttribute('offset_x') ? parseInt(helpButton.getAttribute('offset_x')) : 0,
              y:helpButton.getAttribute('offset_y') ? parseInt(helpButton.getAttribute('offset_y')) : 0}
    });

    bubble.trigger = helpButton;

    helpButton.observe('mouseover', function(){
      clearTimeout(this.hoverTimeout)
      this.hoverTimeout = setTimeout(function(){
        this.show();
        this.trigger.writeAttribute('src','/images/info_button_active.png');
      }.bind(this), 400)
    }.bind(bubble));

    helpButton.observe('mouseout', function(){
      clearTimeout(this.hoverTimeout)
      this.hoverTimeout = setTimeout(function(){
        this.hide();
        this.trigger.writeAttribute('src','/images/info_button.png');
      }.bind(this), 300)
    }.bind(bubble));

    helpButton.observe('click', function(){
      clearTimeout(this.hoverTimeout)
      this.show();
      this.trigger.writeAttribute('src','/images/info_button_active.png');
    }.bind(bubble));
  });
}

Event.observe(document,'dom:loaded',setupHelpButtons)

if(Object.isUndefined(Effect))
  throw("dragdrop.js requires including script.aculo.us' effects.js library");

var Droppables = {
  drops: [],

  remove: function(element) {
    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
  },

  add: function(element) {
    element = $(element);
    var options = Object.extend({
      greedy:     true,
      hoverclass: null,
      tree:       false
    }, arguments[1] || { });

    if(options.containment) {
      options._containers = [];
      var containment = options.containment;
      if(Object.isArray(containment)) {
        containment.each( function(c) { options._containers.push($(c)) });
      } else {
        options._containers.push($(containment));
      }
    }

    if(options.accept) options.accept = [options.accept].flatten();

    Element.makePositioned(element); // fix IE
    options.element = element;

    this.drops.push(options);
  },

  findDeepestChild: function(drops) {
    deepest = drops[0];

    for (i = 1; i < drops.length; ++i)
      if (Element.isParent(drops[i].element, deepest.element))
        deepest = drops[i];

    return deepest;
  },

  isContained: function(element, drop) {
    var containmentNode;
    if(drop.tree) {
      containmentNode = element.treeNode;
    } else {
      containmentNode = element.parentNode;
    }
    return drop._containers.detect(function(c) { return containmentNode == c });
  },

  isAffected: function(point, element, drop) {
    return (
      (drop.element!=element) &&
      ((!drop._containers) ||
        this.isContained(element, drop)) &&
      ((!drop.accept) ||
        (Element.classNames(element).detect(
          function(v) { return drop.accept.include(v) } ) )) &&
      Position.within(drop.element, point[0], point[1]) );
  },

  deactivate: function(drop) {
    if(drop.hoverclass)
      Element.removeClassName(drop.element, drop.hoverclass);
    this.last_active = null;
  },

  activate: function(drop) {
    if(drop.hoverclass)
      Element.addClassName(drop.element, drop.hoverclass);
    this.last_active = drop;
  },

  show: function(point, element) {
    if(!this.drops.length) return;
    var drop, affected = [];

    this.drops.each( function(drop) {
      if(Droppables.isAffected(point, element, drop))
        affected.push(drop);
    });

    if(affected.length>0)
      drop = Droppables.findDeepestChild(affected);

    if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
    if (drop) {
      Position.within(drop.element, point[0], point[1]);
      if(drop.onHover)
        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));

      if (drop != this.last_active) Droppables.activate(drop);
    }
  },

  fire: function(event, element) {
    if(!this.last_active) return;
    Position.prepare();

    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
      if (this.last_active.onDrop) {
        this.last_active.onDrop(element, this.last_active.element, event);
        return true;
      }
  },

  reset: function() {
    if(this.last_active)
      this.deactivate(this.last_active);
  }
}

var Draggables = {
  drags: [],
  observers: [],

  register: function(draggable) {
    if(this.drags.length == 0) {
      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
      this.eventKeypress  = this.keyPress.bindAsEventListener(this);

      Event.observe(document, "mouseup", this.eventMouseUp);
      Event.observe(document, "mousemove", this.eventMouseMove);
      Event.observe(document, "keypress", this.eventKeypress);
    }
    this.drags.push(draggable);
  },

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

  activate: function(draggable) {
    if(draggable.options.delay) {
      this._timeout = setTimeout(function() {
        Draggables._timeout = null;
        window.focus();
        Draggables.activeDraggable = draggable;
      }.bind(this), draggable.options.delay);
    } else {
      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
      this.activeDraggable = draggable;
    }
  },

  deactivate: function() {
    this.activeDraggable = null;
  },

  updateDrag: function(event) {
    if(!this.activeDraggable) return;
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
    this._lastPointer = pointer;

    this.activeDraggable.updateDrag(event, pointer);
  },

  endDrag: function(event) {
    if(this._timeout) {
      clearTimeout(this._timeout);
      this._timeout = null;
    }
    if(!this.activeDraggable) return;
    this._lastPointer = null;
    this.activeDraggable.endDrag(event);
    this.activeDraggable = null;
  },

  keyPress: function(event) {
    if(this.activeDraggable)
      this.activeDraggable.keyPress(event);
  },

  addObserver: function(observer) {
    this.observers.push(observer);
    this._cacheObserverCallbacks();
  },

  removeObserver: function(element) {  // element instead of observer fixes mem leaks
    this.observers = this.observers.reject( function(o) { return o.element==element });
    this._cacheObserverCallbacks();
  },

  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
    if(this[eventName+'Count'] > 0)
      this.observers.each( function(o) {
        if(o[eventName]) o[eventName](eventName, draggable, event);
      });
    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
  },

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

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

var Draggable = Class.create({
  initialize: function(element) {
    var defaults = {
      handle: false,
      reverteffect: function(element, top_offset, left_offset) {
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
          queue: {scope:'_draggable', position:'end'}
        });
      },
      endeffect: function(element) {
        var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
          queue: {scope:'_draggable', position:'end'},
          afterFinish: function(){
            Draggable._dragging[element] = false
          }
        });
      },
      zindex: 1000,
      revert: false,
      quiet: false,
      scroll: false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
      delay: 0
    };

    if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
      Object.extend(defaults, {
        starteffect: function(element) {
          element._opacity = Element.getOpacity(element);
          Draggable._dragging[element] = true;
          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
        }
      });

    var options = Object.extend(defaults, arguments[1] || { });

    this.element = $(element);

    if(options.handle && Object.isString(options.handle))
      this.handle = this.element.down('.'+options.handle, 0);

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

    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
      options.scroll = $(options.scroll);
      this._isScrollChild = Element.childOf(this.element, options.scroll);
    }

    Element.makePositioned(this.element); // fix IE

    this.options  = options;
    this.dragging = false;

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

    Draggables.register(this);
  },

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

  currentDelta: function() {
    return([
      parseInt(Element.getStyle(this.element,'left') || '0'),
      parseInt(Element.getStyle(this.element,'top') || '0')]);
  },

  initDrag: function(event) {
    if (typeof(event.target) == "function") {
            return
    }

    if(!Object.isUndefined(Draggable._dragging[this.element]) &&
      Draggable._dragging[this.element]) return;
    if(Event.isLeftClick(event)) {
      var src = Event.element(event);
      if((tag_name = src.tagName.toUpperCase()) && (
        tag_name=='INPUT' ||
        tag_name=='SELECT' ||
        tag_name=='OPTION' ||
        tag_name=='BUTTON' ||
        tag_name=='TEXTAREA')) return;

      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      var pos     = Position.cumulativeOffset(this.element);
      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });

      Draggables.activate(this);
      Event.stop(event);
    }
  },

  startDrag: function(event) {
    this.dragging = true;
    if(!this.delta)
      this.delta = this.currentDelta();

		dragged_elem = this.element;


    if(this.options.zindex) {
      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
      this.element.style.zIndex = this.options.zindex;
    }

    if(this.options.ghosting) {
      this._clone = this.element.cloneNode(true);
      this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
      if (!this._originallyAbsolute)
        Position.absolutize(this.element);
      this.element.parentNode.insertBefore(this._clone, this.element);
    }

    if(this.options.scroll) {
      if (this.options.scroll == window) {
        var where = this._getWindowScroll(this.options.scroll);
        this.originalScrollLeft = where.left;
        this.originalScrollTop = where.top;
      } else {
        this.originalScrollLeft = this.options.scroll.scrollLeft;
        this.originalScrollTop = this.options.scroll.scrollTop;
      }
    }

    Draggables.notify('onStart', this, event);

    if(this.options.starteffect) this.options.starteffect(this.element);
  },

  updateDrag: function(event, pointer) {
    if(!this.dragging) this.startDrag(event);

    if(!this.options.quiet){
      Position.prepare();
      Droppables.show(pointer, this.element);
    }

    Draggables.notify('onDrag', this, event);

    this.draw(pointer);
    if(this.options.change) this.options.change(this);

    if(this.options.scroll) {
      this.stopScrolling();

      var p;
      if (this.options.scroll == window) {
        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
      } else {
        p = Position.page(this.options.scroll);
        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
        p[1] += this.options.scroll.scrollTop + Position.deltaY;
        p.push(p[0]+this.options.scroll.offsetWidth);
        p.push(p[1]+this.options.scroll.offsetHeight);
      }
      var speed = [0,0];
      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
      this.startScrolling(speed);
    }

    if(Prototype.Browser.WebKit) window.scrollBy(0,0);

    Event.stop(event);
  },

  finishDrag: function(event, success) {
    this.dragging = false;

    if(this.options.quiet){
      Position.prepare();
      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      Droppables.show(pointer, this.element);
    }

    if(this.options.ghosting) {
      if (!this._originallyAbsolute)
        Position.relativize(this.element);
      delete this._originallyAbsolute;
      Element.remove(this._clone);
      this._clone = null;
    }

    var dropped = false;
    if(success) {
      dropped = Droppables.fire(event, this.element);
      if (!dropped) dropped = false;
    }
    if(dropped && this.options.onDropped) this.options.onDropped(this.element);
    Draggables.notify('onEnd', this, event);

    var revert = this.options.revert;
    if(revert && Object.isFunction(revert)) revert = revert(this.element);

    var d = this.currentDelta();
    if(revert && this.options.reverteffect) {
      if (dropped == 0 || revert != 'failure')
        this.options.reverteffect(this.element,
          d[1]-this.delta[1], d[0]-this.delta[0]);
    } else {
      this.delta = d;
    }

    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if(this.options.endeffect)
      this.options.endeffect(this.element);

    Draggables.deactivate(this);
    Droppables.reset();
  },

  keyPress: function(event) {
    if(event.keyCode!=Event.KEY_ESC) return;
    this.finishDrag(event, false);
    Event.stop(event);
  },

  endDrag: function(event) {
    if(!this.dragging) return;
    this.stopScrolling();
    this.finishDrag(event, true);
    Event.stop(event);
  },

  draw: function(point) {
    var pos = Position.cumulativeOffset(this.element);
    if(this.options.ghosting) {
      var r   = Position.realOffset(this.element);
      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
    }

    var d = this.currentDelta();
    pos[0] -= d[0]; pos[1] -= d[1];

    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
    }

    var p = [0,1].map(function(i){
      return (point[i]-pos[i]-this.offset[i])
    }.bind(this));

    if(this.options.snap) {
      if(Object.isFunction(this.options.snap)) {
        p = this.options.snap(p[0],p[1],this);
      } else {
      if(Object.isArray(this.options.snap)) {
        p = p.map( function(v, i) {
          return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this))
      } else {
        p = p.map( function(v) {
          return (v/this.options.snap).round()*this.options.snap }.bind(this))
      }
    }}

    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
      style.left = p[0] + "px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
      style.top  = p[1] + "px";

    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },

  stopScrolling: function() {
    if(this.scrollInterval) {
      clearInterval(this.scrollInterval);
      this.scrollInterval = null;
      Draggables._lastScrollPointer = null;
    }
  },

  startScrolling: function(speed) {
    if(!(speed[0] || speed[1])) return;
    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
    this.lastScrolled = new Date();
    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
  },

  scroll: function() {
    var current = new Date();
    var delta = current - this.lastScrolled;
    this.lastScrolled = current;
    if(this.options.scroll == window) {
      with (this._getWindowScroll(this.options.scroll)) {
        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
          var d = delta / 1000;
          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
        }
      }
    } else {
      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
    }

    Position.prepare();
    Droppables.show(Draggables._lastPointer, this.element);
    Draggables.notify('onDrag', this);
    if (this._isScrollChild) {
      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
      if (Draggables._lastScrollPointer[0] < 0)
        Draggables._lastScrollPointer[0] = 0;
      if (Draggables._lastScrollPointer[1] < 0)
        Draggables._lastScrollPointer[1] = 0;
      this.draw(Draggables._lastScrollPointer);
    }

    if(this.options.change) this.options.change(this);
  },

  _getWindowScroll: function(w) {
    var T, L, W, H;
    with (w.document) {
      if (w.document.documentElement && documentElement.scrollTop) {
        T = documentElement.scrollTop;
        L = documentElement.scrollLeft;
      } else if (w.document.body) {
        T = body.scrollTop;
        L = body.scrollLeft;
      }
      if (w.innerWidth) {
        W = w.innerWidth;
        H = w.innerHeight;
      } else if (w.document.documentElement && documentElement.clientWidth) {
        W = documentElement.clientWidth;
        H = documentElement.clientHeight;
      } else {
        W = body.offsetWidth;
        H = body.offsetHeight
      }
    }
    return { top: T, left: L, width: W, height: H };
  }
});

Draggable._dragging = { };

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

var SortableObserver = Class.create({
  initialize: function(element, observer) {
    this.element   = $(element);
    this.observer  = observer;
    this.lastValue = Sortable.serialize(this.element);
  },

  onStart: function() {
    this.lastValue = Sortable.serialize(this.element);
  },

  onEnd: function() {
    Sortable.unmark();
    if(this.lastValue != Sortable.serialize(this.element))
      this.observer(this.element)
  }
});

var Sortable = {
  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,

  sortables: { },

  _findRootElement: function(element) {
    while (element.tagName.toUpperCase() != "BODY") {
      if(element.id && Sortable.sortables[element.id]) return element;
      element = element.parentNode;
    }
  },

  options: function(element) {
    element = Sortable._findRootElement($(element));
    if(!element) return;
    return Sortable.sortables[element.id];
  },

  destroy: function(element){
    var s = Sortable.options(element);

    if(s) {
      Draggables.removeObserver(s.element);
      s.droppables.each(function(d){ Droppables.remove(d) });
      s.draggables.invoke('destroy');

      delete Sortable.sortables[s.element.id];
    }
  },

  create: function(element) {
    element = $(element);
    var options = Object.extend({
      element:     element,
      tag:         'li',       // assumes li children, override with tag: 'tagname'
      dropOnEmpty: false,
      tree:        false,
      treeTag:     'ul',
      overlap:     'vertical', // one of 'vertical', 'horizontal'
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
      containment: element,    // also takes array of elements (or id's); or false
      handle:      false,      // or a CSS class
      only:        false,
      delay:       0,
      hoverclass:  null,
      ghosting:    false,
      quiet:       false,
      scroll:      false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      format:      this.SERIALIZE_RULE,

      elements:    false,
      handles:     false,

      onChange:    Prototype.emptyFunction,
      onUpdate:    Prototype.emptyFunction
    }, arguments[1] || { });

    this.destroy(element);

    var options_for_draggable = {
      revert:      true,
      quiet:       options.quiet,
      scroll:      options.scroll,
      scrollSpeed: options.scrollSpeed,
      scrollSensitivity: options.scrollSensitivity,
      delay:       options.delay,
      ghosting:    options.ghosting,
      constraint:  options.constraint,
      handle:      options.handle };

    if(options.starteffect)
      options_for_draggable.starteffect = options.starteffect;

    if(options.reverteffect)
      options_for_draggable.reverteffect = options.reverteffect;
    else
      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
        element.style.top  = 0;
        element.style.left = 0;
      };

    if(options.endeffect)
      options_for_draggable.endeffect = options.endeffect;

    if(options.zindex)
      options_for_draggable.zindex = options.zindex;

    var options_for_droppable = {
      overlap:     options.overlap,
      containment: options.containment,
      tree:        options.tree,
      hoverclass:  options.hoverclass,
      onHover:     Sortable.onHover
    }

    var options_for_tree = {
      onHover:      Sortable.onEmptyHover,
      overlap:      options.overlap,
      containment:  options.containment,
      hoverclass:   options.hoverclass
    }

    Element.cleanWhitespace(element);

    options.draggables = [];
    options.droppables = [];

    if(options.dropOnEmpty || options.tree) {
      Droppables.add(element, options_for_tree);
      options.droppables.push(element);
    }

    (options.elements || this.findElements(element, options) || []).each( function(e,i) {
      var handle = options.handles ? $(options.handles[i]) :
        (options.handle ? $(e).select('.' + options.handle)[0] : e);
      options.draggables.push(
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
      Droppables.add(e, options_for_droppable);
      if(options.tree) e.treeNode = element;
      options.droppables.push(e);
    });

    if(options.tree) {
      (Sortable.findTreeElements(element, options) || []).each( function(e) {
        Droppables.add(e, options_for_tree);
        e.treeNode = element;
        options.droppables.push(e);
      });
    }

    this.sortables[element.id] = options;

    Draggables.addObserver(new SortableObserver(element, options.onUpdate));

  },

  findElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.tag);
  },

  findTreeElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.treeTag);
  },

  onHover: function(element, dropon, overlap) {
    if(Element.isParent(dropon, element)) return;

    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
      return;
    } else if(overlap>0.5) {
      Sortable.mark(dropon, 'before');
      if(dropon.previousSibling != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, dropon);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    } else {
      Sortable.mark(dropon, 'after');
      var nextElement = dropon.nextSibling || null;
      if(nextElement != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, nextElement);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    }
  },

  onEmptyHover: function(element, dropon, overlap) {
    var oldParentNode = element.parentNode;
    var droponOptions = Sortable.options(dropon);

    if(!Element.isParent(dropon, element)) {
      var index;

      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
      var child = null;

      if(children) {
        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);

        for (index = 0; index < children.length; index += 1) {
          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
            offset -= Element.offsetSize (children[index], droponOptions.overlap);
          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
            child = index + 1 < children.length ? children[index + 1] : null;
            break;
          } else {
            child = children[index];
            break;
          }
        }
      }

      dropon.insertBefore(element, child);

      Sortable.options(oldParentNode).onChange(element);
      droponOptions.onChange(element);
    }
  },

  unmark: function() {
    if(Sortable._marker) Sortable._marker.hide();
  },

  mark: function(dropon, position) {
    var sortable = Sortable.options(dropon.parentNode);
    if(sortable && !sortable.ghosting) return;

    if(!Sortable._marker) {
      Sortable._marker =
        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
          hide().addClassName('dropmarker').setStyle({position:'absolute'});
      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
    }
    var offsets = Position.cumulativeOffset(dropon);
    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});

    if(position=='after')
      if(sortable.overlap == 'horizontal')
        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
      else
        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});

    Sortable._marker.show();
  },

  _tree: function(element, options, parent) {
    var children = Sortable.findElements(element, options) || [];

    for (var i = 0; i < children.length; ++i) {
      var match = children[i].id.match(options.format);

      if (!match) continue;

      var child = {
        id: encodeURIComponent(match ? match[1] : null),
        element: element,
        parent: parent,
        children: [],
        position: parent.children.length,
        container: $(children[i]).down(options.treeTag)
      }

      /* Get the element containing the children and recurse over it */
      if (child.container)
        this._tree(child.container, options, child)

      parent.children.push (child);
    }

    return parent;
  },

  tree: function(element) {
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag: sortableOptions.tag,
      treeTag: sortableOptions.treeTag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format
    }, arguments[1] || { });

    var root = {
      id: null,
      parent: null,
      children: [],
      container: element,
      position: 0
    }

    return Sortable._tree(element, options, root);
  },

  /* Construct a [i] index for a particular node */
  _constructIndex: function(node) {
    var index = '';
    do {
      if (node.id) index = '[' + node.position + ']' + index;
    } while ((node = node.parent) != null);
    return index;
  },

  sequence: function(element) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[1] || { });

    return $(this.findElements(element, options) || []).map( function(item) {
      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
    });
  },

  setSequence: function(element, new_sequence) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[2] || { });

    var nodeMap = { };
    this.findElements(element, options).each( function(n) {
        if (n.id.match(options.format))
            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
        n.parentNode.removeChild(n);
    });

    new_sequence.each(function(ident) {
      var n = nodeMap[ident];
      if (n) {
        n[1].appendChild(n[0]);
        delete nodeMap[ident];
      }
    });
  },

  serialize: function(element) {
    element = $(element);
    var options = Object.extend(Sortable.options(element), arguments[1] || { });
    var name = encodeURIComponent(
      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);

    if (options.tree) {
      return Sortable.tree(element, arguments[1]).children.map( function (item) {
        return [name + Sortable._constructIndex(item) + "[id]=" +
                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
      }).flatten().join('&');
    } else {
      return Sortable.sequence(element, arguments[1]).map( function(item) {
        return name + "[]=" + encodeURIComponent(item);
      }).join('&');
    }
  }
}

Element.isParent = function(child, element) {
  if (!child.parentNode || child == element) return false;
  if (child.parentNode == element) return true;
  return Element.isParent(child.parentNode, element);
}

Element.findChildren = function(element, only, recursive, tagName) {
  if(!element.hasChildNodes()) return null;
  tagName = tagName.toUpperCase();
  if(only) only = [only].flatten();
  var elements = [];
  $A(element.childNodes).each( function(e) {
    if(e.tagName && e.tagName.toUpperCase()==tagName &&
      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
        elements.push(e);
    if(recursive) {
      var grandchildren = Element.findChildren(e, only, recursive, tagName);
      if(grandchildren) elements.push(grandchildren);
    }
  });

  return (elements.length>0 ? elements.flatten() : []);
}

Element.offsetSize = function (element, type) {
  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
}


WINDOW_FOCUS = true;
window.onfocus = function(){
  WINDOW_FOCUS = true;
}
window.onblur = function(){
  WINDOW_FOCUS = false
}
document.onfocusin = function(){
  WINDOW_FOCUS = true;
}
document.onfocusout = function() {
  WINDOW_FOCUS = false
}

var blinkTimeoutId = null;

function blink(msg) {
  if (!WINDOW_FOCUS) {
    if (document.title == msg)
      document.title = "drop.io " + BUCKET_URL;
    else
      document.title = msg;

    clearTimeout(blinkTimeoutId);
    blinkTimeoutId = setTimeout(function() { blink(msg); }, 1000);
  } else {
    document.title = "drop.io " + BUCKET_URL;
    clearTimeout(blinkTimeoutId);
  }
}

function rawUploadUrl(uploadUrl) {
	if( !uploadUrl )
		uploadUrl = UPLOAD_URL;
	return uploadUrl.replace(/\?bucket=.+/,"");
}

function logAnalytics(url) {
  try {
    var pageTracker = _gat._getTracker("UA-5625478-1");
    pageTracker._trackPageview(url);
  } catch(err) {}
}

function showSystemMessage(message) {
  var notification_div = $("systemNotification");
	if (notification_div) {
		notification_div.fade({
			duration: .3,
			afterFinish: function(){
				notification_div.innerHTML = message;
				notification_div.appear({duration: .3 })
			}
		})
	}
}

function changeLocation(newLat,newLon) {
	var map_div = $("mini_map_canvas");
	if (map_div) {
		if (map_div.up().style.display == "none") {
			map_div.up().appear();
		}
		saved_location = {};
		saved_location.lat = newLat;
		saved_location.lon = newLon;
		loadMiniGMap();
	}
}

function changeLogo(logo_url) {
	var logo_div = $("logo");
	if (logo_div) {
	  if( logo_url == "" ) {
	    logo_div.fade({duration:0.3});
	    logo_div.update("")
	  } else {
  		logo_div.update("<img src='"+logo_url+"' style='max-height:175px; max-width:175px;'>")
	  	if (logo_div.style.display == "none")
	  		logo_div.appear({duration: .3})
	  }
	}
}

function changeBackground(the_bg_url,the_bg_color,the_bg_repeat) {
  bgurl = the_bg_url == null ? window.background_url : the_bg_url;
  window.background_url = bgurl;
  repeat = the_bg_repeat == null ? window.background_repeat : the_bg_repeat
  window.background_repeat = repeat;
  bgcolor = the_bg_color == null ? window.background_color : the_bg_color
  window.background_color = bgcolor
	document.body.style.background = "url("+bgurl+") "+repeat+" "+bgcolor;
}

function changeAccentColor(accent_color) {
  classes = [getCssClass(".accent"),getCssClass(".paginate .pagination a"),getCssClass(".more_pagination a")];
  classes.each(function(c) {
    if( typeof(c) != "undefined" )
      c.style.color = accent_color;
  })
}

function changeDescription(description) {
  var desc_div = $("description");
	if (desc_div) {
		desc_div.innerHTML = description;
		if( description == "" )
		  desc_div.fade({duration:0.3});
		else if (desc_div.style.display == "none")
			desc_div.appear({duration: .3})
	}
}

function isAssetUnchanged(newData, attributeToCheck) {
  if( !newData || newData.id == null ) return false;

  curAssetData = getAssetData(newData.id)
  isUnchanged = true;

  if((typeof(attributeToCheck) != 'undefined') && (typeof(curAssetData[attributeToCheck]) != 'undefined'))
    return (newData[attributeToCheck] == curAssetData[attributeToCheck]);


  for( k in newData ) {
    if( newData[k] != curAssetData[k] )
      isUnchanged = false;
  }
  return isUnchanged;
}

function buildAsset(data, theme, template) {
  if( !data || !data.type ) return "";
  if( data.contents != null ) {
    data.display_contents = data.contents.toString().gsub("&gt;",">").gsub("&lt;","<").gsub("&amp;","&").gsub("&quot;","\""); // the extra gsub is needed for IE // dont use unescapeHTML as it behaves differently in IE
    data.editor_contents = data.display_contents.gsub("&gt;","&amp;gt;").gsub("&lt;","&amp;lt;"); // doubly escape for editor
  }
  if( data.conversion_message != null )
    data.conversion_message = data.conversion_message.replace("&lt;br /&gt;","<br />")
  if( data.artist == "Unknown" )
    data.artist = "";
  if( data.track_title == "Unknown" )
    data.track_title = data.name

  if( data.converted )
	data.converted = data.converted + "?" + randomNumber(10)
  if( data.thumbnail )
	data.thumbnail = data.thumbnail + "?" + randomNumber(10)

  assetHTML = ASSET_TEMPLATES[theme][template].interpolate(data);
  tmp = new Element("div")
  tmp.innerHTML = assetHTML
  asset = tmp.down();

  if(['image','movie'].include(data.type)) {
    resizeAsset(asset,data.width,data.height);
  }


  return asset;
}

function registerWithModalWindow(asset) {
  asset.observe('click', function(e) {
      if($(e.target).hasClassName('show_modal')) {
      	if (typeof(dragged_elem) != "undefined" && dragged_elem != null) {
      		dragged_elem = null;
      	}
      	else {
      		assed_id = asset.getAttribute("asset_id");
          modalWindow.load(assed_id);
      	}
      }
  });
  if( modalWindow ) modalWindow.addAssetToPlaylist(asset.getAttribute("asset_id"));
}

function registerWithCommentBubble(asset) {
  if( !asset ) return;

  type = asset.getAttribute("asset_type");

  if( ["image","movie","note","link","document","audio","other"].include(type) ) {
    comment_bubble = asset.down('.commentCount');
    if( comment_bubble ) {
      Event.observe(comment_bubble,'click',function(){
        asset_id = asset.getAttribute("asset_id");
        modalWindow.load(asset_id, {showComments:true});
      });
    }
  }
}

function convertStandardStreamData(data) {
  if( data.created_at != null )
    data.created_at = dbDateToBlogDate(data.created_at)
  if( data.filesize != null )
    data.filesize = format_bytes(data.filesize);
  return data;
}


function dbDateToBlogDate(theDate) {
  if( !theDate || theDate == "" ) return "";
  blogDate = theDate;
  if( blogDate == TODAY ) blogDate = "Today"
  return blogDate;
}


function format_bytes(bytes) {
  if (!bytes)
    return "0 KB"

  var giga = 1073741824;
  var mega = 1048576;
  var kilo = 1024;

  if (bytes < giga) {
    if (bytes < mega)
      return Math.round((bytes/kilo)) + " KB"
    else
      return Math.round((bytes/mega)) + " MB"
  } else
    return Math.round((bytes/giga)) + " GB"
}

function getAssetData(asset_id) {
	ad = {};

	if( !assetExists(asset_id) ) return ad;

	ac = $("asset_"+asset_id);

	ad.id = asset_id;
	ad.type = ac.readAttribute("asset_type");
	ad.comment_count = ac.readAttribute("comment_count");
	ad.name = ac.readAttribute("single_asset_url").split("/")[5];
	ad.file = ac.readAttribute("save_url");
	ad.thumbnail = ac.readAttribute("thumbnail");
	ad.url = ac.readAttribute("url");
	ad.created_at = ac.readAttribute("created_at");
	ad.filesystem_icon = ac.readAttribute("filesystem_icon");
	ad.status = ac.readAttribute("status");

	if( ad.type == "note" )
	  ad.contents = ac.down(".note_content").down().next().innerHTML.escapeHTML();
	else if( ad.type == "link" )
	  ad.description = ac.down(".link_content").down().next().next().innerHTML;
	else if( ad.type == "sidebar_link" )
	  ad.contents = ac.down().readAttribute("title");

	ad.title = ac.readAttribute("title");
	ad.track_title = ac.readAttribute("track_title");
	ad.filesize = ac.readAttribute("filesize");
	ad.media_view_order = ac.readAttribute("media_view_order");

	return ad;
}

window.assetUpdateRetries = {}
window.ASSET_UPDATE_RETRY_INTERVAL = 5000;
function retryUpdateAsset(data) {
  if( !assetUpdateRetries[data.id] || assetUpdateRetries[data.id].lastTriedAt < (new Date()) - ASSET_UPDATE_RETRY_INTERVAL ) {
    assetUpdateRetries[data.id] = { lastTriedAt: new Date() }
    updateAsset(data);
  }
  else
    delete assetUpdateRetries[data.id]
}

function resizeAsset(asset,naturalWidth,naturalHeight) {
  if( naturalWidth == 0 || naturalHeight == 0 ) return;

  var ratio = 1;

  if (naturalWidth >= naturalHeight && naturalWidth > 100)
  	ratio = 100 / naturalWidth;

  if (naturalHeight >= naturalWidth && naturalHeight > 100)
  	ratio = 100 / naturalHeight;

  asset.down().style.width = (naturalWidth * ratio) + "px";
  asset.down().style.height = (naturalHeight * ratio) + "px";

}

function assetExists(assetId) {
  return $('asset_' + assetId) != null
}

function pluralizeCategory(type) {
  if( $w("audio").indexOf(type) != -1 ) return type;
  return type + "s";
}


function loadJS(url, callbackFunction) {
  var script = document.createElement('script');
  script.setAttribute('src',url);
  script.setAttribute('type','text/javascript');
  var loaded = false;
  var loadFunction = function()
  {
    if (loaded) return;
    loaded=true;
    callbackFunction();
  };
  script.onload = loadFunction;
  script.onreadystatechange = loadFunction;
  document.getElementsByTagName('head')[0].appendChild(script);
}

function startPicLens(mediaRSS) {
  loadJS("http://lite.piclens.com/current/piclens.js", function () {
    picLensCheck(mediaRSS);
  });
}

function picLensCheck(mediaRSS) {
  if (PicLensLite.hasCooliris()) {
		PicLensLite.start({
			feedUrl: mediaRSS
	  })
	}
	else {
		installPlugin = confirm("You don't not have the PicLens plugin installed. Would you like to install it now?");
		if (installPlugin) {
			window.location = "http://download.piclens.com/partner/1600fa39928c"
		}
	}
}

/**
 * Used by the views to initialize an inplace text editor for renaming assets
 *
 * @param linkName CSS handle of the text that will be renamed
 * @param assetId Asset ID of the asset to be renamed
 * @param renameLink CSS handle of the text which triggers the renaming
 */
function setUpInPlaceEditor(linkName, assetId, renameLink) {
  if(linkName != undefined && assetId != undefined && renameLink != undefined) {
    var tempEditor = new Ajax.InPlaceEditor(linkName, '/' + BUCKET_URL + '/asset/' + assetId + '/update_name',
    {
      externalControlOnly: true,
      externalControl: renameLink,
      okControl: 'link',
      okText: 'Save',
      cancelText: 'Cancel',
      highlightColor: '#FFFFFF',
      onComplete: function(req, element) {
        if(req != null) {
          var i_sep = req.responseText.indexOf(" ");
          var newurl = req.responseText.substring(0,i_sep);
          var newname = req.responseText.substring(i_sep+1);
          element.innerHTML = newname;
          $$("#asset_edit_" + assetId + " #title").first().value = newname;
        }
      }
    });
  }
}

function embedFlashUploader(max_bytes,current_bytes,current_files) {
  $("flashUploaderContainer").innerHTML = '<div id="fileUploader" class="clearfix"><h2>drop.io requires that you have Flash Player 9 or higher installed. Please install <a href="http://adobe.com/go/getflashplayer/">Adobe Flash Player</a>. Thanks a ton!</h2></div>';
  var flashvars = {
    max_files: 0,
    max_bytes: max_bytes,
    current_bytes: current_bytes,
    current_files: current_files,
    player_size: "large",
    checksum: CHECKSUM,
    message_src: "/FileUploaderMessages.xml",
    player_style: "initExpanded",
    wmode: "transparent",
    upload_url: UPLOAD_URL
  };
  var params = {
    wmode: "transparent",
    bgcolor: "#E6E6E6"
  };
  var attributes = { };

  swfobject.embedSWF("/swf/FileUploaderApp.swf", "fileUploader", "530px", "70px",
                      "9.0.0", null, flashvars, params, attributes);
}

function onFilesAdded() {
  $('bucket-dropit-submit-container').show();
}

function onUploadCancel() {
  $('bucket-dropit-submit-container').show();
  window.onbeforeunload = null;
  DropitButton = $('bucket-dropit-submit');
  DropitButton.update("Drop It");
  DropitButton.setAttribute('disabled', false);
  DropitButton.disabled = false;
  DropitButton.removeClassName("disabled")
  DropitButton.parentNode.removeClassName("disabledBorder")
}


function resizeFlashDiv(h) {
  h += "px";
  $('fileUploader').style.height = h;
}

function b4unload(){
  dontleave="Are you sure you want to abandon your upload?";
  return dontleave;
}

function beginUpload(e) {
	window.onbeforeunload = b4unload;
  DropitButton = $('bucket-dropit-submit');
  DropitButton.update("Uploading...");
  DropitButton.setAttribute('disabled', true);
  DropitButton.disabled = true;
  DropitButton.addClassName("disabled")
  DropitButton.parentNode.addClassName("disabledBorder")
 	$('fileUploader').startQueue();
}

function onUploadComplete() {
	window.onbeforeunload = null;
	resetUploader();
	if( !STREAM_ACTIVE )
  	location.reload();
}

function resetUploader() {
  DropitButton = $('bucket-dropit-submit');
  DropitButton.update("Drop It");
  DropitButton.setAttribute('disabled', false);
  DropitButton.disabled = false;
  DropitButton.removeClassName("disabled")
  DropitButton.parentNode.removeClassName("disabledBorder")
  $('bucket-dropit-submit-container').hide();
  $('bucket-dropit-submit').setAttribute("onclick","beginUpload()")

  new Ajax.Request(LIMITS_URL, {
    parameters: "uid="+UID+"&checksum="+CHECKSUM+"&format=json",
    method: "post",
    onComplete: function(transport) {
      var response = transport.responseJSON;
      if( response['success'] == true ) {
      	if( $("addContainer").style.display == "" )
      	  showGlobalSubnavigation("add"); // hide the tab
        $("fileUploader").remove();
        embedFlashUploader(response['max_bytes'],response['current_bytes'],response['current_files'])
      }
    }
  })
}

function submitWysihatNote() {
  var content = DropioNote.getEditor("bucket-note").content()

  $("bucket-note-buttons").hide();
  $("bucket-note-spinner").show();

  new Ajax.Request(NOTE_URL, {
    parameters: {"note[contents]":content,"note[title]":$("bucket-note-title").value},
    method: "post",
    onComplete: function(transport) {
      var response = transport.responseJSON;
      if( response["success"] == true ) {
      	if( $("addContainer").style.display == "" )
      	  showGlobalSubnavigation("add"); // hide the tab
      	$("bucket-note-title").value = "";
        clearWysihatEditor("bucket-note");
      	data = convertStreamData(response['note'])
        addAsset(data);
        showSystemMessage(response['message']);
      }
      else {
        $("noteError").innerHTML = response["message"]
        new Effect.Appear($("noteError"));
      }
      $("bucket-note-buttons").show();
      $("bucket-note-spinner").hide();
    }
  });
}

function clearWysihatEditor(id) {
  DropioNote.getEditor(id).setRawContent("");
}

function submitLink() {
  params = $("create_link_form").serialize();
  new Ajax.Request(LINK_URL, {
    parameters: params,
    method: "post",
    onComplete: function(transport) {
      var response = transport.responseJSON;
      if( response["success"] == true ) {
        if( $("addContainer").style.display == "" )
      	  showGlobalSubnavigation("add"); // hide the tab
      	$("bucket-link-url").value = "";
      	$("bucket-link-title").value = "";
      	$("bucket-link-comment").value = "";
      	data = convertStreamData(response['link'])
        addAsset(data);
        showSystemMessage(response['message'])
      }
      else {
        $("linkError").innerHTML = response["message"]
        new Effect.Appear($("linkError"));
      }
    }
  });
}

function submitUrlToFetch() {
  params = $("add_via_url_form").serialize();

  if( $('add_via_url_input').value.search(/^http[s]*:[/]+(.*)$/) == -1 ) {
      alert("URL is invalid. Please be sure it starts with http:// or https://");
      return false;
  }

  new Ajax.Request(ADD_VIA_URL_URL, {
    parameters: params,
    method: "post",
    onComplete: function(transport) {
      var response = transport.responseJSON;
      if( response["success"] == true ) {
    	  $('add_via_url_input').value = '';
    	  $("fetchUrlSuccess").innerHTML = response["message"];
    	  $("fetchUrlSuccess").appear({afterFinish:function(){
	        $("fetchUrlSuccess").fade({delay:2});
		  }});
       } else {
    	  $("fetchUrlError").innerHTML = response["message"];
    	  $("fetchUrlError").appear({afterFinish:function(){
	        $("fetchUrlError").fade({delay:5});
	     }});

      }
    }
  });
}

/**
 * Handles the transitions for the homepage tabs to add
 * different assets (Files, Notes, Links, More)
 *
 * @param tabId the id of the desired tab to show
 */

function showTab(tabId, triggerId) {
  var tabToShow = $(tabId);
  var newTrigger = $(triggerId);

  if(!(typeof(window.currentTab) == "undefined")) {
    currentTab.hide();
    currentTabTrigger.removeClassName('selected');
  }
  tabToShow.show();
  newTrigger.addClassName('selected');

  window.currentTab = tabToShow;
  window.currentTabTrigger = newTrigger;

  if( tabId == "addNotes" ) {
    if( typeof(DropioNote) != "undefined" )
      DropioNote.getEditor("bucket-note").insertHTML(""); // hack to focus iframe in IE
  }
}
Event.observe(window, 'load', function() { if ( $$('.selected').size > 0) { $$('.selected')[0].onclick(); } })

function showGlobalSubnavigation(tabname, calee) {
   	mainContainer = $('globalSubnavigationContainer');

     mainContainer.show();

      if($$('#globalSubNav li.selected').length > 0) {
        $$('#globalSubNav li.selected').first().removeClassName('selected');
		    var otherTabsOpen = true;
      } else {
	  	  var otherTabsOpen = false;
	    }

	   theTab = $$('#globalSubNav li#' + tabname).first();

     if(theTab)
       theTab.addClassName('selected');




   tabs = ['add', 'view', 'share']
   tabs.each(function(containername) {
	 container = containername + "Container";
     if(containername == tabname) {
       if($(container).style.display != "") {

					if(tabname == 'add') {
		     	  showTab('addFiles', 'addFilesTrigger');
			     }

         if(theTab)
           theTab.addClassName('selected');
		     mainContainer.hide();
         $(container).show();
      		if (otherTabsOpen) {
      			mainContainer.show();
      		} else {
      			mainContainer.appear({duration:0.3});
      		}
       } else {
					mainContainer.fade({
						duration: .5,
						to: .01,
						afterFinish: function() {
	  	        if($$('#globalSubNav li.selected').length > 0)
                $$('#globalSubNav li.selected').first().removeClassName('selected');
            }
					})
					$(container).style.display = "block";
					mainContainer.blindUp({duration:0.5});
       }
     } else {
       $(container).hide();
     }
   });

   $("linkError").hide();
   $("noteError").hide();
 }

 function showViewDescription(divName) {
  showViewShareDescription("view",divName);
 }

 function showShareDescription(divName) {
  showViewShareDescription("share",divName);
 }

 function showViewShareDescription(which,divName) {
   $$('#'+which+'Container .rightSide .selectedView').each(function(element) {
     element.removeClassName('selectedView');
     element.addClassName('hidden');
   });

   if( $$('#'+which+'Container .rightSide').length > 0 )
    $$('#'+which+'Container .rightSide').first().down('#' + divName).removeClassName('hidden');
  if( $$('#'+which+'Container .rightSide').length > 0 )
     $$('#'+which+'Container .rightSide').first().down('#' + divName).addClassName('selectedView');
 }

 CapsLockWarning = Class.create();
 Object.extend(CapsLockWarning.prototype, {
   numInstances: 0,

   initialize: function(password_field_id, extra_styles, warning_text) {
     if( typeof extra_styles == "undefined" ) extra_styles = {};
     if( typeof warning_text == "undefined" ) warning_text = "caps lock is on";
     this.caps = document.createElement("p");
     this.caps.className = "caps";
     this.caps.id = "caps" + (this.numInstances++)
     this.caps.style.display = 'none';
     this.caps.innerHTML = warning_text;
     this.pass_field = $(password_field_id);
     this.pass_field.parentNode.appendChild(this.caps);
     $(this.caps.id).setStyle(extra_styles);
     Event.observe(this.pass_field,"keypress",this.do_warn.bindAsEventListener(this));
     Event.observe(this.pass_field,"blur",this.hide_warn.bindAsEventListener(this));
   },

   do_warn: function(event) {
       if( this.is_caps_on(event) )
         this.show_warn(event);
       else
         this.hide_warn(event);
   },

   hide_warn: function(event) {
     Effect.Fade(this.caps, {duration: 0.4});
   },

   show_warn: function(event) {
     Effect.Appear(this.caps, {duration: 0.4});
   },

   is_caps_on: function(e) {
     if( !e ) { e = window.event; }
     if( !e ) return;
     var theKey = e.which ? e.which : ( e.keyCode ? e.keyCode : ( e.charCode ? e.charCode : 0 ) );
     var theShift = e.shiftKey || ( e.modifiers && ( e.modifiers & 4 ) );
     return (( theKey > 64 && theKey < 91 && !theShift ) || ( theKey > 96 && theKey < 123 && !!theShift ));
   }
 });


 function saveManagerAccount() {

    if($('managerPassword') && $('managerLogin')) {
      var pw = $('managerPassword').value;
      var email = $('managerLogin').value;
      $("managerSettingsErrorMessage").update('');
      if (!checkManagerSettings(pw, email)) { return; }
    }
    $('managerSettingsSpinner').show();

    new Ajax.Request("/" + BUCKET_URL + "/admin/set_manager", {
      method: "post",
      parameters: $("setManagerForm").serialize(),
      onComplete: function(transport) {
        var response = transport.responseJSON;
        if (response['success'] == true) {
          $("managerSettingsErrorMessage").update("Success!");
          Effect.Appear("managerSettingsErrorMessage", {duration: 0.5});
          $('managerSettingsSpinner').hide();
          setTimeout (function() {Effect.Fade('setAdminSettings', {duration: 0.7});}, 1200 );
        } else {
          $("managerSettingsErrorMessage").innerHTML = response['message']
          Effect.Appear("managerSettingsErrorMessage");
          $('managerSettingsSpinner').hide();
        }
      }
    });
 }

/**
 * Hides admin password and admin email form
 */
 function hideSetAdminSettings() {
   $('setAdminSettings').remove();
 }

/**
 * Sets admin password and admin email
 */
 function saveAdminSettings() {
   var pw = $F('adminPassword');
   var email = $F('adminEmail');

   if(pw == '' && email == '' && cookiesEnabled()) {
     Effect.Fade('setAdminSettings', {duration: 0.7});
   } else {
     $("adminSettingsErrorMessage").update('');
     if (!checkAdminSettings(pw, email)) { return; }
     $('adminSettingsSpinner').show();

      new Ajax.Request("/" + BUCKET_URL + "/set_admin_settings", {
        parameters: $("setAdminSettingsForm").serialize(),
        onComplete: function(transport) {
          var response = transport.responseJSON;
          if (response['success'] == true) {
            $("adminSettingsErrorMessage").update("Success!");
            Effect.Appear("adminSettingsErrorMessage", {duration: 0.5});
            $('adminSettingsSpinner').hide();
            if($('adminPasswordLayover')) {
              setTimeout (function() {Effect.Fade('adminPasswordLayover', {duration: 0.7});}, 1200 );
            }
            setTimeout (function() {Effect.Fade('setAdminSettings', {duration: 0.7});}, 1200 );
          } else {
            $("adminSettingsErrorMessage").value = 'There was an error saving your settings.'
            Effect.Appear("adminSettingsErrorMessage");
            $('adminSettingsSpinner').hide();
          }
        }
      });
   }
 }

 var COOKIES_WARNING_MESSAGE = "Warning: cookies are currently disabled. In order for any of the admin features of your drop to work,<br /> you must enable cookies before filling out this form or refreshing the page. <a target='_none' style='text-decoration:underline' href='http://www.google.com/support/websearch/bin/answer.py?hl=en&answer=35851'>Click here for help.</a>";

 function checkAdminSettings(pw, email) {
   var error = "";

   if( !cookiesEnabled() ) {
    error = COOKIES_WARNING_MESSAGE;
    checkCookiesEnabled();
   }
   else if (email != '' && !validateEmail(email))
    error = "Invalid e-mail address"

   if (error != "") {
     $("adminSettingsErrorMessage").update(error);
     if($("adminSettingsErrorMessage").visible()) {
       Effect.Shake("adminSettingsErrorMessage", {distance: 5, duration: 0.2});
     } else {
       Effect.Appear($("adminSettingsErrorMessage"));
     }
     return false;
   } else {
     return true;
   }
 }

 function checkManagerSettings(pw, login) {
   var error = "";

   if( !cookiesEnabled() ) {
    error = COOKIES_WARNING_MESSAGE;
    checkCookiesEnabled();
   }
   else if (login == "" || pw == "")
    error = "You must enter both a login and password"

   if (error != "") {
     $("managerSettingsErrorMessage").update(error);
     if($("managerSettingsErrorMessage").visible()) {
       Effect.Shake("managerSettingsErrorMessage", {distance: 5, duration: 0.2});
     } else {
       Effect.Appear($("managerSettingsErrorMessage"));
     }
     return false;
   } else {
     return true;
   }
 }

 function createBucketArchive(button) {
   disableButton(button);
   if( $("shareContainer").visible() )
  	showGlobalSubnavigation("share"); // hide the tab
   modalWindow.hide();
   new Ajax.Request("/" + BUCKET_URL + "/archive", {
     parameters: "format=json",
     method: "post",
     onComplete: function(transport) {
       var response = transport.responseJSON;
       if( response["success"] == true ) {

       } else {

       }
     }
   });
 }

 function disableButton(element) {
   element.ancestors()[0].addClassName('disabledBorder');
   element.addClassName('disabled');
   element.writeAttribute('disabled', 'true');
 }

 function enableButton(element) {
   element.ancestors()[0].removeClassName('disabledBorder');
   element.removeClassName('disabled');
   element.writeAttribute('disabled', false);
 }

/**
 * A validation function for emails
 */
 function validateEmail(email) {
   var regex = /^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/;
   if (email.match(regex)) {
     return true;
   } else {
     return false;
   }
 }

function setDefaultView(view) {
  new Ajax.Request("/" + BUCKET_URL + "/update", {
    parameters: "bucket[default_view]=" + view + "&format=json",
    onComplete: function(transport) {
      var response = transport.responseJSON;
      if (response['success'] == true) {
      }
    }
  });
}

/**
 * Toggles the display of the note editor
 */
function displayNoteEditor(id) {
  $('asset_'+id).down(".note_content").hide();
  $('asset_edit_'+id).show();
  if( typeof(DropioNote) != "undefined" )
    DropioNote.getEditor("note_txtarea_"+id).insertHTML(""); // hack to focus iframe in IE
}

/**
 * Toggles the display of the note editor
 */
function hideNoteEditor(id) {
  $('asset_'+id).down(".note_content").show();
  $('asset_edit_'+id).hide();
}

/**
 * AJAX call to update a note
 */
function updateNote(id) {
  var content = DropioNote.getEditor("note_txtarea_"+id).content()

  $("note_txtarea_buttons_"+id).hide();
  $("note_txtarea_spinner_"+id).show();

	new Ajax.Request("/" + BUCKET_URL + "/update_note/"+id, {
	  method: 'post',
		parameters: {"value":content,"title":$('edit_note_title_'+id).value},
		onComplete: function(transport) {
  	  var res = transport.responseJSON;
  	  if( res && res["success"] ) {
        updateAsset(res["note"]);
    	  if( res["show_title"] == false ) {
    		  $$('#asset_' + id + ' h3').first().style.display = "none";
    		}
  	  }
  	  else {
  	    alert("Sorry, the note failed to save. Please try again.")
        $("note_txtarea_buttons_"+id).show();
        $("note_txtarea_spinner_"+id).hide();
  	  }
		}
	});
}

/**
 * Toggles the display of the link editor
 */
function displayLinkEditor(id) {
  $('asset_'+id).down(".link_content").hide();
  $('asset_edit_'+id).show();
}

/**
 * Toggles the display of the link editor
 */
function hideLinkEditor(id) {
  $('asset_'+id).down(".link_content").show();
  $('asset_edit_'+id).hide();
}

/**
 * AJAX call to update a link
 */
function updateLink(id) {
  var url = escape($('link_url_' + id).value);
  var title = escape($('link_title_' + id).value);
  var desc = escape($('link_desc_' + id).value);
  var asset_url = $('asset_' + id).readAttribute('name');

  $("link_buttons_" + id).hide();
  $("link_spinner_" + id).show();

	new Ajax.Request("/" + BUCKET_URL + "/asset/" + asset_url + "/update_link", {
	  method: 'post',
		parameters: "content=" + url + "&title=" + title + "&description=" + desc,
		onComplete: function(transport) {
  	  var res = transport.responseJSON;
  	  if( res && res["success"] ) {
        updateAsset(res["link"]);
  	  } else {
  	    alert("Sorry, the link failed to save. Please try again.")
        $("link_buttons_"+id).show();
        $("link_spinner_"+id).hide();
  	  }
		}
	});
}

/*
 * Used for inline posting of comments
 */

 function deleteComment(assetId, assetUrl, commentId) {
  	var answer = confirm("Are you sure you want to delete?")
  	if (answer) {
  		new Ajax.Request("/" + BUCKET_URL +"/asset/" + assetUrl + "/comment/" + commentId + "/destroy" , {
  		  postBody: '{"format": "json"}',
  		  method: 'put',
        contentType: 'application/json',
  			onComplete: function(transport){

          var response = transport.responseJSON;

  			  if(response.status == "success") {
  			    removeComment(assetId, {id:commentId, comment_count:response.count});
  			    if( isAssetOpenInModal(assetId))
  			      removeModalComment(commentId,true,true);
  			  }
  			}
  		})
  	}
}

function showStandardComments(assetId) {
  if($('comments_' + assetId)) {
    $('comments_' + assetId).remove();
  } else {
    clearAllActionTabs(assetId);

		var commentsContainer = new Element('div');
		commentsContainer.id = "comments_" + assetId;
    commentsContainer.className = "assetNotes actionTab clearfix";
		$('asset_' + assetId).insert(commentsContainer);
		commentsContainer.update("<div style='padding:8px 0px 0px 12px;'><img src='/images/spinner.gif' style='vertical-align:middle'> loading comments</div>");
    new Ajax.Request("/" + BUCKET_URL + "/assets/" + assetId + "/comments.json", {
      method: 'get',
      contentType: 'application/json',
      onComplete: function(transport) {
				if (false) {
					return
				}
				else {
        	var response = transport.responseJSON;
	        commentsList = new Element('ul');
	        commentsList.className = "comments"
	        commentsContainer.update(commentsList)
	        if( response['count'] == 0 ) {
            addFillerComment(assetId);
	        }
	        else
	          response['comments'].reverse().each(function(comment) { addStandardComment(assetId,comment,false,false) })
				}
        if(CAN_COMMENT) {
          commentsContainer.insert('<h4>Post a comment</h4>' +
            '<textarea id="commentBox_' + assetId + '"></textarea>' +
            "<a href=\"#\" onClick=\"postStandardComment(" + assetId + "); return false;\"><span class=\"btnBorder\" style=\"margin-top: 5px; margin-left: 10px; margin-bottom: 5px;\"><div id=\"postCommentButton\" class=\"btn\">Post Comment</div></span></a>");
        } else {
          commentsContainer.insert('<h4>Posting comments disabled</h4>');
        }

      }
    });
  }
}

function standardCommentExists(assetId,commentId) {
  return $("asset_"+assetId+"_comment_" + commentId) != null
}

function addStandardComment(assetId,comment,updateCount,animate) {
  if( !assetExists(assetId) ) return;

  if( standardCommentExists(assetId,comment['id']) ) return;

  if( updateCount ) setStandardCommentCount(assetId,comment.comment_count)

  commentsList = $('comments_' + assetId)
  if( commentsList ) {
     commentsList = commentsList.down('ul');

     if( ncsu = $("asset_"+assetId+"_comment_0") ) ncsu.remove(); // get rid of "no comments speak up!"

    var html = '<li id="asset_'+assetId+'_comment_' + comment['id'] + '" class="alternate clearfix" style="display:none">';
    if($('asset_' + assetId).readAttribute("comment_count") == "1" ||
      (commentsList.childElements().length > 0 && commentsList.childElements().last().hasClassName('alternate'))) {
      var html = '<li id="asset_'+assetId+'_comment_' + comment['id'] + '" class="clearfix" style="display:none">';
    }

    if( CAN_DELETE && comment['id'] != 0 )
      html += "<div class='inlineCommentDelete'><img src='/images/iconDelete.jpg' onclick='deleteComment("+assetId+",\""+getAssetData(assetId).name+"\"," + comment['id'] + ")'/></div>";

    html += '<div class="content" >' + comment['contents'].gsub("\\n", "<br />") + '</div>';

    if( d = commentDate(comment['updated_at']) )
      html += '<div class="date">' + d + '</div>';
    html += '</li>'

    commentsList.insert({ bottom: html });

    if( animate )
      new Effect.Appear("asset_"+assetId+"_comment_"+comment['id'],{duration:1,from:0,to:1});
    else
      $("asset_"+assetId+"_comment_" + comment['id']).style.display = "";
  }
}

function addFillerComment(assetId) {
  theContent = CAN_COMMENT ? "No comments - speak up!" : "No comments"
  addStandardComment(assetId,{contents: theContent, id:0},false,false)
}

function removeStandardComment(assetId,comment) {
  if( !assetExists(assetId) ) return;

  if( cwR = $("comments_" + assetId) ) {
    if( cR = cwR.down('ul').down("#asset_" + assetId + "_comment_"+comment.id) ) {
      setStandardCommentCount(assetId,comment.comment_count);
      cR.setAttribute("id","removing_comment_"+comment.id)
      new Effect.Fade(cR,{duration:1,from:1,to:0,afterFinish:function(){
        shouldAddFiller = $("removing_comment_"+comment.id).siblings().length == 0;
        $("removing_comment_"+comment.id).remove();
        recolorStandardComments(assetId)
        if( shouldAddFiller ) addFillerComment(assetId);
      }})
    }
  } else
    setStandardCommentCount(assetId,comment.comment_count);
}

function updateStandardComment(assetId,comment) {
  if( cwU = $("comments_" + assetId) )
    if( cU = cwU.down('ul').down("#asset_" + assetId + "_comment_"+comment['id']) )
      cU.remove();
  addStandardComment(assetId,comment,false,true);
}

function recolorStandardComments(assetId) {
  if( !assetExists(assetId) ) return;
  if( !(cwRC = $("comments_" + assetId)) ) return;
  if( !(cRC = cwRC.down('ul')) ) return;
  count = 0;
  ces = cRC.childElements();
  ces.each(function(li) {
    if( count != 0 ) {
      if( ces[count-1].readAttribute("class") == "clearfix" ) {
        li.setAttribute("class","alternate clearfix")
        li.setAttribute("className","alternate clearfix")
      } else {
        li.setAttribute("class","clearfix");
        li.setAttribute("className","clearfix")
      }
    }
    count = count + 1;
  });
}

function setStandardCommentCount(assetId,count) {
  return;

  if( !assetExists(assetId) ) return;

  type = $('asset_' + assetId).readAttribute("asset_type")


  asset = $('asset_' + assetId);
  asset.writeAttribute("comment_count", count);

  var cb = $$("#asset_"+assetId+" .commentCount")[0]
  if( cb ) {
    cb.down("span").update(count);
    if( parseInt(count) == 0 ) {
      cb.addClassName("count_0")
      cb.down("span").hide();
      cb.down(".commentBackground").show();
    } else {
      cb.removeClassName("count_0")
      cb.down("span").show();
      cd.down(".commentBackground").hide();
    }
  }
}

function commentDate(theDate) {
  if( !theDate ) return null;

  temp_date = new Date(theDate.replace(/-/g,"/"));
  hours = temp_date.getHours();
  ampm = "am";
  if(hours > 11) {
    ampm = "pm"
    if(hours > 12) {
      hours = hours - 12;
    }
  }

  if(hours == 0) {
    hours = 12;
  }

  date = temp_date.getDate();
  if(date < 10) {
    date = "0" + date;
  }

  minutes = temp_date.getMinutes();
  if(minutes < 10) {
    minutes = "0" + minutes;
  }

  month = temp_date.getMonth() + 1
  day = date
  year = temp_date.getFullYear()

  return month + "." + day + "." + year + " (" + hours + ":" + minutes + ampm + ")"
}

/*
 * Used for inline posting of comments
 */
function postStandardComment(assetId) {
  var tempResult = $("commentBox_" + assetId).value;
  $('commentBox_' + assetId).value = "";
  new Ajax.Request("/" + BUCKET_URL + "/assets/" + assetId + "/post_comment", {
    postBody: '{"comment": {"content": ' + tempResult.toJSON() + '}, "format": "json", "_method": "put"}',
    method: 'put',
    contentType: 'application/json',
    onComplete: function(transport) {
      var response = transport.responseJSON;
      if(response['status'] == 'success') {
          comment_hash = response['comment'];
          comment_hash.comment_count = response['count'];
          addStandardComment(assetId,comment_hash,true);
      }
    }
  });
}

/*
 * Sidebar Module Drag & Drop Functionality
 */

function editSidebar() {
  thisSidebar.edit();
}

function removeModule(moduleName) {
  thisSidebar.remove(moduleName);
}

var CustomSidebar = Class.create({
  initialize: function() {
    this.modules = $$('#sidebar .wrapperSidebar');
    this.editing = false;
    this.sidebar = $('sidebar');
    this.sidebarContainer = $('sidebarContainer');
  },

  getModules: function() {
    return this.modules;
  },

  edit: function() {
    if(!this.editing) {
      this.editing = true;
      this.sidebarContainer.style.width = "220px";
      this.sidebarContainer.style.position = "relative";
      this.sidebarContainer.style.left = "10px";
      this.sidebarContainer.down("#editContainer a").update("Finish customizing");
      this.sortable = Sortable.create('sidebar', {
        tag: 'div'
      });
      this.sidebar.addClassName("editingSidebar");
      $('editContainer').insert({bottom: '<div id="addMoreModulesContainer" style="display:none;"><a id="addMoreModules" href="/' + BUCKET_URL + '/admin/sidebar">Add more widgets</a></div>'});
      this.modules.each(function(module) {
        module.insert({top: '<div class="overlay"></div>'});
        module.insert({top: '<a href="#" onClick="removeModule(\'' + module.id + '\'); return false;" class="remove"></a>'});
      });
      Effect.SlideDown($('addMoreModulesContainer'), {duration: 0.3});
    } else {
      this.save();
      Sortable.destroy('sidebar');
      this.sidebarContainer.style.width = "200px";
      this.sidebarContainer.style.position = "";
      this.sidebarContainer.style.left = "";
      this.sidebarContainer.down("#editContainerLink").update("Customize this sidebar");
      this.sidebar.removeClassName("editingSidebar");
      this.editing = false;
      $('addMoreModulesContainer').remove();
      $$('#sidebar .overlay').invoke('remove');
      $$('#sidebar .remove').invoke('remove');
    }
  },

  remove: function(moduleName) {
    $(moduleName).remove();
  },

  save: function() {
    var sidebarOrder = ""
    $$('.wrapperSidebar').each(function(module) {
      sidebarOrder += module.id + " "
    });

    new Ajax.Request("/" + BUCKET_URL +"/reorder_sidebar", {
		  parameters: "sidebar_order="+escape(sidebarOrder)
		});
  }
});

var PasswordDisplay = Class.create({
  initialize: function(passId,triggerId) {
    this.passwordField = $(passId);
    this.passwordDisplay = new Element("input",{"type":"text","class":this.passwordField.className,"style":this.passwordField.cssText,"id":this.passwordField.id + "_passwordDisplay"});
    this.passwordDisplay.style.display = "none";
    this.passwordField.insert({after:this.passwordDisplay})
    this.trigger = $(triggerId);
    this.showPlain = false;
    this.trigger.observe("click",this.toggle.bind(this));
    this.passwordField.observe("keyup",this.updateDisplay.bind(this));
    this.passwordDisplay.observe("keyup",this.updateField.bind(this));
  },

  updateDisplay: function() {
    this.passwordDisplay.value = this.passwordField.value
    if(this.passwordField.value == "") {
      this.trigger.hide();
    } else {
      this.trigger.show();
    }
  },

  updateField: function() {
    this.passwordField.value = this.passwordDisplay.value
    if(this.passwordField.value == "") {
      this.trigger.hide();
      this.hide();
      this.passwordField.focus();
    } else {
      this.trigger.show();
    }
  },

  toggle: function() {
    if( !this.showPlain ) {
      this.show();
    }
    else {
      this.hide();
    }
  },

  show: function() {
    this.showPlain = true;
    this.trigger.update("(hide password)");
    this.passwordField.hide();
    this.passwordDisplay.show();
  },

  hide: function() {
    this.trigger.update("(show password)");
    this.showPlain = false;
    this.passwordField.show();
    this.passwordDisplay.hide();
  }
});

function getAssetHost() {
  if( ASSET_HOST_TEMPLATE ) {
    var randNum = Math.floor(Math.random()*4) + 1;
    return ASSET_HOST_TEMPLATE.interpolate({which:randNum});
  }
  return "http://static-1.drop.io"
}

function randomNumber(len) {
	Math.round((Math.random() * len))
}
/**
 * Context menu class
 * requires: prototype.js
 */
var ContextMenu = Class.create({

  /**
   * Initialize variables and set up event listeners
   * The callback is called right before the context menu is displayed
   * this is so any dynamic contant can be placed on the menu (like
   * the comment count).
   * @constructor
   */
  initialize: function(css_selector, callback) {
    if(typeof(Prototype) == "undefined")
      throw "ContextMenu requires Prototype to be loaded.";

    this.menuGroups = [];
    this.isVisible = false;
    this.menuDisplay = new Element('div');

    this.menuDisplay.id = 'context_menu';
    this.menuDisplay.className = 'context_menu';
    this.menuDisplay.style.position = 'absolute';
    this.menuDisplay.style.zIndex = '99998';
    this.menuDisplay.style.display = "none";
    this.cssSelector = css_selector;
    this.callback = callback;

    Event.observe(window, "load", function(e){
		this.findElementsToObserve(e);

      document.observe('mousedown', function(e) {
        if(this.isVisible) {
          actionItem = this.actionItemClicked(e)
          if(e.isLeftClick() && (actionItem)){
            actionItem.action();
            this.hide(e);
          }else if(e.isLeftClick()) {
            this.hide(e);
          }
        }

      }.bind(this))

      Element.insert(document.body, this.menuDisplay, {postition: 'bottom'});
    }.bind(this));

  },

  actionItemClicked: function(e) {
    if(e.target.action) {
      actionItem = e.target;
    } else if(this.isDescendantOfActionItem(e.target)) {
      actionItem = this.isDescendantOfActionItem(e.target);
    } else {
		  actionItem = false;
		}
		return actionItem
  },

  findElementsToObserve: function(e) {

      $$(this.cssSelector).each(function(temp_element) {
        this.register(temp_element);
      }.bind(this));
  },

  register: function(temp_element) {
    temp_element.observe('contextmenu', function(e) { this.beginShow(e) }.bind(this));
    var openMenu = temp_element.down(".openMenu");
    if( openMenu )
      openMenu.observe('click', function(e) { this.beginShow(e) }.bind(this));
  },

  beginShow: function(e) {
    found_class = false;
    if(e.target.className != "") {
      class_names = e.target.className.split(" ");
      class_names.each(function(temp_class_name) {
        if(this.cssSelector.substring(1) == temp_class_name) {
          found_class = true;
        }
      }.bind(this));
    }

    if(found_class) {
      this.selectedElement = e.target;
    } else {
      this.selectedElement = Element.up(e.target, this.cssSelector);
    }

    e.stop();
    this.show(e);
  },

  setSelectedElement: function(element) {
    this.selectedElement = element;
  },

  isDescendantOfActionItem: function(theElement) {
    foundAncestor = null;
    $(theElement).ancestors().each(function(tempAncestor) {
      if(tempAncestor.action) {
        foundAncestor =  tempAncestor;
      }
    });

    return foundAncestor;
  },

  /**
   * Shows the context menu
   */
  show: function(e) {
    selected_assets = $$(".lassoLoopSelected");
  	if (selected_assets.length > 1) {
  		var initial_type = selected_assets[0].readAttribute("asset_type");
  		var all_same_type = true;
  		selected_assets.each(function(asset){
  			if (initial_type != asset.readAttribute("asset_type")) {
  				all_same_type = false;
  			}
  		});
  		if (all_same_type) {
  			visible_type = initial_type;
  		} else {
  			visible_type = false;
  		}
  	} else {
  		visible_type = this.selectedElement.getAttribute('asset_type');
  	}

    this.menuGroups.each(function(tempMenuGroup) {
      class_names = tempMenuGroup.htmlDisplay.className.split(" ");
      found_visible_class = false;
      class_names.each(function(tempClassName) {
        if(tempClassName == 'all_types' || tempClassName == visible_type) {
          found_visible_class = true;
        }
      });

      if(found_visible_class) {
        Element.insert(this.menuDisplay, tempMenuGroup.htmlDisplay);
      } else if(tempMenuGroup.htmlDisplay.parentNode != null) {
        tempMenuGroup.htmlDisplay.remove();
      }
    }.bind(this));

    if(this.callback != null) {
      this.callback();
    }

    this.menuGroups.each(function(tempMenuGroup) {
      tempMenuGroup.itemMembers.each(function(tempMenuItem){
        this.checkVisibility(tempMenuItem, visible_type);

        if(tempMenuItem.submenu) {
          var tempSubMenuItems = tempMenuItem.submenu.itemMembers;
          tempSubMenuItems.each(function(tempSubMenuItem) {
            this.checkVisibility(tempSubMenuItem, visible_type);
          }.bind(this));
        }

    		if(tempMenuItem.css_requirement != null) {
          var hasCssSelector = this.selectedElement.getAttribute(tempMenuItem.css_requirement);
          if(hasCssSelector == null || hasCssSelector == "") {
            tempMenuItem.htmlDisplay.hide();
          }
        }

    		if(tempMenuItem.disallowed_views != null && tempMenuItem.disallowed_views.split(" ").indexOf(VIEW) != -1) {
            tempMenuItem.htmlDisplay.hide();
        }

    		if(selected_assets.length > 1 && !tempMenuItem.can_do_multiple()) {
    			tempMenuItem.htmlDisplay.hide();
    		}

      }.bind(this));
    }.bind(this));

    var xPos = (Event.pointerX(e) + 5);
    var yPos = (Event.pointerY(e) + 3);
    var dOffsets = document.viewport.getScrollOffsets();

    if( xPos + parseInt(this.menuDisplay.getDimensions().width) < dOffsets.left + document.viewport.getWidth() )
      this.menuDisplay.style.left = xPos  + "px";
    else
      this.menuDisplay.style.left = (xPos - parseInt(this.menuDisplay.getDimensions().width)) + "px";

    if( yPos + parseInt(this.menuDisplay.getDimensions().height) < dOffsets.top + document.viewport.getHeight() )
      this.menuDisplay.style.top = yPos + "px";
    else
      this.menuDisplay.style.top = (yPos - parseInt(this.menuDisplay.getDimensions().height)) + "px";

    this.menuDisplay.style.display = ""
    this.isVisible = true;
  },

  checkVisibility: function(item, type) {
    found_visible_class = false;
    class_names = item.display_types.split(" ");
    class_names.each(function(tempClassName) {
      if(tempClassName == 'all_types' || tempClassName == type) {
        found_visible_class = true;
      }
    });
    if(found_visible_class) {
      item.htmlDisplay.show();
    } else {
      item.htmlDisplay.hide();
    }
  },

  /**
   * Hides the context menu
   */
  hide: function(e) {
    this.menuDisplay.style.display = "none";
    this.isVisible = false;

    return true;
  },

  /**
   * Adds a new menu group to this context menu
   */
  addMenuGroup: function(newMenuGroup, type) {
    if(this.menuGroups.length > 0) {
      tempMenuGroup = this.menuGroups[this.menuGroups.length - 1]
      tempMenuGroup.htmlDisplay.addClassName('menu_group');
    }
    if(type) {
      newMenuGroup.htmlDisplay.addClassName(type);
    } else {
      newMenuGroup.htmlDisplay.addClassName("all_types");
    }

    this.menuGroups.push(newMenuGroup);
  }

});

/**
 * MenuGroup class. This holds an array of MenuItems and
 * SubmenuItems. Each MenuGroup will be separated from other
 * MenuGroups by a divider (like an <hr />). The actual context
 * menu consists of an array of MenuGroups, which in turn contain
 * an array of MenuItems and SubmenuItems.
 */
var MenuGroup = Class.create({
  /**
   * Create the Div element for the MenuGroup and set up itemMembers array
   * @constructor
   */
  initialize: function() {
    this.itemMembers = []
    this.htmlDisplay = new Element('ul');

  },

  /**
   * Adds a new menu item (either menuItem or submenuItem) to this menu group
   */
  add: function(menuItem) {
    this.itemMembers.push(menuItem)
    Element.insert(this.htmlDisplay, menuItem.htmlDisplay);
  }
});

/**
 * SubmenuItem class. This is a MenuItem which when hovered over
 * will expand and show and additional menu with more options. It
 * contains an array of MenuItems (or more SubmenuItems).
 */
var SubmenuItem = Class.create({
  /**
   * Initialize variables and set up event handlers
   * @constructor
   */
  initialize: function(displayText, submenu, options) {
  	options = $H(options);
    this.display_types = options.get('display_types')
    if(this.display_types == null) {
      this.display_types = 'all_types';
    }

    this.displayText = displayText;
    this.submenu = submenu;
    this.showingSubmenu = false;

    this.htmlDisplay = new Element('li');
    this.htmlDisplay.update("<img id='context_more' src='" + getAssetHost() + "/images/context_more.png' style='float:right;margin-top:3px;margin-right:8px;' /><div style='margin-left: 23px;'>" + this.displayText + "</div>");
    this.htmlDisplay.style.cursor = "pointer";
    this.htmlDisplay.className = "submenu_item";

    this.htmlDisplay.observe('mouseover', function(e) {
      this.htmlDisplay.addClassName("selected_submenu_item");
      this.submenu.htmlDisplay.style.top = (this.htmlDisplay.cumulativeOffset().top) + "px";
      this.submenu.htmlDisplay.style.left = (this.htmlDisplay.cumulativeOffset().left + this.htmlDisplay.clientWidth - 1) + "px";
      this.submenu.htmlDisplay.style.display = "";
    }.bind(this));

    this.htmlDisplay.observe('mouseout', function(e) {
      this.submenu.htmlDisplay.style.display = "none";
      this.htmlDisplay.removeClassName("selected_submenu_item");
    }.bind(this));
  },

  can_do_multiple: function() {
	  can_do_multiple_answer = false;
  	this.submenu.itemMembers.each(function(item){
  		if (item.can_do_multiple()) {
  			can_do_multiple_answer = true;
  		}
  	});
  	return can_do_multiple_answer;
  }
});

var Submenu = Class.create({
  /**
   * Create the Div element for the MenuGroup and set up itemMembers array
   * @constructor
   */
  initialize: function() {

    this.itemMembers = [];
    this.htmlDisplay = new Element('ul');

    this.htmlDisplay.className = 'submenu';
    this.htmlDisplay.style.display = "none";
    this.htmlDisplay.style.position = "absolute";
    this.htmlDisplay.style.backgroundColor = "#fff";
    this.htmlDisplay.style.borderTop = "solid 1px #ccc";
    this.htmlDisplay.style.borderLeft = "solid 1px #ccc";
    this.htmlDisplay.style.borderBottom = "solid 2px #ccc";
    this.htmlDisplay.style.borderRight = "solid 2px #ccc";
    this.htmlDisplay.style.listStyleType = "none";
    this.htmlDisplay.style.zIndex = "99999";


    Event.observe(window, "load", function(e){
      Event.observe(this.htmlDisplay, 'mouseover', function(e) {
        Element.addClassName(this.htmlDisplay, "selected_menu_item");
        this.htmlDisplay.style.display = "";
      }.bind(this));

      Event.observe(this.htmlDisplay, 'mouseout', function(e) {
        Element.removeClassName(this.htmlDisplay, "selected_menu_item");
        this.htmlDisplay.style.display = "none";
      }.bind(this));

      Element.insert(document.body, this.htmlDisplay, {postition: 'bottom'});
    }.bind(this));
  },

  /**
   * Adds a new menu item (either menuItem or submenuItem) to this menu group
   */
  add: function(menuItem) {
    this.itemMembers.push(menuItem)
    Element.insert(this.htmlDisplay, menuItem.htmlDisplay);
  }

});

/**
 * MenuItem Class. This is the class which represents an action
 * menu item which will cause an action (rotate image, delete asset, etc).
 */
var MenuItem = Class.create({
  /**
   * Initialize variables and set up event handlers
   * @constructor
   */
  initialize: function(displayText, action, options) {
  	options = $H(options);
  	this.css_requirement = options.get("css_requirement");
  	this.disallowed_views = options.get("disallowed_views");
  	icon_url = options.get("icon_url");
  	this.can_do_multiple_attribute = options.get("can_do_multiple");

    this.display_types = options.get("display_types");
    if(this.display_types == null) {
      this.display_types = 'all_types';
    }

    if(displayText != null) {
      this.displayText = displayText;
    } else {
      this.displayText = "";
    }

    if(action != null) {
      this.action = action;
    } else {
      this.action = null;
    }

    this.htmlDisplay = Object.extend(new Element('li'), { action: this.action });
    this.htmlDisplay.id = "action_item";
    if(icon_url) {
      this.htmlDisplay.update("<div class=\"icon_holder\"><img src="+ getAssetHost() + icon_url + " /></div><span class=\"displayText\" >" + this.displayText + "</span>");
    }
    else {
      this.htmlDisplay.update("<div class=\"icon_holder\">&nbsp;</div><span class=\"displayText\">" + this.displayText) + "</span>";
    }
    this.htmlDisplay.style.cursor = "pointer";
    this.htmlDisplay.className = "menu_item " + displayText;

    this.htmlDisplay.observe('mouseover', function(e) {
      this.htmlDisplay.addClassName("selected_menu_item");
      this.htmlDisplay.style.color = "#fff";
    }.bind(this));

    this.htmlDisplay.observe('mouseout', function(e) {
      this.htmlDisplay.removeClassName("selected_menu_item");
      this.htmlDisplay.style.color = "#555";
    }.bind(this));

  },

  can_do_multiple: function() {
	  return this.can_do_multiple_attribute;
  }
});


function findClassName(element, className) {
  found_class = false;
  class_names = element.className.split(" ");
  class_names.each(function(temp_class_name) {
    if(temp_class_name == className) {
      found_class = true;
    }
  });
  return found_class;
}

/**
 * BEGIN STANDARD CONTEXT MENU ACTIONS
 */

/**
 * assumes that css_selector is a sincle css class of
 * the format '.className' (with the leading period)
 */
function show_asset_preview(selected_element) {

  if( curModalAssetId() != null ) {
    modalWindow.refresh();
    return;
  }

  asset = selected_element;
  assetIdParts = asset.getAttribute('id').split("_");
  type = asset.readAttribute("asset_type");
  assetId = asset.readAttribute("asset_id");
  if(type == "document") {
    modalWindow.loadScribdDoc(assetId);
  } else {
    modalWindow.load(assetId);
  }
}

function go_to_asset(selected_element) {
  url = selected_element.getAttribute("single_asset_url");
  if( url ) window.location = url;
}

function show_asset_comments(selected_element) {
  if( curModalAssetId() != null )
    showDrawer("comments");
  else {
    var assetId = parseInt(selected_element.readAttribute("asset_id"));
    var type = selected_element.readAttribute("asset_type");
    if( VIEW != "system" && ["image","movie"].indexOf(type) == -1 )
      showStandardComments(assetId);
    else
      modalWindow.load(assetId, {showComments:true});
  }
}

function download_asset(selected_element) {
  download_url = selected_element.getAttribute("save_url");
  DropioStream.RemoteControl.downloadAsset(download_url);
  window.location = download_url;
}

function show_rename_asset(selected_element) {
  if( VIEW == "asset" && singleAssetNameEditor )
    singleAssetNameEditor.enterEditMode();
  else if( curModalAssetId() == null ) {
    var renamewhat = selected_element.readAttribute('name')
    if( $$(".lassoLoopSelected").length > 1 )
      renamewhat = $$(".lassoLoopSelected").length + " Files";
    show_action_box($('renameAsset'), renamewhat);

  } else if( modalWindowRenameEditor )
    modalWindowRenameEditor.enterEditMode();
}

function show_asset_links(selected_element) {
  if( curModalAssetId() == null ) {
    var assetId = selected_element.readAttribute("asset_id");
    load_action_box("/" + BUCKET_URL + '/asset_links/' + assetId );
  } else
    showDrawer('link');
}

function edit_asset(selected_element) {
  var assetId = parseInt(selected_element.readAttribute("asset_id"));
  var assetType = selected_element.readAttribute("asset_type");
  if( curModalAssetId() != null ) modalWindow.hide();
  if( assetType == "note" )
    displayNoteEditor(assetId);
  else if( assetType == "link" )
    displayLinkEditor(assetId);
  else if ( assetType == "image" ) {
    var url = selected_element.readAttribute("aviary_url");
    if( url )
      window.location = url;
  }
}

function show_embed_code(selected_element) {
  if( curModalAssetId() == null )  {
    var assetId = selected_element.readAttribute("asset_id");
    load_action_box("/" + BUCKET_URL + '/asset_embed/' + assetId);
  } else
    showDrawer('embed');
}

function show_edit_description(selected_element) {
	if( curModalAssetId() == null )  {
	  var assetId = selected_element.readAttribute("asset_id");
	  load_action_box("/" + BUCKET_URL + '/asset_description/' + assetId)
	} else if( typeof(modalWindowDescriptionEditor) != "undefined" )
	  modalWindowDescriptionEditor.enterEditMode("click");
}

function save_asset_description(assetId) {
	if( $("editAssetDescriptionForm") ) {
		new Ajax.Request('/' + BUCKET_URL + "/asset/" + assetId + "/update_description", {
			method: "POST",
			parameters: $("editAssetDescriptionForm").serialize(),
			onComplete: function(transport) {
				var res = transport.responseJSON;

				if( res['success'] ) {
					$$("#modalWindow .success")[0].appear();
					setTimeout(function() { modalWindow.hide() }, 3000);
				} else
					$$("#modalWindow .errors")[0].appear();
			}
		})
	}
}

function show_send_to(which) {
  var contents = { "drop" : "sendToAnotherDrop",
                   "email" : "sendToEmail",
                   "phone" : "sendToMMS",
                   "fax" : "sendToFax" }
  if( curModalAssetId() == null ) {
    var asset_name = rightClickMenu.selectedElement.readAttribute("name");
    if( $$(".lassoLoopSelected").length > 1 )
      asset_name = $$(".lassoLoopSelected").length + " Files";
    asset_name += " ";
    var asset_id = rightClickMenu.selectedElement.readAttribute("asset_id");
    $$("#" + contents[which] + " h2 span")[0].update(asset_name.truncate(40));
    $$("#" + contents[which] + " .sendToFormContainer")[0].writeAttribute("asset_id",asset_id);
    show_action_box($(contents[which]))
  } else
    showDrawer("modal_" + contents[which])
}

function delete_assets() {
  var selected_asset = rightClickMenu.selectedElement;

  var conf = "Are you sure you want to delete"
  if( $$(".lassoLoopSelected").length > 1 ) conf += " these " + $$(".lassoLoopSelected").length + " files";
  else conf += " " + selected_asset.readAttribute("name").truncate(30);
  conf += "?"

	var answer = confirm(conf);
	if (answer) {
		modalWindow.hide(); // in case we are on System View, calling delete from the modal preview window (ack!)
		var ids = [];

		var selected_assets = $$(".lassoLoopSelected");
		if (selected_assets.length > 0) {
			selected_assets.each(function(asset){
				ids.push(asset.getAttribute("asset_id"));
			});
		} else {
			ids.push(selected_asset.getAttribute('asset_id'));
			selected_assets.push(selected_asset)
		}

		var parameters = "ids[]=";
		parameters += ids.join("&ids[]=");

		new Ajax.Request("/" + BUCKET_URL+"/asset/destroy", {
			parameters: parameters,
			onComplete: function(transport){
				selected_assets.each(function(asset){
					if( VIEW != "asset" ) Effect.Fade(asset);
					else window.location = "/" + BUCKET_URL;
				});
			}
		});
		return true;
	}
	return false;
}

function center_actionbox() {
	$("action_box").style.left = (document.viewport.getDimensions().width / 2) - ($("action_box").getWidth() / 2) + "px";
	$("action_box").style.top = (document.viewport.getDimensions().height / 2) - ($("action_box").getHeight() / 2) + document.viewport.getScrollOffsets().top + "px";
}

function show_action_box(content,asset_name) {
  $$(".submenu").each(function(tempMenu) {
    tempMenu.hide();
  });
  if( asset_name ) content.down(".asset_name").update(asset_name);
  modalWindow.showCustomModal('<div id="assetContent" class="customModal" style="overflow:hidden">' + content.innerHTML + '</div>');
}

function load_action_box(url) {
  $$(".submenu").each(function(tempMenu) {
    tempMenu.hide();
  });
  modalWindow.loadCustomModal(url);
}

function retry_conversion(asset) {
  if( asset ) {
    var url = asset.readAttribute("retry_conversion_url");
    if( url )
      window.location = url;
  }
}

function send_assets_to_drop(selected_asset) {
	var ids = [];
	var selected_assets = $$(".lassoLoopSelected");
	if (selected_assets.length > 0) {
		selected_assets.each(function(asset){
			ids.push(asset.getAttribute("asset_id"));
		});
	} else if (selected_asset.getAttribute('asset_id') != null) {
		ids.push(selected_asset.getAttribute('asset_id'));
		selected_assets.push(selected_asset)
	} else if ((selected_asset.getAttribute('id') != null) &&
			       (selected_asset.getAttribute('id').match('asset_'))) {
	    parts = selected_asset.getAttribute('id').split('_');
	    asset_id = parts[parts.length - 1];
	    ids.push(asset_id);
 	}

	var parameters = "send_to=drop";
	parameters += "&bucket_url="+$$("#modalWindow .bucket_url")[0].value;
	parameters += "&ids[]=";
	parameters += ids.join("&ids[]=");


	new Ajax.Request("/" + BUCKET_URL +"/asset/send", {
		parameters: parameters,
		onComplete: function(transport) {
			response = eval("("+transport.responseText+")");
			if(response['success'] == true) {
			  $$("#modalWindow .sendToDrop .failure")[0].hide();
			  Effect.Appear($$("#modalWindow .sendToDrop .success")[0]);
			  setTimeout(function() { modalWindow.hide() }, 2000);
			}
			else {
			  $$("#modalWindow .sendToDrop .success")[0].hide();
			  Effect.Appear($$("#modalWindow .sendToDrop .failure")[0]);
			}
		}
	});
}

function email_assets(selected_asset) {
	var ids = [];

	var selected_assets = $$(".lassoLoopSelected");
	if (selected_assets.length > 0) {
		selected_assets.each(function(asset){
			ids.push(asset.getAttribute("asset_id"));
		});
	} else if (selected_asset.getAttribute('asset_id') != null) {
		ids.push(selected_asset.getAttribute('asset_id'));
		selected_assets.push(selected_asset)
	} else if ((selected_asset.getAttribute('id') != null) &&
			       (selected_asset.getAttribute('id').match('asset_'))) {
	    parts = selected_asset.getAttribute('id').split('_');
	    asset_id = parts[parts.length - 1];
	    ids.push(asset_id);
 	}
	var parameters = "send_to=email";
	parameters += "&emails="+ $$("#modalWindow .email_addresses")[0].value;
  parameters += "&message="+ $$("#modalWindow .email_message")[0].value;
  parameters += "&ids[]=";
	parameters += ids.join("&ids[]=");

  new Ajax.Request("/" + BUCKET_URL +"/asset/send", {
		parameters: parameters,
		onComplete: function(transport) {
			response = eval("("+transport.responseText+")");
      if(response['success'] == true) {
			  $$("#modalWindow  .sendToEmail .failure")[0].hide();
			  Effect.Appear($$("#modalWindow  .sendToEmail .success")[0]);
			  setTimeout(function() { modalWindow.hide() }, 2000);
			}
			else {
			  $$("#modalWindow .sendToEmail .success")[0].hide();
			  Effect.Appear($$("#modalWindow .sendToEmail .failure")[0]);
			}
		}
	})
}

function send_assets_to_phone(selected_asset) {
	var ids = [];

	var selected_assets = $$(".lassoLoopSelected");
	if (selected_assets.length > 0) {
		selected_assets.each(function(asset){
			ids.push(asset.getAttribute("asset_id"));
		});
	} else if (selected_asset.getAttribute('asset_id') != null) {
		ids.push(selected_asset.getAttribute('asset_id'));
		selected_assets.push(selected_asset)
	} else if ((selected_asset.getAttribute('id') != null) &&
			       (selected_asset.getAttribute('id').match('asset_'))) {
	    parts = selected_asset.getAttribute('id').split('_');
	    asset_id = parts[parts.length - 1];
	    ids.push(asset_id);
 	}

	var parameters = "send_to=phone";
	parameters += "&phone_number="+$$("#modalWindow .phone_number")[0].value;
	parameters += "&cell_provider[id]="+$$("#modalWindow .cell_provider_id")[0].value;
	parameters += "&ids[]=";
	parameters += ids.join("&ids[]=");

	new Ajax.Request("/" + BUCKET_URL +"/asset/send", {
		parameters: parameters,
		onComplete: function(transport) {
			response = eval("("+transport.responseText+")");
  		if(response['success'] == true) {
			  $$("#modalWindow  .sendToMMS .failure")[0].hide();
			  Effect.Appear($$("#modalWindow  .sendToMMS .success")[0]);
			  setTimeout(function() { modalWindow.hide() }, 2000);
			}
			else {
			  $$("#modalWindow .sendToMMS .success")[0].hide();
			  Effect.Appear($$("#modalWindow .sendToMMS .failure")[0]);
			}
		}
	});
}

function send_assets_to_fax(selected_asset) {
	var ids = [];
	var selected_assets = $$(".lassoLoopSelected");
	if (selected_assets.length > 0) {
		selected_assets.each(function(asset){
			ids.push(asset.getAttribute("asset_id"));
		});
	} else if (selected_asset.getAttribute('asset_id') != null) {
		ids.push(selected_asset.getAttribute('asset_id'));
		selected_assets.push(selected_asset)
	} else if ((selected_asset.getAttribute('id') != null) &&
			       (selected_asset.getAttribute('id').match('asset_'))) {
	    parts = selected_asset.getAttribute('id').split('_');
	    asset_id = parts[parts.length - 1];
	    ids.push(asset_id);
 	}

	var parameters = "send_to=fax";
	parameters += "&fax_number="+$$("#modalWindow .fax_num")[0].value;
	parameters += "&ids[]=";
	parameters += ids.join("&ids[]=");

	new Ajax.Request("/" + BUCKET_URL +"/asset/send", {
		parameters: parameters,
		onComplete: function(transport) {
			response = eval("("+transport.responseText+")");
      if(response['success'] == true) {
			  $$("#modalWindow  .sendToFax .failure")[0].hide();
			  Effect.Appear($$("#modalWindow  .sendToFax .success")[0]);
			  setTimeout(function() { modalWindow.hide() }, 2000);
			}
			else {
			  $$("#modalWindow .sendToFax .success")[0].hide();
			  Effect.Appear($$("#modalWindow .sendToFax .failure")[0]);
			}
		}
	})
}

var rotations = [];
function rotate_asset(selected_asset, amount) {
  $$(".submenu").each(function(tempMenu) {
    tempMenu.hide();
  });

  selected_assets = $$('.lassoLoopSelected');
  if (selected_assets.length == 0) {
	selected_assets.push(selected_asset);
  }

  for (i = 0; i++; i < selected_assets.length) {
	if ((rotations[selected_assets[i].getAttribute("asset_id")]) ||
		(!amount || amount == 0) ||
		(!selected_asset || !selected_assets[i].getAttribute("asset_id") || !selected_assets[i].getAttribute("id"))) {
		selected_assets[i].pop();
	}
  }

  var the_image;
  var overlay_div;
  var asset_id;

  selected_assets.each(function(asset){
	if (!rotations[asset.getAttribute("asset_id")]){
	    the_image = asset.down("img");
	    overlay_div = new Element('div');
		asset_id = asset.getAttribute("asset_id");

	    overlay_div.addClassName("rotate_overlay")
	    overlay_div.setStyle('position: absolute');
	    overlay_div.setStyle('background-color: #fff');
	    overlay_div.setStyle('background-image: url("/images/conversion_spinner.gif");');
	    overlay_div.setStyle('background-repeat: no-repeat');
	    overlay_div.setStyle('background-position: center center');
	    overlay_div.setStyle('left:0px;');
	    overlay_div.setStyle('width:100%');
	    overlay_div.setStyle('top:0px;');
	    overlay_div.setStyle('height:100%');
	    overlay_div.setOpacity(0.7);

	    asset.insert(overlay_div, {position: top});

	    rotations[asset_id] = true;

	    new Ajax.Request("/" + BUCKET_URL + "/assets/" + asset_id + "/rotate", {
	      postBody: '{"asset": {"amount": ' + amount + '}, "format": "json", "_method": "put"}',
	      method: 'put',
	      contentType: 'application/json',
	      onComplete: function(transport) {
	        var response = transport.responseJSON;
	        if (response['status'] == 'queued' || response['status'] == 'in_progress') {
	          setTimeout(function() { check_rotate_process(asset) }, 2000);
	          return;
	        } else {
	          alert('done')
	        }

	        draw_assets();
	      }
	    });
	}
  });

}

function check_rotate_process(asset) {
  new Ajax.Request("/" + BUCKET_URL + "/assets/" + asset.getAttribute("asset_id") + "/rotate_status.json", {
    method: 'get',
    contentType: 'application/json',
    onComplete: function(transport) {
      var response = transport.responseJSON;
      if (response['status'] == 'success') {
        asset.down(".rotate_overlay").remove();
        rotations[asset.getAttribute("asset_id")] = null;
      } else if (response['status'] == 'in_progress') {
        setTimeout(function() { check_rotate_process(asset) }, 2000);
      }
    }
  });
}

function rename_assets(selected_element) {
	var ids = [];
	var selected_assets = $$(".lassoLoopSelected");
	if (selected_assets.length > 0) {
		selected_assets.each(function(asset){
			ids.push(asset.getAttribute("asset_id"));
		});
	} else {
    var id = selected_element.getAttribute('id');
    selected_asset = $(id);
    selected_assets.push(selected_asset);
		ids.push(selected_asset.getAttribute('asset_id'));
	}

  var i = 0;
  var data = new Array();
  var new_name = $$('#modalWindow .rename_to')[0].value;
  ids.each(function(id) {
    var name = new_name;
    if(i != 0) {
      var name = name + '-' + (i + 1);
    }

    data.push('{"id":' + id + ', "name": "' + name + '"}');
    i++;
  });
  data = data.join(',')

  new Ajax.Request("/" + BUCKET_URL + "/assets", {
    postBody: '{"assets": [' + data + '], "format": "json", "_method": "put"}',
    method: 'put',
    contentType: 'application/json',
    onComplete: function(transport) {
      var response = transport.responseJSON;
      response.each(function(obj) {
        if($('system_view')) {
          assets.each(function(s) {
            if(s['id'] == obj['id']) {
              s['name'] = obj['title'];
              s['first_letter'] = obj['title'].substring(0,1).toUpperCase();
            }
          });
          draw_assets();
        } else {
          selected_assets.each(function(s) {
            var h3 = s.getElementsBySelector("H3")[0];
            if(h3 != null) {
              var a = h3.getElementsBySelector("A")[0];
              if(a != null) {
                a.innerHTML = obj['title'];
              } else {
                h3.innerHTML = obj['title'];
              }
            }
          });
        }
      });

	  if( $$("#modalWindow")[0] ) {
		if( response[0]["saved"] ) {
			$$("#modalWindow .success")[0].appear();
			setTimeout(function() { modalWindow.hide() }, 3000);
		}
		else
      		$$("#modalWindow .errors")[0].appear();
	  }

      return false;
    }
  });
}



/**
 * Creates a default context menu which is used (currently) on
 * the Media, Blog, Chronological, and Page views. It takes in all of
 * the permissions needed (comment, delete, etc)
 */
var rightClickMenu = null;

function generateDefaultContextMenu(forSystemView) {
  rightClickMenu = new ContextMenu(".asset", function() {rightClickCallback()});

  var assetMenuGroup = new MenuGroup();

  assetMenuGroup.add(new MenuItem("Preview", function() { show_asset_preview(rightClickMenu.selectedElement) }, {icon_url:"/images/zoom_icon.png", disallowed_views:"asset"}));
  assetMenuGroup.add(new MenuItem("Go To File", function() { go_to_asset(rightClickMenu.selectedElement) }, {icon_url:"/images/go.png", disallowed_views:"asset",css_requirement:"single_asset_url"}));
  assetMenuGroup.add(new MenuItem("Comment", function() { show_asset_comments(rightClickMenu.selectedElement) }, {icon_url:"/images/comment_icon.png", disallowed_views:"asset"}));

  if(CAN_DOWNLOAD)
    assetMenuGroup.add(new MenuItem("Download", function() { download_asset(rightClickMenu.selectedElement) }, {icon_url:"/images/download_arrow_green.png", css_requirement:"save_url"}));

  assetMenuGroup.add(new MenuItem("Link", function() { show_asset_links(rightClickMenu.selectedElement) }, {icon_url: "/images/link.png"}));
  assetMenuGroup.add(new MenuItem("Embed", function() { show_embed_code(rightClickMenu.selectedElement) }, {icon_url:"/images/embed.png"}));

  if( CAN_EDIT )
	assetMenuGroup.add(new MenuItem("Edit Description", function() { show_edit_description(rightClickMenu.selectedElement) }, {icon_url: "/images/edit.png"}));

  if(CAN_DELETE) {
    if( forSystemView ) {
      assetMenuGroup.add(new MenuItem("Rename", function() { show_rename_asset(rightClickMenu.selectedElement) }, {icon_url:"/images/edit_name.png", can_do_multiple:true}));

    	assetMenuGroup.add(new MenuItem("Delete", function() {
			  delete_assets();
			  ids_to_delete = [];
			  $$(".lassoLoopSelected").each(function(asset_to_delete){
			    ids_to_delete.push(asset_to_delete.getAttribute("asset_id"));
			  });
			  remove_assets_from_client(ids_to_delete);
		  }, {icon_url:"/images/cross.png", can_do_multiple:true}));
    } else {
      assetMenuGroup.add(new MenuItem("Rename", function() { show_rename_asset(rightClickMenu.selectedElement) }, {icon_url:"/images/edit_name.png", can_do_multiple:true}));

      assetMenuGroup.add(new MenuItem("Delete", function()
        {delete_assets()}, {icon_url:"/images/cross.png", can_do_multiple:true}));
    }
  }

  assetMenuGroup.add(new MenuItem("Retry Conversion", function() { retry_conversion(rightClickMenu.selectedElement) }, {icon_url:"/images/retry.png", css_requirement:"retry_conversion_url"}));

  rightClickMenu.addMenuGroup(assetMenuGroup);

  if( !forSystemView && CAN_EDIT ) {
    var noteLinkSpecificGroup = new MenuGroup();
    noteLinkSpecificGroup.add(new MenuItem("Edit", function() { edit_asset(rightClickMenu.selectedElement) }, {icon_url:"/images/edit.png"}));
    rightClickMenu.addMenuGroup(noteLinkSpecificGroup, "note");
    rightClickMenu.addMenuGroup(noteLinkSpecificGroup, "link");
  }

  if(CAN_DELETE && CAN_EDIT) {
    var assetTypeSpecificGroup = new MenuGroup();

    assetTypeSpecificGroup.add(new MenuItem("Edit", function() { edit_asset(rightClickMenu.selectedElement) }, {icon_url:"/images/edit.png", css_requirement: "aviary_url"}));
    var imageRotateSubmenu = new Submenu(true);
    imageRotateSubmenu.add(new MenuItem("Right", function() {rotate_asset(rightClickMenu.selectedElement, 90)},
	                         {can_do_multiple:true}));
    imageRotateSubmenu.add(new MenuItem("Left", function() {rotate_asset(rightClickMenu.selectedElement, -90)},
	                         {can_do_multiple:true}));
    imageRotateSubmenu.add(new MenuItem("Upside Down", function() {rotate_asset(rightClickMenu.selectedElement, 180)},
	                         {can_do_multiple:true}));
    assetTypeSpecificGroup.add(new SubmenuItem("Rotate", imageRotateSubmenu));

    rightClickMenu.addMenuGroup(assetTypeSpecificGroup, "image");
  }

  var additionalActionsGroup = new MenuGroup();
  var sendToSubmenu = new Submenu();

  sendToSubmenu.add(new MenuItem("Existing Drop", function() { show_send_to("drop") },	{can_do_multiple:true}));
  sendToSubmenu.add(new MenuItem("Mobile Phone (MMS)", function() { show_send_to("phone") },	{can_do_multiple:true}));
  sendToSubmenu.add(new MenuItem("Email Recipient", function() { show_send_to("email") }, {can_do_multiple:true}));

	if(IS_PREMIUM) {
  	sendToSubmenu.add(new MenuItem("Fax", function() { show_send_to("fax") },
   {can_do_multiple:true, display_types:'document'}));
  }

  if(CAN_DOWNLOAD) {
    additionalActionsGroup.add(new SubmenuItem("Send to", sendToSubmenu));
    rightClickMenu.addMenuGroup(additionalActionsGroup);
  }


}


function rightClickCallback() {
  var temp_element = $$('#context_menu ul li.Comment .displayText')[0];
  if(rightClickMenu.selectedElement.getAttribute("comment_count") != "0") {
    temp_element.update("Comment (" + rightClickMenu.selectedElement.getAttribute("comment_count") + ")");
  } else {
    temp_element.update("Comment");
  }

  var temp_element = $$('#context_menu ul li.Preview .displayText')[0];
  if(curModalAssetId()) {
    temp_element.update("Refresh");
  } else {
    temp_element.update("Preview");
  }
}
/*! SWFObject v2.1 <http://code.google.com/p/swfobject/>
	Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
	This software 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",

		win = window,
		doc = document,
		nav = navigator,

		domLoadFnArr = [],
		regObjArr = [],
		objIdArr = [],
		listenersArr = [],
		script,
		timer = null,
		storedAltContent = null,
		storedAltContentId = null,
		isDomLoaded = false,
		isExpressInstallActive = false;

	/* Centralized function for browser feature detection
		- Proprietary feature detection (conditional compiling) is used to detect Internet Explorer's features
		- User agent string detection is only used when no alternative is possible
		- Is executed directly for optimal performance
	*/
	var ua = function() {
		var w3cdom = typeof doc.getElementById != UNDEF && typeof doc.getElementsByTagName != UNDEF && typeof doc.createElement != UNDEF,
			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+
				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] = /r/.test(d) ? parseInt(d.replace(/^.*r(.*)$/, "$1"), 10) : 0;
			}
		}
		else if (typeof win.ActiveXObject != UNDEF) {
			var a = null, fp6Crash = false;
			try {
				a = new ActiveXObject(SHOCKWAVE_FLASH_AX + ".7");
			}
			catch(e) {
				try {
					a = new ActiveXObject(SHOCKWAVE_FLASH_AX + ".6");
					playerVersion = [6,0,21];
					a.AllowScriptAccess = "always";	 // Introduced in fp6.0.47
				}
				catch(e) {
					if (playerVersion[0] == 6) {
						fp6Crash = true;
					}
				}
				if (!fp6Crash) {
					try {
						a = new ActiveXObject(SHOCKWAVE_FLASH_AX);
					}
					catch(e) {}
				}
			}
			if (!fp6Crash && a) { // a will return null when ActiveX is disabled
				try {
					d = a.GetVariable("$version");	// Will crash fp6.0.21/23/29
					if (d) {
						d = d.split(" ")[1].split(",");
						playerVersion = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)];
					}
				}
				catch(e) {}
			}
		}
		var u = nav.userAgent.toLowerCase(),
			p = nav.platform.toLowerCase(),
			webkit = /webkit/.test(u) ? parseFloat(u.replace(/^.*webkit\/(\d+(\.\d+)?).*$/, "$1")) : false, // returns either the webkit version or false if not webkit
			ie = false,
			windows = p ? /win/.test(p) : /win/.test(u),
			mac = p ? /mac/.test(p) : /mac/.test(u);
		/*@cc_on
			ie = true;
			@if (@_win32)
				windows = true;
			@elif (@_mac)
				mac = true;
			@end
		@*/
		return { w3cdom:w3cdom, pv:playerVersion, webkit:webkit, ie:ie, win:windows, mac:mac };
	}();

	/* Cross-browser onDomLoad
		- Based on Dean Edwards' solution: http://dean.edwards.name/weblog/2006/06/again/
		- Will fire an event as soon as the DOM of a page is loaded (supported by Gecko based browsers - like Firefox -, IE, Opera9+, Safari)
	*/
	var onDomLoad = function() {
		if (!ua.w3cdom) {
			return;
		}
		addDomLoadEvent(main);
		if (ua.ie && ua.win) {
			try {	 // Avoid a possible Operation Aborted error
				doc.write("<scr" + "ipt id=__ie_ondomload defer=true src=//:></scr" + "ipt>"); // String is split into pieces to avoid Norton AV to add code that can cause errors
				script = getElementById("__ie_ondomload");
				if (script) {
					addListener(script, "onreadystatechange", checkReadyState);
				}
			}
			catch(e) {}
		}
		if (ua.webkit && typeof doc.readyState != UNDEF) {
			timer = setInterval(function() { if (/loaded|complete/.test(doc.readyState)) { callDomLoadFunctions(); }}, 10);
		}
		if (typeof doc.addEventListener != UNDEF) {
			doc.addEventListener("DOMContentLoaded", callDomLoadFunctions, null);
		}
		addLoadEvent(callDomLoadFunctions);
	}();

	function checkReadyState() {
		if (script.readyState == "complete") {
			script.parentNode.removeChild(script);
			callDomLoadFunctions();
		}
	}

	function callDomLoadFunctions() {
		if (isDomLoaded) {
			return;
		}
		if (ua.ie && ua.win) { // Test if we can really add elements to the DOM; we don't want to fire it too early
			var s = createElement("span");
			try { // Avoid a possible Operation Aborted error
				var t = doc.getElementsByTagName("body")[0].appendChild(s);
				t.parentNode.removeChild(t);
			}
			catch (e) {
				return;
			}
		}
		isDomLoaded = true;
		if (timer) {
			clearInterval(timer);
			timer = null;
		}
		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() { // Static publishing only
		var rl = regObjArr.length;
		for (var i = 0; i < rl; i++) { // For each registered object element
			var id = regObjArr[i].id;
			if (ua.pv[0] > 0) {
				var obj = getElementById(id);
				if (obj) {
					regObjArr[i].width = obj.getAttribute("width") ? obj.getAttribute("width") : "0";
					regObjArr[i].height = obj.getAttribute("height") ? obj.getAttribute("height") : "0";
					if (hasPlayerVersion(regObjArr[i].swfVersion)) { // Flash plug-in version >= Flash content version: Houston, we have a match!
						if (ua.webkit && ua.webkit < 312) { // Older webkit engines ignore the object element's nested param elements
							fixParams(obj);
						}
						setVisibility(id, true);
					}
					else if (regObjArr[i].expressInstall && !isExpressInstallActive && hasPlayerVersion("6.0.65") && (ua.win || ua.mac)) { // Show the Adobe Express Install dialog if set by the web page author and if supported (fp6.0.65+ on Win/Mac OS only)
						showExpressInstall(regObjArr[i]);
					}
					else { // Flash plug-in and Flash content version mismatch: display alternative content instead of Flash content
						displayAltContent(obj);
					}
				}
			}
			else {	// If no fp is installed, we let the object element do its job (show alternative content)
				setVisibility(id, true);
			}
		}
	}

	/* Fix nested param elements, which are ignored by older webkit engines
		- This includes Safari up to and including version 1.2.2 on Mac OS 10.3
		- Fall back to the proprietary embed element
	*/
	function fixParams(obj) {
		var nestedObj = obj.getElementsByTagName(OBJECT)[0];
		if (nestedObj) {
			var e = createElement("embed"), a = nestedObj.attributes;
			if (a) {
				var al = a.length;
				for (var i = 0; i < al; i++) {
					if (a[i].nodeName == "DATA") {
						e.setAttribute("src", a[i].nodeValue);
					}
					else {
						e.setAttribute(a[i].nodeName, a[i].nodeValue);
					}
				}
			}
			var c = nestedObj.childNodes;
			if (c) {
				var cl = c.length;
				for (var j = 0; j < cl; j++) {
					if (c[j].nodeType == 1 && c[j].nodeName == "PARAM") {
						e.setAttribute(c[j].getAttribute("name"), c[j].getAttribute("value"));
					}
				}
			}
			obj.parentNode.replaceChild(e, obj);
		}
	}

	/* Show the Adobe Express Install dialog
		- Reference: http://www.adobe.com/cfusion/knowledgebase/index.cfm?id=6a253b75
	*/
	function showExpressInstall(regObj) {
		isExpressInstallActive = true;
		var obj = getElementById(regObj.id);
		if (obj) {
			if (regObj.altContentId) {
				var ac = getElementById(regObj.altContentId);
				if (ac) {
					storedAltContent = ac;
					storedAltContentId = regObj.altContentId;
				}
			}
			else {
				storedAltContent = abstractAltContent(obj);
			}
			if (!(/%$/.test(regObj.width)) && parseInt(regObj.width, 10) < 310) {
				regObj.width = "310";
			}
			if (!(/%$/.test(regObj.height)) && parseInt(regObj.height, 10) < 137) {
				regObj.height = "137";
			}
			doc.title = doc.title.slice(0, 47) + " - Flash Player Installation";
			var pt = ua.ie && ua.win ? "ActiveX" : "PlugIn",
				dt = doc.title,
				fv = "MMredirectURL=" + win.location + "&MMplayerType=" + pt + "&MMdoctitle=" + dt,
				replaceId = regObj.id;
			if (ua.ie && ua.win && obj.readyState != 4) {
				var newObj = createElement("div");
				replaceId += "SWFObjectNew";
				newObj.setAttribute("id", replaceId);
				obj.parentNode.insertBefore(newObj, obj); // Insert placeholder div that will be replaced by the object element that loads expressinstall.swf
				obj.style.display = "none";
				var fn = function() {
					obj.parentNode.removeChild(obj);
				};
				addListener(win, "onload", fn);
			}
			createSWF({ data:regObj.expressInstall, id:EXPRESS_INSTALL_ID, width:regObj.width, height:regObj.height }, { flashvars:fv }, replaceId);
		}
	}

	/* Functions to abstract and display alternative content
	*/
	function displayAltContent(obj) {
		if (ua.ie && ua.win && obj.readyState != 4) {
			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";
			var fn = function() {
				obj.parentNode.removeChild(obj);
			};
			addListener(win, "onload", fn);
		}
		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 (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) { // IE, the object element and 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, like Object.prototype.toJSONString = function() {}
						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 if (ua.webkit && ua.webkit < 312) { // Older webkit engines ignore the object element's nested param elements: fall back to the proprietary embed element
				var e = createElement("embed");
				e.setAttribute("type", FLASH_MIME_TYPE);
				for (var k in attObj) {
					if (attObj[k] != Object.prototype[k]) { // Filter out prototype additions from other potential libraries
						if (k.toLowerCase() == "data") {
							e.setAttribute("src", attObj[k]);
						}
						else if (k.toLowerCase() == "styleclass") { // 'class' is an ECMA4 reserved keyword
							e.setAttribute("class", attObj[k]);
						}
						else if (k.toLowerCase() != "classid") { // Filter out IE specific attribute
							e.setAttribute(k, attObj[k]);
						}
					}
				}
				for (var l in parObj) {
					if (parObj[l] != Object.prototype[l]) { // Filter out prototype additions from other potential libraries
						if (l.toLowerCase() != "movie") { // Filter out IE specific param element
							e.setAttribute(l, parObj[l]);
						}
					}
				}
				el.parentNode.replaceChild(e, el);
				r = e;
			}
			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" || obj.nodeName == "EMBED")) {
			if (ua.ie && ua.win) {
				if (obj.readyState == 4) {
					removeObjectInIE(id);
				}
				else {
					win.attachEvent("onload", function() {
						removeObjectInIE(id);
					});
				}
			}
			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) {
		if (ua.ie && ua.mac) {
			return;
		}
		var h = doc.getElementsByTagName("head")[0], s = createElement("style");
		s.setAttribute("type", "text/css");
		s.setAttribute("media", "screen");
		if (!(ua.ie && ua.win) && typeof doc.createTextNode != UNDEF) {
			s.appendChild(doc.createTextNode(sel + " {" + decl + "}"));
		}
		h.appendChild(s);
		if (ua.ie && ua.win && typeof doc.styleSheets != UNDEF && doc.styleSheets.length > 0) {
			var ls = doc.styleSheets[doc.styleSheets.length - 1];
			if (typeof ls.addRule == OBJECT) {
				ls.addRule(sel, decl);
			}
		}
	}

	function setVisibility(id, isVisible) {
		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 ? 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() {
				var ll = listenersArr.length;
				for (var i = 0; i < ll; i++) {
					listenersArr[i][0].detachEvent(listenersArr[i][1], listenersArr[i][2]);
				}
				var il = objIdArr.length;
				for (var j = 0; j < il; j++) {
					removeSWF(objIdArr[j]);
				}
				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/SWFObject_2_0_documentation
		*/
		registerObject: function(objectIdStr, swfVersionStr, xiSwfUrlStr) {
			if (!ua.w3cdom || !objectIdStr || !swfVersionStr) {
				return;
			}
			var regObj = {};
			regObj.id = objectIdStr;
			regObj.swfVersion = swfVersionStr;
			regObj.expressInstall = xiSwfUrlStr ? xiSwfUrlStr : false;
			regObjArr[regObjArr.length] = regObj;
			setVisibility(objectIdStr, false);
		},

		getObjectById: function(objectIdStr) {
			var r = null;
			if (ua.w3cdom) {
				var o = getElementById(objectIdStr);
				if (o) {
					var n = o.getElementsByTagName(OBJECT)[0];
					if (!n || (n && typeof o.SetVariable != UNDEF)) {
							r = o;
					}
					else if (typeof n.SetVariable != UNDEF) {
						r = n;
					}
				}
			}
			return r;
		},

		embedSWF: function(swfUrlStr, replaceElemIdStr, widthStr, heightStr, swfVersionStr, xiSwfUrlStr, flashvarsObj, parObj, attObj) {
			if (!ua.w3cdom || !swfUrlStr || !replaceElemIdStr || !widthStr || !heightStr || !swfVersionStr) {
				return;
			}
			widthStr += ""; // Auto-convert to string
			heightStr += "";
			if (hasPlayerVersion(swfVersionStr)) {
				setVisibility(replaceElemIdStr, false);
				var att = {};
				if (attObj && typeof attObj === OBJECT) {
					for (var i in attObj) {
						if (attObj[i] != Object.prototype[i]) { // Filter out prototype additions from other potential libraries
							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) {
						if (parObj[j] != Object.prototype[j]) { // Filter out prototype additions from other potential libraries
							par[j] = parObj[j];
						}
					}
				}
				if (flashvarsObj && typeof flashvarsObj === OBJECT) {
					for (var k in flashvarsObj) {
						if (flashvarsObj[k] != Object.prototype[k]) { // Filter out prototype additions from other potential libraries
							if (typeof par.flashvars != UNDEF) {
								par.flashvars += "&" + k + "=" + flashvarsObj[k];
							}
							else {
								par.flashvars = k + "=" + flashvarsObj[k];
							}
						}
					}
				}
				addDomLoadEvent(function() {
					createSWF(att, par, replaceElemIdStr);
					if (att.id == replaceElemIdStr) {
						setVisibility(replaceElemIdStr, true);
					}
				});
			}
			else if (xiSwfUrlStr && !isExpressInstallActive && hasPlayerVersion("6.0.65") && (ua.win || ua.mac)) {
				isExpressInstallActive = true; // deferred execution
				setVisibility(replaceElemIdStr, false);
				addDomLoadEvent(function() {
					var regObj = {};
					regObj.id = regObj.altContentId = replaceElemIdStr;
					regObj.width = widthStr;
					regObj.height = heightStr;
					regObj.expressInstall = xiSwfUrlStr;
					showExpressInstall(regObj);
				});
			}
		},

		getFlashPlayerVersion: function() {
			return { major:ua.pv[0], minor:ua.pv[1], release:ua.pv[2] };
		},

		hasFlashPlayerVersion: hasPlayerVersion,

		createSWF: function(attObj, parObj, replaceElemIdStr) {
			if (ua.w3cdom) {
				return createSWF(attObj, parObj, replaceElemIdStr);
			}
			else {
				return undefined;
			}
		},

		removeSWF: function(objElemIdStr) {
			if (ua.w3cdom) {
				removeSWF(objElemIdStr);
			}
		},

		createCSS: function(sel, decl) {
			if (ua.w3cdom) {
				createCSS(sel, decl);
			}
		},

		addDomLoadEvent: addDomLoadEvent,

		addLoadEvent: addLoadEvent,

		getQueryParamValue: function(param) {
			var q = doc.location.search || doc.location.hash;
			if (param == null) {
				return urlEncodeIfNecessary(q);
			}
			if (q) {
				var pairs = q.substring(1).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 "";
		},

		expressInstallCallback: function() {
			if (isExpressInstallActive && storedAltContent) {
				var obj = getElementById(EXPRESS_INSTALL_ID);
				if (obj) {
					obj.parentNode.replaceChild(storedAltContent, obj);
					if (storedAltContentId) {
						setVisibility(storedAltContentId, true);
						if (ua.ie && ua.win) {
							storedAltContent.style.display = "block";
						}
					}
					storedAltContent = null;
					storedAltContentId = null;
					isExpressInstallActive = false;
				}
			}
		}
	};
}();

var ClipboardCopier = Class.create({
  initialize: function(copyText,options) {
    if( !options ) options = {}
    if( !options.style ) options.style = "";
    if( !options.bottom && !options.top && !options.before && !options.after ) options.bottom = document.body;
    if( !options.buttonWidth ) options.buttonWidth = 16;
    if( !options.buttonHeight ) options.buttonHeight = 16;
    if( !options.upIcon ) options.upIcon = "/images/copyToClipboard.png";
    if( !options.downIcon ) options.downIcon = "/images/copiedToClipboard.png";

    this.upIcon = options.upIcon;
    this.downIcon = options.downIcon;
    this.onClickCallbacks = options.onClick ? [options.onClick] : [];

    var cb_outer = new Element("div",{"style":"overflow:hidden; position:relative; z-index: 1; width: " + options.buttonWidth + "px; height: " + options.buttonHeight + "px; " + options.style})
    cb_outer.id = "clipboard" + ClipboardCopier.numInstances + "_wrapper";
    var cb_replace = new Element("div");
    cb_replace.id = "clipboard" + ClipboardCopier.numInstances
    cb_outer.innerHTML = "<img style='position: absolute; z-index: -1; top:0px; left: 0px;' src='" + options.upIcon + "' />";
    cb_outer.insert({bottom:cb_replace});

    if( options.bottom )      Element.insert(options.bottom,{bottom:cb_outer});
    else if( options.top )    Element.insert(options.top,{top:cb_outer});
    else if( options.before ) Element.insert(options.before,{before:cb_outer});
    else if( options.after )  Element.insert(options.after,{after:cb_outer});

    var flashvars = {
      copyText: copyText,
      copierID: ClipboardCopier.numInstances,
      allowScriptAccess: "sameDomain"
    };

	  var params = { wmode: "transparent" };

    var attributes = { };

	  swfobject.embedSWF("/swf/Copier.swf", cb_replace.id, "100px", "100px",
	                     "9.0.0", null, flashvars, params, attributes);

    this.copier = $(cb_replace.id);
    this.img = cb_outer.down("img");

    ClipboardCopier.instances[ClipboardCopier.numInstances++] = this;
  },

  setStringToCopy: function(str) {
    if( this.copier )
      this.copier.setStringToCopy(str);
  },

  registerOnClickCallback: function(fn) {
    this.onClickCallbacks.push(fn);
  },

  onDown: function() {
    this.img.src = this.downIcon;
  },

  onUp: function() {
    this.img.src = this.upIcon;
    this.onClickCallbacks.each(function(fn) {
      try {
        fn();
      } catch(e) {
        ClipboardCopier.trace("CLIPBOARD ERROR: " + e.toString());
      }
    });
  }
});

ClipboardCopier.numInstances = 0;
ClipboardCopier.instances = [];

ClipboardCopier.onDown = function(id) {
  ClipboardCopier.instances[id].onDown();
}
ClipboardCopier.onUp = function(id) {
  ClipboardCopier.instances[id].onUp();
}

ClipboardCopier.DEBUG = false;
ClipboardCopier.trace = function(str) {
  if( ClipboardCopier.DEBUG == true ) {
    if( typeof(console) != "undefined" ) {
      console.info("CLIPBOARD: " + str);
    } else {
      alert("CLIPBOARD: " + str)
    }
  }
}
/*  WysiHat - WYSIWYG JavaScript framework, version 0.1
 *  (c) 2008-2009 Joshua Peek
 *
 *  WysiHat is freely distributable under the terms of an MIT-style license.
 *--------------------------------------------------------------------------*/


var WysiHat = {};

WysiHat.Editor = {
  attach: function(textarea, options, block) {
    options = $H(options);
    textarea = $(textarea);
    textarea.hide();

    var model = options.get('model') || WysiHat.iFrame;
    var initializer = block;

    return model.create(textarea, function(editArea) {
      var document = editArea.getDocument();
      var window = editArea.getWindow();

      editArea.load();

      Event.observe(window, 'focus', function(event) { editArea.focus(); });
      Event.observe(window, 'blur', function(event) { editArea.blur(); });

      editArea._observeEvents();

      if (Prototype.Browser.Gecko) {
        editArea.execCommand('undo', false, null);
      }

      if (initializer)
        initializer(editArea);

      editArea.focus();
    });
  },

  include: function(module) {
    this.includedModules = this.includedModules || $A([]);
    this.includedModules.push(module);
  },

  extend: function(object) {
    var modules = this.includedModules || $A([]);
    modules.each(function(module) {
      Object.extend(object, module);
    });
  }
};

WysiHat.Commands = (function() {
  function boldSelection() {
    this.execCommand('bold', false, null);
  }

  function boldSelected() {
    return this.queryCommandState('bold');
  }

  function underlineSelection() {
    this.execCommand('underline', false, null);
  }

  function underlineSelected() {
    return this.queryCommandState('underline');
  }

  function italicSelection() {
    this.execCommand('italic', false, null);
  }

  function italicSelected() {
    return this.queryCommandState('italic');
  }

  function strikethroughSelection() {
    this.execCommand('strikethrough', false, null);
  }

  function blockquoteSelection() {
    this.execCommand('blockquote', false, null);
  }

  function colorSelection(color) {
    this.execCommand('forecolor', false, color);
  }

  function colorSelected(color) {
    return this.queryCommandValue('forecolor');
  }

  function linkSelection(url) {
    this.execCommand('createLink', false, url);
  }

  function unlinkSelection() {
    var node = this.selection.getNode();
    if (this.linkSelected())
      this.selection.selectNode(node);

    this.execCommand('unlink', false, null);
  }

  function linkSelected() {
    var node = this.selection.getNode();
    return node ? node.tagName.toUpperCase() == 'A' : false;
  }

  function insertOrderedList() {
    this.execCommand('insertorderedlist', false, null);
  }

  function insertUnorderedList() {
    this.execCommand('insertunorderedlist', false, null);
  }

  function fontSizeSelection(size) {
    this.execCommand("fontsize", false, size )
  }

  function fontSizeSelected() {
    return this.queryCommandValue("fontsize");
  }

  function fontFamilySelection(fontFamily) {
    this.execCommand("fontname",false,fontFamily);
  }

  function fontFamilySelected() {
    return this.queryCommandValue("fontname");
  }

  function insertImage(url) {
    this.execCommand('insertImage', false, url);
  }

  function insertHTML(html) {
    if (Prototype.Browser.IE) {
      this.selection.moveToPreviousRange(); // added // for IE
      var range = this.selection.getRange();  // removed _
      range.pasteHTML(html);
      range.collapse(false);
      range.select();
    } else {
      this.execCommand('insertHTML', false, html);
    }
  }

  function execCommand(command, ui, value) {
    if (Prototype.Browser.IE) this.selection.moveToPreviousRange(); // added // for IE

    var document = this.getDocument();

    var handler = this.commands.get(command)
    if (handler)
      handler.bind(this)(value);
    else {
      try { // added try
        document.execCommand(command, ui, value);
      } catch(e) {}
    }
  }

  function queryCommandState(state) {
    var document = this.getDocument();

    var handler = this.queryCommands.get(state)
    if (handler)
      return handler.bind(this)();
    else
      return document.queryCommandState(state);
  }

  function queryCommandValue(state) {
    var document = this.getDocument();
    return document.queryCommandValue(state);
  }

  return {
    boldSelection:          boldSelection,
    boldSelected:           boldSelected,
    underlineSelection:     underlineSelection,
    underlineSelected:      underlineSelected,
    italicSelection:        italicSelection,
    italicSelected:         italicSelected,
    strikethroughSelection: strikethroughSelection,
    blockquoteSelection:    blockquoteSelection,
    colorSelection:         colorSelection,
    linkSelection:          linkSelection,
    unlinkSelection:        unlinkSelection,
    linkSelected:           linkSelected,
    insertOrderedList:      insertOrderedList,
    insertUnorderedList:    insertUnorderedList,
    insertImage:            insertImage,
    insertHTML:             insertHTML,
    execCommand:            execCommand,
    queryCommandState:      queryCommandState,

    fontSizeSelection: fontSizeSelection,
    fontSizeSelected: fontSizeSelected,
    fontFamilySelection: fontFamilySelection,
    fontFamilySelected: fontFamilySelected,
    colorSelected: colorSelected,
    queryCommandValue: queryCommandValue,

    commands: $H({}),

    queryCommands: $H({
      link: linkSelected
    })
  };
})();

WysiHat.Editor.include(WysiHat.Commands);
WysiHat.Events = (function() {
  var eventsToFoward = [
    'click',
    'dblclick',
    'mousedown',
    'mouseup',
    'mouseover',
    'mousemove',
    'mouseout',
    'keypress',
    'keydown',
    'keyup'
  ];

  function forwardEvents(document, editor) {
    eventsToFoward.each(function(event) {
      Event.observe(document, event, function(e) {
        editor.fire('wysihat:' + event);
      });
    });
  }

  function observePasteEvent(window, document, editor) {
    Event.observe(document, 'keydown', function(event) {
      if (event.keyCode == 86)
        editor.fire("wysihat:paste");
    });

    Event.observe(window, 'paste', function(event) {
      editor.fire("wysihat:paste");
    });
  }

  function observeFocus(window, editor) {
    Event.observe(window, 'focus', function(event) {
      editor.fire("wysihat:focus");
    });

    Event.observe(window, 'blur', function(event) {
      editor.fire("wysihat:blur");
    });
  }

  function observeSelections(document, editor) {
    Event.observe(document, 'mouseup', function(event) {
      var range = editor.selection.getRange();
      if (!range.collapsed)
        editor.fire("wysihat:select");
    });
  }

  function observeChanges(document, editor) {
    var previousContents = editor.rawContent();
    Event.observe(document, 'keyup', function(event) {
      var contents = editor.rawContent();
      if (previousContents != contents) {
        editor.fire("wysihat:change");
        previousContents = contents;
      }
    });
  }

  function observeCursorMovements(document, editor) {
    var previousRange = editor.selection.getRange();
    var handler = function(event) {
      var range = editor.selection.getRange();
      if (previousRange != range) {
        editor.fire("wysihat:cursormove");
        editor.previousRange = range;
      }
    };

    Event.observe(document, 'keyup', handler);
    Event.observe(document, 'mouseup', handler);
  }

  function observeEvents() {
    if (this._observers_setup)
      return;

    var document = this.getDocument();
    var window = this.getWindow();

    forwardEvents(document, this);
    observePasteEvent(window, document, this);
    observeFocus(window, this);
    observeSelections(document, this);
    observeChanges(document, this);
    observeCursorMovements(document, this);

    this._observers_setup = true;
  }

  return {
    _observeEvents: observeEvents
  };
})();

WysiHat.Editor.include(WysiHat.Events);
WysiHat.Persistence = (function() {
  function outputFilter(text) {
    return text.formatHTMLOutput();
  }

  function inputFilter(text) {
    return text.formatHTMLInput();
  }

  function content() {
    return this.outputFilter(this.rawContent());
  }

  function setContent(text) {
    this.setRawContent(this.inputFilter(text));
  }

  function save() {
    this.textarea.value = this.content();
  }

  function load() {
    this.setContent(this.textarea.value);
  }

  function reload() {
    this.selection.setBookmark();
    this.save();
    this.load();
    this.selection.moveToBookmark();
  }

  return {
    outputFilter: outputFilter,
    inputFilter:  inputFilter,
    content:      content,
    setContent:   setContent,
    save:         save,
    load:         load,
    reload:       reload
  };
})();

WysiHat.Editor.include(WysiHat.Persistence);
WysiHat.Window = (function() {
  function getDocument() {
    return this.contentDocument || this.contentWindow.document;
  }

  function getWindow() {
	if (this.contentWindow.document)
      return this.contentWindow;
	else if (this.contentDocument)
      return this.contentDocument.defaultView;
    else
      return null;
  }

  function focus() {
    this.getWindow().focus();

    if (this.hasFocus)
      return;

    this.hasFocus = true;
  }

  function blur() {
    this.hasFocus = false;
  }

  return {
    getDocument: getDocument,
    getWindow: getWindow,
    focus: focus,
    blur: blur
  };
})();

WysiHat.Editor.include(WysiHat.Window);
WysiHat.iFrame = {
  create: function(textarea, callback) {
    var editArea = new Element('iframe', { 'id': textarea.id + '_editor', 'class': 'editor' });
    editArea.addClassName("editor"); // added for IE8

    Object.extend(editArea, WysiHat.iFrame.Methods);
    WysiHat.Editor.extend(editArea);

    editArea.attach(textarea, callback);
    textarea.insert({before: editArea});

    return editArea;
  }
};

WysiHat.iFrame.Methods = {
  attach: function(element, callback) {
    this.textarea = element;

    this.observe('load', function() {
      try {
        var document = this.getDocument();
      } catch(e) { return; } // No iframe, just stop

      this.selection = new WysiHat.Selection(this);

      if (this.ready && document.designMode == 'on')
        return;

      this.setStyle({});
      document.designMode = 'on';
      callback(this);
      this.ready = true;
      this.fire('wysihat:ready');
    });
  },

  whenReady: function(callback) {
    if (this.ready) {
      callback(this);
    } else {
      var editor = this;
      editor.observe('wysihat:ready', function() { callback(editor); });
    }
    return this;
  },

  setStyle: function(styles) {
    var document = this.getDocument();

    var element = this;
    if (!this.ready)
      return setTimeout(function() { element.setStyle(styles); }, 1);

    if (Prototype.Browser.IE) {
      var style = document.createStyleSheet();
      style.addRule("body", "border: 0");
      style.addRule("p", "margin: 0");

      $H(styles).each(function(pair) {
        var value = pair.first().underscore().dasherize() + ": " + pair.last();
        style.addRule("body", value);
      });
    } else if (Prototype.Browser.Opera) {
      var style = new Element('style').update("p { margin: 0; }"); // added "new" for IE8 on vista
      var head = document.getElementsByTagName('head')[0];
      head.appendChild(style);
    } else {
      Element.setStyle(document.body, styles);
    }

    return this;
  },

  rawContent: function() {
    var document = this.getDocument();

    if (document.body)
      return document.body.innerHTML;
    else
      return "";
  },

  setRawContent: function(text) {
    var document = this.getDocument();
    if (document.body)
      document.body.innerHTML = text;
  }
};
WysiHat.Editable = {
  create: function(textarea, callback) {
    var editArea = new Element('div', {
      'id': textarea.id + '_editor',
      'class': 'editor',
      'contenteditable': 'true'
    });
    editArea.textarea = textarea;

    WysiHat.Editor.extend(editArea);
    Object.extend(editArea, WysiHat.Editable.Methods);

    callback(editArea);

    textarea.insert({before: editArea});

    return editArea;
  }
};

WysiHat.Editable.Methods = {
  getDocument: function() {
    return document;
  },

  getWindow: function() {
    return window;
  },

  rawContent: function() {
    return this.innerHTML;
  },

  setRawContent: function(text) {
    this.innerHTML = text;
  }
};

Object.extend(String.prototype, (function() {
  function formatHTMLOutput() {
    var text = String(this);
    text = text.tidyXHTML();

    text = text.gsub("\n", " "); // Feeding \n's into the labyrinth below causes excess <br/>s.

    if (Prototype.Browser.WebKit) {
      text = text.replace(/(<div>)+/g, "\n");
      text = text.replace(/(<\/div>)+/g, "");

      text = text.replace(/<p>\s*<\/p>/g, "");

      text = text.replace(/<br \/>(\n)*/g, "\n");
    } else if (Prototype.Browser.Gecko) {
      text = text.replace(/<p>/g, "");
      text = text.replace(/<\/p>(\n)?/g, "\n");

    } else if (Prototype.Browser.IE || Prototype.Browser.Opera) {
      text = text.replace(/<p>(&nbsp;|&#160;|\s)<\/p>/g, "<p></p>");


      text = text.replace(/<p>/g, '');

      text = text.replace(/&nbsp;/g, '');

      text = text.replace(/<\/p>(\n)?/g, "\n");

      text = text.gsub(/^<p>/, '');
      text = text.gsub(/<\/p>$/, '');
    }

    text = text.gsub(/<b>/, "<strong>");
    text = text.gsub(/<\/b>/, "</strong>");

    text = text.gsub(/<i>/, "<em>");
    text = text.gsub(/<\/i>/, "</em>");

    text = text.gsub("\n","<br />");

    text = '<p>' + text + '</p>';

    text = text.replace(/<p>\s*/g, "<p>");
    text = text.replace(/\s*<\/p>/g, "</p>");

    var element = new Element("body"); // added "new" for IE8 on vista
    element.innerHTML = text;

    if (Prototype.Browser.WebKit || Prototype.Browser.Gecko) {
      var replaced;
      do {
        replaced = false;
        element.select('span').each(function(span) {
          if (span.hasClassName('Apple-style-span')) {
            span.removeClassName('Apple-style-span');
            if (span.className == '')
              span.removeAttribute('class');
            replaced = true;
          } else if (span.style.fontWeight == 'bold') { // } else if (span.getStyle('fontWeight') == 'bold') { // changed ( causes infinite loop if nested spans)
            span.setStyle({fontWeight: ''});
            if (span.style.length == 0)
              span.removeAttribute('style');
            span.update('<strong>' + span.innerHTML + '</strong>');
            replaced = true;
          } else if (span.style.fontStyle == 'italic') { // } else if (span.getStyle('fontStyle') == 'italic') { // changed ( causes infinite loop if nested spans)
            span.setStyle({fontStyle: ''});
            if (span.style.length == 0)
              span.removeAttribute('style');
            span.update('<em>' + span.innerHTML + '</em>');
            replaced = true;
          } else if (span.style.textDecoration == 'underline') { // } else if (span.getStyle('textDecoration') == 'underline') { // changed ( causes infinite loop if nested spans)
            span.setStyle({textDecoration: ''});
            if (span.style.length == 0)
              span.removeAttribute('style');
            span.update('<u>' + span.innerHTML + '</u>');
            replaced = true;
          } else if (span.attributes.length == 0) {
            span.replace(span.innerHTML);
            replaced = true;
          }
        });

      } while (replaced);

    }
    var acceptableBlankTags = $A(['BR', 'IMG', 'PARAM', 'EMBED', 'SCRIPT', 'IFRAME']);  // added param, embed, script

    for (var i = 0; i < element.descendants().length; i++) {
      var node = element.descendants()[i];
      if (node.innerHTML.blank() && !acceptableBlankTags.include(node.nodeName) && node.id != 'bookmark')
        node.remove();
    }

    text = element.innerHTML;
    text = text.tidyXHTML();

    text = text.replace(/<br \/>(\n)*/g, "<br />\n");
    text = text.replace(/<\/p>\n<p>/g, "</p>\n\n<p>");

    text = text.replace(/<p>\s*<\/p>/g, "");

    text = text.replace(/\s*$/g, "");

    text = text.gsub(/(<ol>|<ul>|<\/li>)(<br \/>|<br>)/,"#{1}")
    text = text.gsub(/(<br \/>|<br>)(<li>|<\/ul>)/,"#{2}")

    return text;
  }

  function formatHTMLInput() {
    var text = String(this);

    var element = new Element("body"); // added "new" for IE8 on vista
    element.innerHTML = text;

    if (Prototype.Browser.Gecko || Prototype.Browser.WebKit) {
      element.select('strong').each(function(element) {
        element.replace('<span style="font-weight: bold;">' + element.innerHTML + '</span>');
      });
      element.select('em').each(function(element) {
        element.replace('<span style="font-style: italic;">' + element.innerHTML + '</span>');
      });
      element.select('u').each(function(element) {
        element.replace('<span style="text-decoration: underline;">' + element.innerHTML + '</span>');
      });
    }

    if (Prototype.Browser.WebKit)
      element.select('span').each(function(span) {
        if (span.getStyle('fontWeight') == 'bold')
          span.addClassName('Apple-style-span');

        if (span.getStyle('fontStyle') == 'italic')
          span.addClassName('Apple-style-span');

        if (span.getStyle('textDecoration') == 'underline')
          span.addClassName('Apple-style-span');
      });

    text = element.innerHTML;
    text = text.tidyXHTML();

    text = text.replace(/<\/p>(\n)*<p>/g, "\n\n");

    text = text.replace(/(\n)?<br( \/)?>(\n)?/g, "\n");

    text = text.replace(/^<p>/g, '');
    text = text.replace(/<\/p>$/g, '');

    if (Prototype.Browser.Gecko) {
      text = text.replace(/\n/g, "<br>");
      text = text + '<br>';
    } else if (Prototype.Browser.WebKit) {
      text = text.replace(/\n/g, "</div><div>");
      text = '<div>' + text + '</div>';
      text = text.replace(/<div><\/div>/g, "<div><br></div>");
    } else if (Prototype.Browser.IE || Prototype.Browser.Opera) {
      text = text.replace(/\n/g, "<br />"); // changed
    }

    text = text.gsub(/(<ol>|<ul>|<\/li>)(<br \/>|<br>)/,"#{1}")
    text = text.gsub(/(<br \/>|<br>)(<li>|<\/ul>)/,"#{2}")

    return text;
  }

  function tidyXHTML() {
    var text = String(this);

    text = text.gsub(/\r\n?/, "\n");

    text = text.gsub(/<([A-Z]+)([^>]*)>/, function(match) {
      return '<' + match[1].toLowerCase() + match[2] + '>';
    });

    text = text.gsub(/<\/([A-Z]+)>/, function(match) {
      return '</' + match[1].toLowerCase() + '>';
    });

    text = text.replace(/<br>/g, "<br />");

    return text;
  }

  return {
    formatHTMLOutput: formatHTMLOutput,
    formatHTMLInput:  formatHTMLInput,
    tidyXHTML:        tidyXHTML
  };
})());
Object.extend(String.prototype, {
  sanitize: function(options) {
    return new Element("div").update(this).sanitize(options).innerHTML.tidyXHTML(); // added "new" for IE8 on vista
  }
});

Element.addMethods({
  sanitize: function(element, options) {
    element = $(element);
    options = $H(options);
    var allowed_tags = $A(options.get('tags') || []);
    var allowed_attributes = $A(options.get('attributes') || []);
    var sanitized = new Element(element.nodeName); // added "new" for IE8 on vista

    $A(element.childNodes).each(function(child) {
      if (child.nodeType == 1) {
        var children = $(child).sanitize(options).childNodes;

        if (allowed_tags.include(child.nodeName.toLowerCase())) {
          var new_child = new Element(child.nodeName); // added "new" for IE8 on vista
          allowed_attributes.each(function(attribute) {
            if ((value = child.readAttribute(attribute)))
              new_child.writeAttribute(attribute, value);
          });
          sanitized.appendChild(new_child);

          $A(children).each(function(grandchild) { new_child.appendChild(grandchild); });
        } else {
          $A(children).each(function(grandchild) { sanitized.appendChild(grandchild); });
        }
      } else if (child.nodeType == 3) {
        sanitized.appendChild(child);
      }
    });
    return sanitized;
  }
});


if (typeof Range == 'undefined') {
  Range = function(ownerDocument) {
    this.ownerDocument = ownerDocument;

    this.startContainer = this.ownerDocument.documentElement;
    this.startOffset    = 0;
    this.endContainer   = this.ownerDocument.documentElement;
    this.endOffset      = 0;

    this.collapsed = true;
    this.commonAncestorContainer = this._commonAncestorContainer(this.startContainer, this.endContainer);

    this.detached = false;

    this.START_TO_START = 0;
    this.START_TO_END   = 1;
    this.END_TO_END     = 2;
    this.END_TO_START   = 3;
  }

  Range.CLONE_CONTENTS   = 0;
  Range.DELETE_CONTENTS  = 1;
  Range.EXTRACT_CONTENTS = 2;

  if (!document.createRange) {
    document.createRange = function() {
      return new Range(this);
    };
  }

  Object.extend(Range.prototype, (function() {
    function cloneContents() {
      return _processContents(this, Range.CLONE_CONTENTS);
    }

    function cloneRange() {
      try {
        var clone = new Range(this.ownerDocument);
        clone.startContainer          = this.startContainer;
        clone.startOffset             = this.startOffset;
        clone.endContainer            = this.endContainer;
        clone.endOffset               = this.endOffset;
        clone.collapsed               = this.collapsed;
        clone.commonAncestorContainer = this.commonAncestorContainer;
        clone.detached                = this.detached;

        return clone;

      } catch (e) {
        return null;
      };
    }

    function collapse(toStart) {
      if (toStart) {
        this.endContainer = this.startContainer;
        this.endOffset    = this.startOffset;
        this.collapsed    = true;
      } else {
        this.startContainer = this.endContainer;
        this.startOffset    = this.endOffset;
        this.collapsed      = true;
      }
    }

    function compareBoundaryPoints(compareHow, sourceRange) {
      try {
        var cmnSelf, cmnSource, rootSelf, rootSource;

        cmnSelf   = this.commonAncestorContainer;
        cmnSource = sourceRange.commonAncestorContainer;

        rootSelf = cmnSelf;
        while (rootSelf.parentNode) {
          rootSelf = rootSelf.parentNode;
        }

        rootSource = cmnSource;
        while (rootSource.parentNode) {
          rootSource = rootSource.parentNode;
        }

        switch (compareHow) {
          case this.START_TO_START:
            return _compareBoundaryPoints(this, this.startContainer, this.startOffset, sourceRange.startContainer, sourceRange.startOffset);
            break;
          case this.START_TO_END:
            return _compareBoundaryPoints(this, this.startContainer, this.startOffset, sourceRange.endContainer, sourceRange.endOffset);
            break;
          case this.END_TO_END:
            return _compareBoundaryPoints(this, this.endContainer, this.endOffset, sourceRange.endContainer, sourceRange.endOffset);
            break;
          case this.END_TO_START:
            return _compareBoundaryPoints(this, this.endContainer, this.endOffset, sourceRange.startContainer, sourceRange.startOffset);
            break;
        }
      } catch (e) {};

      return null;
    }

    function deleteContents() {
      try {
        _processContents(this, Range.DELETE_CONTENTS);
      } catch (e) {}
    }

    function detach() {
      this.detached = true;
    }

    function extractContents() {
      try {
        return _processContents(this, Range.EXTRACT_CONTENTS);
      } catch (e) {
        return null;
      };
    }

    function insertNode(newNode) {
      try {
        var n, newText, offset;

        switch (this.startContainer.nodeType) {
          case Node.CDATA_SECTION_NODE:
          case Node.TEXT_NODE:
            newText = this.startContainer.splitText(this.startOffset);
            this.startContainer.parentNode.insertBefore(newNode, newText);
            break;
          default:
            if (this.startContainer.childNodes.length == 0) {
              offset = null;
            } else {
              offset = this.startContainer.childNodes(this.startOffset);
            }
            this.startContainer.insertBefore(newNode, offset);
        }
      } catch (e) {}
    }

    function selectNode(refNode) {
      this.setStartBefore(refNode);
      this.setEndAfter(refNode);
    }

    function selectNodeContents(refNode) {
      this.setStart(refNode, 0);
      this.setEnd(refNode, refNode.childNodes.length);
    }

    function setStart(refNode, offset) {
      try {
        var endRootContainer, startRootContainer;

        this.startContainer = refNode;
        this.startOffset    = offset;

        endRootContainer = this.endContainer;
        while (endRootContainer.parentNode) {
          endRootContainer = endRootContainer.parentNode;
        }
        startRootContainer = this.startContainer;
        while (startRootContainer.parentNode) {
          startRootContainer = startRootContainer.parentNode;
        }
        if (startRootContainer != endRootContainer) {
          this.collapse(true);
        } else {
          if (_compareBoundaryPoints(this, this.startContainer, this.startOffset, this.endContainer, this.endOffset) > 0) {
            this.collapse(true);
          }
        }

        this.collapsed = _isCollapsed(this);

        this.commonAncestorContainer = _commonAncestorContainer(this.startContainer, this.endContainer);
      } catch (e) {}
    }

    function setStartAfter(refNode) {
      this.setStart(refNode.parentNode, _nodeIndex(refNode) + 1);
    }

    function setStartBefore(refNode) {
      this.setStart(refNode.parentNode, _nodeIndex(refNode));
    }

    function setEnd(refNode, offset) {
      try {
        this.endContainer = refNode;
        this.endOffset    = offset;

        endRootContainer = this.endContainer;
        while (endRootContainer.parentNode) {
          endRootContainer = endRootContainer.parentNode;
        }
        startRootContainer = this.startContainer;
        while (startRootContainer.parentNode) {
          startRootContainer = startRootContainer.parentNode;
        }
        if (startRootContainer != endRootContainer) {
          this.collapse(false);
        } else {
          if (_compareBoundaryPoints(this, this.startContainer, this.startOffset, this.endContainer, this.endOffset) > 0) {
            this.collapse(false);
          }
        }

        this.collapsed = _isCollapsed(this);

        this.commonAncestorContainer = _commonAncestorContainer(this.startContainer, this.endContainer);

      } catch (e) {}
    }

    function setEndAfter(refNode) {
      this.setEnd(refNode.parentNode, _nodeIndex(refNode) + 1);
    }

    function setEndBefore(refNode) {
      this.setEnd(refNode.parentNode, _nodeIndex(refNode));
    }

    function surroundContents(newParent) {
      try {
        var n, fragment;

        while (newParent.firstChild) {
          newParent.removeChild(newParent.firstChild);
        }

        fragment = this.extractContents();
        this.insertNode(newParent);
        newParent.appendChild(fragment);
        this.selectNode(newParent);
      } catch (e) {}
    }

    function _compareBoundaryPoints(range, containerA, offsetA, containerB, offsetB) {
      var c, offsetC, n, cmnRoot, childA;
      if (containerA == containerB) {
        if (offsetA == offsetB) {
          return 0; // equal
        } else if (offsetA < offsetB) {
          return -1; // before
        } else {
          return 1; // after
        }
      }

      c = containerB;
      while (c && c.parentNode != containerA) {
        c = c.parentNode;
      }
      if (c) {
        offsetC = 0;
        n = containerA.firstChild;
        while (n != c && offsetC < offsetA) {
          offsetC++;
          n = n.nextSibling;
        }
        if (offsetA <= offsetC) {
          return -1; // before
        } else {
          return 1; // after
        }
      }

      c = containerA;
      while (c && c.parentNode != containerB) {
        c = c.parentNode;
      }
      if (c) {
        offsetC = 0;
        n = containerB.firstChild;
        while (n != c && offsetC < offsetB) {
          offsetC++;
          n = n.nextSibling;
        }
        if (offsetC < offsetB) {
          return -1; // before
        } else {
          return 1; // after
        }
      }

      cmnRoot = range._commonAncestorContainer(containerA, containerB);
      childA = containerA;
      while (childA && childA.parentNode != cmnRoot) {
        childA = childA.parentNode;
      }
      if (!childA) {
        childA = cmnRoot;
      }
      childB = containerB;
      while (childB && childB.parentNode != cmnRoot) {
        childB = childB.parentNode;
      }
      if (!childB) {
        childB = cmnRoot;
      }

      if (childA == childB) {
        return 0; // equal
      }

      n = cmnRoot.firstChild;
      while (n) {
        if (n == childA) {
          return -1; // before
        }
        if (n == childB) {
          return 1; // after
        }
        n = n.nextSibling;
      }

      return null;
    }

    function _commonAncestorContainer(containerA, containerB) {
      var parentStart = containerA, parentEnd;
      while (parentStart) {
        parentEnd = containerB;
        while (parentEnd && parentStart != parentEnd) {
          parentEnd = parentEnd.parentNode;
        }
        if (parentStart == parentEnd) {
          break;
        }
        parentStart = parentStart.parentNode;
      }

      if (!parentStart && containerA.ownerDocument) {
        return containerA.ownerDocument.documentElement;
      }

      return parentStart;
    }

    function _isCollapsed(range) {
      return (range.startContainer == range.endContainer && range.startOffset == range.endOffset);
    }

    function _offsetInCharacters(node) {
      switch (node.nodeType) {
        case Node.CDATA_SECTION_NODE:
        case Node.COMMENT_NODE:
        case Node.ELEMENT_NODE:
        case Node.PROCESSING_INSTRUCTION_NODE:
          return true;
        default:
          return false;
      }
    }

    function _processContents(range, action) {
      try {

        var cmnRoot, partialStart = null, partialEnd = null, fragment, n, c, i;
        var leftContents, leftParent, leftContentsParent;
        var rightContents, rightParent, rightContentsParent;
        var next, prev;
        var processStart, processEnd;
        if (range.collapsed) {
          return null;
        }

        cmnRoot = range.commonAncestorContainer;

        if (range.startContainer != cmnRoot) {
          partialStart = range.startContainer;
          while (partialStart.parentNode != cmnRoot) {
            partialStart = partialStart.parentNode;
          }
        }

        if (range.endContainer != cmnRoot) {
          partialEnd = range.endContainer;
          while (partialEnd.parentNode != cmnRoot) {
            partialEnd = partialEnd.parentNode;
          }
        }

        if (action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) {
          fragment = range.ownerDocument.createDocumentFragment();
        }

        if (range.startContainer == range.endContainer) {
          switch (range.startContainer.nodeType) {
            case Node.CDATA_SECTION_NODE:
            case Node.COMMENT_NODE:
            case Node.TEXT_NODE:
              if (action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) {
                c = range.startContainer.cloneNode();
                c.deleteData(range.endOffset, range.startContainer.data.length - range.endOffset);
                c.deleteData(0, range.startOffset);
                fragment.appendChild(c);
              }
              if (action == Range.EXTRACT_CONTENTS || action == Range.DELETE_CONTENTS) {
                range.startContainer.deleteData(range.startOffset, range.endOffset - range.startOffset);
              }
              break;
            case Node.PROCESSING_INSTRUCTION_NODE:
              break;
            default:
              n = range.startContainer.firstChild;
              for (i = 0; i < range.startOffset; i++) {
                n = n.nextSibling;
              }
              while (n && i < range.endOffset) {
                next = n.nextSibling;
                if (action == Range.EXTRACT_CONTENTS) {
                  fragment.appendChild(n);
                } else if (action == Range.CLONE_CONTENTS) {
                  fragment.appendChild(n.cloneNode());
                } else {
                  range.startContainer.removeChild(n);
                }
                n = next;
                i++;
              }
          }
          range.collapse(true);
          return fragment;
        }


        if (range.startContainer != cmnRoot) {
          switch (range.startContainer.nodeType) {
            case Node.CDATA_SECTION_NODE:
            case Node.COMMENT_NODE:
            case Node.TEXT_NODE:
              if (action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) {
                c = range.startContainer.cloneNode(true);
                c.deleteData(0, range.startOffset);
                leftContents = c;
              }
              if (action == Range.EXTRACT_CONTENTS || action == Range.DELETE_CONTENTS) {
                range.startContainer.deleteData(range.startOffset, range.startContainer.data.length - range.startOffset);
              }
              break;
            case Node.PROCESSING_INSTRUCTION_NODE:
              break;
            default:
              if (action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) {
                leftContents = range.startContainer.cloneNode(false);
              }
              n = range.startContainer.firstChild;
              for (i = 0; i < range.startOffset; i++) {
                n = n.nextSibling;
              }
              while (n && i < range.endOffset) {
                next = n.nextSibling;
                if (action == Range.EXTRACT_CONTENTS) {
                  fragment.appendChild(n);
                } else if (action == Range.CLONE_CONTENTS) {
                  fragment.appendChild(n.cloneNode());
                } else {
                  range.startContainer.removeChild(n);
                }
                n = next;
                i++;
              }
          }

          leftParent = range.startContainer.parentNode;
          n = range.startContainer.nextSibling;
          for(; leftParent != cmnRoot; leftParent = leftParent.parentNode) {
            if (action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) {
              leftContentsParent = leftParent.cloneNode(false);
              leftContentsParent.appendChild(leftContents);
              leftContents = leftContentsParent;
            }

            for (; n; n = next) {
              next = n.nextSibling;
              if (action == Range.EXTRACT_CONTENTS) {
                leftContents.appendChild(n);
              } else if (action == Range.CLONE_CONTENTS) {
                leftContents.appendChild(n.cloneNode(true));
              } else {
                leftParent.removeChild(n);i
              }
            }
            n = leftParent.nextSibling;
          }
        }

        if (range.endContainer != cmnRoot) {
          switch (range.endContainer.nodeType) {
            case Node.CDATA_SECTION_NODE:
            case Node.COMMENT_NODE:
            case Node.TEXT_NODE:
              if (action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) {
                c = range.endContainer.cloneNode(true);
                c.deleteData(range.endOffset, range.endContainer.data.length - range.endOffset);
                rightContents = c;
              }
              if (action == Range.EXTRACT_CONTENTS || action == Range.DELETE_CONTENTS) {
                range.endContainer.deleteData(0, range.endOffset);
              }
              break;
            case Node.PROCESSING_INSTRUCTION_NODE:
              break;
            default:
              if (action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) {
                rightContents = range.endContainer.cloneNode(false);
              }
              n = range.endContainer.firstChild;
              if (n && range.endOffset) {
                for (i = 0; i+1 < range.endOffset; i++) {
                  next = n.nextSibling;
                  if (!next) {
                    break;
                  }
                  n = next;
                }
                for (; n; n = prev) {
                  prev = n.previousSibling;
                  if (action == Range.EXTRACT_CONTENTS) {
                    rightContents.insertBefore(n, rightContents.firstChild);
                  } else if (action == Range.CLONE_CONTENTS) {
                    rightContents.insertBefore(n.cloneNode(True), rightContents.firstChild);
                  } else {
                    range.endContainer.removeChild(n);
                  }
                }
              }
          }

          rightParent = range.endContainer.parentNode;
          n = range.endContainer.previousSibling;
          for(; rightParent != cmnRoot; rightParent = rightParent.parentNode) {
            if (action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) {
              rightContentsParent = rightContents.cloneNode(false);
              rightContentsParent.appendChild(rightContents);
              rightContents = rightContentsParent;
            }

            for (; n; n = prev) {
              prev = n.previousSibling;
              if (action == Range.EXTRACT_CONTENTS) {
                rightContents.insertBefore(n, rightContents.firstChild);
              } else if (action == Range.CLONE_CONTENTS) {
                rightContents.appendChild(n.cloneNode(true), rightContents.firstChild);
              } else {
                rightParent.removeChild(n);
              }
            }
            n = rightParent.previousSibling;
          }
        }

        if (range.startContainer == cmnRoot) {
          processStart = range.startContainer.firstChild;
          for (i = 0; i < range.startOffset; i++) {
            processStart = processStart.nextSibling;
          }
        } else {
          processStart = range.startContainer;
          while (processStart.parentNode != cmnRoot) {
            processStart = processStart.parentNode;
          }
          processStart = processStart.nextSibling;
        }
        if (range.endContainer == cmnRoot) {
          processEnd = range.endContainer.firstChild;
          for (i = 0; i < range.endOffset; i++) {
            processEnd = processEnd.nextSibling;
          }
        } else {
          processEnd = range.endContainer;
          while (processEnd.parentNode != cmnRoot) {
            processEnd = processEnd.parentNode;
          }
        }

        if ((action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) && leftContents) {
          fragment.appendChild(leftContents);
        }

        if (processStart) {
          for (n = processStart; n && n != processEnd; n = next) {
            next = n.nextSibling;
            if (action == Range.EXTRACT_CONTENTS) {
              fragment.appendChild(n);
            } else if (action == Range.CLONE_CONTENTS) {
              fragment.appendChild(n.cloneNode(true));
            } else {
              cmnRoot.removeChild(n);
            }
          }
        }

        if ((action == Range.EXTRACT_CONTENTS || action == Range.CLONE_CONTENTS) && rightContents) {
          fragment.appendChild(rightContents);
        }

        if (action == Range.EXTRACT_CONTENTS || action == Range.DELETE_CONTENTS) {
          if (!partialStart && !partialEnd) {
            range.collapse(true);
          } else if (partialStart) {
            range.startContainer = partialStart.parentNode;
            range.endContainer = partialStart.parentNode;
            range.startOffset = range.endOffset = range._nodeIndex(partialStart) + 1;
          } else if (partialEnd) {
            range.startContainer = partialEnd.parentNode;
            range.endContainer = partialEnd.parentNode;
            range.startOffset = range.endOffset = range._nodeIndex(partialEnd);
          }
        }

        return fragment;

      } catch (e) {
        return null;
      };
    }

    function _nodeIndex(refNode) {
      var nodeIndex = 0;
      while (refNode.previousSibling) {
        nodeIndex++;
        refNode = refNode.previousSibling;
      }
      return nodeIndex;
    }

    return {
      setStart:       setStart,
      setEnd:         setEnd,
      setStartBefore: setStartBefore,
      setStartAfter:  setStartAfter,
      setEndBefore:   setEndBefore,
      setEndAfter:    setEndAfter,

      collapse: collapse,

      selectNode:         selectNode,
      selectNodeContents: selectNodeContents,

      compareBoundaryPoints: compareBoundaryPoints,

      deleteContents:  deleteContents,
      extractContents: extractContents,
      cloneContents:   cloneContents,

      insertNode:       insertNode,
      surroundContents: surroundContents,

      cloneRange: cloneRange,
      toString:   toString,
      detach:     detach,

      _commonAncestorContainer: _commonAncestorContainer
    };
  })());
}

if (!window.getSelection) {
  window.getSelection = function() {
    return Selection.getInstance();
  };

  SelectionImpl = function() {
    this.anchorNode = null;
    this.anchorOffset = 0;
    this.focusNode = null;
    this.focusOffset = 0;
    this.isCollapsed = true;
    this.rangeCount = 0;
    this.ranges = [];
  }

  Object.extend(SelectionImpl.prototype, (function() {
    function addRange(r) {
      return true;
    }

    function collapse() {
      return true;
    }

    function collapseToStart() {
      return true;
    }

    function collapseToEnd() {
      return true;
    }

    function getRangeAt() {
      return true;
    }

    function removeAllRanges() {
      this.anchorNode = null;
      this.anchorOffset = 0;
      this.focusNode = null;
      this.focusOffset = 0;
      this.isCollapsed = true;
      this.rangeCount = 0;
      this.ranges = [];
    }

    function _addRange(r) {
      if (r.startContainer.nodeType != Node.TEXT_NODE) {
        var start = this._getRightStart(r.startContainer);
        var startOffset = 0;
      } else {
        var start = r.startContainer;
        var startOffset = r.startOffset;
      }
      if (r.endContainer.nodeType != Node.TEXT_NODE) {
        var end = this._getRightEnd(r.endContainer);
        var endOffset = end.data.length;
      } else {
        var end = r.endContainer;
        var endOffset = r.endOffset;
      }

      var rStart = this._selectStart(start, startOffset);
      var rEnd   = this._selectEnd(end,endOffset);
      rStart.setEndPoint('EndToStart', rEnd);
      rStart.select();
      document.selection._selectedRange = r;
    }

    function _getRightStart(start, offset) {
      if (start.nodeType != Node.TEXT_NODE) {
        if (start.nodeType == Node.ELEMENT_NODE) {
          start = start.childNodes(offset);
        }
        return getNextTextNode(start);
      } else {
        return null;
      }
    }

    function _getRightEnd(end, offset) {
      if (end.nodeType != Node.TEXT_NODE) {
        if (end.nodeType == Node.ELEMENT_NODE) {
          end = end.childNodes(offset);
        }
        return getPreviousTextNode(end);
      } else {
        return null;
      }
    }

    function _selectStart(start, offset) {
      var r = document.body.createTextRange();
      if (start.nodeType == Node.TEXT_NODE) {
        var moveCharacters = offset, node = start;
        var moveToNode = null, collapse = true;
        while (node.previousSibling) {
          switch (node.previousSibling.nodeType) {
            case Node.ELEMENT_NODE:
              moveToNode = node.previousSibling;
              collapse = false;
              break;
            case Node.TEXT_NODE:
              moveCharacters += node.previousSibling.data.length;
          }
          if (moveToNode != null) {
            break;
          }
          node = node.previousSibling;
        }
        if (moveToNode == null) {
          moveToNode = start.parentNode;
        }

        r.moveToElementText(moveToNode);
        r.collapse(collapse);
        r.move('Character', moveCharacters);
        return r;
      } else {
        return null;
      }
    }

    function _selectEnd(end, offset) {
      var r = document.body.createTextRange(), node = end;
      if (end.nodeType == 3) {
        var moveCharacters = end.data.length - offset;
        var moveToNode = null, collapse = false;
        while (node.nextSibling) {
          switch (node.nextSibling.nodeType) {
            case Node.ELEMENT_NODE:
              moveToNode = node.nextSibling;
              collapse   = true;
              break;
            case Node.TEXT_NODE:
              moveCharacters += node.nextSibling.data.length;
              break;
          }
          if (moveToNode != null) {
            break;
          }
          node = node.nextSibling;
        }
        if (moveToNode == null) {
          moveToNode = end.parentNode;
          collapse   = false;
        }

        switch (moveToNode.nodeName.toLowerCase()) {
          case 'p':
          case 'div':
          case 'h1':
          case 'h2':
          case 'h3':
          case 'h4':
          case 'h5':
          case 'h6':
            moveCharacters++;
        }

        r.moveToElementText(moveToNode);
        r.collapse(collapse);

        r.move('Character', -moveCharacters);
        return r;
      }

      return null;
    }

    function getPreviousTextNode(node) {
      var stack = [];
      var current = null;
      while (node) {
        stack = [];
        current = node;
        while (current) {
          while (current) {
          if (current.nodeType == 3 && current.data.replace(/^\s+|\s+$/, '').length) {
            return current;
          }
          if (current.previousSibling) {
            stack.push (current.previousSibling);
          }
          current = current.lastChild;
        }
        current = stack.pop();
        }
        node = node.previousSibling;
      }
      return null;
    }

    function getNextTextNode(node) {
      var stack = [];
      var current = null;
      while (node) {
        stack = [];
        current = node;
        while (current) {
          while (current) {
            if (current.nodeType == 3 && current.data.replace(/^\s+|\s+$/, '').length) {
                return current;
            }
            if (current.nextSibling) {
                stack.push (current.nextSibling);
            }
            current = current.firstChild;
          }
          current = stack.pop();
        }
        node = node.nextSibling;
      }
      return null;
    }

    return {
      removeAllRanges: removeAllRanges,

      _addRange: _addRange,
      _getRightStart: _getRightStart,
      _getRightEnd: _getRightEnd,
      _selectStart: _selectStart,
      _selectEnd: _selectEnd
    };
  })());

  Selection = new function() {
    var instance = null;
    this.getInstance = function() {
      if (instance == null) {
        return (instance = new SelectionImpl());
      } else {
        return instance;
      }
    };
  };
}

Object.extend(Range.prototype, (function() {
  function getNode() {
    var node = this.commonAncestorContainer;

    if (this.startContainer == this.endContainer)
      if (this.startOffset - this.endOffset < 2)
        node = this.startContainer.childNodes[this.startOffset];

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

    return node;
  }

  return {
    getNode: getNode
  };
})());
WysiHat.Selection = Class.create((function() {
  function initialize(editor) {
    this.window = editor.getWindow();
    this.document = editor.getDocument();
    this.editor = editor;
  }

  function getSelection() {
    return this.window.getSelection ? this.window.getSelection() : this.document.selection;
  }

  function getRange() {
    var selection = this.getSelection();

    try {
      var range;
      if (selection && selection.getRangeAt)  // added selection &&
        range = selection.getRangeAt(0);
      else
        range = selection.createRange();
    } catch(e) { return null; }

    if (Prototype.Browser.WebKit) {
      range.setStart(selection.baseNode, selection.baseOffset);
      range.setEnd(selection.extentNode, selection.extentOffset);
    }

    return range;
  }

  function selectNode(node) {
    var selection = this.getSelection();

    if (Prototype.Browser.IE) {
      var range = createRangeFromElement(this.document, node);
      range.select();
    } else if (Prototype.Browser.WebKit) {
      selection.setBaseAndExtent(node, 0, node, node.innerText.length);
    } else if (Prototype.Browser.Opera) {
      range = this.document.createRange();
      range.selectNode(node);
      selection.removeAllRanges();
      selection.addRange(range);
    } else {
      var range = createRangeFromElement(this.document, node);
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }

  function getNode() {
    var nodes = null, candidates = [], children, el;
    var range = this.getRange();

    if (!range)
      return null;

    var parent;
    if (range.parentElement)
      parent = range.parentElement();
    else
      parent = range.commonAncestorContainer;

    if (parent) {
      while (parent.nodeType != 1) parent = parent.parentNode;
      if (parent.nodeName.toLowerCase() != "body") {
        el = parent;
        do {
          el = el.parentNode;
          candidates[candidates.length] = el;
        } while (el.nodeName.toLowerCase() != "body");
      }
      children = parent.all || parent.getElementsByTagName("*");
      for (var j = 0; j < children.length; j++)
        candidates[candidates.length] = children[j];
      nodes = [parent];
      for (var ii = 0, r2; ii < candidates.length; ii++) {
        r2 = createRangeFromElement(this.document, candidates[ii]);
        if (r2 && compareRanges(range, r2))
          nodes[nodes.length] = candidates[ii];
      }
    }

    return nodes.first();
  }

  function createRangeFromElement(document, node) {
    if (document.body.createTextRange) {
      var range = document.body.createTextRange();
      range.moveToElementText(node);
    } else if (document.createRange) {
      var range = document.createRange();
      range.selectNodeContents(node);
    }
    return range;
  }

  function compareRanges(r1, r2) {
    if (r1.compareEndPoints) {
      return !(
        r2.compareEndPoints('StartToStart', r1) == 1 &&
        r2.compareEndPoints('EndToEnd', r1) == 1 &&
        r2.compareEndPoints('StartToEnd', r1) == 1 &&
        r2.compareEndPoints('EndToStart', r1) == 1
        ||
        r2.compareEndPoints('StartToStart', r1) == -1 &&
        r2.compareEndPoints('EndToEnd', r1) == -1 &&
        r2.compareEndPoints('StartToEnd', r1) == -1 &&
        r2.compareEndPoints('EndToStart', r1) == -1
      );
    } else if (r1.compareBoundaryPoints) {
      return !(
        r2.compareBoundaryPoints(0, r1) == 1 &&
        r2.compareBoundaryPoints(2, r1) == 1 &&
        r2.compareBoundaryPoints(1, r1) == 1 &&
        r2.compareBoundaryPoints(3, r1) == 1
        ||
        r2.compareBoundaryPoints(0, r1) == -1 &&
        r2.compareBoundaryPoints(2, r1) == -1 &&
        r2.compareBoundaryPoints(1, r1) == -1 &&
        r2.compareBoundaryPoints(3, r1) == -1
      );
    }

    return null;
  };

  function setBookmark() {
    var bookmark = this.document.getElementById('bookmark');
    if (bookmark)
      bookmark.parentNode.removeChild(bookmark);

    bookmark = this.document.createElement('span');
    bookmark.id = 'bookmark';
    bookmark.innerHTML = '&nbsp;';

    if (Prototype.Browser.IE) {
      var range = this.document.selection.createRange();
      var parent = this.document.createElement('div');
      parent.appendChild(bookmark);
      range.collapse();
      range.pasteHTML(parent.innerHTML);
    }
    else {
      var range = this.getRange();
      range.insertNode(bookmark);
    }
  }

  function moveToBookmark() {
    var bookmark = this.document.getElementById('bookmark');
    if (!bookmark)
      return;

    if (Prototype.Browser.IE) {
      var range = this.getRange();
      range.moveToElementText(bookmark);
      range.collapse();
      range.select();
    } else if (Prototype.Browser.WebKit) {
      var selection = this.getSelection();
      selection.setBaseAndExtent(bookmark, 0, bookmark, 0);
    } else {
      var range = this.getRange();
      range.setStartBefore(bookmark);
    }

    bookmark.parentNode.removeChild(bookmark);
  }

  function moveToPreviousRange() {
    this.editor.focus();
    if( typeof(this.editor.previousRange) != "undefined" && this.editor.rawContent() != "" )
      this.editor.previousRange.select();
  }

  return {
    initialize:     initialize,
    getSelection:   getSelection,
    getRange:       getRange,
    getNode:        getNode,
    selectNode:     selectNode,
    setBookmark:    setBookmark,
    moveToBookmark: moveToBookmark,
    moveToPreviousRange: moveToPreviousRange
  };
})());
WysiHat.Toolbar = Class.create((function() {
  function initialize(editor) {
    this.editor = editor;
    this.element = this.createToolbarElement();
  }

  function createToolbarElement() {
    var toolbar = new Element('div', { 'class': 'editor_toolbar' });
    this.editor.insert({before: toolbar});
    return toolbar;
  }

  function addButtonSet(set) {
    var toolbar = this;
    $A(set).each(function(button){
      toolbar.addButton(button);
    });
  }

  function addButton(options, handler) {
    options = $H(options);

    if (!options.get('name'))
      options.set('name', options.get('label').toLowerCase());
    var name = options.get('name');

    var button = this.createButtonElement(this.element, options);

    var handler = this.buttonHandler(name, options);
    this.observeButtonClick(button, handler);

    var handler = this.buttonStateHandler(name, options);
    this.observeStateChanges(button, name, handler)
  }

  function createButtonElement(toolbar, options) {
    var button = new Element('a', {
      'class': 'button', 'href': '#'
    }); // added "new" for IE8 on vista
    button.update('<span>' + options.get('label') + '</span>');
    button.addClassName(options.get('name'));

    toolbar.appendChild(button);

    return button;
  }

  function buttonHandler(name, options) {
    if (options.handler)
      return options.handler;
    else if (options.get('handler'))
      return options.get('handler');
    else
      return function(editor) { editor.execCommand(name); };
  }

  function observeButtonClick(element, handler) {
    var toolbar = this;
    element.observe('click', function(event) {
      handler(toolbar.editor);
      toolbar.editor.fire("wysihat:change");
      toolbar.editor.fire("wysihat:cursormove");
      Event.stop(event);
    });
  }

  function buttonStateHandler(name, options) {
    if (options.query)
      return options.query;
    else if (options.get('query'))
      return options.get('query');
    else
      return function(editor) { return editor.queryCommandState(name); };
  }

  function observeStateChanges(element, name, handler) {
    var toolbar = this;
    var previousState = false;
    toolbar.editor.observe("wysihat:cursormove", function(event) {
      var state = handler(toolbar.editor);
      if (state != previousState) {
        previousState = state;
        toolbar.updateButtonState(element, name, state);
      }
    });
  }

  function updateButtonState(element, name, state) {
    if (state)
      element.addClassName('selected');
    else
      element.removeClassName('selected');
  }

  return {
    initialize:           initialize,
    createToolbarElement: createToolbarElement,
    addButtonSet:         addButtonSet,
    addButton:            addButton,
    createButtonElement:  createButtonElement,
    buttonHandler:        buttonHandler,
    observeButtonClick:   observeButtonClick,
    buttonStateHandler:   buttonStateHandler,
    observeStateChanges:  observeStateChanges,
    updateButtonState:    updateButtonState
  };
})());

WysiHat.Toolbar.ButtonSets = {};

WysiHat.Toolbar.ButtonSets.Basic = $A([
  { label: "Bold" },
  { label: "Underline" },
  { label: "Italic" }
]);
var DropioNote = {};

DropioNote.Toolbar = Class.create(WysiHat.Toolbar, {
  /* Overriding default behaviour to use a <div> as the toolbar element. */
  createToolbarElement: function () {
    var toolbar = new Element('div', {'class': 'editor_toolbar'});      // the toolbar
	toolbar.addClassName("editor_toolbar"); // fix for IE8
    this.slider = new Element("div",{'class': 'wysihatSlider', "id": "wysihatSlider"});  // slider image
	this.slider.addClassName("wysihatSlider"); // fix for IE8
    this.slider.innerHTML = "<img src='" + getAssetHost() + "/images/wysihat/slider.png' />";

    this.actionArea = new Element("div",{"class":"wysihatActionArea", "style":"display:none"}); // area that pops out for things like links, color, etc
    this.actionArea.addClassName("wysihatActionArea"); // for IE8
    this.actionArea.editor = this.editor;


    toolbar.insert({bottom: this.slider});

    this.editor.insert({after: this.actionArea});
    this.actionArea.insert({after: toolbar});

    this.buttonSpecs = {}

    return toolbar;
  },
  resizeEditor: function(slider) {
    var curHeight = this.editor.style.height;
    curHeight = curHeight ? parseInt(curHeight.replace("px","")) : 150;     // replace 150 with height of editor in css
    var normDelta = 70;                                                     // replace 70 with height of toolbar in css
    var curDelta = slider.currentDelta()[1];
    var newVal = (curHeight + (curDelta-normDelta));
    if( newVal > 10 )
      this.editor.style.height = newVal + "px";
  },
  revertSlider: function() {
    if( Prototype.Browser.IE )
      this.slider.style.setAttribute("cssText","",0); // for IE
    else
      this.slider.setAttribute("style","");
  },

  /*
  * toggles the action area above the toolbar
  */
  curActionArea: null,
  curAction: null,
  toggleActionArea: function(action) {
    if( typeof(action) == "undefined" ) action = this.curAction;

    var which = action.name;
    if( this.curActionArea == null ) {
      this.openActionArea(action);
    }
    else if( this.curActionArea == which ) {
      new Effect.BlindUp(this.actionArea,{duration:0.4});
      this.curActionArea = null;
      this.curAction = null;
    }
    else if( this.curActionArea != which ) {
      this.swapActionArea(action);
    }
  },
  openActionArea: function(action) {
    var which = action.name;
    if( $$("#wysihatActionAreas #wysihatActionArea_" + which).size != 0 ) {
      this.actionArea.innerHTML = $$("#wysihatActionAreas #wysihatActionArea_" + which).first().innerHTML;
      new Effect.BlindDown(this.actionArea,{duration:0.4});
      if( typeof(action.initActionArea) == "function" )
        action.initActionArea(this.editor);
      this.curActionArea = which;
      this.curAction = action;
    }
  },
  swapActionArea: function(action) {
    new Effect.BlindUp(this.actionArea,{duration:0.4,afterFinish:function() {
      this.openActionArea(action);
    }.bind(this)});
  },

  /* overriding default behavior for building the buttons */
  createButtonElement: function(toolbar, options) {
    var buttonSpec = options;

    var btn = new Element(
      'div',
      {"alt": buttonSpec.get("desc"), "title":  buttonSpec.get("desc")}
    );
	btn.addClassName("wysihatButton") // IE8
    btn.innerHTML = '<img src="' + getAssetHost() + '/images/wysihat/' + buttonSpec.get("name") + '.png" />';

    var div = new Element('div', {'class': 'wysihatActions'});
	div.addClassName("wysihatActions"); //for IE8
    div.insert({bottom:btn});

    toolbar.insert({bottom:div});

    this.buttonSpecs[buttonSpec.get("name")] = buttonSpec;

    return btn;
  },

  updateButtonState: function(element, name, state) {
    var buttonSpec = this.buttonSpecs[name];
    if( typeof(buttonSpec.get("updateState")) == "function" )
      buttonSpec.get("updateState")(this.editor,state);
    else {
      if( state ) {
        element.addClassName('selectedWysihatButton');
      } else {
        element.removeClassName('selectedWysihatButton');
      }
    }
  }
});

/*
* Various class level funtions
*/

DropioNote.NO_QUERY = function (editor, style) {
  return false;
}

/*
* DropioNote Actions
* To create another button, create a new entry in the DropioNote.Actions hash below
* - desc equates to the alt text when the user hovers over the button
* - handler is the function that gets called when the button is pushed
* - query is a function returning a boolean indicating if the button should appear selected
* When making a button that opens the action area
* add the HTML for the action area to bucket/_wysihat_actions
*/
DropioNote.Actions = {};


DropioNote.Actions.Bold = {
  name: 'bold',
  desc: "bold"
};

DropioNote.Actions.Underline = {
  name: 'underline',
  desc: "underline"
};

DropioNote.Actions.Italic = {
  name: 'italic',
  desc: "italic"
};

DropioNote.Actions.Strikethrough = {
  name: 'strikethrough',
  desc: "strikethrough"
}

DropioNote.Actions.Font = {
  name: "font",
  desc: "font family",
  handler: function( editor ) {
    editor.toolbar.toggleActionArea( DropioNote.Actions.Font );
  },
  initActionArea: function(editor) {
    var fontFamily = editor.fontFamilySelected();
    if( fontFamily ) {
      this.updateState(editor,fontFamily);
    }
  },
  doAction: function(editor,fontFamily) {
    if (fontFamily) {
      editor.fontFamilySelection(fontFamily);
    }
    else
      alert("You must select a font first.");
  },
  query: function(editor) {
    return editor.fontFamilySelected();
  },
  updateState: function(editor,state) {
    var lfs = editor.lastFontSelected;
    if( lfs ) lfs.removeClassName("wysihatFontChoiceSelected");
    var cfs = editor.toolbar.actionArea.down(".wysihatFontChoice_"+state.replace(/ /g,""));
    if( cfs ) {
      cfs.addClassName("wysihatFontChoiceSelected");
      editor.lastFontSelected = cfs;
    }
  }
}

DropioNote.Actions.Bigger = {
  name: 'bigger',
  desc: "increase font size",
  handler: function( editor ) {
    var curSize = parseInt(editor.fontSizeSelected());
    if( isNaN(curSize) || curSize == null || typeof(curSize) == "undefined" )
      curSize = 3
    var newSize = (curSize + 1 > 7) ? 7 : curSize + 1;
    editor.fontSizeSelection(newSize);
  },
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Smaller = {
  name: 'smaller',
  desc: "decrease font size",
  handler: function( editor ) {
    var curSize = parseInt(editor.fontSizeSelected());
    if( isNaN(curSize) || curSize == null || typeof(curSize) == "undefined" )
      curSize = 3
    var newSize = (curSize - 1 < 1) ? 1 : curSize - 1;
    editor.fontSizeSelection(newSize);
  },
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Superscript = {
  name: 'superscript',
  desc: "superscript"
}

DropioNote.Actions.Subscript = {
  name: 'subscript',
  desc: "subscript"
}

DropioNote.Actions.Color = {
  name: 'color',
  desc: "font color",
  handler: function( editor ) {
    editor.toolbar.toggleActionArea( DropioNote.Actions.Color );
  },
  initActionArea: function(editor) {
    var cc = editor.colorSelected();
    if( cc ) {
      this.updateState(editor,cc);
    }
  },
  doAction: function(editor,color) {
    editor.colorSelection(DropioNote.Actions.Color.convertToHexColor(color));
  },
  query: function(editor) {
    return editor.colorSelected();
  },
  updateState: function(editor,state) {
    var color = DropioNote.Actions.Color.convertToHexColor(state);
    var lcs = editor.lastColorSelected;
    if( lcs ) lcs.removeClassName("wysihatColorChoiceSelected");
    var ccs = editor.toolbar.actionArea.down(".wysihatColorChoice_"+color.replace("#",""));
    if( ccs ) {
      ccs.addClassName("wysihatColorChoiceSelected");
      editor.lastColorSelected = ccs;
    }

  },
  convertToHexColor: function(rgb) {
    if( typeof(rgb) == "number" ) {
      var result= (parseInt(rgb).toString(16));
      if(result.length == 1) result = ("0" +result);
      return "#" + result.substring(4,6).toUpperCase()+result.substring(2,4).toUpperCase()+result.substring(0,2).toUpperCase();
    }
    else if( typeof(rgb) == "string" ) {
      if( rgb.indexOf("#") != -1 ) return rgb;
      var rgbs = rgb.replace(/[rgb\(\) ]/g,"").split(",");
      var hex = "#"
      rgbs.each(function(rgb) {
       hex += inHex(parseInt(rgb))
      });
      return hex;
    }
    return "#000000";
  }
}

DropioNote.Actions.OrderedList = {
  name: 'insertorderedlist',
  desc: "create a numbered list",
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.UnorderedList = {
  name: 'insertunorderedlist',
  desc: "create a bullet point list",
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Indent = {
  name: 'indent',
  desc: "indent",
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Outdent = {
  name: 'outdent',
  desc: "unindent",
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Link = {
  name: 'link',
  desc: "link to webpage",
  handler: function( editor ) {
    editor.toolbar.toggleActionArea( DropioNote.Actions.Link );
  },
  doAction: function(editor,url) {
    if (url) {
      editor.linkSelection(url);
      editor.toolbar.toggleActionArea( DropioNote.Actions.Link );
    }
    else
      alert("You need to enter a URL first.")
  },
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Unlink = {
  name: 'unlink',
  desc: "remove a link",
  handler: function(editor) { editor.unlinkSelection() },
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Image = {
  name: 'image',
  desc: "insert an image from the web",
  handler: function( editor ) {
    editor.toolbar.toggleActionArea( DropioNote.Actions.Image );
  },
  doAction: function(editor,url) {
    if (url) {
      editor.insertImage(url);
      editor.toolbar.toggleActionArea( DropioNote.Actions.Image );
    }
    else
      alert("You need to enter a URL first.")
  },
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.HTML = {
  name: 'html',
  desc: "edit the raw HTML of this note",
  handler: function( editor ) {
    editor.toolbar.toggleActionArea( DropioNote.Actions.HTML );
  },
  initActionArea: function(editor) {
    editor.toolbar.actionArea.down(".wysihatEditHTML").value = editor.rawContent();
  },
  doAction: function(editor,html) {
    if (html) {
      editor.setRawContent("");
      editor.insertHTML(html);
      editor.toolbar.toggleActionArea( DropioNote.Actions.HTML );
    }
    else
      alert("You must type some HTML first.");
  },
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Undo = {
  name: 'undo',
  desc: "undo",
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Redo = {
  name: 'redo',
  desc: "redo",
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Clear = {
  name: 'clear',
  desc: "clear",
  handler: function (editor) {
    var res = confirm("Are you sure you want to erase the entire contents of this note?");
    if( res )
      editor.setRawContent("");
  },
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Print = {
  name: 'print',
  desc: "print this note",
  handler: function(editor) {
    editor.getWindow().focus();
    editor.getWindow().print();
  },
  query: DropioNote.NO_QUERY
}

DropioNote.Actions.Help = {
  name: 'help',
  desc: "help",
  handler: function( editor ) {
    editor.toolbar.toggleActionArea( DropioNote.Actions.Help );
  },
  query: DropioNote.NO_QUERY
}


DropioNote.Actions.Embed = {
  name: 'embed',
  desc: "embed media (images,videos,flash,etc)",
  handler: function( editor ) {
    editor.toolbar.toggleActionArea( DropioNote.Actions.Embed );
  },
  doAction: function(editor,embedCode) {
    if (embedCode) {
      editor.insertHTML(embedCode);
      editor.toolbar.toggleActionArea( DropioNote.Actions.Embed );
    }
    else
      alert("You need to enter an embed code first.")
  },
  query: DropioNote.NO_QUERY
}


DropioNote.attachEmbedPlaceholders = function(editor) {
  if( !Prototype.Browser.IE ) {
    var embedStyles = new Element('style',{"type":"text/css","media":"screen"});
    embedStyles.innerHTML = "body {font-family: Arial, Tahoma, Verdana, Helvetica, Sans;} object, embed, iframe { width: 200px; height: 200px; border: 1px solid black; display: block; background: url(http://" + window.location.host + "/images/wysihat/EmbedPlaceholder.png); }"
    editor.getDocument().getElementsByTagName("head")[0].appendChild(embedStyles);
  }
}

DropioNote.ButtonSet = []
for( k in DropioNote.Actions )
  DropioNote.ButtonSet.push(DropioNote.Actions[k]);


DropioNote.editors = $H({});
DropioNote.getEditor = function(id) {
  return DropioNote.editors.get(id);
}

DropioNote.registerEditor = function(id) {
  var editor = WysiHat.Editor.attach(id);
  var toolbar = new DropioNote.Toolbar(editor);
  editor.toolbar = toolbar;
  toolbar.addButtonSet(DropioNote.ButtonSet);
  DropioNote.editors.set(id,editor);
  new Draggable(toolbar.slider,{constraint:"vertical", onDrag: toolbar.resizeEditor.bind(toolbar), revert: toolbar.revertSlider.bind(toolbar), scroll: window })
  Event.observe(editor,"load",function(){
    DropioNote.attachEmbedPlaceholders(editor)
  });
}

DropioNote.unregisterEditor = function(id) {
  DropioNote.editors.unset(id);
}

/* ... AND FINALLY CREATE THE EDITOR AND TOOLBAR ELEMENTS. */
Event.observe(window, 'load', function() {
  $$("textarea.wysihat").each(function(e) {
    DropioNote.registerEditor(e.id);
  })
});

var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

/**
 * Encodes a string in base64
 * @param {String} input The string to encode in base64.
 */
function encode64(input) {
   var output = "";
   var chr1, chr2, chr3;
   var enc1, enc2, enc3, enc4;
   var i = 0;

   do {
      chr1 = input.charCodeAt(i++);
      chr2 = input.charCodeAt(i++);
      chr3 = input.charCodeAt(i++);

      enc1 = chr1 >> 2;
      enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
      enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
      enc4 = chr3 & 63;

      if (isNaN(chr2)) {
         enc3 = enc4 = 64;
      } else if (isNaN(chr3)) {
         enc4 = 64;
      }

      output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
         keyStr.charAt(enc3) + keyStr.charAt(enc4);
   } while (i < input.length);

   return output;
}

/**
 * Decodes a base64 string.
 * @param {String} input The string to decode.
 */
function decode64(input) {
   var output = "";
   var chr1, chr2, chr3;
   var enc1, enc2, enc3, enc4;
   var i = 0;

   input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

   do {
      enc1 = keyStr.indexOf(input.charAt(i++));
      enc2 = keyStr.indexOf(input.charAt(i++));
      enc3 = keyStr.indexOf(input.charAt(i++));
      enc4 = keyStr.indexOf(input.charAt(i++));

      chr1 = (enc1 << 2) | (enc2 >> 4);
      chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
      chr3 = ((enc3 & 3) << 6) | enc4;

      output = output + String.fromCharCode(chr1);

      if (enc3 != 64) {
         output = output + String.fromCharCode(chr2);
      }
      if (enc4 != 64) {
         output = output + String.fromCharCode(chr3);
      }
   } while (i < input.length);

   return output;
}
/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function md5_vm_test()
{
  return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length
 */
function core_md5(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << ((len) % 32);
  x[(((len + 64) >>> 9) << 4) + 14] = len;

  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;

  var olda, oldb, oldc, oldd;
  for(var i = 0; i < x.length; i += 16)
  {
    olda = a;
    oldb = b;
    oldc = c;
    oldd = d;

    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
  }
  return [a, b, c, d];
}

/*
 * These functions implement the four basic operations the algorithm uses.
 */
function md5_cmn(q, a, b, x, s, t)
{
  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
 * Calculate the HMAC-MD5, of a key and some data
 */
function core_hmac_md5(key, data)
{
  var bkey = str2binl(key);
  if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); }

  var ipad = new Array(16), opad = new Array(16);
  for(var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
  return core_md5(opad.concat(hash), 512 + 128);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert a string to an array of little-endian words
 * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
 */
function str2binl(str)
{
  var bin = [];
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < str.length * chrsz; i += chrsz)
  {
    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
  }
  return bin;
}

/*
 * Convert an array of little-endian words to a string
 */
function binl2str(bin)
{
  var str = "";
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < bin.length * 32; i += chrsz)
  {
    str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
  }
  return str;
}

/*
 * Convert an array of little-endian words to a hex string.
 */
function binl2hex(binarray)
{
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
  }
  return str;
}

/*
 * Convert an array of little-endian words to a base-64 string
 */
function binl2b64(binarray)
{
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var str = "";
  var triplet, j;
  for(var i = 0; i < binarray.length * 4; i += 3)
  {
    triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16) |
              (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) |
              ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
    for(j = 0; j < 4; j++)
    {
      if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
      else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
    }
  }
  return str;
}
/*
 * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
 * in FIPS PUB 180-1
 * Version 2.1a Copyright Paul Johnston 2000 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for details.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}

/*
 * Perform a simple self-test to see if the VM is working
 */
function sha1_vm_test()
{
  return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
}

/*
 * Calculate the SHA-1 of an array of big-endian words, and a bit length
 */
function core_sha1(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << (24 - len % 32);
  x[((len + 64 >> 9) << 4) + 15] = len;

  var w = new Array(80);
  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;
  var e = -1009589776;

  var i, j, t, olda, oldb, oldc, oldd, olde;
  for (i = 0; i < x.length; i += 16)
  {
    olda = a;
    oldb = b;
    oldc = c;
    oldd = d;
    olde = e;

    for (j = 0; j < 80; j++)
    {
      if (j < 16) { w[j] = x[i + j]; }
      else { w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); }
      t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
                       safe_add(safe_add(e, w[j]), sha1_kt(j)));
      e = d;
      d = c;
      c = rol(b, 30);
      b = a;
      a = t;
    }

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
    e = safe_add(e, olde);
  }
  return [a, b, c, d, e];
}

/*
 * Perform the appropriate triplet combination function for the current
 * iteration
 */
function sha1_ft(t, b, c, d)
{
  if (t < 20) { return (b & c) | ((~b) & d); }
  if (t < 40) { return b ^ c ^ d; }
  if (t < 60) { return (b & c) | (b & d) | (c & d); }
  return b ^ c ^ d;
}

/*
 * Determine the appropriate additive constant for the current iteration
 */
function sha1_kt(t)
{
  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
         (t < 60) ? -1894007588 : -899497514;
}

/*
 * Calculate the HMAC-SHA1 of a key and some data
 */
function core_hmac_sha1(key, data)
{
  var bkey = str2binb(key);
  if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * chrsz); }

  var ipad = new Array(16), opad = new Array(16);
  for (var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
  return core_sha1(opad.concat(hash), 512 + 160);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert an 8-bit or 16-bit string to an array of big-endian words
 * In 8-bit function, characters >255 have their hi-byte silently ignored.
 */
function str2binb(str)
{
  var bin = [];
  var mask = (1 << chrsz) - 1;
  for (var i = 0; i < str.length * chrsz; i += chrsz)
  {
    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
  }
  return bin;
}

/*
 * Convert an array of big-endian words to a string
 */
function binb2str(bin)
{
  var str = "";
  var mask = (1 << chrsz) - 1;
  for (var i = 0; i < bin.length * 32; i += chrsz)
  {
    str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
  }
  return str;
}

/*
 * Convert an array of big-endian words to a hex string.
 */
function binb2hex(binarray)
{
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for (var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8  )) & 0xF);
  }
  return str;
}

/*
 * Convert an array of big-endian words to a base-64 string
 */
function binb2b64(binarray)
{
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var str = "";
  var triplet, j;
  for (var i = 0; i < binarray.length * 4; i += 3)
  {
    triplet = (((binarray[i   >> 2] >> 8 * (3 -  i   %4)) & 0xFF) << 16) |
              (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) |
               ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
    for (j = 0; j < 4; j++)
    {
      if (i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
      else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
    }
  }
  return str;
}
/*
    This program is distributed under the terms of the MIT license.
    Please see the LICENSE file for details.

    Copyright 2006-2008, OGG, LLC
*/

/** File: strophe.js
 *  A JavaScript library for XMPP BOSH.
 *
 *  This is the JavaScript version of the Strophe library.  Since JavaScript
 *  has no facilities for persistent TCP connections, this library uses
 *  Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate
 *  a persistent, stateful, two-way connection to an XMPP server.  More
 *  information on BOSH can be found in XEP 124.
 */

/** PrivateFunction: Function.prototype.bind
 *  Bind a function to an instance.
 *
 *  This Function object extension method creates a bound method similar
 *  to those in Python.  This means that the 'this' object will point
 *  to the instance you want.  See
 *  <a href='http://benjamin.smedbergs.us/blog/2007-01-03/bound-functions-and-function-imports-in-javascript/'>Bound Functions and Function Imports in JavaScript</a>
 *  for a complete explanation.
 *
 *  This extension already exists in some browsers (namely, Firefox 3), but
 *  we provide it to support those that don't.
 *
 *  Parameters:
 *    (Object) obj - The object that will become 'this' in the bound function.
 *
 *  Returns:
 *    The bound function.
 */
if (!Function.prototype.bind) {
    Function.prototype.bind = function (obj)
    {
	var func = this;
	return function () { return func.apply(obj, arguments); };
    };
}

/** PrivateFunction: Function.prototype.prependArg
 *  Prepend an argument to a function.
 *
 *  This Function object extension method returns a Function that will
 *  invoke the original function with an argument prepended.  This is useful
 *  when some object has a callback that needs to get that same object as
 *  an argument.  The following fragment illustrates a simple case of this
 *  > var obj = new Foo(this.someMethod);</code></blockquote>
 *
 *  Foo's constructor can now use func.prependArg(this) to ensure the
 *  passed in callback function gets the instance of Foo as an argument.
 *  Doing this without prependArg would mean not setting the callback
 *  from the constructor.
 *
 *  This is used inside Strophe for passing the Strophe.Request object to
 *  the onreadystatechange handler of XMLHttpRequests.
 *
 *  Parameters:
 *    arg - The argument to pass as the first parameter to the function.
 *
 *  Returns:
 *    A new Function which calls the original with the prepended argument.
 */
if (!Function.prototype.prependArg) {
    Function.prototype.prependArg = function (arg)
    {
	var func = this;

	return function () {
	    var newargs = [arg];
	    for (var i = 0; i < arguments.length; i++)
		newargs.push(arguments[i]);
	    return func.apply(this, newargs);
	};
    };
}

/** PrivateFunction: Array.prototype.indexOf
 *  Return the index of an object in an array.
 *
 *  This function is not supplied by some JavaScript implementations, so
 *  we provide it if it is missing.  This code is from:
 *  http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
 *
 *  Parameters:
 *    (Object) elt - The object to look for.
 *    (Integer) from - The index from which to start looking. (optional).
 *
 *  Returns:
 *    The index of elt in the array or -1 if not found.
 */
if (!Array.prototype.indexOf)
{
    Array.prototype.indexOf = function(elt /*, from*/)
    {
	var len = this.length;

	var from = Number(arguments[1]) || 0;
	from = (from < 0) ? Math.ceil(from) : Math.floor(from);
	if (from < 0)
	    from += len;

	for (; from < len; from++) {
	    if (from in this && this[from] === elt)
		return from;
	}

	return -1;
    };
}


/** Function: $build
 *  Create a Strophe.Builder.
 *  This is an alias for 'new Strophe.Builder(name, attrs)'.
 *
 *  Parameters:
 *    (String) name - The root element name.
 *    (Object) attrs - The attributes for the root element in object notation.
 *
 *  Returns:
 *    A new Strophe.Builder object.
 */
function $build(name, attrs) { return new Strophe.Builder(name, attrs); }
/** Function: $msg
 *  Create a Strophe.Builder with a <message/> element as the root.
 *
 *  Parmaeters:
 *    (Object) attrs - The <message/> element attributes in object notation.
 *
 *  Returns:
 *    A new Strophe.Builder object.
 */
function $msg(attrs) { return new Strophe.Builder("message", attrs); }
/** Function: $iq
 *  Create a Strophe.Builder with an <iq/> element as the root.
 *
 *  Parameters:
 *    (Object) attrs - The <iq/> element attributes in object notation.
 *
 *  Returns:
 *    A new Strophe.Builder object.
 */
function $iq(attrs) { return new Strophe.Builder("iq", attrs); }
/** Function: $pres
 *  Create a Strophe.Builder with a <presence/> element as the root.
 *
 *  Parameters:
 *    (Object) attrs - The <presence/> element attributes in object notation.
 *
 *  Returns:
 *    A new Strophe.Builder object.
 */
function $pres(attrs) { return new Strophe.Builder("presence", attrs); }

/** Class: Strophe
 *  An object container for all Strophe library functions.
 *
 *  This class is just a container for all the objects and constants
 *  used in the library.  It is not meant to be instantiated, but to
 *  provide a namespace for library objects, constants, and functions.
 */
Strophe = {
    /** Constants: XMPP Namespace Constants
     *  Common namespace constants from the XMPP RFCs and XEPs.
     *
     *  NS.HTTPBIND - HTTP BIND namespace from XEP 124.
     *  NS.BOSH - BOSH namespace from XEP 206.
     *  NS.CLIENT - Main XMPP client namespace.
     *  NS.AUTH - Legacy authentication namespace.
     *  NS.ROSTER - Roster operations namespace.
     *  NS.PROFILE - Profile namespace.
     *  NS.DISCO_INFO - Service discovery info namespace from XEP 30.
     *  NS.DISCO_ITEMS - Service discovery items namespace from XEP 30.
     *  NS.MUC - Multi-User Chat namespace from XEP 45.
     *  NS.SASL - XMPP SASL namespace from RFC 3920.
     *  NS.STREAM - XMPP Streams namespace from RFC 3920.
     *  NS.BIND - XMPP Binding namespace from RFC 3920.
     *  NS.SESSION - XMPP Session namespace from RFC 3920.
     */
    NS: {
	HTTPBIND: "http://jabber.org/protocol/httpbind",
	BOSH: "urn:xmpp:xbosh",
	CLIENT: "jabber:client",
	AUTH: "jabber:iq:auth",
	ROSTER: "jabber:iq:roster",
	PROFILE: "jabber:iq:profile",
	DISCO_INFO: "http://jabber.org/protocol/disco#info",
	DISCO_ITEMS: "http://jabber.org/protocol/disco#items",
	MUC: "http://jabber.org/protocol/muc",
	SASL: "urn:ietf:params:xml:ns:xmpp-sasl",
	STREAM: "http://etherx.jabber.org/streams",
	BIND: "urn:ietf:params:xml:ns:xmpp-bind",
	SESSION: "urn:ietf:params:xml:ns:xmpp-session",
	VERSION: "jabber:iq:version"
    },

    /** Constants: Connection Status Constants
     *  Connection status constants for use by the connection handler
     *  callback.
     *
     *  Status.ERROR - An error has occurred
     *  Status.CONNECTING - The connection is currently being made
     *  Status.CONNFAIL - The connection attempt failed
     *  Status.AUTHENTICATING - The connection is authenticating
     *  Status.AUTHFAIL - The authentication attempt failed
     *  Status.CONNECTED - The connection has succeeded
     *  Status.DISCONNECTED - The connection has been terminated
     *  Status.DISCONNECTING - The connection is currently being terminated
     */
    Status: {
	ERROR: 0,
	CONNECTING: 1,
	CONNFAIL: 2,
	AUTHENTICATING: 3,
	AUTHFAIL: 4,
	CONNECTED: 5,
	DISCONNECTED: 6,
	DISCONNECTING: 7
    },

    /** Constants: Log Level Constants
     *  Logging level indicators.
     *
     *  LogLevel.DEBUG - Debug output
     *  LogLevel.INFO - Informational output
     *  LogLevel.WARN - Warnings
     *  LogLevel.ERROR - Errors
     *  LogLevel.FATAL - Fatal errors
     */
    LogLevel: {
	DEBUG: 0,
	INFO: 1,
	WARN: 2,
	ERROR: 3,
	FATAL: 4
    },

    /** PrivateConstants: DOM Element Type Constants
     *  DOM element types.
     *
     *  ElementType.NORMAL - Normal element.
     *  ElementType.TEXT - Text data element.
     */
    ElementType: {
	NORMAL: 1,
	TEXT: 3
    },

    /** PrivateConstants: Timeout Values
     *  Timeout values for error states.  These values are in seconds.
     *  These should not be changed unless you know exactly what you are
     *  doing.
     *
     *  TIMEOUT - Time to wait for a request to return.  This defaults to
     *      70 seconds.
     *  SECONDARY_TIMEOUT - Time to wait for immediate request return. This
     *      defaults to 7 seconds.
     */
    TIMEOUT: 70,
    SECONDARY_TIMEOUT: 7,

    /** Function: forEachChild
     *  Map a function over some or all child elements of a given element.
     *
     *  This is a small convenience function for mapping a function over
     *  some or all of the children of an element.  If elemName is null, all
     *  children will be passed to the function, otherwise only children
     *  whose tag names match elemName will be passed.
     *
     *  Parameters:
     *    (XMLElement) elem - The element to operate on.
     *    (String) elemName - The child element tag name filter.
     *    (Function) func - The function to apply to each child.  This
     *      function should take a single argument, a DOM element.
     */
    forEachChild: function (elem, elemName, func)
    {
	var i, childNode;

	for (i = 0; i < elem.childNodes.length; i++) {
            childNode = elem.childNodes[i];
            if (childNode.nodeType == Strophe.ElementType.NORMAL &&
                (!elemName || this.isTagEqual(childNode, elemName))) {
                func(childNode);
	    }
	}
    },

    /** Function: isTagEqual
     *  Compare an element's tag name with a string.
     *
     *  This function is case insensitive.
     *
     *  Parameters:
     *    (XMLElement) el - A DOM element.
     *    (String) name - The element name.
     *
     *  Returns:
     *    true if the element's tag name matches _el_, and false
     *    otherwise.
     */
    isTagEqual: function (el, name)
    {
	return el.tagName.toLowerCase() == name.toLowerCase();
    },

    /** Function: xmlElement
     *  Create an XML DOM element.
     *
     *  This function creates an XML DOM element correctly across all
     *  implementations. Specifically the Microsoft implementation of
     *  document.createElement makes DOM elements with 43+ default attributes
     *  unless elements are created with the ActiveX object Microsoft.XMLDOM.
     *
     *  Most DOMs force element names to lowercase, so we use the
     *  _realname attribute on the created element to store the case
     *  sensitive name.  This is required to generate proper XML for
     *  things like vCard avatars (XEP 153).  This attribute is stripped
     *  out before being sent over the wire or serialized, but you may
     *  notice it during debugging.
     *
     *  Parameters:
     *    (String) name - The name for the element.
     *    (Array) attrs - An optional array of key/value pairs to use as
     *      element attributes in the following format [['key1', 'value1'],
     *      ['key2', 'value2']]
     *    (String) text - The text child data for the element.
     *
     *  Returns:
     *    A new XML DOM element.
     */
    xmlElement: function (name)
    {
	if (!name) { return null; }

	var node = null;
	if (window.ActiveXObject) {
	    node = new ActiveXObject("Microsoft.XMLDOM").createElement(name);
	} else {
	    node = document.createElement(name);
	}
	if (node.tagName != name)
	    node.setAttribute("_realname", name);

	var a, i;
	for (a = 1; a < arguments.length; a++) {
	    if (!arguments[a]) { continue; }
	    if (typeof(arguments[a]) == "string" ||
		typeof(arguments[a]) == "number") {
		node.appendChild(Strophe.xmlTextNode(arguments[a]));
	    } else if (typeof(arguments[a]) == "object" &&
		       typeof(arguments[a]['sort']) == "function") {
		for (i = 0; i < arguments[a].length; i++) {
		    if (typeof(arguments[a][i]) == "object" &&
			typeof(arguments[a][i]['sort']) == "function") {
			node.setAttribute(arguments[a][i][0],
					  arguments[a][i][1]);
		    }
		}
	    }
	}

	return node;
    },

    /** Function: xmlTextNode
     *  Creates an XML DOM text node.
     *
     *  Provides a cross implementation version of document.createTextNode.
     *
     *  Parameters:
     *    (String) text - The content of the text node.
     *
     *  Returns:
     *    A new XML DOM text node.
     */
    xmlTextNode: function (text)
    {
	if (window.ActiveXObject) {
	    return new ActiveXObject("Microsoft.XMLDOM").createTextNode(text);
	} else {
	    return document.createTextNode(text);
	}
    },

    /** Function: getText
     *  Get the concatenation of all text children of an element.
     *
     *  Parameters:
     *    (XMLElement) elem - A DOM element.
     *
     *  Returns:
     *    A String with the concatenated text of all text element children.
     */
    getText: function (elem)
    {
	if (!elem) return null;

	var str = "";
	if (elem.childNodes.length === 0 && elem.nodeType ==
	    Strophe.ElementType.TEXT) {
	    str += elem.nodeValue;
	}

	for (var i = 0; i < elem.childNodes.length; i++) {
	    if (elem.childNodes[i].nodeType == Strophe.ElementType.TEXT) {
		str += elem.childNodes[i].nodeValue;
	    }
	}

	return str;
    },

    /** Function: copyElement
     *  Copy an XML DOM element.
     *
     *  This function copies a DOM element and all its descendants and returns
     *  the new copy.
     *
     *  Parameters:
     *    (XMLElement) elem - A DOM element.
     *
     *  Returns:
     *    A new, copied DOM element tree.
     */
    copyElement: function (elem)
    {
	var i, el;
	if (elem.nodeType == Strophe.ElementType.NORMAL) {
	    el = Strophe.xmlElement(elem.tagName);

	    for (i = 0; i < elem.attributes.length; i++) {
		el.setAttribute(elem.attributes[i].nodeName.toLowerCase(),
				elem.attributes[i].value);
	    }

	    for (i = 0; i < elem.childNodes.length; i++) {
		el.appendChild(Strophe.copyElement(elem.childNodes[i]));
	    }
	} else if (elem.nodeType == Strophe.ElementType.TEXT) {
	    el = Strophe.xmlTextNode(elem.nodeValue);
	}

	return el;
    },

    /** Function: escapeJid
     *  Escape a JID.
     *
     *  Parameters:
     *    (String) jid - A JID.
     *
     *  Returns:
     *    An escaped JID String.
     */
    escapeJid: function (jid)
    {
	var user = jid.split("@");
	if (user.length == 1)
	    return jid;

	var host = user.splice(user.length - 1, 1)[0];
	user = user.join("@")
	    .replace(/^\s+|\s+$/g, '')
	    .replace(/\\/g,  "\\5c")
	    .replace(/ /g,   "\\20")
	    .replace(/\"/g,  "\\22")
	    .replace(/\&/g,  "\\26")
	    .replace(/\'/g,  "\\27")
	    .replace(/\//g,  "\\2f")
	    .replace(/:/g,   "\\3a")
	    .replace(/</g,   "\\3c")
	    .replace(/>/g,   "\\3e")
	    .replace(/@/g,   "\\40");

	return [user, host].join("@");
    },

    /** Function: unescapeJid
     *  Unescape a JID.
     *
     *  Parameters:
     *    (String) jid - A JID.
     *
     *  Returns:
     *    An unescaped JID String.
     */
    unescapeJid: function (jid)
    {
	return jid.replace(/\\20/g, " ")
	    .replace(/\\22/g, '"')
	    .replace(/\\26/g, "&")
	    .replace(/\\27/g, "'")
	    .replace(/\\2f/g, "/")
	    .replace(/\\3a/g, ":")
	    .replace(/\\3c/g, "<")
	    .replace(/\\3e/g, ">")
	    .replace(/\\40/g, "@")
	    .replace(/\\5c/g, "\\");
    },

    /** Function: getNodeFromJid
     *  Get the node portion of a JID String.
     *
     *  Parameters:
     *    (String) jid - A JID.
     *
     *  Returns:
     *    A String containing the node.
     */
    getNodeFromJid: function (jid)
    {
	if (jid.indexOf("@") < 0)
	    return null;
	return Strophe.escapeJid(jid).split("@")[0];
    },

    /** Function: getDomainFromJid
     *  Get the domain portion of a JID String.
     *
     *  Parameters:
     *    (String) jid - A JID.
     *
     *  Returns:
     *    A String containing the domain.
     */
    getDomainFromJid: function (jid)
    {
	var bare = Strophe.escapeJid(Strophe.getBareJidFromJid(jid));
	if (bare.indexOf("@") < 0)
	    return bare;
	else
	    return bare.split("@")[1];
    },

    /** Function: getResourceFromJid
     *  Get the resource portion of a JID String.
     *
     *  Parameters:
     *    (String) jid - A JID.
     *
     *  Returns:
     *    A String containing the resource.
     */
    getResourceFromJid: function (jid)
    {
	var s = Strophe.escapeJid(jid).split("/");
	if (s.length < 2) return null;
	return s[1];
    },

    /** Function: getBareJidFromJid
     *  Get the bare JID from a JID String.
     *
     *  Parameters:
     *    (String) jid - A JID.
     *
     *  Returns:
     *    A String containing the bare JID.
     */
    getBareJidFromJid: function (jid)
    {
	return this.escapeJid(jid).split("/")[0];
    },

    /** Function: log
     *  User overrideable logging function.
     *
     *  This function is called whenever the Strophe library calls any
     *  of the logging functions.  The default implementation of this
     *  function does nothing.  If client code wishes to handle the logging
     *  messages, it should override this with
     *  > Strophe.log = function (level, msg) {
     *  >   (user code here)
     *  > };
     *
     *  Please note that data sent and received over the wire is logged
     *  via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput().
     *
     *  The different levels and their meanings are
     *
     *    DEBUG - Messages useful for debugging purposes.
     *    INFO - Informational messages.  This is mostly information like
     *      'disconnect was called' or 'SASL auth succeeded'.
     *    WARN - Warnings about potential problems.  This is mostly used
     *      to report transient connection errors like request timeouts.
     *    ERROR - Some error occurred.
     *    FATAL - A non-recoverable fatal error occurred.
     *
     *  Parameters:
     *    (Integer) level - The log level of the log message.  This will
     *      be one of the values in Strophe.LogLevel.
     *    (String) msg - The log message.
     */
    log: function (level, msg)
    {
	return;
    },

    /** Function: debug
     *  Log a message at the Strophe.LogLevel.DEBUG level.
     *
     *  Parameters:
     *    (String) msg - The log message.
     */
    debug: function(msg)
    {
	this.log(this.LogLevel.DEBUG, msg);
    },

    /** Function: info
     *  Log a message at the Strophe.LogLevel.INFO level.
     *
     *  Parameters:
     *    (String) msg - The log message.
     */
    info: function (msg)
    {
	this.log(this.LogLevel.INFO, msg);
    },

    /** Function: warn
     *  Log a message at the Strophe.LogLevel.WARN level.
     *
     *  Parameters:
     *    (String) msg - The log message.
     */
    warn: function (msg)
    {
	this.log(this.LogLevel.WARN, msg);
    },

    /** Function: error
     *  Log a message at the Strophe.LogLevel.ERROR level.
     *
     *  Parameters:
     *    (String) msg - The log message.
     */
    error: function (msg)
    {
	this.log(this.LogLevel.ERROR, msg);
    },

    /** Function: fatal
     *  Log a message at the Strophe.LogLevel.FATAL level.
     *
     *  Parameters:
     *    (String) msg - The log message.
     */
    fatal: function (msg)
    {
	this.log(this.LogLevel.FATAL, msg);
    },

    /** Function: serialize
     *  Render a DOM element and all descendants to a String.
     *
     *  Parameters:
     *    (XMLElement) elem - A DOM element.
     *
     *  Returns:
     *    The serialized element tree as a String.
     */
    serialize: function (elem)
    {
	var result;

	if (!elem) return null;

	var nodeName = elem.nodeName;
	var i, child;

	if (elem.getAttribute("_realname")) {
	    nodeName = elem.getAttribute("_realname");
	}

	result = "<" + nodeName;
	for (i = 0; i < elem.attributes.length; i++) {
               if(elem.attributes[i].nodeName != "_realname") {
		 result += " " + elem.attributes[i].nodeName.toLowerCase() +
	        "='" + elem.attributes[i].value
	            .replace("'", "&#39;").replace("&", "&#x26;") + "'";
	       }
	}

	if (elem.childNodes.length > 0) {
	    result += ">";
	    for (i = 0; i < elem.childNodes.length; i++) {
		child = elem.childNodes[i];
		if (child.nodeType == Strophe.ElementType.NORMAL) {
		    result += Strophe.serialize(child);
		} else if (child.nodeType == Strophe.ElementType.TEXT) {
		    result += child.nodeValue;
		}
	    }
	    result += "</" + nodeName + ">";
	} else {
	    result += "/>";
	}

	return result;
    },

    /** PrivateVariable: _requestId
     *  _Private_ variable that keeps track of the request ids for
     *  connections.
     */
    _requestId: 0
};

/** Class: Strophe.Builder
 *  XML DOM builder.
 *
 *  This object provides an interface similar to JQuery but for building
 *  DOM element easily and rapidly.  All the functions except for toString()
 *  and tree() return the object, so calls can be chained.  Here's an
 *  example using the $iq() builder helper.
 *  > $iq({to: 'you': from: 'me': type: 'get', id: '1'})
 *  >     .c('query', {xmlns: 'strophe:example'})
 *  >     .c('example')
 *  >     .toString()
 *  The above generates this XML fragment
 *  > <iq to='you' from='me' type='get' id='1'>
 *  >   <query xmlns='strophe:example'>
 *  >     <example/>
 *  >   </query>
 *  > </iq>
 *  The corresponding DOM manipulations to get a similar fragment would be
 *  a lot more tedious and probably involve several helper variables.
 *
 *  Since adding children makes new operations operate on the child, up()
 *  is provided to traverse up the tree.  To add two children, do
 *  > builder.c('child1', ...).up().c('child2', ...)
 *  The next operation on the Builder will be relative to the second child.
 */

/** Constructor: Strophe.Builder
 *  Create a Strophe.Builder object.
 *
 *  The attributes should be passed in object notation.  For example
 *  > var b = new Builder('message', {to: 'you', from: 'me'});
 *  or
 *  > var b = new Builder('messsage', {'xml:lang': 'en'});
 *
 *  Parameters:
 *    (String) name - The name of the root element.
 *    (Object) attrs - The attributes for the root element in object notation.
 *
 *  Returns:
 *    A new Strophe.Builder.
 */
Strophe.Builder = function (name, attrs)
{
    this.nodeTree = this._makeNode(name, attrs);

    this.node = this.nodeTree;
};

Strophe.Builder.prototype = {
    /** Function: tree
     *  Return the DOM tree.
     *
     *  This function returns the current DOM tree as an element object.  This
     *  is suitable for passing to functions like Strophe.Connection.send().
     *
     *  Returns:
     *    The DOM tree as a element object.
     */
    tree: function ()
    {
	return this.nodeTree;
    },

    /** Function: toString
     *  Serialize the DOM tree to a String.
     *
     *  This function returns a string serialization of the current DOM
     *  tree.  It is often used internally to pass data to a
     *  Strophe.Request object.
     *
     *  Returns:
     *    The serialized DOM tree in a String.
     */
    toString: function ()
    {
	return Strophe.serialize(this.nodeTree);
    },

    /** Function: up
     *  Make the current parent element the new current element.
     *
     *  This function is often used after c() to traverse back up the tree.
     *  For example, to add two children to the same element
     *  > builder.c('child1', {}).up().c('child2', {});
     *
     *  Returns:
     *    The Stophe.Builder object.
     */
    up: function ()
    {
	this.node = this.node.parentNode;
	return this;
    },

    /** Function: attrs
     *  Add or modify attributes of the current element.
     *
     *  The attributes should be passed in object notation.  This function
     *  does not move the current element pointer.
     *
     *  Parameters:
     *    (Object) moreattrs - The attributes to add/modify in object notation.
     *
     *  Returns:
     *    The Strophe.Builder object.
     */
    attrs: function (moreattrs)
    {
	for (var k in moreattrs)
	    this.node.setAttribute(k, moreattrs[k]);
	return this;
    },

    /** Function: c
     *  Add a child to the current element and make it the new current
     *  element.
     *
     *  This function moves the current element pointer to the child.  If you
     *  need to add another child, it is necessary to use up() to go back
     *  to the parent in the tree.
     *
     *  Parameters:
     *    (String) name - The name of the child.
     *    (Object) attrs - The attributes of the child in object notation.
     *
     *  Returns:
     *    The Strophe.Builder object.
     */
    c: function (name, attrs)
    {
	var child = this._makeNode(name, attrs);
	this.node.appendChild(child);
	this.node = child;
	return this;
    },

    /** Function: cnode
     *  Add a child to the current element and make it the new current
     *  element.
     *
     *  This function is the same as c() except that instead of using a
     *  name and an attributes object to create the child it uses an
     *  existing DOM element object.
     *
     *  Parameters:
     *    (XMLElement) elem - A DOM element.
     *
     *  Returns:
     *    The Strophe.Builder object.
     */
    cnode: function (elem)
    {
	this.node.appendChild(elem);
	this.node = elem;
	return this;
    },

    /** Function: t
     *  Add a child text element.
     *
     *  This *does not* make the child the new current element since there
     *  are no children of text elements.
     *
     *  Parameters:
     *    (String) text - The text data to append to the current element.
     *
     *  Returns:
     *    The Strophe.Builder object.
     */
    t: function (text)
    {
	var child = Strophe.xmlTextNode(text);
	this.node.appendChild(child);
	return this;
    },

    /** PrivateFunction: _makeNode
     *  _Private_ helper function to create a DOM element.
     *
     *  Parameters:
     *    (String) name - The name of the new element.
     *    (Object) attrs - The attributes for the new element in object
     *      notation.
     *
     *  Returns:
     *    A new DOM element.
     */
    _makeNode: function (name, attrs)
    {
	var node = Strophe.xmlElement(name);
	for (var k in attrs)
	    node.setAttribute(k, attrs[k]);
	return node;
    }
};


/** PrivateClass: Strophe.Handler
 *  _Private_ helper class for managing stanza handlers.
 *
 *  A Strophe.Handler encapsulates a user provided callback function to be
 *  executed when matching stanzas are received by the connection.
 *  Handlers can be either one-off or persistant depending on their
 *  return value. Returning true will cause a Handler to remain active, and
 *  returning false will remove the Handler.
 *
 *  Users will not use Strophe.Handler objects directly, but instead they
 *  will use Strophe.Connection.addHandler() and
 *  Strophe.Connection.deleteHandler().
 */

/** PrivateConstructor: Strophe.Handler
 *  Create and initialize a new Strophe.Handler.
 *
 *  Parameters:
 *    (Function) handler - A function to be executed when the handler is run.
 *    (String) ns - The namespace to match.
 *    (String) name - The element name to match.
 *    (String) type - The element type to match.
 *    (String) id - The element id attribute to match.
 *    (String) from - The element from attribute to match.
 *
 *  Returns:
 *    A new Strophe.Handler object.
 */
Strophe.Handler = function (handler, ns, name, type, id, from)
{
    this.handler = handler;
    this.ns = ns;
    this.name = name;
    this.type = type;
    this.id = id;
    this.from = from;

    this.user = true;
};

Strophe.Handler.prototype = {
    /** PrivateFunction: isMatch
     *  Tests if a stanza matches the Strophe.Handler.
     *
     *  Parameters:
     *    (XMLElement) elem - The XML element to test.
     *
     *  Returns:
     *    true if the stanza matches and false otherwise.
     */
    isMatch: function (elem)
    {
	var nsMatch, i;

	nsMatch = false;
	if (!this.ns) {
	    nsMatch = true;
	} else {
	    var self = this;
	    Strophe.forEachChild(elem, null, function (elem) {
		if (elem.getAttribute("xmlns") == self.ns)
		    nsMatch = true;
	    });

	    nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns;
	}

	if (nsMatch &&
	    (!this.name || Strophe.isTagEqual(elem, this.name)) &&
	    (!this.type || elem.getAttribute("type") == this.type) &&
	    (!this.id || elem.getAttribute("id") == this.id) &&
	    (!this.from || elem.getAttribute("from") == this.from)) {
		return true;
	}

	return false;
    },

    /** PrivateFunction: run
     *  Run the callback on a matching stanza.
     *
     *  Parameters:
     *    (XMLElement) elem - The DOM element that triggered the
     *      Strophe.Handler.
     *
     *  Returns:
     *    A boolean indicating if the handler should remain active.
     */
    run: function (elem)
    {
	var result = null;
	try {
	    result = this.handler(elem);
	} catch (e) {
	    if (e.sourceURL) {
		Strophe.fatal("error: " + this.handler +
			      " " + e.sourceURL + ":" +
			      e.line + " - " + e.name + ": " + e.message);
	    } else if (e.fileName) {
		if (typeof(console) != "undefined") {
		    console.trace();
		    console.error(this.handler, " - error - ", e, e.message);
		}
		Strophe.fatal("error: " + this.handler + " " +
			      e.fileName + ":" + e.lineNumber + " - " +
			      e.name + ": " + e.message);
	    } else {
		Strophe.fatal("error: " + this.handler);
	    }

	    throw e;
	}

	return result;
    },

    /** PrivateFunction: toString
     *  Get a String representation of the Strophe.Handler object.
     *
     *  Returns:
     *    A String.
     */
    toString: function ()
    {
	return "{Handler: " + this.handler + "(" + this.name + "," +
            this.id + "," + this.ns + ")}";
    }
};

/** PrivateClass: Strophe.TimedHandler
 *  _Private_ helper class for managing timed handlers.
 *
 *  A Strophe.TimedHandler encapsulates a user provided callback that
 *  should be called after a certain period of time or at regular
 *  intervals.  The return value of the callback determines whether the
 *  Strophe.TimedHandler will continue to fire.
 *
 *  Users will not use Strophe.TimedHandler objects directly, but instead
 *  they will use Strophe.Connection.addTimedHandler() and
 *  Strophe.Connection.deleteTimedHandler().
 */

/** PrivateConstructor: Strophe.TimedHandler
 *  Create and initialize a new Strophe.TimedHandler object.
 *
 *  Parameters:
 *    (Integer) period - The number of milliseconds to wait before the
 *      handler is called.
 *    (Function) handler - The callback to run when the handler fires.  This
 *      function should take no arguments.
 *
 *  Returns:
 *    A new Strophe.TimedHandler object.
 */
Strophe.TimedHandler = function (period, handler)
{
    this.period = period;
    this.handler = handler;

    this.lastCalled = new Date().getTime();
    this.user = true;
};

Strophe.TimedHandler.prototype = {
    /** PrivateFunction: run
     *  Run the callback for the Strophe.TimedHandler.
     *
     *  Returns:
     *    true if the Strophe.TimedHandler should be called again, and false
     *      otherwise.
     */
    run: function ()
    {
	this.lastCalled = new Date().getTime();
	return this.handler();
    },

    /** PrivateFunction: reset
     *  Reset the last called time for the Strophe.TimedHandler.
     */
    reset: function ()
    {
	this.lastCalled = new Date().getTime();
    },

    /** PrivateFunction: toString
     *  Get a string representation of the Strophe.TimedHandler object.
     *
     *  Returns:
     *    The string representation.
     */
    toString: function ()
    {
	return "{TimedHandler: " + this.handler + "(" + this.period +")}";
    }
};

/** PrivateClass: Strophe.Request
 *  _Private_ helper class that provides a cross implementation abstraction
 *  for a BOSH related XMLHttpRequest.
 *
 *  The Strophe.Request class is used internally to encapsulate BOSH request
 *  information.  It is not meant to be used from user's code.
 */

/** PrivateConstructor: Strophe.Request
 *  Create and initialize a new Strophe.Request object.
 *
 *  Parameters:
 *    (String) data - The data to be sent in the request.
 *    (Function) func - The function that will be called when the
 *      XMLHttpRequest readyState changes.
 *    (Integer) rid - The BOSH rid attribute associated with this request.
 *    (Integer) sends - The number of times this same request has been
 *      sent.
 */
Strophe.Request = function (data, func, rid, sends)
{
    this.id = ++Strophe._requestId;
    this.data = data;
    this.origFunc = func;
    this.func = func;
    this.rid = rid;
    this.date = NaN;
    this.sends = sends || 0;
    this.abort = false;
    this.dead = null;
    this.age = function () {
	if (!this.date) return 0;
	var now = new Date();
	return (now - this.date) / 1000;
    };
    this.timeDead = function () {
	if (!this.dead) return 0;
	var now = new Date();
	return (now - this.dead) / 1000;
    };
    this.xhr = this._newXHR();
};

Strophe.Request.prototype = {
    /** PrivateFunction: getResponse
     *  Get a response from the underlying XMLHttpRequest.
     *
     *  This function attempts to get a response from the request and checks
     *  for errors.
     *
     *  Throws:
     *    "parsererror" - A parser error occured.
     *
     *  Returns:
     *    The DOM element tree of the response.
     */
    getResponse: function ()
    {
	var node = null;
	if (this.xhr.responseXML && this.xhr.responseXML.documentElement) {
	    node = this.xhr.responseXML.documentElement;
	    if (node.tagName == "parsererror") {
		Strophe.error("invalid response received");
		Strophe.error("responseText: " + this.xhr.responseText);
		Strophe.error("responseXML: " +
			      Strophe.serialize(this.xhr.responseXML));
		throw "parsererror";
	    }
	} else if (this.xhr.responseText) {
	    Strophe.error("invalid response received");
	    Strophe.error("responseText: " + this.xhr.responseText);
	    Strophe.error("responseXML: " +
			  Strophe.serialize(this.xhr.responseXML));
	}

	return node;
    },

    /** PrivateFunction: _newXHR
     *  _Private_ helper function to create XMLHttpRequests.
     *
     *  This function creates XMLHttpRequests across all implementations.
     *
     *  Returns:
     *    A new XMLHttpRequest.
     */
    _newXHR: function ()
    {
	var xhr = null;
	if (window.XMLHttpRequest) {
	    xhr = new XMLHttpRequest();
	    if (xhr.overrideMimeType) {
		xhr.overrideMimeType("text/xml");
	    }
	} else if (window.ActiveXObject) {
	    xhr = new ActiveXObject("Microsoft.XMLHTTP");
	}

	xhr.onreadystatechange = this.func.prependArg(this);

	return xhr;
    }
};

/** Class: Strophe.Connection
 *  XMPP Connection manager.
 *
 *  Thie class is the main part of Strophe.  It manages a BOSH connection
 *  to an XMPP server and dispatches events to the user callbacks as
 *  data arrives.  It supports SASL PLAIN, SASL DIGEST-MD5, and legacy
 *  authentication.
 *
 *  After creating a Strophe.Connection object, the user will typically
 *  call connect() with a user supplied callback to handle connection level
 *  events like authentication failure, disconnection, or connection
 *  complete.
 *
 *  The user will also have several event handlers defined by using
 *  addHandler() and addTimedHandler().  These will allow the user code to
 *  respond to interesting stanzas or do something periodically with the
 *  connection.  These handlers will be active once authentication is
 *  finished.
 *
 *  To send data to the connection, use send().
 */

/** Constructor: Strophe.Connection
 *  Create and initialize a Strophe.Connection object.
 *
 *  Parameters:
 *    (String) service - The BOSH service URL.
 *
 *  Returns:
 *    A new Strophe.Connection object.
 */
Strophe.Connection = function (service)
{
    /* The path to the httpbind service. */
    this.service = service;
    /* The connected JID. */
    this.jid = "";
    /* request id for body tags */
    this.rid = Math.floor(Math.random() * 4294967295);
    /* The current session ID. */
    this.sid = null;
    this.streamId = null;

    this.do_session = false;
    this.do_bind = false;

    this.timedHandlers = [];
    this.handlers = [];
    this.removeTimeds = [];
    this.removeHandlers = [];
    this.addTimeds = [];
    this.addHandlers = [];

    this._idleTimeout = null;
    this._disconnectTimeout = null;

    this.authenticated = false;
    this.disconnecting = false;
    this.connected = false;

    this.errors = 0;

    this.paused = false;

    this.window = 5;

    this._data = [];
    this._requests = [];
    this._uniqueId = Math.round(Math.random() * 10000);

    this._sasl_success_handler = null;
    this._sasl_failure_handler = null;
    this._sasl_challenge_handler = null;

    this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
};

Strophe.Connection.prototype = {
    /** Function: reset
     *  Reset the connection.
     *
     *  This function should be called after a connection is disconnected
     *  before that connection is reused.
     */
    reset: function ()
    {
	this.rid = Math.floor(Math.random() * 4294967295);

	this.sid = null;
	this.streamId = null;

	this.do_session = false;
	this.do_bind = false;

	this.timedHandlers = [];
	this.handlers = [];
	this.removeTimeds = [];
	this.removeHandlers = [];
	this.addTimeds = [];
	this.addHandlers = [];

	this.authenticated = false;
	this.disconnecting = false;
	this.connected = false;

	this.errors = 0;

	this._requests = [];
	this._uniqueId = Math.round(Math.random()*10000);
    },

    /** Function: pause
     *  Pause the request manager.
     *
     *  This will prevent Strophe from sending any more requests to the
     *  server.  This is very useful for temporarily pausing while a lot
     *  of send() calls are happening quickly.  This causes Strophe to
     *  send the data in a single request, saving many request trips.
     */
    pause: function ()
    {
 	this.paused = true;
    },

    /** Function: resume
     *  Resume the request manager.
     *
     *  This resumes after pause() has been called.
     */
    resume: function ()
    {
 	this.paused = false;
    },

    /** Function: getUniqueId
     *  Generate a unique ID for use in <iq/> elements.
     *
     *  All <iq/> stanzas are required to have unique id attributes.  This
     *  function makes creating these easy.  Each connection instance has
     *  a counter which starts from zero, and the value of this counter
     *  plus a colon followed by the suffix becomes the unique id. If no
     *  suffix is supplied, the counter is used as the unique id.
     *
     *  Suffixes are used to make debugging easier when reading the stream
     *  data, and their use is recommended.  The counter resets to 0 for
     *  every new connection for the same reason.  For connections to the
     *  same server that authenticate the same way, all the ids should be
     *  the same, which makes it easy to see changes.  This is useful for
     *  automated testing as well.
     *
     *  Parameters:
     *    (String) suffix - A optional suffix to append to the id.
     *
     *  Returns:
     *    A unique string to be used for the id attribute.
     */
    getUniqueId: function (suffix)
    {
	if (typeof(suffix) == "string" || typeof(suffix) == "number") {
	    return ++this._uniqueId + ":" + suffix;
	} else {
	    return ++this._uniqueId + "";
	}
    },

    /** Function: connect
     *  Starts the connection process.
     *
     *  As the connection process proceeds, the user supplied callback will
     *  be triggered multiple times with status updates.  The callback
     *  should take two arguments - the status code and the error condition.
     *
     *  The status code will be one of the values in the Strophe.Status
     *  constants.  The error condition will be one of the conditions
     *  defined in RFC 3920 or the condition 'strophe-parsererror'.
     *
     *  Please see XEP 124 for a more detailed explanation of the optional
     *  parameters below.
     *
     *  Parameters:
     *    (String) jid - The user's JID.  This may be a bare JID,
     *      or a full JID.  If a node is not supplied, SASL ANONYMOUS
     *      authentication will be attempted.
     *    (String) pass - The user's password.
     *    (Function) callback The connect callback function.
     *    (Integer) wait - The optional HTTPBIND wait value.  This is the
     *      time the server will wait before returning an empty result for
     *      a request.  The default setting of 60 seconds is recommended.
     *      Other settings will require tweaks to the Strophe.TIMEOUT value.
     *    (Integer) hold - The optional HTTPBIND hold value.  This is the
     *      number of connections the server will hold at one time.  This
     *      should almost always be set to 1 (the default).
     *    (Integer) wind - The optional HTTBIND window value.  This is the
     *      allowed range of request ids that are valid.  The default is 5.
     */
    connect: function (jid, pass, callback, wait, hold, wind)
    {
	this.jid = jid;
	this.pass = pass;
	this.connect_callback = callback;
	this.disconnecting = false;
	this.connected = false;
	this.authenticated = false;
	this.errors = 0;

	if (!wait) wait = 60;
        if (!hold) hold = 1;
	if (wind) this.window = wind;

	this.domain = Strophe.getDomainFromJid(this.jid);

	var body = this._buildBody().attrs({
	    to: this.domain,
	    "xml:lang": "en",
	    wait: wait,
	    hold: hold,
	    window: this.window,
	    content: "text/xml; charset=utf-8",
	    ver: "1.6",
	    "xmpp:version": "1.0",
	    "xmlns:xmpp": Strophe.NS.BOSH
	});

	this.connect_callback(Strophe.Status.CONNECTING, null);

	this._requests.push(
	    new Strophe.Request(body.toString(),
				this._onRequestStateChange.bind(this)
				    .prependArg(this._connect_cb.bind(this)),
				body.tree().getAttribute("rid")));
	this._throttledRequestHandler();
    },

    /** Function: attach
     *  Attach to an already created and authenticated BOSH session.
     *
     *  This function is provided to allow Strophe to attach to BOSH
     *  sessions which have been created externally, perhaps by a Web
     *  application.  This is often used to support auto-login type features
     *  without putting user credentials into the page.
     *
     *  Parameters:
     *    (String) jid - The full JID that is bound by the session.
     *    (String) sid - The SID of the BOSH session.
     *    (String) rid - The current RID of the BOSH session.  This RID
     *      will be used by the next request.
     *    (Function) callback The connect callback function.
     */
    attach: function (jid, sid, rid, callback)
    {
	this.jid = jid;
	this.sid = sid;
	this.rid = rid;
	this.connect_callback = callback;

	this.domain = Strophe.getDomainFromJid(this.jid);

	this.authenticated = true;
	this.connected = true;
    },

    /** Function: rawInput
     *  User overrideable function that receives raw data coming into the
     *  connection.
     *
     *  The default function does nothing.  User code can override this with
     *  > Strophe.Connection.rawInput = function (data) {
     *  >   (user code)
     *  > };
     *
     *  Parameters:
     *    (String) data - The data received by the connection.
     */
    rawInput: function (data)
    {
	return;
    },

    /** Function: rawOutput
     *  User overrideable function that receives raw data sent to the
     *  connection.
     *
     *  The default function does nothing.  User code can override this with
     *  > Strophe.Connection.rawOutput = function (data) {
     *  >   (user code)
     *  > };
     *
     *  Parameters:
     *    (String) data - The data sent by the connection.
     */
    rawOutput: function (data)
    {
	return;
    },

    /** Function: send
     *  Send a stanza.
     *
     *  This function is called to push data onto the send queue to
     *  go out over the wire.  Whenever a request is sent to the BOSH
     *  server, all pending data is sent and the queue is flushed.
     *
     *  Parameters:
     *    (XMLElement) elem - The stanza to send.
     */
    send: function (elem)
    {
	if (elem !== null && typeof(elem["sort"]) == "function") {
	    for (var i = 0; i < elem.length; i++) {
		this._data.push(elem[i]);
	    }
	} else {
	    this._data.push(elem);
	}

	this._throttledRequestHandler();
	clearTimeout(this._idleTimeout);
	this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
    },

    /** PrivateFunction: _sendRestart
     *  Send an xmpp:restart stanza.
     */
    _sendRestart: function ()
    {
	this._data.push("restart");

	this._throttledRequestHandler();
	clearTimeout(this._idleTimeout);
	this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
    },

    /** Function: addTimedHandler
     *  Add a timed handler to the connection.
     *
     *  This function adds a timed handler.  The provided handler will
     *  be called every period milliseconds until it returns false,
     *  the connection is terminated, or the handler is removed.  Handlers
     *  that wish to continue being invoked should return true.
     *
     *  Because of method binding it is necessary to save the result of
     *  this function if you wish to remove a handler with
     *  deleteTimedHandler().
     *
     *  Note that user handlers are not active until authentication is
     *  successful.
     *
     *  Parameters:
     *    (Integer) period - The period of the handler.
     *    (Function) handler - The callback function.
     *
     *  Returns:
     *    A reference to the handler that can be used to remove it.
     */
    addTimedHandler: function (period, handler)
    {
	var thand = new Strophe.TimedHandler(period, handler);
	this.addTimeds.push(thand);
	return thand;
    },

    /** Function: deleteTimedHandler
     *  Delete a timed handler for a connection.
     *
     *  This function removes a timed handler from the connection.  The
     *  handRef parameter is *not* the function passed to addTimedHandler(),
     *  but is the reference returned from addTimedHandler().
     *
     *  Parameters:
     *    (Strophe.TimedHandler) handRef - The handler reference.
     */
    deleteTimedHandler: function (handRef)
    {
	this.removeTimeds.push(handRef);
    },

    /** Function: addHandler
     *  Add a stanza handler for the connection.
     *
     *  This function adds a stanza handler to the connection.  The
     *  handler callback will be called for any stanza that matches
     *  the parameters.  Note that if multiple parameters are supplied,
     *  they must all match for the handler to be invoked.
     *
     *  The handler will receive the stanza that triggered it as its argument.
     *  The handler should return true if it is to be invoked again;
     *  returning false will remove the handler after it returns.
     *
     *  As a convenience, the ns parameters applies to the top level element
     *  and also any of its immediate children.  This is primarily to make
     *  matching /iq/query elements easy.
     *
     *  The return value should be saved if you wish to remove the handler
     *  with deleteHandler().
     *
     *  Parameters:
     *    (Function) handler - The user callback.
     *    (String) ns - The namespace to match.
     *    (String) name - The stanza name to match.
     *    (String) type - The stanza type attribute to match.
     *    (String) id - The stanza id attribute to match.
     *    (String) from - The stanza from attribute to match.
     *
     *  Returns:
     *    A reference to the handler that can be used to remove it.
     */
    addHandler: function (handler, ns, name, type, id, from)
    {
	var hand = new Strophe.Handler(handler, ns, name, type, id, from);
	this.addHandlers.push(hand);
	return hand;
    },

    /** Function: deleteHandler
     *  Delete a stanza handler for a connection.
     *
     *  This function removes a stanza handler from the connection.  The
     *  handRef parameter is *not* the function passed to addHandler(),
     *  but is the reference returned from addHandler().
     *
     *  Parameters:
     *    (Strophe.Handler) handRef - The handler reference.
     */
    deleteHandler: function (handRef)
    {
	this.removeHandlers.push(handRef);
    },

    /** Function: disconnect
     *  Start the graceful disconnection process.
     *
     *  This function starts the disconnection process.  This process starts
     *  by sending unavailable presence and sending BOSH body of type
     *  terminate.  A timeout handler makes sure that disconnection happens
     *  even if the BOSH server does not respond.
     *
     *  The user supplied connection callback will be notified of the
     *  progress as this process happens.
     */
    disconnect: function ()
    {
	Strophe.info("disconnect was called");
	if (this.connected) {
	    this._disconnectTimeout = this._addSysTimedHandler(
		3000, this._onDisconnectTimeout.bind(this));
	    this._sendTerminate();
	}
    },

    /** PrivateFunction: _buildBody
     *  _Private_ helper function to generate the <body/> wrapper for BOSH.
     *
     *  Returns:
     *    A Strophe.Builder with a <body/> element.
     */
    _buildBody: function ()
    {
	var bodyWrap = $build('body', {
	    rid: this.rid++,
	    xmlns: Strophe.NS.HTTPBIND
	});

	if (this.sid !== null) {
	    bodyWrap.attrs({sid: this.sid});
	}

	return bodyWrap;
    },

    /** PrivateFunction: _removeRequest
     *  _Private_ function to remove a request from the queue.
     *
     *  Parameters:
     *    (Strophe.Request) req - The request to remove.
     */
    _removeRequest: function (req)
    {
	Strophe.debug("removing request");

	var i;
	for (i = this._requests.length - 1; i >= 0; i--) {
	    if (req == this._requests[i]) {
		this._requests.splice(i, 1);
	    }
	}

	req.xhr.onreadystatechange = function () {};

	this._throttledRequestHandler();
    },

    /** PrivateFunction: _restartRequest
     *  _Private_ function to restart a request that is presumed dead.
     *
     *  Parameters:
     *    (Integer) i - The index of the request in the queue.
     */
    _restartRequest: function (i)
    {
	var req = this._requests[i];
	if (req.dead === null) {
	    req.dead = new Date();
	}

	this._processRequest(i);
    },

    /** PrivateFunction: _processRequest
     *  _Private_ function to process a request in the queue.
     *
     *  This function takes requests off the queue and sends them and
     *  restarts dead requests.
     *
     *  Parameters:
     *    (Integer) i - The index of the request in the queue.
     */
    _processRequest: function (i)
    {
	var req = this._requests[i];
	var reqStatus = -1;

	try {
	    if (req.xhr.readyState == 4) {
		reqStatus = req.xhr.status;
	    }
	} catch (e) {
	    Strophe.error("caught an error in _requests[" + i +
			  "], reqStatus: " + reqStatus);
	}

	if (typeof(reqStatus) == "undefined") {
	    reqStatus = -1;
	}

	var now = new Date();
	var time_elapsed = req.age();
	var primaryTimeout = (!isNaN(time_elapsed) &&
			      time_elapsed > Strophe.TIMEOUT);
	var secondaryTimeout = (req.dead !== null &&
				req.timeDead() > Strophe.SECONDARY_TIMEOUT);
	var requestCompletedWithServerError = (req.xhr.readyState == 4 &&
					       (reqStatus < 1 ||
						reqStatus >= 500));
	var oldreq;

	if (primaryTimeout || secondaryTimeout ||
	    requestCompletedWithServerError) {
	    if (secondaryTimeout) {
		Strophe.error("Request " +
			      this._requests[i].id +
			      " timed out (secondary), restarting");
	    }
	    req.abort = true;
	    req.xhr.abort();
	    oldreq = req;
	    this._requests[i] = new Strophe.Request(req.data,
						    req.origFunc,
						    req.rid,
						    req.sends);
	    req = this._requests[i];
	}

	if (req.xhr.readyState === 0) {
	    Strophe.debug("request id " + req.id +
			  "." + req.sends + " posting");

	    req.date = new Date();
	    try {
		req.xhr.open("POST", this.service, true);
	    } catch (e) {
		Strophe.error("XHR open failed.");
		if (!this.connected)
		    this.connect_callback(Strophe.Status.CONNFAIL,
					  "bad-service");
		this.disconnect();
		return;
	    }

      var sendFunc = function () {
	  req.xhr.send(req.data);
      };

      if (req.sends > 1) {
          var backoff = Math.pow(req.sends, 3) * 1000;
          setTimeout(sendFunc, backoff);
      } else {
          sendFunc();
      }

      req.sends++;

	    this.rawOutput(req.data);
	} else {
	    Strophe.debug("_throttledRequestHandler: " +
			  (i === 0 ? "first" : "second") +
			  " request has readyState of " +
			  req.xhr.readyState);
	}
    },

    /** PrivateFunction: _throttledRequestHandler
     *  _Private_ function to throttle requests to the connection window.
     *
     *  This function makes sure we don't send requests so fast that the
     *  request ids overflow the connection window in the case that one
     *  request died.
     */
    _throttledRequestHandler: function ()
    {
	if (!this._requests) {
	    Strophe.debug("_throttledRequestHandler called with " +
			  "undefined requests");
	} else {
	    Strophe.debug("_throttledRequestHandler called with " +
			  this._requests.length + " requests");
	}

	if (!this._requests || this._requests.length === 0) {
	    return;
	}

	if (this._requests.length > 0) {
	    this._processRequest(0);
	}

	if (this._requests.length > 1 &&
	    Math.abs(this._requests[0].rid -
		     this._requests[1].rid) < this.window - 1) {
	    this._processRequest(1);
	}
    },

    /** PrivateFunction: _onRequestStateChange
     *  _Private_ handler for Strophe.Request state changes.
     *
     *  This function is called when the XMLHttpRequest readyState changes.
     *  It contains a lot of error handling logic for the many ways that
     *  requests can fail, and calls the request callback when requests
     *  succeed.
     *
     *  Parameters:
     *    (Function) func - The handler for the request.
     *    (Strophe.Request) req - The request that is changing readyState.
     */
    _onRequestStateChange: function (func, req)
    {
	Strophe.debug("request id " + req.id +
		      "." + req.sends + " state changed to " +
		      req.xhr.readyState);

	if (req.abort) {
	    req.abort = false;
	    return;
	}

	var reqStatus;
	if (req.xhr.readyState == 4) {
	    reqStatus = 0;
	    try {
		reqStatus = req.xhr.status;
	    } catch (e) {
	    }

	    if (typeof(reqStatus) == "undefined") {
		reqStatus = 0;
	    }

	    if (this.disconnecting) {
		if (reqStatus >= 400) {
		    this._hitError(reqStatus);
		    return;
		}
	    }

	    var reqIs0 = (this._requests[0] == req);
	    var reqIs1 = (this._requests[1] == req);

	    if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) {
		this._removeRequest(req);
		Strophe.debug("request id " +
			      req.id +
			      " should now be removed");
	    }

	    if (reqStatus == 200) {
		if (reqIs1 ||
		    (reqIs0 && this._requests.length > 0 &&
 		     this._requests[0].age() > Strophe.SECONDARY_TIMEOUT)) {
		    this._restartRequest(0);
		}
		Strophe.debug("request id " +
			      req.id + "." +
			      req.sends + " got 200");
		func(req);
		this.errors = 0;
	    } else {
		Strophe.error("request id " +
			      req.id + "." +
			      req.sends + " error " + reqStatus +
			      " happened");
		if (reqStatus === 0 ||
		    (reqStatus >= 400 && reqStatus < 600) ||
		    reqStatus >= 12000) {
		    this._hitError(reqStatus);
		    if (reqStatus >= 400 && reqStatus < 500) {
			this.connect_callback(Strophe.Status.DISCONNECTING,
					      null);
			this._doDisconnect();
		    }
		}
	    }

	    if (!((reqStatus > 0 && reqStatus < 10000) ||
		  req.sends > 5)) {
		this._throttledRequestHandler();
	    }
	}
    },

    /** PrivateFunction: _hitError
     *  _Private_ function to handle the error count.
     *
     *  Requests are resent automatically until their error count reaches
     *  5.  Each time an error is encountered, this function is called to
     *  increment the count and disconnect if the count is too high.
     *
     *  Parameters:
     *    (Integer) reqStatus - The request status.
     */
    _hitError: function (reqStatus)
    {
	this.errors++;
	Strophe.warn("request errored, status: " + reqStatus +
		     ", number of errors: " + this.errors);
	if (this.errors > 4) {
	    this._onDisconnectTimeout();
	}
    },

    /** PrivateFunction: _doDisconnect
     *  _Private_ function to disconnect.
     *
     *  This is the last piece of the disconnection logic.  This resets the
     *  connection and alerts the user's connection callback.
     */
    _doDisconnect: function ()
    {
	Strophe.info("_doDisconnect was called");
	this.authenticated = false;
	this.disconnecting = false;
	this.sid = null;
	this.streamId = null;
	this.rid = Math.floor(Math.random() * 4294967295);

	if (this.connected) {
	    this.connect_callback(Strophe.Status.DISCONNECTED, null);
	    this.connected = false;
	}

	this.handlers = [];
	this.timedHandlers = [];
	this.removeTimeds = [];
	this.removeHandlers = [];
	this.addTimeds = [];
	this.addHandlers = [];
    },

    /** PrivateFunction: _dataRecv
     *  _Private_ handler to processes incoming data from the the connection.
     *
     *  Except for _connect_cb handling the initial connection request,
     *  this function handles the incoming data for all requests.  This
     *  function also fires stanza handlers that match each incoming
     *  stanza.
     *
     *  Parameters:
     *    (Strophe.Request) req - The request that has data ready.
     */
    _dataRecv: function (req)
    {
	try {
	    var elem = req.getResponse();
	} catch (e) {
	    if (e != "parsererror") throw e;

	    this.connect_callback(Strophe.Status.DISCONNECTING,
				  "strophe-parsererror");
	    this.disconnect();
	}
	if (elem === null) return;

	if (this.disconnecting && this._requests.length == 0) {
	    this.deleteTimedHandler(this._disconnectTimeout);
	    this._disconnectTimeout = null;
	    this._doDisconnect();
	}

	this.rawInput(Strophe.serialize(elem));

	var typ = elem.getAttribute("type");
	var cond, conflict;
	if (typ !== null && typ == "terminate") {
	    cond = elem.getAttribute("condition");
	    conflict = elem.getElementsByTagName("conflict");
	    if (cond !== null) {
		if (cond == "remote-stream-error" && conflict.length > 0) {
		    cond = "conflict";
		}
		this.connect_callback(Strophe.Status.CONNFAIL, cond);
	    } else {
		this.connect_callback(Strophe.Status.CONNFAIL, "unknown");
	    }
	    this.connect_callback(Strophe.Status.DISCONNECTING, null);
	    this.disconnect();
	    return;
	}

	var i, hand;
	while (this.removeHandlers.length > 0) {
	    hand = this.removeHandlers.pop();
	    i = this.handlers.indexOf(hand);
	    if (i >= 0)
		this.handlers.splice(i, 1);
	}

	while (this.addHandlers.length > 0) {
	    this.handlers.push(this.addHandlers.pop());
	}

	var self = this;
	Strophe.forEachChild(elem, null, function (child) {
	    var i, newList;
	    newList = self.handlers;
	    self.handlers = [];
	    for (i = 0; i < newList.length; i++) {
		var hand = newList[i];
		if (hand.isMatch(child) &&
		    (self.authenticated || !hand.user)) {
		    if (hand.run(child)) {
			self.handlers.push(hand);
		    }
		} else {
		    self.handlers.push(hand);
		}
	    }
	});
    },

    /** PrivateFunction: _sendTerminate
     *  _Private_ function to send initial disconnect sequence.
     *
     *  This is the first step in a graceful disconnect.  It sends
     *  the BOSH server a terminate body and includes an unavailable
     *  presence if authentication has completed.
     */
    _sendTerminate: function ()
    {
	Strophe.info("_sendTerminate was called");
	var body = this._buildBody().attrs({type: "terminate"});

	var presence, i;
	if (this.authenticated) {
	    body.c('presence', {
		xmlns: Strophe.NS.CLIENT,
		type: 'unavailable'
	    });
	}

	this.disconnecting = true;

	var req = new Strophe.Request(body.toString(),
				      this._onRequestStateChange.bind(this)
					  .prependArg(this._dataRecv.bind(this)),
				      body.tree().getAttribute("rid"));

	var r;
	while (this._requests.length > 0) {
	    r = this._requests.pop();
	    r.xhr.abort();
	    r.abort = true;
	}

	this._requests.push(req);
	this._throttledRequestHandler();
    },

    /** PrivateFunction: _connect_cb
     *  _Private_ handler for initial connection request.
     *
     *  This handler is used to process the initial connection request
     *  response from the BOSH server. It is used to set up authentication
     *  handlers and start the authentication process.
     *
     *  SASL authentication will be attempted if available, otherwise
     *  the code will fall back to legacy authentication.
     *
     *  Parameters:
     *    (Strophe.Request) req - The current request.
     */
    _connect_cb: function (req)
    {
	Strophe.info("_connect_cb was called");

	this.connected = true;
	var bodyWrap = req.getResponse();
	if (!bodyWrap) return;

	this.rawInput(Strophe.serialize(bodyWrap));

	var typ = bodyWrap.getAttribute("type");
	var cond, conflict;
	if (typ !== null && typ == "terminate") {
	    cond = bodyWrap.getAttribute("condition");
	    conflict = bodyWrap.getElementsByTagName("conflict");
	    if (cond !== null) {
		if (cond == "remote-stream-error" && conflict.length > 0) {
		    cond = "conflict";
		}
		this.connect_callback(Strophe.Status.CONNFAIL, cond);
	    } else {
		this.connect_callback(Strophe.Status.CONNFAIL, "unknown");
	    }
	    return;
	}

	this.sid = bodyWrap.getAttribute("sid");
	this.stream_id = bodyWrap.getAttribute("authid");

	var do_sasl_plain = false;
	var do_sasl_digest_md5 = false;
	var do_sasl_anonymous = false;

	var mechanisms = bodyWrap.getElementsByTagName("mechanism");
	var i, mech, auth_str, hashed_auth_str;
	if (mechanisms.length > 0) {
	    for (i = 0; i < mechanisms.length; i++) {
		mech = Strophe.getText(mechanisms[i]);
		if (mech == 'DIGEST-MD5') {
		    do_sasl_digest_md5 = true;
		} else if (mech == 'PLAIN') {
		    do_sasl_plain = true;
		} else if (mech == 'ANONYMOUS') {
		    do_sasl_anonymous = true;
		}
	    }
	}

	if (Strophe.getNodeFromJid(this.jid) === null &&
	    do_sasl_anonymous) {
	    this.connect_callback(Strophe.Status.AUTHENTICATING, null);
	    this._sasl_success_handler = this._addSysHandler(
		this._sasl_success_cb.bind(this), null,
		"success", null, null);
	    this._sasl_failure_handler = this._addSysHandler(
		this._sasl_failure_cb.bind(this), null,
		"failure", null, null);

	    this.send($build("auth", {
		xmlns: Strophe.NS.SASL,
		mechanism: "ANONYMOUS"
	    }).tree());
	} else if (Strophe.getNodeFromJid(this.jid) === null) {
	    this.connect_callback(Strophe.Status.CONNFAIL, null);
	    this.disconnect();
	} else if (do_sasl_digest_md5) {
	    this.connect_callback(Strophe.Status.AUTHENTICATING, null);
	    this._sasl_challenge_handler = this._addSysHandler(
		this._sasl_challenge1_cb.bind(this), null,
		"challenge", null, null);
            this._sasl_failure_handler = this._addSysHandler(
		this._sasl_failure_cb.bind(this), null,
		"failure", null, null);

	    this.send($build("auth", {
		xmlns: Strophe.NS.SASL,
		mechanism: "DIGEST-MD5"
	    }).tree());
	} else if (do_sasl_plain) {
	    auth_str = Strophe.escapeJid(
		Strophe.getBareJidFromJid(this.jid));
	    auth_str = auth_str + "\u0000";
	    auth_str = auth_str + Strophe.getNodeFromJid(this.jid);
	    auth_str = auth_str + "\u0000";
	    auth_str = auth_str + this.pass;

	    this.connect_callback(Strophe.Status.AUTHENTICATING, null);
	    this._sasl_success_handler = this._addSysHandler(
		this._sasl_success_cb.bind(this), null,
		"success", null, null);
	    this._sasl_failure_handler = this._addSysHandler(
		this._sasl_failure_cb.bind(this), null,
		"failure", null, null);

	    hashed_auth_str = encode64(auth_str);
	    this.send($build("auth", {
		xmlns: Strophe.NS.SASL,
		mechanism: "PLAIN"
	    }).t(hashed_auth_str).tree());
	} else {
	    this.connect_callback(Strophe.Status.AUTHENTICATING, null);
	    this._addSysHandler(this._auth1_cb.bind(this), null, null,
				null, "_auth_1");

	    this.send($iq({
		type: "get",
		to: this.domain,
		id: "_auth_1"
	    }).c("query", {
		xmlns: Strophe.NS.AUTH
	    }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree());
	}
    },

    /** PrivateFunction: _sasl_challenge1_cb
     *  _Private_ handler for DIGEST-MD5 SASL authentication.
     *
     *  Parameters:
     *    (XMLElement) elem - The challenge stanza.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _sasl_challenge1_cb: function (elem)
    {
	var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/;

        var challenge = decode64(Strophe.getText(elem));
        var cnonce = hex_md5(Math.random() * 1234567890);
	var realm = "";
	var host = null;
	var nonce = "";
	var qop = "";
	var matches;

        this.deleteHandler(this._sasl_failure_handler);

	while (challenge.match(attribMatch)) {
	    matches = challenge.match(attribMatch);
	    challenge = challenge.replace(matches[0], "");
	    matches[2] = matches[2].replace(/^"(.+)"$/, "$1");
	    switch (matches[1]) {
	    case "realm":
		realm = matches[2];
		break;
	    case "nonce":
		nonce = matches[2];
		break;
	    case "qop":
		qop = matches[2];
		break;
	    case "host":
		host = matches[2];
		break;
	    }
	}

	var digest_uri = "xmpp/" + realm;
	if (host !== null) {
	    digest_uri = digest_uri + "/" + host;
	}

        var A1 = str_md5(Strophe.getNodeFromJid(this.jid) +
			 ":" + realm + ":" + this.pass) +
	    ":" + nonce + ":" + cnonce;
	var A2 = 'AUTHENTICATE:' + digest_uri;

	var responseText = "";
	responseText += 'username="' +
            Strophe.getNodeFromJid(this.jid) + '",';
	responseText += 'realm="' + realm + '",';
	responseText += 'nonce="' + nonce + '",';
	responseText += 'cnonce="' + cnonce + '",';
	responseText += 'nc="00000001",';
	responseText += 'qop="auth",';
	responseText += 'digest-uri="' + digest_uri + '",';
	responseText += 'response="' + hex_md5(hex_md5(A1) + ":" +
					       nonce + ":00000001:" +
					       cnonce + ":auth:" +
					       hex_md5(A2)) + '",';
	responseText += 'charset="utf-8"';

        this._sasl_challenge_handler = this._addSysHandler(
	    this._sasl_challenge2_cb.bind(this), null,
	    "challenge", null, null);
	this._sasl_success_handler = this._addSysHandler(
	    this._sasl_success_cb.bind(this), null,
	    "success", null, null);
        this._sasl_failure_handler = this._addSysHandler(
	    this._sasl_failure_cb.bind(this), null,
	    "failure", null, null);

        this.send($build('response', {
	    xmlns: Strophe.NS.SASL
	}).t(encode64(responseText)).tree());

	return false;
    },

    /** PrivateFunction: _sasl_challenge2_cb
     *  _Private_ handler for second step of DIGEST-MD5 SASL authentication.
     *
     *  Parameters:
     *    (XMLElement) elem - The challenge stanza.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _sasl_challenge2_cb: function (elem)
    {
	this.deleteHandler(this._sasl_success_handler);
	this.deleteHandler(this._sasl_failure_handler);

	this._sasl_success_handler = this._addSysHandler(
	    this._sasl_success_cb.bind(this), null,
	    "success", null, null);
	this._sasl_failure_handler = this._addSysHandler(
	    this._sasl_failure_cb.bind(this), null,
	    "failure", null, null);
	this.send($build('response', {xmlns: Strophe.NS.SASL}).tree());
	return false;
    },

    /** PrivateFunction: _auth1_cb
     *  _Private_ handler for legacy authentication.
     *
     *  This handler is called in response to the initial <iq type='get'/>
     *  for legacy authentication.  It builds an authentication <iq/> and
     *  sends it, creating a handler (calling back to _auth2_cb()) to
     *  handle the result
     *
     *  Parameters:
     *    (XMLElement) elem - The stanza that triggered the callback.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _auth1_cb: function (elem)
    {
	var use_digest = false;
	var check_query, check_digest;

	if (elem.getAttribute("type") == "result") {
	    check_query = elem.childNodes[0];
	    if (check_query) {
		check_digest = check_query.getElementsByTagName("digest")[0];
		if (check_digest) {
		    use_digest = true;
		}
	    }
	}

	var iq = $iq({type: "set", id: "_auth_2"})
	    .c('query', {xmlns: Strophe.NS.AUTH})
	    .c('username', {}).t(Strophe.getNodeFromJid(this.jid));
	if (use_digest) {
	    iq.up().c("digest", {})
	        .t(hex_sha1(this.stream_id + this.pass));
	} else {
	    iq.up().c('password', {}).t(this.pass);
	}
	if (!Strophe.getResourceFromJid(this.jid)) {
	    this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe';
	}
	iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid));

	this._addSysHandler(this._auth2_cb.bind(this), null,
			    null, null, "_auth_2");

	this.send(iq.tree());

	return false;
    },

    /** PrivateFunction: _sasl_success_cb
     *  _Private_ handler for succesful SASL authentication.
     *
     *  Parameters:
     *    (XMLElement) elem - The matching stanza.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _sasl_success_cb: function (elem)
    {
	Strophe.info("SASL authentication succeeded.");

	this.deleteHandler(this._sasl_failure_handler);
	this._sasl_failure_handler = null;
	if (this._sasl_challenge_handler) {
	    this.deleteHandler(this._sasl_challenge_handler);
	    this._sasl_challenge_handler = null;
	}

	this._addSysHandler(this._sasl_auth1_cb.bind(this), null,
			    "stream:features", null, null);

	this._sendRestart();

	return false;
    },

    /** PrivateFunction: _sasl_auth1_cb
     *  _Private_ handler to start stream binding.
     *
     *  Parameters:
     *    (XMLElement) elem - The matching stanza.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _sasl_auth1_cb: function (elem)
    {
	var i, child;

	for (i = 0; i < elem.childNodes.length; i++) {
	    child = elem.childNodes[i];
	    if (child.nodeName == 'bind') {
		this.do_bind = true;
	    }

	    if (child.nodeName == 'session') {
		this.do_session = true;
	    }
	}

	if (!this.do_bind) {
	    this.connect_callback(Strophe.Status.AUTHFAIL, null);
	    return false;
	} else {
	    this._addSysHandler(this._sasl_bind_cb.bind(this), null, null,
				null, "_bind_auth_2");

	    var resource = Strophe.getResourceFromJid(this.jid);
	    if (resource)
		this.send($iq({type: "set", id: "_bind_auth_2"})
		          .c('bind', {xmlns: Strophe.NS.BIND})
		          .c('resource', {}).t(resource).tree());
	    else
		this.send($iq({type: "set", id: "_bind_auth_2"})
		          .c('bind', {xmlns: Strophe.NS.BIND})
		          .tree());
	}

	return false;
    },

    /** PrivateFunction: _sasl_bind_cb
     *  _Private_ handler for binding result and session start.
     *
     *  Parameters:
     *    (XMLElement) elem - The matching stanza.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _sasl_bind_cb: function (elem)
    {
	if (elem.getAttribute("type") == "error") {
	    Strophe.info("SASL binding failed.");
	    this.connect_callback(Strophe.Status.AUTHFAIL, null);
	    return false;
	}

	var bind = elem.getElementsByTagName("bind");
	var jidNode;
	if (bind.length > 0) {
	    jidNode = bind[0].getElementsByTagName("jid");
	    if (jidNode.length > 0) {
		this.jid = Strophe.getText(jidNode[0]);

		if (this.do_session) {
		    this._addSysHandler(this._sasl_session_cb.bind(this),
					null, null, null, "_session_auth_2");

		    this.send($iq({type: "set", id: "_session_auth_2"})
			          .c('session', {xmlns: Strophe.NS.SESSION})
			          .tree());
		}
	    }
	} else {
	    Strophe.info("SASL binding failed.");
	    this.connect_callback(Strophe.Status.AUTHFAIL, null);
	    return false;
	}
    },

    /** PrivateFunction: _sasl_session_cb
     *  _Private_ handler to finish successful SASL connection.
     *
     *  This sets Connection.authenticated to true on success, which
     *  starts the processing of user handlers.
     *
     *  Parameters:
     *    (XMLElement) elem - The matching stanza.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _sasl_session_cb: function (elem)
    {
	if (elem.getAttribute("type") == "result") {
	    this.authenticated = true;
	    this.connect_callback(Strophe.Status.CONNECTED, null);
	} else if (elem.getAttribute("type") == "error") {
	    Strophe.info("Session creation failed.");
	    this.connect_callback(Strophe.Status.AUTHFAIL, null);
	    return false;
	}

	return false;
    },

    /** PrivateFunction: _sasl_failure_cb
     *  _Private_ handler for SASL authentication failure.
     *
     *  Parameters:
     *    (XMLElement) elem - The matching stanza.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _sasl_failure_cb: function (elem)
    {
	if (this._sasl_success_handler) {
	    this.deleteHandler(this._sasl_success_handler);
	    this._sasl_success_handler = null;
	}
	if (this._sasl_challenge_handler) {
	    this.deleteHandler(this._sasl_challenge_handler);
	    this._sasl_challenge_handler = null;
	}

	this.connect_callback(Strophe.Status.AUTHFAIL, null);
	return false;
    },

    /** PrivateFunction: _auth2_cb
     *  _Private_ handler to finish legacy authentication.
     *
     *  This handler is called when the result from the jabber:iq:auth
     *  <iq/> stanza is returned.
     *
     *  Parameters:
     *    (XMLElement) elem - The stanza that triggered the callback.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _auth2_cb: function (elem)
    {
	if (elem.getAttribute("type") == "result") {
	    this.authenticated = true;
	    this.connect_callback(Strophe.Status.CONNECTED, null);
	} else if (elem.getAttribute("type") == "error") {
	    this.connect_callback(Strophe.Status.AUTHFAIL, null);
	    this.disconnect();
	}

	return false;
    },

    /** PrivateFunction: _addSysTimedHandler
     *  _Private_ function to add a system level timed handler.
     *
     *  This function is used to add a Strophe.TimedHandler for the
     *  library code.  System timed handlers are allowed to run before
     *  authentication is complete.
     *
     *  Parameters:
     *    (Integer) period - The period of the handler.
     *    (Function) handler - The callback function.
     */
    _addSysTimedHandler: function (period, handler)
    {
	var thand = new Strophe.TimedHandler(period, handler);
	thand.user = false;
	this.addTimeds.push(thand);
	return thand;
    },

    /** PrivateFunction: _addSysHandler
     *  _Private_ function to add a system level stanza handler.
     *
     *  This function is used to add a Strophe.Handler for the
     *  library code.  System stanza handlers are allowed to run before
     *  authentication is complete.
     *
     *  Parameters:
     *    (Function) handler - The callback function.
     *    (String) ns - The namespace to match.
     *    (String) name - The stanza name to match.
     *    (String) type - The stanza type attribute to match.
     *    (String) id - The stanza id attribute to match.
     */
    _addSysHandler: function (handler, ns, name, type, id)
    {
	var hand = new Strophe.Handler(handler, ns, name, type, id);
	hand.user = false;
	this.addHandlers.push(hand);
	return hand;
    },

    /** PrivateFunction: _onDisconnectTimeout
     *  _Private_ timeout handler for handling non-graceful disconnection.
     *
     *  If the graceful disconnect process does not complete within the
     *  time allotted, this handler finishes the disconnect anyway.
     *
     *  Returns:
     *    false to remove the handler.
     */
    _onDisconnectTimeout: function ()
    {
	Strophe.info("_onDisconnectTimeout was called");

	var req;
	while (this._requests.length > 0) {
	    req = this._requests.pop();
	    req.xhr.abort();
	    req.abort = true;
	}

	this._doDisconnect();

	return false;
    },

    /** PrivateFunction: _onIdle
     *  _Private_ handler to process events during idle cycle.
     *
     *  This handler is called every 100ms to fire timed handlers that
     *  are ready and keep poll requests going.
     */
    _onIdle: function ()
    {
	var i, thand, since, newList;

	while (this.removeTimeds.length > 0) {
	    thand = this.removeTimeds.pop();
	    i = this.timedHandlers.indexOf(thand);
	    if (i >= 0)
		this.timedHandlers.splice(i, 1);
	}

	while (this.addTimeds.length > 0) {
	    this.timedHandlers.push(this.addTimeds.pop());
	}

	var now = new Date().getTime();
	newList = [];
	for (i = 0; i < this.timedHandlers.length; i++) {
	    thand = this.timedHandlers[i];
	    if (this.authenticated || !thand.user) {
		since = thand.lastCalled + thand.period;
		if (since - now <= 0) {
		    if (thand.run()) {
			newList.push(thand);
		    }
		} else {
		    newList.push(thand);
		}
	    }
	}
	this.timedHandlers = newList;

	var body, time_elapsed;

	if (this.authenticated && this._requests.length === 0 &&
	    this._data.length === 0 && !this.disconnecting) {
	    Strophe.info("no requests during idle cycle, sending " +
			 "blank request");
	    this.send(null);
	} else {
	    if (this._requests.length < 2 && this._data.length > 0 &&
	       !this.paused) {
		body = this._buildBody();
		for (i = 0; i < this._data.length; i++) {
		    if (this._data[i] !== null) {
			if (this._data[i] === "restart") {
			    body.attrs({
				to: this.domain,
				"xml:lang": "en",
				"xmpp:restart": "true",
				"xmlns:xmpp": Strophe.NS.BOSH
			    })
			} else {
			    body.cnode(this._data[i]).up();
			}
		    }
		}
		delete this._data;
		this._data = [];
		this._requests.push(
		    new Strophe.Request(body.toString(),
					this._onRequestStateChange.bind(this)
					    .prependArg(this._dataRecv.bind(this)),
					body.tree().getAttribute("rid")));
		this._processRequest(this._requests.length - 1);
	    }

	    if (this._requests.length > 0) {
		time_elapsed = this._requests[0].age();
		if (this._requests[0].dead !== null) {
		    if (this._requests[0].timeDead() >
			Strophe.SECONDARY_TIMEOUT) {
			this._throttledRequestHandler();
		    }
		}

		if (time_elapsed > Strophe.TIMEOUT) {
		    Strophe.warn("Request " +
				 this._requests[0].id +
				 " timed out, over " + Strophe.TIMEOUT +
				 " seconds since last activity");
		    this._throttledRequestHandler();
		}
	    }
	}

	clearTimeout(this._idleTimeout);
	this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
    }
};
/*
xml2json v 1.1
copyright 2005-2007 Thomas Frank

This program is free software under the terms of the
GNU General Public License version 2 as published by the Free
Software Foundation. It is distributed without any warranty.
*/

xml2json={
	parser:function(xmlcode,ignoretags,debug){
		if(!ignoretags){ignoretags=""};
		xmlcode=xmlcode.replace(/\s*\/>/g,'/>');
		xmlcode=xmlcode.replace(/<\?[^>]*>/g,"").replace(/<\![^>]*>/g,"");
		if (!ignoretags.sort){ignoretags=ignoretags.split(",")};
		var x=this.no_fast_endings(xmlcode);
		x=this.attris_to_tags(x);
		x=escape(x);
		x=x.split("%3C").join("<").split("%3E").join(">").split("%3D").join("=").split("%22").join("\"");
		for (var i=0;i<ignoretags.length;i++){
			x=x.replace(new RegExp("<"+ignoretags[i]+">","g"),"*$**"+ignoretags[i]+"**$*");
			x=x.replace(new RegExp("</"+ignoretags[i]+">","g"),"*$***"+ignoretags[i]+"**$*")
		};
		x='<JSONTAGWRAPPER>'+x+'</JSONTAGWRAPPER>';
		this.xmlobject={};
		var y=this.xml_to_object(x).jsontagwrapper;
		if(debug){y=this.show_json_structure(y,debug)};
		return y
	},
	xml_to_object:function(xmlcode){
		var x=xmlcode.replace(/<\//g,"§");
		x=x.split("<");
		var y=[];
		var level=0;
		var opentags=[];
		for (var i=1;i<x.length;i++){
			var tagname=x[i].split(">")[0];
			opentags.push(tagname);
			level++
			y.push(level+"<"+x[i].split("§")[0]);
			while(x[i].indexOf("§"+opentags[opentags.length-1]+">")>=0){level--;opentags.pop()}
		};
		var oldniva=-1;
		var objname="this.xmlobject";
		for (var i=0;i<y.length;i++){
			var preeval="";
			var niva=y[i].split("<")[0];
			var tagnamn=y[i].split("<")[1].split(">")[0];
			tagnamn=tagnamn.toLowerCase();
			var rest=y[i].split(">")[1];
			if(niva<=oldniva){
				var tabort=oldniva-niva+1;
				for (var j=0;j<tabort;j++){objname=objname.substring(0,objname.lastIndexOf("."))}
			};
			objname+="."+tagnamn;
			var pobject=objname.substring(0,objname.lastIndexOf("."));
			if (eval("typeof "+pobject) != "object"){preeval+=pobject+"={value:"+pobject+"};\n"};
			var objlast=objname.substring(objname.lastIndexOf(".")+1);
			var already=false;
			for (k in eval(pobject)){if(k==objlast){already=true}};
			var onlywhites=true;
			for(var s=0;s<rest.length;s+=3){
				if(rest.charAt(s)!="%"){onlywhites=false}
			};
			if (rest!="" && !onlywhites){
				if(rest/1!=rest){
					rest="'"+rest.replace(/\'/g,"\\'")+"'";
					rest=rest.replace(/\*\$\*\*\*/g,"</");
					rest=rest.replace(/\*\$\*\*/g,"<");
					rest=rest.replace(/\*\*\$\*/g,">")
				}
			}
			else {rest="{}"};
			if(rest.charAt(0)=="'"){rest='unescape('+rest+')'};
			if (already && !eval(objname+".sort")){preeval+=objname+"=["+objname+"];\n"};
			var before="=";after="";
			if (already){before=".push(";after=")"};
			var toeval=preeval+objname+before+rest+after;
			eval(toeval);
			if(eval(objname+".sort")){objname+="["+eval(objname+".length-1")+"]"};
			oldniva=niva
		};
		return this.xmlobject
	},
	show_json_structure:function(obj,debug,l){
		var x='';
		if (obj.sort){x+="[\n"} else {x+="{\n"};
		for (var i in obj){
			if (!obj.sort){x+=i+":"};
			if (typeof obj[i] == "object"){
				x+=this.show_json_structure(obj[i],false,1)
			}
			else {
				if(typeof obj[i]=="function"){
					var v=obj[i]+"";
					x+=v
				}
				else if(typeof obj[i]!="string"){x+=obj[i]+",\n"}
				else {x+="'"+obj[i].replace(/\'/g,"\\'").replace(/\n/g,"\\n").replace(/\t/g,"\\t").replace(/\r/g,"\\r")+"',\n"}
			}
		};
		if (obj.sort){x+="],\n"} else {x+="},\n"};
		if (!l){
			x=x.substring(0,x.lastIndexOf(","));
			x=x.replace(new RegExp(",\n}","g"),"\n}");
			x=x.replace(new RegExp(",\n]","g"),"\n]");
			var y=x.split("\n");x="";
			var lvl=0;
			for (var i=0;i<y.length;i++){
				if(y[i].indexOf("}")>=0 || y[i].indexOf("]")>=0){lvl--};
				tabs="";for(var j=0;j<lvl;j++){tabs+="\t"};
				x+=tabs+y[i]+"\n";
				if(y[i].indexOf("{")>=0 || y[i].indexOf("[")>=0){lvl++}
			};
			if(debug=="html"){
				x=x.replace(/</g,"&lt;").replace(/>/g,"&gt;");
				x=x.replace(/\n/g,"<BR>").replace(/\t/g,"&nbsp;&nbsp;&nbsp;&nbsp;")
			};
			if (debug=="compact"){x=x.replace(/\n/g,"").replace(/\t/g,"")}
		};
		return x
	},
	no_fast_endings:function(x){
		x=x.split("/>");
		for (var i=1;i<x.length;i++){
			var t=x[i-1].substring(x[i-1].lastIndexOf("<")+1).split(" ")[0];
			x[i]="></"+t+">"+x[i]
		}	;
		x=x.join("");
		return x
	},
	attris_to_tags: function(x){
		var d=' ="\''.split("");
		x=x.split(">");
		for (var i=0;i<x.length;i++){
			var temp=x[i].split("<");
			for (var r=0;r<4;r++){temp[0]=temp[0].replace(new RegExp(d[r],"g"),"_jsonconvtemp"+r+"_")};
			if(temp[1]){
				temp[1]=temp[1].replace(/'/g,'"');
				temp[1]=temp[1].split('"');
				for (var j=1;j<temp[1].length;j+=2){
					for (var r=0;r<4;r++){temp[1][j]=temp[1][j].replace(new RegExp(d[r],"g"),"_jsonconvtemp"+r+"_")}
				};
				temp[1]=temp[1].join('"')
			};
			x[i]=temp.join("<")
		};
		x=x.join(">");
		x=x.replace(/ ([^=]*)=([^ |>]*)/g,"><$1>$2</$1");
		x=x.replace(/>"/g,">").replace(/"</g,"<");
		for (var r=0;r<4;r++){x=x.replace(new RegExp("_jsonconvtemp"+r+"_","g"),d[r])}	;
		return x
	}
};


if(!Array.prototype.push){
	Array.prototype.push=function(x){
		this[this.length]=x;
		return true
	}
};

if (!Array.prototype.pop){
	Array.prototype.pop=function(){
  		var response = this[this.length-1];
  		this.length--;
  		return response
	}
};

function XMPPGateway(domain, url,  drop_name, stream_key) {
    var connection = new Strophe.Connection(url);
    this.connection = function() { return connection; };

    this.drop_name = drop_name;
    this.stream_key = stream_key;
    this.domain = domain;

    this.inRoom = false;
    this.pendingNick = null;
    this.myNick = null;
    this.roster = new RoomRoster();

    this.trace = function(msg) {
      DropioStream.trace(msg);
    }.bind(this)

    this.rawInput = function(data) {
      if (DropioStream.DEBUG)
        this.trace('RECV: ' + data);
    }.bind(this);
    this.connection().rawInput = this.rawInput;

    this.rawOutput = function(data) {
      if (DropioStream.DEBUG)
        this.trace('SENT: ' + data);
      this.fire(DropioStream.STREAM_RECONNECT_TOKEN_CHANGED, [this.connection().jid,
                                                              this.connection().sid,
                                                              this.connection().rid,
                                                              this.myNick,
                                                              (new Date).valueOf()
                                                             ].join("|"));
    }.bind(this);
    this.connection().rawOutput = this.rawOutput;

    this.onConnect = function(status)
	  {
	    if (status == Strophe.Status.CONNECTING) {
		    this.trace('----------> Strophe is connecting.');
	    } else if (status == Strophe.Status.CONNFAIL) {
    		this.trace('----------> Strophe failed to connect.');
    		this.fire(DropioStream.STREAM_ERROR);
	    } else if (status == Strophe.Status.DISCONNECTING) {
		    this.trace('----------> Strophe is disconnecting.');
	    } else if (status == Strophe.Status.AUTHENTICATING) {
		    this.trace('----------> Strophe is authenticating.');
	    } else if (status == Strophe.Status.AUTHFAIL) {
    		this.trace('----------> Strophe failed to authenticate.');
    		this.fire(DropioStream.STREAM_ERROR);
	    } else if (status == Strophe.Status.DISCONNECTED) {
    		this.trace('----------> Strophe is disconnected.');
    		this.fire(DropioStream.STREAM_DISCONNECTED);
	    } else if (status == Strophe.Status.CONNECTED) {
    		this.trace('----------> Strophe is connected.' );
    		this.fire(DropioStream.STREAM_CONNECTED);
    		this.fire(DropioStream.LOGGED_IN);
	    }
	  }.bind(this)


    this.connect = function(reconnect_token) {
      if (reconnect_token) {
        var jid_sid_rid_time = reconnect_token.split("|");
        var jid = jid_sid_rid_time[0];
        var sid = jid_sid_rid_time[1];
        var rid = parseInt(jid_sid_rid_time[2]);
        var nick = jid_sid_rid_time[3];
        var time = parseInt(jid_sid_rid_time[4]);

        var now = (new Date).valueOf();

        if (!(jid == '' || sid == '' || rid == NaN || nick == '' || now-time > 50000)) {
          this.trace("Reattaching as "+nick);
          this.myNick = nick;
          this.connection().attach(jid, sid, rid, this.onConnect);

          var beConnected = function() {
            this.onConnect(Strophe.Status.CONNECTED);
            DropioStream.stopObserving(DropioStream.STREAM_RECONNECT_TOKEN_CHANGED, beConnected);
          }.bind(this);

          DropioStream.observe(DropioStream.STREAM_RECONNECT_TOKEN_CHANGED, beConnected);
        }
        else {
          this.trace("Reconnect token invalid; connecting as usual");
          this.connection().connect(this.domain, '', this.onConnect);
        }
      } else {
        this.trace("No reconnect token; connecting as usual");
        this.connection().connect(this.domain, '', this.onConnect);
      }
    	this.register_handlers();
    }.bind(this);

    this.disconnect = function() {
    	this.connection().disconnect();
    	this.inRoom = false;
    }.bind(this)

    this.remove = function() {
	    this.connection().disconnect();
    }.bind(this)

    this.connected = function() {
	    return this.connection().connected;
    }.bind(this)

    this.isActive = function() {
	    return this.connection().connected;
    }.bind(this)

    this.isRoomActive = function() {
    	return this.inRoom == true;
    }.bind(this)


    var joining = false;
    this.joinChat = function(nick) {
      if( this.isRoomActive() || joining ) return;
      joining = true;

      this.trace("Gateway is joining chat as " + nick);

      this.pendingNick = nick;
      this.myNick = null;

      var join = null

      var stopJoining = function() {
        join.stop();
        joining = false;
        DropioStream.stopObserving(DropioStream.JOINED_CHAT, stopJoining);
      }.bind(this);
      DropioStream.observe(DropioStream.JOINED_CHAT, stopJoining);

      join = new PeriodicalExecuter(function() {
        var msg = $pres({to: this.drop_name + '@conference.' + this.domain + '/' + nick,
                         from: this.connection().jid })
                       .c('x', {xmlns: Strophe.NS.MUC})
                         .c('password').t(this.stream_key).up()
                       .up()
                       .c('x', {xmlns: 'http://drop.io/xmpp/muc/reattach'});
        this.connection().send(msg.tree());
      }.bind(this), 0.5);
    }.bind(this)

    this.leaveChat = function() {
      if( !this.isRoomActive() ) return;

      var msg = $pres({from: this.connection().jid,
                       to: this.drop_name + "@conference." + this.domain + "/" + this.myNick,
                       type: "unavailable" });
    	this.connection().send(msg.tree());
    }

    this.changeNickname = function(newNick) {
      if( !this.isRoomActive() ) return;

      this.pendingNick = newNick;

      var msg = $pres({from: this.connection().jid,
                       to:   this.drop_name + "@conference." + this.domain + "/" + newNick})
      this.connection().send(msg.tree());
    }

    this.sendMessage = function(str) {
      if( !this.isRoomActive() ) return;
      var msg = $msg({to:   this.drop_name + '@conference.' + this.domain,
                      from: this.connection().jid,
                      type: "groupchat" }).c('body').t(str)
      this.connection().send(msg.tree());
    }

    this.sendDataMessage = function(data) {
      if( !this.isRoomActive() ) return;
      var msg = $msg({to:   this.drop_name + '@conference.' + this.domain,
                      from: this.connection().jid,
                      type: "groupchat" });
      msg = this.addDataTo(msg,data);
      this.connection().send(msg.tree());
    }

	  this.addDataTo = function(stanza,data) {
	    for( k in data ) {
	      if( typeof(data[k]) == "object" ) {
	        var substanza = stanza.c(k);
	        stanza = this.addDataTo(substanza,data[k]);
	      }
	      else
	        stanza.c(k).t(data[k]).up();
	    }
	    return stanza;
	  }


    this.register_handlers = function() {
    	this.connection().addHandler(this.handle_user_presence, 'jabber:client', 'presence');
    	this.connection().addHandler(this.handle_message, 'jabber:client','message');
    }.bind(this)

    this.handle_user_presence = function(stanza) {
    	var from = stanza.getAttribute("from");
      var type = stanza.getAttribute("type");

      if( from == "" || from == null ) return;

      var nickname = this.nickFromJID(from);

      if( type == "error" ) {
        var ecode = this.getErrorCodeFromPresence(stanza);

        if( ecode == "403" ) {
        	this.trace("Gateway could not join chat");
        	this.inRoom = false;
          this.fire(DropioStream.ROOM_UNAVAILABLE)
        }

        else if( ecode == "409" ) {
          var from = stanza.getAttribute("from")
          if( from == null || from == "" ) return;
          var nick = this.nickFromJID(from);
          this.trace("Got a conflicted nick: " + nick);
          this.pendingNick = null;
          this.fire(DropioStream.NICK_CONFLICT,{nickname:nick});
        }

        else if( ecode == "503" ) {
          this.fire(DropioStream.ROOM_FULL);
          this.connection().disconnect();
        }
      }

      else if( type == "unavailable" ) {
        var code = this.getStatusCodeFromPresence(stanza);
        var role = this.getRoleFromPresence(stanza);

        if( code == "303" ) {
          var newNick = this.getNewNickFromPresence(stanza);
          if( newNick == "" ) return;

          this.roster.update(nickname,newNick,role);

          if( this.pendingNick == newNick ) {
            this.myNick = newNick;
            this.pendingNick = null;
          }

          this.fire(DropioStream.NICK_CHANGED,{from:nickname,to:newNick});
        }

        else {
          this.roster.remove(nickname);

          if( nickname == this.myNick ) {
          	this.inRoom = false;
          	this.myNick = null;
            this.fire(DropioStream.LEFT_CHAT);
          }

          this.fire(DropioStream.USER_DEPARTED, {nickname:nickname});
        }
      }

      else  {
        var role = this.getRoleFromPresence(stanza);
        if( role == "" || role == null ) return;


        if( !this.roster.exists(nickname) ) {
          this.roster.add(nickname,role);

          if( this.pendingNick == nickname ) {
          	joinedChatAs(nickname);
          }

          this.fire(DropioStream.USER_JOINED, {nickname:nickname,role:role});
    	  }

        else if( this.roster.getRole(nickname) != role ) {
          this.roster.update(nickname,nickname,role);

          this.fire(DropioStream.USER_ROLE_UPDATED, {nickname:nickname, role:role} )
        }
      }

    	return true;
    }.bind(this)

    var joinedChatAs = function(nickname) {
      this.inRoom = true;
      this.myNick = nickname;
      this.pendingNick = null;
      var role = this.roster.getRole(this.myNick);
      this.fire(DropioStream.JOINED_CHAT, {role:role,nickname:nickname});
    }.bind(this);

    this.handle_message = function(stanza) {
    	var from = stanza.getAttribute('from');
    	var type = stanza.getAttribute('type');

    	var nick = this.nickFromJID(from);

    	var timestamp = null;
    	var body = null;
    	var system_message = null;
    	var code = null;

    	Strophe.forEachChild(stanza,'x', function(x) {
    	  timestamp = x.getAttribute('stamp');
    	  Strophe.forEachChild(x,'status', function(s) {
    	    code = s.getAttribute("code");
    	  });
    	});
    	Strophe.forEachChild(stanza,'body', function(x) { body = Strophe.getText(x); });
    	Strophe.forEachChild(stanza,"dropEventData", function(x) { system_message = x; });

      if( type == "error" ) {
        var ecode = this.getErrorCodeFromMessage(stanza);
        if( ecode == "403" )
          this.fire(DropioStream.MESSAGE_REJECTED, { nickname: nick, message: body });
      }
      else if( type == "groupchat" ) {
      	if (system_message != null) {
    	    CurrSys = system_message; //debugging

    	    var contents = Strophe.serialize(system_message);
          contents = this.parseEventData(contents).dropeventdata;

          this.fire(DropioStream.RECEIVED_EVENT_DATA, {time: timestamp, sender: from, nickname: nick, message: body, eventData: contents});
      	} else if (body) {
      	  if( code != "100" )
      	    this.fire(DropioStream.RECEIVED_MESSAGE, {time: timestamp, sender: from, nickname: nick, message: body});
      	} else {
    	    var contents = Strophe.serialize(stanza);
            contents = xml2json.parser(contents).message;
			this.fire(DropioStream.RECEIVED_DATA, {time: timestamp, sender: from, nickname: nick, data: contents})
		}
     }

    	return true; //if you don't do this the handler will be unregistered
    }.bind(this)

    this.fire = function(event_type, data) {
      this.trace("firing " + event_type + "...");
    	DropioStream.fire({event: event_type, params: data});
    }.bind(this)



    this.nickFromJID = function(jid) {
      var base = jid.split("/",1)
      var nick = jid.replace(base+"/","");
      return nick;
    }

    this.getRoleFromPresence = function(stanza) {
      var role = "";
      Strophe.forEachChild(stanza, 'x', function(x) {
        Strophe.forEachChild(x, "item", function(i) {
          role = i.getAttribute("role");
        })
      });
      if( role == null ) role = "";
      return role;
    }

    this.getStatusCodeFromPresence = function(stanza) {
      var code = "";
      Strophe.forEachChild(stanza, 'x', function(x) {
        Strophe.forEachChild(x, 'status', function(s) {
          code = s.getAttribute("code");
        })
      });
      if( code == null ) code = "";
      return code;
    }

    this.getErrorCodeFromPresence = function(stanza) {
    	var ecode = "";
    	Strophe.forEachChild(stanza,'error', function(e) {
    	  ecode = e.getAttribute('code');
      });
      if( ecode == null ) ecode = "";
      return ecode;
    }

    this.getErrorCodeFromMessage = function(stanza) {
    	var ecode = "";
    	Strophe.forEachChild(stanza,'error', function(e) {
    	  ecode = e.getAttribute('code');
      });
      if( ecode == null ) ecode = "";
      return ecode;
    }

    this.getNewNickFromPresence = function(stanza) {
      var nick = "";
      Strophe.forEachChild(stanza, 'x', function(x) {
        Strophe.forEachChild(x, "item", function(i) {
          nick = i.getAttribute("nick");
        })
      });
      if( nick == null ) nick = "";
      return nick;
    }

    this.parseEventData = function(eventData) {
      var parsed = xml2json.parser(eventData);
      var params = parsed.dropeventdata.params
      for( k in params ) {
        if( typeof(params[k]) == "object") {
          h = $H(params[k])
          if(h.keys().length == 0 || (h.keys().length == 1 && h.keys().first() == "nil") )
            params[k] = ""
        }
      }
      parsed.dropeventdata.params = params;
      return parsed;
    }

    this.getNickname = function() {
      return this.myNick;
    }

    this.trace("initialized: domain=" + this.domain + ", drop_name=" + this.drop_name + ", stream_key=" + this.stream_key);
}

function RoomRoster() {
  this.occupants = {};

  this.add = function(nick,role) {
    this.occupants[nick] = role;
  }

  this.update = function(oldNick,newNick,newRole) {
    this.remove(oldNick);
    this.add(newNick,newRole);
  }

  this.remove = function(nick) {
    this.occupants[nick] = null;
  }

  this.exists = function(nick) {
    return this.occupants[nick] != null
  }

  this.getRole = function(nick) {
    return this.occupants[nick];
  }
}

var DropioStream = {
	authorized: [],  // an array of jids that have permission to send data to the chat room
	observings: {},  // hash of functions observing events

  start: function(drop_name, stream_key, server, port, reconnect_token) {
  	DropioStream.drop_name = drop_name;
  	DropioStream.stream_key = stream_key;
  	DropioStream.server = server == null ? "drop.io" : server;
  	DropioStream.jport = port == null ? 5222 : port;

    DropioStream.registerAuthorized(""); // register the conf room itself
    DropioStream.xmpp_gateway = new XMPPGateway(server, '/http-bind', drop_name, stream_key);
    DropioStream.gateway().connect(reconnect_token);
  },

  observe: function(events, fn, wantsBacklog) {
	  if( wantsBacklog == null ) wantsBacklog = false;

	  if( typeof(events) != "object" ) events = [events]

	  events.each(function(event) {
  		if (typeof(DropioStream.observings[event]) == "undefined") DropioStream.observings[event] = []
  		DropioStream.observings[event].push({callback:fn,wantsBacklog:wantsBacklog});
    });
	},

	stopObserving: function(events, fn) {
	  if( typeof(events) != "object" ) events = [events]
	  events.each(function(event) {
  	  DropioStream.observings[event].each(function(h) {
  	    if( h["callback"] == fn )
    	    DropioStream.observings[event] = DropioStream.observings[event].without(h);
  	  });
  	});
	},

  fire: function(data) {
  	if( !SHOULD_FIRE_STREAM_EVENTS && data.event != DropioStream.LOGGED_IN ) return;

    var dataToPass = data.params

    isBacklogged = DropioStream.isHistoricalData(dataToPass);

    if( data.event == DropioStream.RECEIVED_EVENT_DATA ) {
      if( DropioStream.IAE.indexOf(dataToPass.eventData.event) == -1 && !DropioStream.isAuthorized( data.params.sender) ) {
    		DropioStream.trace("Dropping event data message which didn't come from an authorized jid, it came from: " + data.params.sender);
    		return;
      }

      if( DropioStream.isMe( data.params.nickname ) ) {
        DropioStream.trace("Dropping event data message because it came from me");
        return;
      }

      if( dataToPass.time != null && dataToPass.time != "" )
        dataToPass.eventData.params.time = dataToPass.time;

      if( dataToPass.eventData.params.message == null )
        dataToPass.eventData.params.message = dataToPass.message

      if( dataToPass.eventData.params.nickname == null )
        dataToPass.eventData.params.nickname = dataToPass.nickname

      DropioStream.doFire(dataToPass.eventData.event,dataToPass.eventData.params,isBacklogged);

      var params = dataToPass.eventData.params
      var event = dataToPass.eventData.event

      toFire = {}
      for(k in params) {
        if( DropioStream.SUB_EVENT_MAPPING[event] && DropioStream.SUB_EVENT_MAPPING[event][k] ) {
          if( !toFire[DropioStream.SUB_EVENT_MAPPING[event][k]] )
            toFire[DropioStream.SUB_EVENT_MAPPING[event][k]] = {}
          toFire[DropioStream.SUB_EVENT_MAPPING[event][k]][k] = params[k]
        }
      }

      for(k in toFire) {
        if( params.id ) toFire[k].id = params.id
        if( params.type ) toFire[k].type = params.type
        DropioStream.doFire(k,toFire[k],isBacklogged)
      }
    }
    else {
      DropioStream.doFire(data.event,dataToPass,isBacklogged);
    }
  },

	doFire: function(event, data, isBacklogged) {
	  try {
  	  if( event == DropioStream.USER_JOINED && data.role == "moderator" && DropioStream.isValidBotName(data.nickname) ) return;

  	  if( data == null ) data = {}
  	  if (DropioStream.observings[event])	{
  	 	DropioStream.observings[event].each(function(h){
  			if( !isBacklogged || (isBacklogged && h["wantsBacklog"] == true) ) {
		       h["callback"](data);
  	    	}
		});
  	  }
    } catch(e) {
      DropioStream.trace("STREAM EXCEPTION CAUGHT:")
      DropioStream.trace(e);
    }
	},

	isValidBotName: function( nick ) {
	  try {
	    parts = nick.split("-")
	    return (parts[0] == DropioStream.drop_name && !isNaN(parseInt(parts[1])))
	  } catch( e ) {
	    return false;
	  }
	},

	registerAuthorized: function(nick) {
		var jid = DropioStream.fullJID(nick);
    if( DropioStream.authorized.indexOf(jid) == -1 )
      DropioStream.authorized.push(jid);
  },

  removeAuthorized: function(nick) {
		var jid = DropioStream.fullJID(nick);
    DropioStream.authorized = DropioStream.authorized.without(jid);
  },

	isAuthorized: function(jid) {
	  return DropioStream.authorized.indexOf(jid) != -1;
	},

	fullJID: function(nick) {
		var jid = DropioStream.drop_name + '@conference.' + DropioStream.server;
	  if( nick != null && nick != "" )
	    jid += "/" + nick;
	  return jid;
	},

	gateway: function() {
	  return DropioStream.xmpp_gateway;
	},

  reconnect: function(nick) {
    if( DropioStream.gateway() ) {
      if( !DropioStream.isActive() )
        DropioStream.connect();
      else if( !DropioStream.isRoomActive() ) {
        DropioStream.joinChat(nick);
      }
    }
  },

  requestRoomCreation: function(callback) {
    new Ajax.Request("/stream/create_muc", {
      parameters: "drop="+DropioStream.drop_name,
      method: "post",
      onComplete: function(transport) {
        var res = transport.responseJSON.success
        callback(res);
      }
    })
  },

  requestPromotion: function(nick) {
    if( !nick )
      return;
    new Ajax.Request("/stream/promote", {
      parameters: "drop="+DropioStream.drop_name+"&nick="+nick,
      method: "post"
    })
  },

  requestDemotion: function(nick) {
    if( !nick )
      return;

    new Ajax.Request("/stream/demote", {
      parameters: "drop="+DropioStream.drop_name+"&nick="+nick,
      method: "post"
    })
  },

  connect: function() {
    if( DropioStream.gateway() )
      DropioStream.gateway().connect();
  },

  disconnect: function() {
    if( DropioStream.gateway() )
      DropioStream.gateway().disconnect();
  },

  joinChat: function(nick) {
    if(!nick)
      return

    if( DropioStream.gateway() )
      DropioStream.gateway().joinChat(nick);
  },

  leaveChat: function() {
    if( DropioStream.gateway() )
      DropioStream.gateway().leaveChat();
  },

  changeNickname: function(newNick) {
    if( DropioStream.gateway() )  {
      DropioStream.gateway().changeNickname(newNick);
    }
  },

  sendMessage: function(theMessage) {
     if(DropioStream.gateway() )
      DropioStream.gateway().sendMessage(theMessage);
  },

  sendEventData: function(event,params) {
     if(DropioStream.gateway() )
      DropioStream.gateway().sendDataMessage({dropEventData: { event: event, params: params }});
  },

  sendData: function(data) {
	 if(DropioStream.gateway() )
      DropioStream.gateway().sendDataMessage(data);
  },

  isActive: function() {
     if( DropioStream.gateway() )
      return DropioStream.gateway().isActive();
     else
      return false;
  },

  isRoomActive: function() {
     if( DropioStream.gateway() )
      return DropioStream.gateway().isRoomActive();
     else
      return false;
  },

  getNickname: function() {
    if (DropioStream.gateway())
      return DropioStream.gateway().getNickname();
  },

  getRandomNick: function() {
    var randNum = Math.floor(Math.random() * 10001);
    return "Guest" + randNum;
  },

  isMe: function(nick) {
    return DropioStream.gateway().getNickname() == nick;
  },

  isHistoricalData: function(data) {
    return (data != null && data != {} && data.time != null && typeof(data.time) != "undefined" && data.time != "");
  }
};

DropioStream.DEBUG = false;
DropioStream.trace = function(msg) {
  if( DropioStream.DEBUG ) {
	  if( typeof(console) != "undefined" )
	    console.info(msg);
	}
}


DropioStream.STREAM_CONNECTED = "streamConnected";                  // => {}
DropioStream.STREAM_DISCONNECTED = "streamDisconnected";            // => {}
DropioStream.STREAM_ERROR = "streamError";                          // => {message}
DropioStream.STREAM_RECONNECT_TOKEN_CHANGED = "streamReconnectTokenChanged"; // => token
DropioStream.LOGGED_IN = "loggedIn";                                // => {}
DropioStream.JOINED_CHAT = "joinedChat";                            // => {role,nickname}
DropioStream.LEFT_CHAT = "leftChat";                                // => {}
DropioStream.NICK_CONFLICT = "nickConflict";                        // => {nickname}
DropioStream.NICK_CHANGED = "nickChanged";                          // => {from,to}
DropioStream.ROOM_UNAVAILABLE = "roomUnavailable";                  // => {}
DropioStream.ROOM_FULL = "roomFull";                                // => {}
DropioStream.USER_DEPARTED = "userDeparted";                        // => {nickname}
DropioStream.USER_JOINED = "userJoined";                            // => {nickname,role}
DropioStream.USER_ROLE_UPDATED = "userRoleUpdated";                 // => {nickname,role}
DropioStream.RECEIVED_MESSAGE = "receivedMessage";                  // => {time,nickname,message}
DropioStream.MESSAGE_REJECTED = "messageRejected";                  // => {nickname,message}
DropioStream.RECEIVED_EVENT_DATA = "receivedEventData";             // => {time,nickname,message,eventData}
DropioStream.RECEIVED_DATA = "receivedData";                        // => {time,nickname,message,data}


DropioStream.ASSET_ADDED = "assetAdded";                            // => {id, name, title, description*, file*, hidden_url*, created_at, thumbnail*, filesize*, status*, type, url*, converted*, height*, width*, duration*, track_title*, artist pages*, contents*, media_view_order*}
DropioStream.ASSET_DELETED = "assetDeleted";                        // => {id}
DropioStream.ASSET_UPDATED = "assetUpdated";                        // => {id, name, title, description*, file*, hidden_url*, created_at, thumbnail*, filesize*, status*, type, url*, converted*, height*, width*, duration*, track_title*, artist pages*, contents*, media_view_order*}
DropioStream.COMMENT_ADDED = "commentAdded";                        // => {id, asset_id, created_at, contents}
DropioStream.COMMENT_DELETED = "commentDeleted";                    // => {id, asset_id}
DropioStream.COMMENT_UPDATED = "commentUpdated";                    // => {id, asset_id, contents}
DropioStream.DROP_UPDATED = "dropUpdated";                          // => {description*, background_url*, background_repeat*, background_color*, accent_color*, logo_url*, location*, show_navigation*, media_view_category*, media_view_order*}
DropioStream.DROP_DELETED = "dropDeleted";                          // => {}
DropioStream.SYSTEM_MESSAGE = "systemMessage";                      // => {message}
DropioStream.DROP_OVERFULL = "dropOverfull";                        // => {}
DropioStream.NOTICE_POSTED = "noticePosted";                        // => {payload}

DropioStream.DROP_DESCRIPTION_CHANGED = "dropDescriptionChanged";      // => {description}
DropioStream.DROP_BACKGROUND_CHANGED = "dropBackgroundChanged";        // => {background_color, background_url, background_repeat}
DropioStream.DROP_LOGO_CHANGED = "dropLogoChanged";                    // => {logo_url}
DropioStream.DROP_LOCATION_CHANGED = "dropLocationChanged";            // => {location: {lat, lon}}
DropioStream.DROP_NAVIGATION_CHANGED = "dropNavigationChanged";        // => {show_navigation}
DropioStream.DROP_ACCENT_COLOR_CHANGED = "dropAccentColorChanged";     // => {accent_color}
DropioStream.DROP_GROUP_ORDER_CHANGED = "dropGroupOrderChanged";       // => {group_order}
DropioStream.DROP_ASSET_ORDER_CHANGED = "dropAssetOrderChanged";       // => {media_view_group,media_view_order}

DropioStream.ASSET_NAME_CHANGED = "assetNameChanged";                  // => {id,new_name}
DropioStream.ASSET_DESCRIPTION_CHANGED = "assetDescriptionChanged";    // => {id,description}
DropioStream.ASSET_TITLE_CHANGED = "assetTitleChanged";                // => {id,title}
DropioStream.ASSET_STATUS_CHANGED = "assetStatusChanged";              // => {id,status}
DropioStream.ASSET_TYPE_CHANGED = "assetTypeChanged";                  // => {id,type}
DropioStream.ASSET_URL_CHANGED = "assetUrlChanged";                    // => {id,url}
DropioStream.ASSET_CONTENTS_CHANGED = "assetContentsChanged";          // => {id,contents}
DropioStream.ASSET_HIDDEN_URL_CHANGED = "assetHiddenUrlChanged";       // => {id,hidden_url}
DropioStream.ASSET_THUMBNAIL_CHANGED = "assetThumbnailChanged";        // => {id,thumbnail}
DropioStream.ASSET_FILESIZE_CHANGED = "assetFilesizeChanged";          // => {id,filesize}
DropioStream.ASSET_CONVERTED_CHANGED = "assetConvertedChanged";        // => {id,converted}
DropioStream.ASSET_DIMENSIONS_CHANGED = "assetDimensionsChanged";      // => {id,width,height}
DropioStream.ASSET_DURATION_CHANGED = "assetDurationChanged";          // => {id,duration}
DropioStream.ASSET_TRACK_TITLE_CHANGED = "assetTrackTitleChanged";     // => {id,track_title}
DropioStream.ASSET_ARTIST_CHANGED = "assetArtistChanged";              // => {id,artist}
DropioStream.ASSET_PAGES_CHANGED = "assetPagesChanges";                // => {id,pages}
DropioStream.ASSET_SCRIBD_CONVERTED = "assetScribdConverted"           // => {id,type, scribd_id, scribd_key}

DropioStream.REMOTE_CONTROL_STARTED = "rcStarted";                      // => {}
DropioStream.REMOTE_CONTROL_ENDED = "rcEnded";                          // => {}
DropioStream.USER_JOINED_REMOTE_CONTROL = "rcUserJoined";               // => {nickname}
DropioStream.USER_LEFT_REMOTE_CONTROL = "rcUserLeft";                   // => {nickname}
DropioStream.ASSET_OPENED_IN_MODAL = "assetOpenedInModal";              // => {id}
DropioStream.MODAL_WINDOW_CLOSED = "modalWindowClosed";                 // => {}
DropioStream.MEDIA_PLAYED = "mediaPlayed";                              // => {id, position}          (audio/video)
DropioStream.MEDIA_PAUSED = "mediaPaused";                              // => {id, position}          (audio/video)
DropioStream.MEDIA_JUMPED_TO_POSITION = "mediaJumpedToPosition";        // => {id,position} (audio/video)
DropioStream.DOCUMENT_PAGE_SHOWN = "documentPageShown";                 // => {id,page}     (documents)
DropioStream.DOCUMENT_VIEW_CHANGED = "documentViewChanged";               // => {view_mode,zoom, full_screen}     (documents)
DropioStream.LASER_POINTER_MOVED = "laserPointerMoved";                 // => {x,y} x is relative to containerViews, y is absolute to the top
DropioStream.LASER_POINTER_HIDDEN = "laserPointerHidden";               // => {}
DropioStream.LINK_OPENED = "linkOpened";                                // => {url}
DropioStream.FULL_SCREEN_ENTERED = "fullScreenEntered";                 // => {id}           (video/maybe docs)
DropioStream.FULL_SCREEN_EXITED = "fullScreenExited";                   // => {}             (video/maybe docs)
DropioStream.CHAT_LAYER_OPENED = "chatLayerOpened";                     // => {}
DropioStream.CHAT_LAYER_CLOSED = "chatLayerClosed";                     // => {}
DropioStream.PLAYLIST_REPEAT_CHANGED = "playlistRepeatChanged";         // => {on}
DropioStream.PLAYLIST_SHUFFLE_CHANGED = "playlistShuffleChanged";       // => {on}
DropioStream.PLAYLIST_SONG_STATE_CHANGED = "playlistSongStateChanged";  // => {playing,song=>{url,id,title},percentage}
DropioStream.ASSET_DOWNLOADED = "assetDownloaded";                      // => {}
DropioStream.MODAL_DRAWER_CHANGED = "modalDrawerChanged";               // => {drawerId}

DropioStream.SUB_EVENT_MAPPING = {
  dropUpdated: {
    description: DropioStream.DROP_DESCRIPTION_CHANGED,
    background_color: DropioStream.DROP_BACKGROUND_CHANGED,
    background_url: DropioStream.DROP_BACKGROUND_CHANGED,
    background_repeat: DropioStream.DROP_BACKGROUND_CHANGED,
    logo_url: DropioStream.DROP_LOGO_CHANGED,
    location: DropioStream.DROP_LOCATION_CHANGED,
    show_navigation: DropioStream.DROP_NAVIGATION_CHANGED,
    accent_color: DropioStream.DROP_ACCENT_COLOR_CHANGED,
    group_order: DropioStream.DROP_GROUP_ORDER_CHANGED,
    media_view_order: DropioStream.DROP_ASSET_ORDER_CHANGED,
    media_view_group: DropioStream.DROP_ASSET_ORDER_CHANGED
  },

  assetUpdated: {
    new_name: DropioStream.ASSET_NAME_CHANGED,
    description: DropioStream.ASSET_DESCRIPTION_CHANGED,
    title: DropioStream.ASSET_TITLE_CHANGED,
    status: DropioStream.ASSET_STATUS_CHANGED,
    type: DropioStream.ASSET_TYPE_CHANGED,
    url: DropioStream.ASSET_URL_CHANGED,
    contents: DropioStream.ASSET_CONTENTS_CHANGED,
    hidden_url: DropioStream.ASSET_HIDDEN_URL_CHANGED,
    thumbnail: DropioStream.ASSET_THUMBNAIL_CHANGED,
    filesize: DropioStream.ASSET_FILESIZE_CHANGED,
    converted: DropioStream.ASSET_CONVERTED_CHANGED,
    width: DropioStream.ASSET_DIMENSIONS_CHANGED,
    height: DropioStream.ASSET_DIMENSIONS_CHANGED,
    duration: DropioStream.ASSET_DURATION_CHANGED,
    track_title: DropioStream.ASSET_TRACK_TITLE_CHANGED,
    artist: DropioStream.ASSET_ARTIST_CHANGED,
    pages: DropioStream.ASSET_PAGES_CHANGED,
    scribd_id: DropioStream.ASSET_SCRIBD_CONVERTED,
    scribd_key: DropioStream.ASSET_SCRIBD_CONVERTED
  }
};

DropioStream.IAE = [
  DropioStream.USER_JOINED_REMOTE_CONTROL,
  DropioStream.USER_LEFT_REMOTE_CONTROL
];
DropioStream.SharedListeners = {
	streamConnected: function() {},

	loggedIn: function(nickname) {
	  DropioStream.joinChat(nickname);
	},

  reconnectAttempts: 0,
  MAX_RECONNECT_ATTEMPTS: 10,
	streamDisconnected: function() {
    window.STREAM_ACTIVE = false;
    if($("dropio_is_stream_active_for_firefox_extension"))
      $("dropio_is_stream_active_for_firefox_extension").update("false");

    if( this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS ) {
      this.reconnectAttempts++;
      setTimeout("DropioStream.reconnect('" + this.getNickname() + "')",2000);
	  }
	},

	streamError: function() {
		DropioStream.trace("Stream error...")
		if(window.STREAM_ACTIVE)
	    DropioStream.SharedListeners.streamDisconnected()
	},

	joinedRoom: function() {
    window.STREAM_ACTIVE = true;
    if($("dropio_is_stream_active_for_firefox_extension"))
      $("dropio_is_stream_active_for_firefox_extension").update("true");
    if( IS_ADMIN ) DropioStream.requestPromotion(DropioStream.getNickname());
	},

	requestedRoomCreation: false,
	roomJoinError: function() {
	  DropioStream.trace("Error joining room...")

	  if( !this.requestedRoomCreation ) {
  	  this.requestedRoomCreation = true;
      DropioStream.requestRoomCreation(function(res) {
        if(res) DropioStream.SharedListeners.streamDisconnected()
      })
    } else
      DropioStream.SharedListeners.streamDisconnected()
	},

	roomFull: function() {
    new Ajax.Request("/stream/drop_overfull", {
      parameters: "drop="+DropioStream.drop_name,
      method: "post"
    })
	},

	leftRoom: function() {
	  DropioStream.trace("Left chat room...")
	},

	growlAdded: function(data) {
		if( !data || data.message == null ) return;
		growl(ChatParser.sanitize(data.message));
	},

	descriptionChanged: function(data) {
		if( !data || data.description == null ) return;
		changeDescription(data.description);
	  blink("description changed")
	},

	navigationChanged: function(data) {
		if( !data || data.show_navigation == null ) return;
		var which = data.show_navigation.toString() == "true" ? '1' : '0';
	  toggleNavigation(which,false);
	  blink("navigation changed")
	},

	accentColorChanged: function(data) {
		if( !data || data.accent_color == null ) return;
		changeAccentColor(data.accent_color);
	  blink("text color changed")
	},

	backgroundChanged: function(data) {
	  if( !data ) return;
	  repeatVal = (data.background_repeat == null ? null : (data.background_repeat == "true" ? "repeat" : "no-repeat" ))
	  changeBackground(data.background_url,data.background_color,repeatVal);
	  blink("background changed")
	},

	logoChanged: function(data) {
		if( !data || data.logo_url == null ) return;
    changeLogo(data.logo_url);
	  blink("logo changed")
	},

	locationChanged: function(data) {
	  if( !data || data.location == null ) return;
	  if( data.location.lat == null || data.location.lon == null ) return;
	  changeLocation(data.location.lat,data.location.lon);
	  blink("location changed")
	},

	systemMessage: function(data) {
		if( !data || data.message == null ) return;
		showSystemMessage(data.message);
	  blink(data.message)
	},

	assetAdded: function(data) {
	  if( !data ) return;
	  data = convertStreamData(data)

	  if(typeof(addAsset) == "function") {
	    addAsset(data);
    }
	  blink(data.type + " added")
	},

	assetUpdated: function(data) {
	  if( !data ) return;
	  data = convertStreamData(data)

	  if(typeof(updateAsset) == "function") {
	   updateAsset(data);
    }
	  blink(data.type + " updated")
	},

	historicalAssetUpdated: function(data) {
	  if( (typeof(updateAsset) == "function") && DropioStream.isHistoricalData(data) && !isAssetUnchanged(data,'status') ) {
	    updateAsset(data);
	  }
	},

	assetDeleted: function(data) {
	  if( !data || data.id == null ) return;
	  data = convertStreamData(data);
	  if(typeof(removeAsset) == "function") {
	    removeAsset(data.id);
    }
	  blink(data.type + " deleted")
	},

	commentAdded: function(data) {
	  if( !data || data.asset_id == null ) return;
	  if(typeof(addComment) == "function") {
	    addComment(data.asset_id,data);
    }
	  blink("comment added")
	},

  commentUpdated: function(data) {
	  if( !data || data.asset_id == null ) return;
	  if(typeof(updateComment) == "function") {
	    updateComment(data.asset_id,data);
    }
	  blink("comment updated")
	},

	commentDeleted: function(data) {
	  if( !data || data.asset_id == null ) return;
	  if(typeof(removeComment) == "function") {
    	removeComment(data.asset_id,data);
	  }
	  blink("comment deleted")
	},

	dropDeleted: function(data) {
	  alert("This drop has been deleted!")
	  top.location = "/"
	},

	receivedMessage: function(data) {
	  if( data && data.message )
	    blink(data.message)
	}

}
DropioStream.ModalListeners = {
  assetUpdated: function(data) {
    if( !data || data.id == null ) return;
	  if( isAssetOpenInModal(data.id ) )
	    updateModalAsset();
  },

  assetDeleted: function(data) {
    if( !data || data.id == null ) return;
	  if( isAssetOpenInModal(data.id ) )
	    removeModalAsset();
  },

  commentAdded: function(data) {
    if( !data || data.asset_id == null ) return;
    if( isAssetOpenInModal(data.asset_id ) )
      addModalComment(data,true,true);
  },

  commentUpdated: function(data) {
    if( !data || data.asset_id == null ) return;
    if( isAssetOpenInModal(data.asset_id ) )
      updateModalComment(data);
  },

  commentDeleted: function(data) {
    if( !data || data.id == null || data.asset_id == null ) return;
    if( isAssetOpenInModal(data.asset_id ))
    removeModalComment(data.id,true,true);
  }
}
document.observe("dom:loaded", function() {
  loadAjaxlessInPlaceEditor();


  if ($('chatInput')) {
    $('chatInput').observe('keypress', function(event) {
      if (Event.KEY_RETURN == event.keyCode) {
        theChatLayer.addSelfMessage();
      }
    });
  };

  if ($('muteButton')) {
    $('muteButton').observe('click', function(event) {
      event.stop();
      $('muteButton').toggleClassName('muted');
      ChatAlerts.mute($('muteButton').hasClassName('muted'));
    });
  };
});

var Chat = Class.create({
  initialize: function() {
    this.loggedInWithNick = false;
    this.otherUserCount = 0;
    this.scrollThreshold = 50;
    this.wildWestMode = false;
    this.visibleNickList = true;
    this.mostRecentChatUser = null;
    this.recievedMessages = 0;
    this.maxMessageLimit = 300;
    this.disconnectedStatus = false;
    this.loggingInFromDisconnect = false;
    this.chatInput = $$('#chatLayer #chatInput').first();
    this.userNickDiv = $$('#chatLayer #yourNickname').first();
    this.chatContent = $$('#chatLayer #chatContent').first();

    setTimeout(function() {this.checkInitialConnection()}.bind(this), 15000);

    this.chatContainer().scrollTop = this.chatContainer().scrollHeight - this.chatContainer().offsetHeight;
  },

  chatContainer: function() {
    return $$('#chatLayer #mainChatContainer').first();
  },

  nickList: function() {
    return $$('#chatLayer #nickList').first();
  },

  addUserMessage: function(data) {
    if(!data)
      return;

    if(this.detectHistoricalData(data) && this.loggingInFromDisconnect) {
      return;
    } else if(!this.detectHistoricalData(data) && this.loggingInFromDisconnect) {
      this.loggingInFromDisconnect = false;
    }

    this.loggingInFromDisconnect = false;
    var user = data['nickname'];
    if(user == this.getNickname() && (data.time == null || data.time == ""))
      return;
    user = user.escapeHTML().substring(0, 14);

    var theMessage = this.sanitizeAndlinkifyMessage(data['message']);
    var messageAboutMe = ChatParser.isAboutMe(theMessage, this.getNickname().escapeHTML()) ? "aboutMe" : "";
    if(theMessage == "")
      return;

      if(data.time) {
        formattedDates = this.formatHistoricalDate(data.time);
        this.mostRecentMessageType = 'historical';
      } else {
        formattedDates = this.getFormattedDate();
      }

      if(this.mostRecentChatUser == user) {
        assetHTML = CHAT_TEMPLATES['continued_message'].interpolate({
          fulltime: formattedDates['long'],
          message: theMessage,
          aboutMe: messageAboutMe
        });
      } else {
        assetHTML = CHAT_TEMPLATES['other_chat'].interpolate({
          fulltime: formattedDates['long'],
          shorttime: formattedDates['short'],
          nick: user,
          message: theMessage,
          aboutMe: messageAboutMe
        });
      }

      tmp = new Element("div")
      tmp.innerHTML = assetHTML
      asset = tmp.down();

      this.incrementReceivedMessages();
      this.chatContainer().insert(asset);
      this.mostRecentChatUser = user;

      this.scrollToBottomAfterImageLoads(asset);
      this.scrollToBottomOfChat();

  },

  addSelfMessage: function() {
    if(this.chatInput.value == "")
      return;

    var theMessage = this.sanitizeAndlinkifyMessage(this.chatInput.value);
    if(theMessage == "") {
      $('chatInput').value= "";
      return;
    }
    formattedDates = this.getFormattedDate();

    if(this.mostRecentChatUser == this.getNickname() && this.mostRecentMessageType != 'historical') {
      assetHTML = CHAT_TEMPLATES['continued_message'].interpolate({
        fulltime: formattedDates['long'],
        message: theMessage
      });
    } else {
      assetHTML = CHAT_TEMPLATES['self_chat'].interpolate({
        fulltime: formattedDates['long'],
        shorttime: formattedDates['short'],
        nick: this.getNickname().escapeHTML(),
        message: theMessage
      });
    }

    tmp = new Element("div")
    tmp.innerHTML = assetHTML
    asset = tmp.down();

    this.incrementReceivedMessages();
    this.chatContainer().insert(asset);

    DropioStream.sendMessage(this.chatInput.value.stripScripts().escapeHTML());

    this.scrollToBottomAfterImageLoads(asset);
    this.scrollToBottomOfChat();

    $('chatInput').value= "";
    this.mostRecentChatUser = this.getNickname();
    this.mostRecentMessageType = 'new'
  },

  messageRejected: function(data) {
    if( !data ) return;
    this.addSystemMessage("Your last message could not be sent because guests are not currently allowed to speak in this chat room.")
  },

  assetConverted: function(data) {

  },

  commentAdded: function(data) {
    this.actionUpdate(data)
  },

  youJoinedChat: function(data) {
    document.fire("chat:loaded");
    if(data && data.role == "moderator") {
      document.fire("chat:promoted");
    }
    this.loggedInWithNick = true;

    this.updateNickname(this.getNickname().escapeHTML());
    this.setupNicknameEditor();

    this.enableChatInput();
  },

  checkInitialConnection: function() {
  },

  userJoined: function(data) {
    if(data && data.nickname != this.getNickname()) {
      if(this.otherUserCount == 0) {
        this.nickList().down('#emptyNotice').remove();
      }
      this.otherUserCount++;

      var escaped = data.nickname.escapeHTML();
      var targetLocation = this.findSortedLocation(escaped);
      this.insertNickAtlocation(escaped, targetLocation);

      Event.observe($(escaped.escapeHTML()), 'click', function(e) { this.appendName(escaped) }.bind(this));
    }

    if(data.role=="moderator") {
      this.showAdminStatus(data.nickname);
    }

    this.updateUserCountDisplay();
  },

  userRoleUpdated: function(data) {
    if(data.role=="moderator") {
      if(data.nickname == this.getNickname()) {
        document.fire("chat:promoted");
      }
      this.showAdminStatus(data.nickname);
    }
  },

  showAdminStatus: function(nick) {
    var nickDiv = null;
    if(this.getNickname() == nick) {
      nickDiv = $('yourNicknameContainer');
    } else if($(nick)) {
      nickDiv = $(nick);
    }

    nickDiv.title = "Admin";
    nickDiv.insert('<img title="Admin" class="adminIcon" src="' + getAssetHost() + '/images/adminIcon.png">');
  },

  appendName: function(name) {
    var identifier = "@" + name;
    if(this.chatInputHasDefaultMessage()) {
      this.chatInput.removeClassName('hint').value = identifier + ": ";
    } else {
      this.chatInput.removeClassName('hint').value = this.chatInput.value + identifier + " ";
    }

    this.chatInput.focus();
  },

  chatInputHasDefaultMessage: function() {
    if(this.chatInput.value == "Enter your message here (then press enter)") {
      return true;
    }

    return false;
  },

  userLeft: function(data) {
    if(!data)
      return;

    if( $(data.nickname ) ) {
      DropioStream.RemoteControl.userLeft(data);
      $(data.nickname).remove();
      this.otherUserCount--;
      this.updateUserCountDisplay();
      if(this.otherUserCount == 0) {
        this.nickList().insert('<div id="emptyNotice" class="nickName" style="text-align:center;">No other users</div>');
      }
    }
  },

  youLeft: function(data) {
    this.otherUserCount = 0;
    $$('.nickName').each(function(element){
      if(element.id != "yourNicknameContainer") {
        element.remove();
      }
    });
    this.nickList().insert('<div id="emptyNotice" class="nickName" style="text-align:center;">No other users</div>');
  },

  nickChanged: function(data) {
    var oldName = data.from.escapeHTML();
    var newName = data.to.escapeHTML();

    if(data.to.escapeHTML() == this.getNickname().escapeHTML()) {
      this.updateNickname(this.getNickname().escapeHTML());
      document.fire('chat:myNickChanged');
      return;
    }

    if( DropioStream.RemoteControl.isControlling ) {
      var targetNick = $$('#participantsList .' + data.from.stripScripts().gsub(" ","_")).first();
      if(targetNick) {
        targetNick.update(data.to.stripScripts());
        targetNick.removeClassName(data.from.stripScripts().gsub(" ","_")).addClassName(data.to.stripScripts().gsub(" ","_"));
      }
    }

    if($(oldName)) {
      Event.stopObserving($(oldName), 'click');
      $(oldName).remove();
    }

    var targetLocation = this.findSortedLocation(newName);
    this.insertNickAtlocation(data.to, targetLocation);

    Event.observe($(newName), 'click', function(e) { this.appendName(newName) }.bind(this));
    this.addSystemMessage(data.from + " is now known as " + data.to, {data: data});
  },

  insertNickAtlocation: function(nick, location) {
    if(location) {
      location.insert({'before': "<div id=\"" + nick.escapeHTML() + "\" class=\"nickName\">" + nick + "</div>"});
    } else {
      this.nickList().insert("<div id=\"" + nick.escapeHTML() + "\" class=\"nickName\">" + nick + "</div>");
    }
  },

  addSystemMessage: function(systemMessage, params) {

    if(!systemMessage)
      return;

    if(typeof params == 'undefined') params = {};

    var theMessage = systemMessage;

    if(!params.disableSanitization) {
      theMessage = this.sanitizeAndlinkifyMessage(systemMessage);
    }

    if(theMessage == "")
      return;

    if(this.detectHistoricalData(params.data)) {
      formattedDates = this.formatHistoricalDate(params.data.time);
      this.mostRecentMessageType = 'historical';
    } else {
      formattedDates = this.getFormattedDate();
      this.mostRecentMessageType = 'new';
    }

    if(this.mostRecentChatUser == "_SYSTEM") {
      assetHTML = CHAT_TEMPLATES['continued_system_message'].interpolate({
        fulltime: formattedDates['long'],
        message: theMessage
      });
    } else {
      assetHTML = CHAT_TEMPLATES['system_message'].interpolate({
        fulltime: formattedDates['long'],
        shorttime: formattedDates['short'],
        message: theMessage
      });
    }


    tmp = new Element("div")
    tmp.innerHTML = assetHTML
    asset = tmp.down();

    this.mostRecentChatUser = "_SYSTEM";
    this.incrementReceivedMessages();
    this.chatContainer().insert(asset);
    this.scrollToBottomOfChat();
  },

  actionUpdate: function(data) {
    if(!data)
      return;

    this.addSystemMessage(data.message, {data:data, displayMinimized:true});
  },

  dropOverfull: function(data) {
    if (IS_ADMIN)
      this.addSystemMessage("A user tried to enter the chat, but the drop has reached its realtime user limit.  <a href='"+UPGRADE_BUCKET_URL+"'>Upgrade your drop</a> to support unlimited users.", {data:data, displayMinimized:false});
  },

  disconnected: function() {
    this.disconnectedStatus = true;
    this.emptyUsersList();
    this.addSystemMessage("<span class=\"disconnected\">You have been disconnected</span>", {displayMinimized:true, disableSanitization:true});
    this.disableChatInput("disconnected...");
  },

  emptyUsersList: function() {
    this.otherUserCount = 0;
    $$('.nickName').each(function(e) {
      if(e.id != "yourNicknameContainer")
        e.remove();
    })
    this.nickList().insert('<div id="emptyNotice" class="nickName" style="text-align:center;">No other users</div>');
  },

  loggedin: function() {
    if(this.disconnectedStatus) {
      this.loggingInFromDisconnect = true;
    }
  },

  enableChatInput: function() {
    c = $("chatInput")
    if( c ) {
      if( CAN_CHAT ) {
        c.removeClassName("disabled");
        c.removeAttribute("disabled");
        c.disabled = false;
        c.value = "Enter your message here (then press enter)";
        c.defaultValueActsAsHint();
      } else {
        this.disableChatInput("Chat disabled for guests");
      }
    }
  },

  disableChatInput: function(msg) {
    c = $("chatInput")
    if( c ) {
      c.addClassName("disabled");
      c.setAttribute("disabled","disabled");
      c.disabled = true;
      c.value = msg;
    }
  },

  changeNickname: function(targetNickname) {
    if(!targetNickname)
      return;

    if(this.getNickname().strip() == targetNickname.strip())
      return;

    this.addSystemMessage("You are now known as " + targetNickname);
    DropioStream.changeNickname(targetNickname);
  },

  detectHistoricalData: function(data) {
    return data && data.time
  },

  buildSingleAssetViewUrl: function(name) {
    return '/' + BUCKET_URL + '/asset/' + name;
  },

  nickConflict: function(data) {
    document.fire('chat:nickConflict');
    if(!this.loggedInWithNick) {
      this.updateNickname(this.getNickname().escapeHTML());
      DropioStream.joinChat(this.getNickname());
    } else {
      this.updateNickname(this.getNickname().escapeHTML());
      this.addSystemMessage("the name '" + data.nickname + "' is taken, try another name", {data: data});
    }
  },

  getNickname: function() {
    return DropioStream.getNickname();
  },

  getRandomNick: function() {
    return DropioStream.getRandomNick();
  },

  sanitizeAndlinkifyMessage: function(message) {
    if(this.wildWestMode)
      return message;

    return ChatParser.linkify(ChatParser.sanitize(message));
  },

  incrementReceivedMessages: function() {
    this.recievedMessages++;
    if(this.recievedMessages > this.maxMessageLimit) {
      this.chatContainer().childElements().first().remove();
    }
  },

  formatHistoricalDate: function(date) {
    var year = date.substring(0, 4);
    var month = date.substring(4, 6);
    var day = date.substring(6, 8);
    var hour = date.substring(9, 11);
    var minute = date.substring(12, 14);

    var offset = (new Date()).getTimezoneOffset() / 60;
    hour = hour - offset;

    if(hour <= 0)
      hour = hour + 12;

    if (hour < 12) {
      amPm = "am";
    } else {
      amPm = "pm";
    }

    if (hour == 0) {
      hour = 12;
    } else if (hour > 12) {
      hour = hour - 12;
    }

    var longTime = month + "/" + day + "/" +
                   year + " " + hour + ":" +
                   minute + amPm;
    var shortTime = hour + ":" + minute;
    return {long: longTime, short: shortTime};
  },

  getFormattedDate: function() {
    var amPm = "";
    var theDate = new Date();
    var curHour = theDate.getHours();
    if (curHour < 12) {
      amPm = "am";
    } else {
      amPm = "pm";
    }

    if (curHour == 0) {
      curHour = 12;
    } else if (curHour > 12) {
      curHour = curHour - 12;
    }

    var curMin = theDate.getMinutes();
    curMin = curMin + "";
    if (curMin.length == 1) {
      curMin = "0" + curMin;
    }

    var tempMonth = theDate.getMonth();
    tempMonth++;

    var longTime = tempMonth + "/" + theDate.getDate() + "/" +
                   theDate.getFullYear() + " " + curHour + ":" +
                   curMin + amPm;
    var shortTime = curHour + ":" + curMin;
    return {long: longTime, short: shortTime};
  },

  updateNickname: function(name) {
    this.userNickDiv.update(name);
  },

  getDatatypeName: function(data) {
    var dataType = data.type;
    if(data.type == 'other') {
      dataType = "file";
    }

    return dataType;
  },

  findSortedLocation: function(escapedNick) {
    var users = $('nickList').childElements();

    users.splice(0, 2);
    var targetElement = users.detect(function(element) {return element.innerHTML.toLowerCase() > escapedNick.toLowerCase()});
    return targetElement;
  },

  scrollToBottomOfChat: function() {
    this.chatContainer().scrollTop = this.chatContainer().scrollHeight - this.chatContainer().offsetHeight;
  },

  scrollToBottomAfterImageLoads: function(asset) {
    if(asset.down('img')) {
      Event.observe(asset.down('img'), 'load', function() {
        this.scrollToBottomOfChat();
      }.bind(this));
    }
  },

  setupNicknameEditor: function() {
    new AjaxlessInPlaceEditor('yourNickname', '', {
      externalControl: 'editNick',
      highlightcolor: '#E9E9E9',
      highlightendcolor: '#E9E9E9',
      okControl: 'link',
      cancelControl: false,
      okText: 'save',
      savingText: 'saving...',
      maxLength: '13',
      callback: function(theForm, nickname) {
        theChatLayer.changeNickname(nickname);
        return false;
      }
    });

    if($('editNick')) $('editNick').show();
  }
});

ChatParser = {
  sanitize: function(message) {
    return message.stripScripts().sanitize({tags:['a','strong','em', 'b', 'i', 'img'],attributes:['href', 'src', 'target']});
  },

  linkify: function(message) {
    var emailRegEx = /[0-9a-z\._]+@[0-9a-z]+\..+?\b/i;
    var badUrlRegEx = /(src=.|href=.)(https?:\/\/|www.)([-\w\.]+)+(:\d+)?(\/([\w/_\.]*(\?\S+)?)?)?/
    var urlRegEx = /(https?:\/\/|www.)([\w\.\_\-\~\!\*\'\(\)\;\:\@\&\=\+\$\,\/\?\#\[\]]+)/;

    if(!message.match(badUrlRegEx) && message.match(urlRegEx)) {
      var urlMatch = message.match(urlRegEx)[0];
      if(urlMatch) {
        if(!urlMatch.startsWith('http')) {
          urlMatch = 'http://' + urlMatch;
        }
        message = message.gsub(urlRegEx, "<a href=\""+urlMatch+"\" target=\"_blank\">#{0}</a>");
      }
    }

    message = message.gsub(emailRegEx, "<a href=\"mailto:#{0}\">#{0}</a>");

    message = message.replace(/\*(\S*?)\*/g, "<b>$1</b>");

    return message;
  },

  isAboutMe: function(message, name) {
    if(message.match(name) != null) {
      return true;
    }

    return false;
  }
}


Sound = {
  tracks: {},
  _enabled: true,
  template:
    new Template('<embed style="height:0" id="sound_#{track}_#{id}" src="#{url}" loop="false" autostart="true" hidden="true"/>'),
  enable: function(){
    Sound._enabled = true;
  },
  disable: function(){
    Sound._enabled = false;
  },
  play: function(url){
    if(!Sound._enabled) return;
    var options = Object.extend({
      track: 'global', url: url, replace: false
    }, arguments[1] || {});

    if(options.replace && this.tracks[options.track]) {
      $R(0, this.tracks[options.track].id).each(function(id){
        var sound = $('sound_'+options.track+'_'+id);
        sound.Stop && sound.Stop();
        sound.remove();
      });
      this.tracks[options.track] = null;
    }

    if(!this.tracks[options.track])
      this.tracks[options.track] = { id: 0 };
    else
      this.tracks[options.track].id++;

    options.id = this.tracks[options.track].id;
    $$('body')[0].insert(
      Prototype.Browser.IE ? new Element('bgsound',{
        id: 'sound_'+options.track+'_'+options.id,
        src: options.url, loop: 1, autostart: true
      }) : Sound.template.evaluate(options));
  }
};

if(Prototype.Browser.Gecko && navigator.userAgent.indexOf("Win") > 0){
  if(navigator.plugins && $A(navigator.plugins).detect(function(p){ return p.name.indexOf('QuickTime') != -1 }))
    Sound.template = new Template('<object id="sound_#{track}_#{id}" width="0" height="0" type="audio/mpeg" data="#{url}"/>');
  else
    Sound.play = function(){};
}
ChatAlerts = (function() {
  var background = false;
  var muted = false;

  document.observe("dom:loaded", function() {
    Event.observe(document, "blur", function() {
      background = true;
    });

    Event.observe(document, "focus", function() {
      background = false;
    });
  });

  return {
    receivedMessage: function(data) {
      if (background && !muted)
        Sound.play('/sounds/droplet.wav');
    },

    mute: function(shouldMute) {
      muted = shouldMute;
    }
  };
})();
document.observe("dom:loaded", function() {
  MINIMIZE_PNG_URL = window.MINIMIZE_PNG_URL || "/images/minimizeChat.png";
  MAXIMIZE_PNG_URL = window.MAXIMIZE_PNG_URL || "/images/maximizeChat.png";

  if ($("collapseContainer")) {
    $("collapseContainer").stopObserving("click");
    $('collapseContainer').observe('click', function() {
      theChatLayer.toggleNickList();
    });
  };

  AutoRefreshCookie(DropWindowCookie, "chatLayerHeight", 600);
  AutoRefreshCookie(DropWindowCookie, "toldUserRoomFull", 600);
});

var ChatLayer = Class.create(Chat, {
  initialize: function($super) {
    $super();

    this.topBar = $$('#chatLayer #topBar').first();
    this.collapseContainer = $$('#chatLayer #collapseContainer').first();
    this.openCloseButton = $$('#chatLayer #openClose').first();
    this.minimizedMessage = $$('#chatLayer #minimizedDisplay #minimizedMessage').first();
    this.chatLabel = $$('#chatLayer #newLabel').first();

    this.lastKnownChatHeight = 230;
    this.chatHeightMinimum = 110;
    this.chatClosed = true;

    this.minimizedChatNotificationEnabled = true;

    this.openCloseButton.observe('click', function(event) {
      var holdLastChatSize = this.lastKnownChatHeight;
      this.refreshSize();
      this.lastKnownChatHeight = holdLastChatSize;
      this.toggleOpenClose();
      Event.stop(event);
    }.bind(this));

    Event.observe(this.topBar, 'click', function(event) {
      if(this.chatClosed) {
        this.toggleOpenClose();
      }
    }.bind(this));

    this.minimizedDisplay().observe('click', function(event) {
      if(this.chatClosed) {
        this.toggleOpenClose();
      }
    }.bind(this));

    if (EXPAND_CHAT_LAYER && this.chatClosed)
      this.toggleOpenClose();

    var previousHeight = parseInt(DropWindowCookie.get("chatLayerHeight"));
    if (this.chatClosed && previousHeight > 0) {
      this.lastKnownChatHeight = previousHeight;
      this.toggleOpenClose();
    } else if (!this.chatClosed && previousHeight == 0) {
      this.toggleOpenClose();
    }

    new Draggable($(this.topBar), {
      constraint: 'vertical',
      starteffect: this.dragTopBarEffect.bind(this),
      change: this.draggingTopBar.bind(this),
      ghosting: this.ghostWhenDraggingTopBar(),
      onStart: this.willDragTopBar.bind(this),
      onEnd: this.didDragTopBar.bind(this)
    });

    if (this.chatClosed)
      Effect.Appear('chatLayer', {duration: 0.5});
    else
      $('chatLayer').show();

    this.observeWindow();
  },

  ghostWhenDraggingTopBar: function() {
    return true;
  },

  dragTopBarEffect: function(element) {
    element.setOpacity(0.5);
  },
  willDragTopBar: function(draggable) {},
  draggingTopBar: function(draggable) {},
  didDragTopBar: function(draggable) {
    this.refreshSize();
    if(this.chatClosed) {
      this.toggleOpenClose();
    }
  },

  observeWindow: function() {
    Event.observe(window, 'resize', function() {
      this.refreshSize();
    }.bind(this));
  },

  minimizedDisplay: function() {
    return $$('#chatLayer #minimizedDisplay').first();
  },

  refreshSize: function(setTotalChatHeight) {
    if(this.chatClosed)
      return;

    var topBarHeight = 15;
    var messageInputHeight = 39;

    if(setTotalChatHeight) {
      heightOfTotalChat = setTotalChatHeight;
    } else {
      var viewableHeight = document.viewport.getHeight();
      if($$('#chatLayer #topBar').first().cumulativeOffset().top < 0) {
        var topBarOffsetTop = 0;
      } else {
        var topBarOffsetTop = $$('#chatLayer #topBar').first().cumulativeOffset().top;
      }

      heightOfTotalChat = viewableHeight - topBarOffsetTop;
    }

    if(heightOfTotalChat < this.chatHeightMinimum && !this.chatClosed) {
      heightOfTotalChat = this.chatHeightMinimum;
    }

    var heightOfVisibleChat = heightOfTotalChat - topBarHeight - messageInputHeight;
    var heightOfNickList = heightOfTotalChat - topBarHeight - 20;

    this.topBar.style.top = "0px"

    if(this.chatContainer().getStyle('height')){
      changeInChatSize = this.chatContainer().getStyle('height').gsub('px', '') - heightOfVisibleChat;
    } else {
      changeInChatSize = 0;
    }

    if(heightOfVisibleChat < 0) heightOfVisibleChat = 0;
    if(heightOfNickList < 0) heightOfNickList = 0;
    if(heightOfTotalChat < 0) heightOfTotalChat = 0;

    this.lastKnownChatHeight = heightOfTotalChat;
    $('chatLayer').setStyle({height: heightOfTotalChat + 'px'});
    this.chatContainer().setStyle({height: heightOfVisibleChat + 'px'});
    this.nickList().setStyle({height: heightOfNickList + 'px'});

    if(changeInChatSize > 0) {
      this.chatContainer().scrollTop = this.chatContainer().scrollTop + changeInChatSize;
    }

    DropWindowCookie.set("chatLayerHeight", this.lastKnownChatHeight);
  },

  toggleNickList: function() {
    if(this.visibleNickList) {
      this.nickList().addClassName('minimized');
      this.whosOnline().hide();
      this.whosOnlineDisplay().addClassName('minimized');
      this.visibleNickList = false;
    } else {
      this.nickList().removeClassName('minimized');
      this.whosOnline().style.display = "";
      this.whosOnlineDisplay().removeClassName('minimized');
      this.visibleNickList = true;
    }
  },

  addUserMessage: function($super, data) {
    if(!data)
      return;
    var user = data['nickname'];
    if(user == this.getNickname() && (data.time == null || data.time == ""))
    return;

    user = user.escapeHTML().substring(0, 14);
    $super(data);
    this.displayMinimizedNotice(ChatParser.sanitize(data['message']), data);
  },

  addSystemMessage: function($super, systemMessage, params) {
    $super(systemMessage, params);

    if(params && !params.disableSanitization) {
      theMessage = this.sanitizeAndlinkifyMessage(systemMessage);
    }
  },

  displayMinimizedNotice: function(message, data) {
    if(this.chatClosed && data && !this.detectHistoricalData(data) && this.minimizedChatNotificationEnabled) {

      highlightNotice = !this.minimizedDisplay().visible()

      if (this.minimizedNoticeEffect) {
        this.minimizedNoticeEffect.cancel();
      }

      this.minimizedDisplay().show();
      this.minimizedDisplay().morph("top:-51px",{duration:0.3});

      nickString = "<span class='minimizedNick'>" + data.nickname + ": </span>"
      this.minimizedMessage.innerHTML = nickString + message.stripTags().truncate(165);

      this.minimizedNoticeEffect = new Effect.Morph(this.minimizedDisplay(), {style:"top:0px",delay:4,duration:1});
    }
  },

  assetAdded: function(data) {
    if(!data)
      return;

    this.addSystemMessage("The " + this.getDatatypeName(data) + " '<span class='modalPopup' onclick='theChatLayer.modalWindowLoad(" + data.id + ")'>" + this.sanitizeAndlinkifyMessage(data.title) + "</span>' was added",
                          {disableSanitization:true, displayMinimized:true, data:data});
  },

  modalWindowLoad: function(id) {
    if( IS_ADMIN || !RC_ACTIVE || !RC_ALLOWED ) modalWindow.load(id);
  },

  roomFull: function() {
    $('chatLayer').hide();

    if (!DropWindowCookie.get("toldUserRoomFull")) {
      $('dropFull').appear({to: 0.75});
      DropWindowCookie.set("toldUserRoomFull", true);
    }
  },

  userCountDisplay: function() {
    return $$('#chatLayer #topBar #onlineCount').first();
  },

  whosOnlineDisplay: function() {
    return this.topBar.down('h3');
  },

  whosOnline: function() {
    return this.topBar.down('#whosOnline');
  },

  updateUserCountDisplay: function() {
    var targetUserCount = this.otherUserCount + 1;
    if(this.userCountDisplay()) {
      this.userCountDisplay().update(targetUserCount);
    } else {
      this.whosOnlineDisplay().update("Who's Online (<span id='onlineCount'>" + targetUserCount + "</span>)");
      this.whosOnlineDisplay().writeAttribute('id','onlineCountContainer');
    }

    new Effect.Morph(this.userCountDisplay(),
      {style: {color:'#FFF183'},
       duration: 0.4,
       queue: {position: 'end', limit: 2, scope: 'usercountscope'}});
    new Effect.Morph(this.userCountDisplay(), {
      style: {color:'#FFFFFF'},
      duration: 0.4,
      queue: {position: 'end', limit: 2, scope: 'usercountscope'}
    });
  },

  toggleOpenClose: function() {
    if(this.chatClosed) {
      $('chatLayer').removeClassName('closed');
      $('chatLayer').addClassName('open');
      this.openCloseButton.update('<span class="openCloseText">minimize</span> <img src="'+MINIMIZE_PNG_URL+'">');
      this.openCloseButton.addClassName("closed");
      this.chatClosed = false;
      this.refreshSize(this.lastKnownChatHeight);
      this.scrollToBottomOfChat();

      this.minimizedDisplay().hide();
      this.minimizedDisplay().setStyle('top','0px');

      DropWindowCookie.set("chatLayerHeight", this.lastKnownChatHeight);
    } else {
      $('chatLayer').style.height = "";
      $('chatLayer').addClassName('closed');
      $('chatLayer').removeClassName('open');
      this.openCloseButton.update('<span class="openCloseText">open chat</span> <img src="'+MAXIMIZE_PNG_URL+'">');
      this.openCloseButton.removeClassName("closed");
      this.chatClosed = true;

      this.minimizedDisplay().show();

      DropWindowCookie.set("chatLayerHeight", 0);
    }
  },

  disableMinimizedChatNotification: function() {
    this.minimizedDisplay().hide();
    this.minimizedDisplay().setStyle('top','0px');
    this.minimizedChatNotificationEnabled = false;
  },

  enableMinimizedChatNotification: function() {
    this.minimizedDisplay().show();
    this.minimizedChatNotificationEnabled = true;
  },

  showChatInfo: function() {
	modalWindow.showCustomModal('<div id="assetContent" class="customModal" style="overflow:hidden">' + $("chatInfoWrapper").innerHTML + '</div>');
  }

});



document.observe("dom:loaded", function() {
  DropioStream.RemoteControl.presenterControl = $('presenter');
  DropioStream.RemoteControl.participantControl = $('participantInfo');

  document.observe('click', function(e) {
    if(DropioStream.RemoteControl.pointerKeyDown) {
      DropioStream.RemoteControl.moveLaserPointer(e);
    }
  });

  document.observe('keydown', function(e) {DropioStream.RemoteControl.setPointerKey(e, true)})
          .observe('keyup', function(e) {DropioStream.RemoteControl.setPointerKey(e, false)});

  if($('startPresentationFromChat')) {
    $('startPresentationFromChat').observe('click', function(e) {
      e.stop();
    });
  }
});

document.observe("modal:opened", function(event) {
  if(event.memo.id) {
    var assetType = $('asset_' + event.memo.id).getAttribute('asset_type');
    if(DropioStream.RemoteControl.isControlling) {
      var showCommentsOption = event.memo.options.showComments ? true : false
      DropioStream.RemoteControl.openAssetInModal(event.memo.id, showCommentsOption);
      if(assetType == 'document' || assetType == 'movie') {
        $('toggleFullScreen').show();
      } else {
        $('toggleFullScreen').hide();
      }
    }
  }
});

document.observe("modal:closed", function(event) {
  if(DropioStream.RemoteControl.isControlling) {
    $('toggleFullScreen').hide();
    DropioStream.RemoteControl.closeModal();
  }
});

document.observe("chat:loaded", function() {
  if(RC_FORCE_PRESENTATION && !IS_ADMIN) {
    RC_ALLOWED = true;
    DropioStream.RemoteControl.joinPresentation();
  }

  if(RC_ACTIVE && RC_ALLOWED && !IS_ADMIN) {
    DropioStream.RemoteControl.joinPresentation();
  } else if(RC_ACTIVE && !RC_ALREADY_SAID_NO && !IS_ADMIN) {
    DropioStream.RemoteControl.promptToJoinExistingPresentation();
  }

  if(RC_ACTIVE && IS_ADMIN) {
    DropioStream.RemoteControl.isControlling = true;
    DropioStream.RemoteControl.showPresenterUI();
    DropioStream.RemoteControl.RCActive();
    $('participantInformation').hide();
  }
});

document.observe("chat:promoted", function() {
  if($('waitingToJoinPresentationButton')) {
    $('waitingToJoinPresentationButton').hide();
    if(!DropioStream.RemoteControl.isControlling) {
      if($('startPresentationButton'))
        $('startPresentationButton').show();
    }
  }

  if($('startPresentationFromChat')) {
    RC_ACTIVE ? $('startPresentationFromChat').update('stop presentation') :
                $('startPresentationFromChat').update('start presentation');
  }

})

Event.observe(window, 'resize', function() {
  if($('presenter'))
    DropioStream.RemoteControl.keepBadgeInBounds($('presenter'));

  if($('participantInformation'))
    DropioStream.RemoteControl.keepBadgeInBounds($('participantInformation'));
})

DropioStream.RemoteControl = {
  isControlling: false,
  tid: null, // timeout id
  presenterControl: null,
  pointerKeyDown: false,
  isFullscreen: false,
  documentLastPage: 1,


  startRC: function() {
    if( !DropioStream.RemoteControl.isControlling )  {
      DropioStream.RemoteControl.isControlling = true;
      DropioStream.sendEventData(DropioStream.REMOTE_CONTROL_STARTED,null);
      DropioStream.RemoteControl.RCActive();
      DropioStream.RemoteControl.showPresenterUI();
      $('participantInformation').show();
    }
    else {} // inform the user that he/she has already started an rc session

  },

  endRC: function() {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.RemoteControl.isControlling = false;
      DropioStream.sendEventData(DropioStream.REMOTE_CONTROL_ENDED,null);
      DropioStream.RemoteControl.RCInactive();

      if($('startPresentationButton'))
        $$('#startPresentationButton .btn').first().textContent = "Start Presentation"
      if($('stopPresentationButton'))
        $('stopPresentationButton').hide();
      if($('startPresentationButton'))
        $('startPresentationButton').show();
      $('startPresentationFromChat').update("start presentation");
      RC_STARTED_AT = "";
      DropioStream.RemoteControl.alreadySetPresentationStartedTime = false;
      document.body.removeClassName('presentation')
      $('presentorInfo').fade();
      this.presenterControl.fade();
    }
  },

  toggleRC: function() {
    DropioStream.RemoteControl.isControlling? this.endRC() : this.startRC();
  },

  joinRC: function() {
    DropioStream.RemoteControl.RCAllowed();
    DropioStream.sendEventData(DropioStream.USER_JOINED_REMOTE_CONTROL,{nickname:theChatLayer.getNickname()});
    theChatLayer.disableMinimizedChatNotification();
  },

  leaveRC: function() {
    if( RC_ACTIVE ) {
      if($('leavePresentation').style.display != "none") {
        $('leavePresentation').fade({from: 0.75, duration: 0.3});
      }

      DropioStream.RemoteControl.leavePresentationUI();
      DropioStream.RemoteControl.RCDisallowed();
      DropioStream.sendEventData(DropioStream.USER_LEFT_REMOTE_CONTROL,{nickname:theChatLayer.getNickname()});
      RC_STARTED_AT = "";
      DropioStream.RemoteControl.alreadySetPresentationStartedTime = false;
      theChatLayer.enableMinimizedChatNotification();
    }
  },

  openAssetInModal: function(id, showComments) {
    if( DropioStream.RemoteControl.isControlling ) {
      var assetType = $('asset_' + id).getAttribute('asset_type');
      if(assetType == 'document') {
        document.observe("scribd:loaded", function(event) {
          if($('minScribdControlBlocker') == null) {
            $('mediaPlayer').insert('<div id="minScribdControlBlocker" class="scribdControlBlocker admin"></div>');
          }
          document.stopObserving("scribd:loaded");
        });
      }

      DropioStream.sendEventData(DropioStream.ASSET_OPENED_IN_MODAL,{id:id, showComments:showComments});
    }
  },

  closeModal: function() {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.RemoteControl.documentLastPage = 1;
      DropioStream.sendEventData(DropioStream.MODAL_WINDOW_CLOSED,null);
    }

  },

  playMedia: function(id, position) {
    if( DropioStream.RemoteControl.isControlling )
      DropioStream.sendEventData(DropioStream.MEDIA_PLAYED,{id:id, position:position});
  },

  pauseMedia: function(id, position) {
    if( DropioStream.RemoteControl.isControlling )
      DropioStream.sendEventData(DropioStream.MEDIA_PAUSED,{id:id, position:position});
  },

  jumpMediaToPosition: function(id,position)  {
    if( DropioStream.RemoteControl.isControlling )
      DropioStream.sendEventData(DropioStream.MEDIA_JUMPED_TO_POSITION,{id:id,position:position});
  },

  showDocumentPage: function(id,page) {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.RemoteControl.documentLastPage = page;
      DropioStream.sendEventData(DropioStream.DOCUMENT_PAGE_SHOWN,{id:id,page:page});
    }

  },

  changeDocumentView: function() {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.sendEventData(DropioStream.DOCUMENT_PAGE_SHOWN,{view:scribd_doc.api.getViewMode(),zoom:scribd_doc.api.getZoom(), full_screen:scribd_doc.api.getFullscreen()});
    }
  },

  moveLaserPointer: function(clickEvent) {
    if( DropioStream.RemoteControl.isControlling ) {
      if($('modalWindow').style.display != "none") {
        var centerContainerOffsetLeft = $('modalWindow').cumulativeOffset().left;
        var centerContainerOffsetTop = clickEvent.pointerY() - $('modalWindow').cumulativeOffset().top;
      } else {
        var centerContainerOffsetLeft = $('containerViews').cumulativeOffset().left;
        var centerContainerOffsetTop =  clickEvent.pointerY();
      }

      var pointerOffsetLeft = document.viewport.getScrollOffsets()[0] + clickEvent.pointerX();
      var xLoc = pointerOffsetLeft - centerContainerOffsetLeft;
      DropioStream.sendEventData(DropioStream.LASER_POINTER_MOVED,{x:xLoc, y:centerContainerOffsetTop});
    }
  },

  hideLaserPointer: function() {
    if( DropioStream.RemoteControl.isControlling )
      DropioStream.sendEventData(DropioStream.LASER_POINTER_HIDDEN,null);
  },

  openLink: function(url) {
    if( DropioStream.RemoteControl.isControlling )
      DropioStream.sendEventData(DropioStream.LINK_OPENED,{url:url});
  },

  enterFullScreen: function(id) {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.sendEventData(DropioStream.FULL_SCREEN_ENTERED,{id:id});

      var assetType = $('asset_' + id).getAttribute('asset_type');
      switch(assetType) {
        case "document":
          $('minScribdControlBlocker').remove();
          document.body.insert('<div id="maxScribdControlBlocker" class="scribdControlBlocker admin"></div>');
          $('closeModal').hide();
          var docEmbed = $('mediaPlayer').childElements().first();
          docEmbed.style.position = "fixed";
          docEmbed.style.top = "0px";
          docEmbed.style.left = "0px";
          docEmbed.style.width = "100%";
          docEmbed.style.height = "100%";
          scribd_doc.addEventListener('iPaperReady', DropioStream.RemoteControl.setDocumentFullHeight);
          document.observe("scribd:loaded", function(event) {
            if(scribd_doc.api.setPage)
              scribd_doc.api.setPage(DropioStream.RemoteControl.documentLastPage);
            document.stopObserving("scribd:loaded");
          });
          break;
        case "movie":
          $('closeModal').hide();
          var videoEmbed = $('videoPlayer_' + id);
          videoEmbed.style.position = "fixed";
          videoEmbed.style.top = "0px";
          videoEmbed.style.left = "0px";
          videoEmbed.style.width = "100%";
          videoEmbed.style.height = "100%";
        break;
      }
    }

  },

  exitFullScreen: function(id) {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.sendEventData(DropioStream.FULL_SCREEN_EXITED,{id:id});

      var assetType = $('asset_' + id).getAttribute('asset_type');
      switch(assetType) {
        case "document":
          $('closeModal').show();
          $('maxScribdControlBlocker').remove();
          if(Prototype.Browser.IE) {
            loadScribdDoc(scribd_doc.api.getPage());
          } else {
            scribd_doc.removeEventListener('iPaperReady', DropioStream.RemoteControl.setDocumentFullHeight);
          }
          var docEmbed = $('mediaPlayer').childElements().first();
          docEmbed.style.position = "";
          docEmbed.style.top = "";
          docEmbed.style.left = "";
          docEmbed.style.height = "500px";
          docEmbed.style.width = "578px";
          document.observe("scribd:loaded", function(event) {
            if(scribd_doc.api.setPage)
              scribd_doc.api.setPage(DropioStream.RemoteControl.documentLastPage);
            document.stopObserving("scribd:loaded");
          });
          if($('minScribdControlBlocker') == null) {
            $('mediaPlayer').insert('<div id="minScribdControlBlocker" class="scribdControlBlocker admin"></div>');
          }
          break;
        case "movie":
          $('closeModal').show();
          var videoEmbed = $('videoPlayer_' + id);
          videoEmbed.style.position = "";
          videoEmbed.style.top = "";
          videoEmbed.style.left = "";
          videoEmbed.style.width = "578px";
          videoEmbed.style.height = "435px";
          loadMovie();
        break;
      }
    }

  },

  openChatLayer: function() {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.sendEventData(DropioStream.CHAT_LAYER_OPENED,null);
      if(theChatLayer.chatClosed) {
        theChatLayer.toggleOpenClose();
      }
    }

  },

  closeChatLayer: function() {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.sendEventData(DropioStream.CHAT_LAYER_CLOSED,null);

      if(!theChatLayer.chatClosed) {
        theChatLayer.toggleOpenClose();
      }
    }

  },

  changePlaylistRepeat: function(on) {
    if( DropioStream.RemoteControl.isControlling )
      DropioStream.sendEventData(DropioStream.PLAYLIST_REPEAT_CHANGED,{on:on});
  },

  changePlaylistShuffle: function(on) {
    if( DropioStream.RemoteControl.isControlling )
      DropioStream.sendEventData(DropioStream.PLAYLIST_SHUFFLE_CHANGED,{on:on});
  },

  changePlaylistSongState: function(playing,song,percentage) {
    if( DropioStream.RemoteControl.isControlling )
      DropioStream.sendEventData(DropioStream.PLAYLIST_SONG_STATE_CHANGED,{playing:playing,song:song,percentage:percentage});
  },

  downloadAsset: function(url) {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.RemoteControl.showPresenterInfoNotification('Participants are now being prompted to download the file you clicked.');
      DropioStream.sendEventData(DropioStream.ASSET_DOWNLOADED,{url:url});
    }

  },

  changeModalDrawer: function(drawerId) {
    if( DropioStream.RemoteControl.isControlling ) {
      DropioStream.sendEventData(DropioStream.MODAL_DRAWER_CHANGED,{drawerId: drawerId});
    }
  },


  rcStarted: function(data) {
    if( !RC_ALLOWED ) {
      $('participantNameInput').value = theChatLayer.getNickname();
      if($$('#joinPresentation .username').first()) {
        $$('#joinPresentation .username').first().update(data.nickname.stripScripts());
      } else {
        $$('#joinPresentation h1').first().update(data.nickname.stripScripts() + " would like to start a presentation");
      }

      $('joinPresentation').appear({to: 0.75});

      DropioStream.RemoteControl.setPresentationStartedTime();

      document.body.addClassName('presentation')
    }
  },

  rcEnded: function(data) {
    if(RC_ALLOWED && RC_ACTIVE) {
      $('leavePresentation').appear({to: 0.75});
      document.body.removeClassName('presentation');
    }
  },

  userJoined: function(data) {
    if( RC_ACTIVE ) {
      if(!DropioStream.RemoteControl.alreadyAParticipant(data.nickname.stripScripts())) {
        $('participantsList').insert("<li class=" + data.nickname.stripScripts().gsub(" ","_") + ">" + data.nickname.stripScripts() + "</li>");
        DropioStream.RemoteControl.updateGuestsNotInPresentationCount();
      }
    }
  },

  userLeft: function(data) {
    if( RC_ACTIVE ) {
      var user = $$('#participantsList .' + data.nickname.stripScripts().gsub(" ","_")).first();
      if(user) {
        user.remove();
      }

      DropioStream.RemoteControl.updateGuestsNotInPresentationCount();
    }
  },

  updateGuestsNotInPresentationCount: function() {
    $('otherParticipantCount').update(theChatLayer.otherUserCount - $('participantsList').childElements().length);
  },

  assetOpenedInModal: function(data) {
    if( RC_ALLOWED ) {
      if(data.showcomments == 'true') {
        modalWindow.load(data.id, {showComments: true});
      } else {
        modalWindow.load(data.id);
      }

      $('modalOverlay').hide();

      var assetType = $('asset_' + data.id).getAttribute('asset_type');
      document.observe("scribd:loaded", function(event) {
        if($('minScribdControlBlocker') == null) {
          $('mediaPlayer').insert('<div id="minScribdControlBlocker" class="scribdControlBlocker"></div>');
        }
        document.stopObserving("scribd:loaded");
      });
    }
  },

  modalClosed: function(data) {
    if( RC_ALLOWED ) {
      DropioStream.RemoteControl.documentLastPage = 1;
      modalWindow.hide();
    }
  },

  mediaPlayed: function(data) {
    if( RC_ALLOWED ) {

      var assetType = $('asset_' + data.id).getAttribute('asset_type');
      switch(assetType) {
        case "audio":
          FlashInterface.Audio.play(data.id, data.position);
          break;
        case "movie":
          FlashInterface.Video.play(data.id, data.position);
          break;
      }
    }
  },

  mediaPaused: function(data) {
    if( RC_ALLOWED ) {
      var assetType = $('asset_' + data.id).getAttribute('asset_type');
      switch(assetType) {
        case "audio":
          FlashInterface.Audio.pause(data.id, data.position);
          break;
        case "movie":
          FlashInterface.Video.pause(data.id, data.position);
          break;
      }
    }
  },

  mediaJumpedToPosition: function(data)  {
    if( RC_ALLOWED ) {
      var assetType = $('asset_' + data.id).getAttribute('asset_type');
      switch(assetType) {
        case "audio":
          FlashInterface.Audio.seek(data.id,data.position);
          break;
        case "movie":
          FlashInterface.Video.seek(data.id, data.position);
          break;
      }
    }
  },

  documentPageShown: function(data) {
    if( RC_ALLOWED ) {
      DropioStream.RemoteControl.documentLastPage = data.page
      if(DropioStream.RemoteControl.isAssetOpen(data.id)) {
        scribd_doc.api.setPage(data.page);
      } else {
        modalWindow.load(data.id);
        document.observe("scribd:loaded", function(event) {
          scribd_doc.api.setPage(data.page);
          $('modalWindow').stopObserving("scribd:loaded");
        });
      }
    }
  },

  documentViewChanged: function(data) {
    if( RC_ALLOWED ) {
    }
  },

  laserPointerMoved: function(data) {
    if( RC_ALLOWED ) {
      var containerViewOffsetTop = 0;
      if($('modalWindow').style.display != "none") {
        var containerViewOffsetLeft = $('modalWindow').cumulativeOffset().left;
        containerViewOffsetTop = $('modalWindow').cumulativeOffset().top;
      } else {
        var containerViewOffsetLeft = $('containerViews').cumulativeOffset().left;
      }

      var targetLeft = containerViewOffsetLeft + data.x - 15;
      var targetTop = (containerViewOffsetTop + data.y) - 16;
      $('pointer').style.left = targetLeft + "px";
      $('pointer').style.top = targetTop + "px";

      if (this.pointerShownEffect) {
        clearTimeout(this.pointerShownEffect);
      }

      $('pointer').show();

      if(targetTop > (document.viewport.getScrollOffsets()[1] + document.viewport.getHeight())) {
        Effect.ScrollTo($('pointer'), {duration: 0.5, offset: 200});
        this.pointerShownEffect = setTimeout(function(){$('pointer').hide()}, 2000);
      } else if(targetTop < document.viewport.getScrollOffsets()[1]) {
        Effect.ScrollTo($('pointer'), {duration: 0.5, offset: -200});
        this.pointerShownEffect = setTimeout(function(){$('pointer').hide()}, 2000);
      } else {
        this.pointerShownEffect = setTimeout(function(){$('pointer').hide()}, 1500);
      }

    }
  },

  laserPointerHidden: function(data) {
    if( RC_ALLOWED ) {
    }
  },

  linkOpened: function(data) {
    if( RC_ALLOWED ) {
    }
  },

  fullScreenEntered: function(data) {
    if( RC_ALLOWED ) {
      var assetType = $('asset_' + data.id).getAttribute('asset_type');
      switch(assetType) {
        case "document":
          $('minScribdControlBlocker').remove();
          document.body.insert('<div id="maxScribdControlBlocker" class="scribdControlBlocker"></div>');
          var docEmbed = $('mediaPlayer').childElements().first();
          docEmbed.style.position = "fixed";
          docEmbed.style.top = "0px";
          docEmbed.style.left = "0px";
          docEmbed.style.width = "100%";
          docEmbed.style.height = "100%";
          document.observe("scribd:loaded", function(event) {
            if(scribd_doc.api.setPage) {
              scribd_doc.api.setPage(DropioStream.RemoteControl.documentLastPage);
            }
            document.stopObserving("scribd:loaded");
          });

          scribd_doc.addEventListener('iPaperReady', DropioStream.RemoteControl.setDocumentFullHeight);
          break;
        case "movie":
          var videoEmbed = $('videoPlayer_' + data.id);
          videoEmbed.style.position = "fixed";
          videoEmbed.style.top = "0px";
          videoEmbed.style.left = "0px";
          videoEmbed.style.width = "100%";
          videoEmbed.style.height = "100%";
        break;
      }
    }
  },

  fullScreenExited: function(data) {
    if( RC_ALLOWED ) {
      var assetType = $('asset_' + data.id).getAttribute('asset_type');
      switch(assetType) {
        case "document":
          if(Prototype.Browser.IE) {
            loadScribdDoc(scribd_doc.api.getPage());
          } else {
            scribd_doc.removeEventListener('iPaperReady', DropioStream.RemoteControl.setDocumentFullHeight);
          }

          var docEmbed = $('mediaPlayer').childElements().first();
          docEmbed.style.position = "";
          docEmbed.style.top = "";
          docEmbed.style.left = "";
          docEmbed.style.height = "500px";
          docEmbed.style.width = "578px";
          document.observe("scribd:loaded", function(event) {
            if(scribd_doc.api.setPage)
              scribd_doc.api.setPage(DropioStream.RemoteControl.documentLastPage);
            document.stopObserving("scribd:loaded");
          });
          $('maxScribdControlBlocker').remove();
          if($('minScribdControlBlocker') == null) {
            $('mediaPlayer').insert('<div id="minScribdControlBlocker" class="scribdControlBlocker"></div>');
          }
          break;
        case "movie":
          var videoEmbed = $('videoPlayer_' + data.id);
          videoEmbed.style.position = "";
          videoEmbed.style.top = "";
          videoEmbed.style.left = "";
          videoEmbed.style.width = "578px";
          videoEmbed.style.height = "435px";
          window.loadMovie();
          break;
      }
    }
  },

  chatLayerOpened: function(data) {
    if( RC_ALLOWED ) {
      if(theChatLayer.chatClosed) {
        theChatLayer.toggleOpenClose();
      }
    }
  },

  chatLayerClosed: function(data) {
    if( RC_ALLOWED ) {
      if(!theChatLayer.chatClosed) {
        theChatLayer.toggleOpenClose();
      }
    }
  },

  playlistRepeatChanged: function(data) {
    if( RC_ALLOWED ) {
    }
  },

  playlistShuffleChanged: function(data) {
    if( RC_ALLOWED ) {
    }
  },

  playlistSongStateChanged: function(data) {
    if( RC_ALLOWED ) {
    }
  },

  assetDownloaded: function(data) {
    if( RC_ALLOWED ) {
      $('downloadContainer').src = data.url;
    }
  },

  modalDrawerChanged: function(data) {
    if( RC_ALLOWED ) {
      if($('assetDrawer')) {
        showDrawer(data.drawerid);
      }
    }
  },


  joinPresentation: function() {
    DropioStream.RemoteControl.joinRC();
    document.body.addClassName('presentation')
    $('participantScreen').show().observe('click', function(event){
      if($('clickNotification').style.display == "none") {
        $('clickNotification').appear();
      } else {
        $('clickNotification').shake({duration: 0.3, distance: 5})
        this.clickNotificationEffect.cancel();
      }

      this.clickNotificationEffect = new Effect.Fade('clickNotification', {delay:4,duration:1});
    });

    new Draggable(this.participantControl.appear({to: 0.9}), {
      starteffect: false,
      endeffect: false,
      zindex: 99998,
      onStart: function() {
        $('participantInfo').removeClassName('nearBottom');
      },
      change: function(theDraggable){
        DropioStream.RemoteControl.keepBadgeInBounds(theDraggable.element);
      }
    });

    DropioStream.RemoteControl.setPresentationStartedTime();
    DropioStream.RemoteControl.updateTimer(function() { return RC_ALLOWED }, $$('#participantInfo .timer').first());

    $('joinPresentation').fade({from: 0.75, duration: 0.3});
  },

  toggleFullscreen: function() {
    var id = modalWindow.currentPlayingIndex;
    if(DropioStream.RemoteControl.isFullscreen) {
      $$('#toggleFullScreen .btn').first().innerHTML = "View Fullscreen";
      DropioStream.RemoteControl.isFullscreen = false;
      DropioStream.RemoteControl.exitFullScreen(id);
    } else{
      $$('#toggleFullScreen .btn').first().innerHTML = "Exit Fullscreen";
      DropioStream.RemoteControl.isFullscreen = true;
      DropioStream.RemoteControl.enterFullScreen(id);
    }
  },

  setDocumentFullHeight: function(e) {
    var docEmbed = $('mediaPlayer').childElements().first();
    docEmbed.style.height = "100%";
  },

  changeNickAndJoinPresentation: function(nick) {
    $('joinPresentationSpinner').show();
    $('presentationNickConflict').hide();
    if(nick != theChatLayer.getNickname() && nick != "") {
      theChatLayer.changeNickname(nick);
      document.observe('chat:myNickChanged', function() {
        $('joinPresentationSpinner').hide();
        DropioStream.RemoteControl.joinPresentation();
        Event.stopObserving(document, 'chat:myNickChanged');
      });
      document.observe('chat:nickConflict', function() {
        $('joinPresentationSpinner').hide();
        $('presentationNickConflict').show();
        Event.stopObserving(document, 'chat:myNickChanged');
      });
    } else {
      $('joinPresentationSpinner').hide();
      DropioStream.RemoteControl.joinPresentation();
    }
  },

  promptToJoinExistingPresentation: function() {
    $$('.notificationContainer h1').first().update("A presentation is currently taking place");
    $('joinPresentation').appear({to: 0.75});
  },

  ignorePresentation: function() {
    $('joinPresentation').fade({from: 0.75, duration: 0.5});
    DropioStream.RemoteControl.RCDisallowed();
  },

  showPresenterUI: function() {
    $('participantsList').update("");
    if($('stopPresentationButton'))
      $('stopPresentationButton').show();
    if($('startPresentationButton'))
      $('startPresentationButton').hide();
    if($('waitingToJoinPresentationButton'))
      $('waitingToJoinPresentationButton').hide();
    $('startPresentationFromChat').update("stop presentation");
    $('otherParticipantCount').update(theChatLayer.otherUserCount);

    DropioStream.RemoteControl.setPresentationStartedTime();
    DropioStream.RemoteControl.updateTimer(function() { return DropioStream.RemoteControl.isControlling}, $$('#presenter .timer').first());

    $('presentorInfo').appear({duration: 0.7})

    new Draggable(this.presenterControl.appear({duration: 0.7}), {
      starteffect: false,
      endeffect: false,
      zindex: 99998,
      onStart: function() {
        $('presenter').removeClassName('nearBottom');
      },
      change: function(theDraggable){
        DropioStream.RemoteControl.keepBadgeInBounds(theDraggable.element);
      }
    });
  },

  currentlyInPresentation: function() {
    if(!DropioStream.RemoteControl.isControlling && RC_ACTIVE && RC_ALLOWED)
      return true;

    return false;
  },

  keepBadgeInBounds: function(theDraggable) {
    var verticalPosition = parseInt(theDraggable.style.top.gsub("px", ""));
    if(verticalPosition < 0) {
      theDraggable.style.top = "0px";
    } else if(verticalPosition > document.viewport.getHeight() - theDraggable.clientHeight) {
      theDraggable.style.top = (document.viewport.getHeight() - theDraggable.clientHeight) + "px";
    }

    var horizontalPosition = parseInt(theDraggable.style.left.gsub("px", ""));
    if(horizontalPosition < 0) {
      theDraggable.style.left = "0px";
    } else if(horizontalPosition > document.viewport.getWidth() - theDraggable.clientWidth) {
      theDraggable.style.left = document.viewport.getWidth() - theDraggable.clientWidth + "px";
    }

  },

  showPresenterInfoNotification: function(info) {
    var infoElement = $$('#presenter #infoNotification').first();
    infoElement.update(info);
    infoElement.show();
    infoElement.fade({delay: 4});
  },

  leavePresentationUI: function() {
    $('participantInfo').fade();
    $('participantScreen').fade();
    modalWindow.hide();
  },

  setPointerKey: function(e, value) {
    if(DropioStream.RemoteControl.isControlling && e.keyCode == 18) {
      this.pointerKeyDown = value;

      if(this.pointerKeyDown) {
        if(!$('pointerClickLayer')) {
          var pointerClickLayer = new Element('div');
          pointerClickLayer.id = "pointerClickLayer";
          document.body.insert(pointerClickLayer);
        }
      } else {
        if($('pointerClickLayer')) {
          $('pointerClickLayer').remove();
        }
      }
    }
  },

  isAssetOpen: function(assetId) {
    if($('modalWindow').style.display != "" || modalWindow.currentPlayingIndex != assetId) {
      return false
    }
    return true;
  },

  alreadyAParticipant: function(name) {
    return $('participantsList').childElements().find(function(e) {return e.innerHTML == name}) != null
  },

  setPresentationStartedTime: function() {
    if(!DropioStream.RemoteControl.alreadySetPresentationStartedTime) {
      DropioStream.RemoteControl.alreadySetPresentationStartedTime = true;
      if(RC_STARTED_AT) {
        RC_STARTED_AT = new Date(RC_STARTED_AT);
      } else {
        RC_STARTED_AT = new Date();
      }
    }
  },

  updateTimer: function(keepTimingCondition, theElement) {
    if(keepTimingCondition()) {
      var msDiff = (Date.parse(new Date()) - Date.parse(RC_STARTED_AT));
      var secDiff = Math.floor((msDiff / 1000) % 60);
      var minDiff = Math.floor(((msDiff / 1000) / 60) % 60);
      var hourDiff = Math.floor((msDiff / 1000) / 3600);

      var updateString = "";
      if(hourDiff > 0) {
        updateString += DropioStream.RemoteControl.padDigits(hourDiff) + ":"
      }
      updateString += DropioStream.RemoteControl.padDigits(minDiff) + ":" +
                      DropioStream.RemoteControl.padDigits(secDiff);

      theElement.update(updateString);
      setTimeout(function() {DropioStream.RemoteControl.updateTimer(keepTimingCondition, theElement)}, 1000);
    }
  },

  padDigits: function(num) {
    num = num.toString();
    var padded = '';
    if (num.length < 2) {
      padded += "0";
    }
    return padded + num.toString();
  },


  registerController: function(data) {
    if( data.role == "moderator" )
      DropioStream.registerAuthorized(data.nickname);
  },

  changeController: function(data) {
    if(DropioStream.isAuthorized(DropioStream.fullJID(data.from))) {
      DropioStream.removeAuthorized(data.from);
      DropioStream.registerAuthorized(data.to);
    }
  },

  removeController: function(data) {
    DropioStream.removeAuthorized(data.nickname);
  },

  RCActive: function() {
    new Ajax.Request(RC_ACTIVE_URL,{method:"post",parameters:"drop="+BUCKET_URL});
    if( DropioStream.RemoteControl.tid ) clearTimeout(DropioStream.RemoteControl.tid);
    DropioStream.RemoteControl.tid = setTimeout("DropioStream.RemoteControl.RCActive()",15*60*1000); // every 15 mins
    RC_ACTIVE = true;
  },

  RCInactive: function() {
    new Ajax.Request(RC_INACTIVE_URL,{method:"post",parameters:"drop="+BUCKET_URL});
    if( DropioStream.RemoteControl.tid ) clearTimeout(DropioStream.RemoteControl.tid);
    RC_ACTIVE = false
    RC_ALLOWED = false
  },

  RCAllowed: function() {
    new Ajax.Request(RC_ALLOWED_URL,{method:"post",parameters:"drop="+BUCKET_URL});
    RC_ALLOWED = true
    RC_ACTIVE = true
  },

  RCDisallowed: function() {
    new Ajax.Request(RC_DISALLOWED_URL,{method:"post",parameters:"drop="+BUCKET_URL});
    RC_ALLOWED = false
  }
};
Cookie = {
  set: function(name, value, days, path) {
    var expires;

    if (days) {
      var date = new Date();
      date.setTime(date.getTime()+(days*24*60*60*1000));
      expires = "; expires="+date.toGMTString();
    }
    else {
      expires = "";
    }

    path = path || "/";

    document.cookie = name+"="+value+expires+"; path="+path;
  },

  get: 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;
  },

  clear: function(name) {
    this.set(name,"",-1);
  }
};

DropCookie = (function() {
  function dropName() {
    return window.location.pathname.split('/')[1];
  }

  return {
    scopedName: function(name) {
      return "drop:"+dropName()+":"+name;
    },

    get: function(name) {
      return Cookie.get(DropCookie.scopedName(name));
    },

    set: function(name, value, days) {
      Cookie.set(DropCookie.scopedName(name), value, days);
    },

    clear: function(name) {
      Cookie.clear(DropCookie.scopedName(name));
    }
  };
})();


WindowCookie = (function() {
  function windowName() {
    function randomString(string_length) {
      var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
      var randomstring = '';
      for (var i=0; i<string_length; i++) {
        var rnum = Math.floor(Math.random() * chars.length);
        randomstring += chars.substring(rnum,rnum+1);
      }
      return randomstring;
    }

    if (window.name == "") window.name = randomString(8);
    return window.name;
  }

  return {
    scopedName: function(name) {
      return "window:"+windowName()+":"+name;
    },

    get: function(name) {
      return Cookie.get(WindowCookie.scopedName(name));
    },

    set: function(name, value, days) {
      Cookie.set(WindowCookie.scopedName(name), value, days);
    },

    clear: function(name) {
      Cookie.clear(WindowCookie.scopedName(name));
    }
  };
})();

DropWindowCookie = {
  get: function(name) {
    return DropCookie.get(WindowCookie.scopedName(name));
  },

  set: function(name, value, days) {
    DropCookie.set(WindowCookie.scopedName(name), value, days);
  },

  clear: function(name) {
    DropCookie.clear(WindowCookie.scopedName(name));
  }
}

AutoRefreshCookie = function(type, name, seconds, refreshSeconds) {
  new PeriodicalExecuter(function() {
    var currentValue = type.get(name);
    if (currentValue)
      type.set(name, currentValue, seconds/24/60/60);
  }, (refreshSeconds || 1));
}
var Modal = Class.create({
  initialize: function() {
    this.currentIndex = 0;
		this.playlist = [];
		this.currentPlayingIndex = 0;
  },

  createModalWindow: function() {
    if($("modal_window") == null) {
      var modalOverlay = new Element('div');
      modalOverlay.id = "modalOverlay";
      modalOverlay.style.display = "none";
      modalOverlay.setOpacity(0.7);

			var modalSpinner = new Element("img");
			modalSpinner.id = "modalSpinner";
			modalSpinner.src = "/images/modalSpinner.gif";
			modalSpinner.style.display = "none";

    	var modalDiv = new Element('div');
      modalDiv.id = "modalWindow";
      modalDiv.style.display = "none";

      Element.insert(document.body, modalDiv, {postition: 'bottom'});
      Element.insert(document.body, modalOverlay, {postition: 'bottom'});
			Element.insert(modalOverlay, modalSpinner, {postition: 'bottom'});
    }
    this.modalWindow = modalDiv;
    this.modalOverlay = modalOverlay;
		this.modalSpinner = modalSpinner;
  },

  load: function(asset_id, options) {
    if (typeof options == 'undefined') options = {};

    Event.observe($('modalOverlay'), 'click', function() {
      modalWindow.hide();
    });

    this.currentPlayingIndex = asset_id;
    this.currentIndex = this.findAssedIndexInPlaylist(asset_id);

    document.fire("modal:opened", {id: this.playlist[this.currentIndex], options: options});
    this.modalOverlay.show();
		this.modalSpinner.show();
    this.loadingTimeout = setTimeout(function() {
    }, 500);

    this.cancelView = false;
    new Ajax.Request('/' + this.bucketURL + '/asset/' + asset_id + '/player',{
			  evalJS: false,
				onComplete: function(request){
						modalWindow.modalSpinner.hide();
		        modalWindow.show(request.responseText);
		        if(options.showComments){
		          showDrawer('comments');
		        }
		    }
		});
  },

  loadScribdDoc: function(asset_id) {
    Event.observe($('modalOverlay'), 'click', function() {
      modalWindow.hide();
    });

    this.currentIndex = this.findAssedIndexInPlaylist(asset_id);
    this.modalOverlay.show();
    this.loadingTimeout = setTimeout(function() {
    }, 500);
    this.cancelView = false;

    loadJS("http://www.scribd.com/javascripts/view.js", function () {
      new Ajax.Request('/' + BUCKET_URL + '/asset/' + asset_id + '/player',{
  			  evalJS: false,
  				onComplete: function(request){
  		        modalWindow.show(request.responseText);
  		    }
  		});
  	});
  },

  findAssedIndexInPlaylist: function(asset_id) {
     for(i = 0; i < this.playlist.length; i = i +1) {
       if(this.playlist[i] == asset_id) {
         return i
       }
     }

     return -1;
   },

  addAssetToPlaylist: function(asset_id) {
    if( this.findAssedIndexInPlaylist(asset_id) == -1 )
      this.playlist.push(asset_id)
  },

  show: function(content) {
    if(this.cancelView == null || !this.cancelView) {
      clearTimeout(this.loadingTimeout);
      $('modalWindow').update(content);

      if(!DropioStream.RemoteControl.currentlyInPresentation()) {
        if( $('nextAsset') && $('previousAsset') ) {
          Event.observe($('nextAsset'), 'click', function() {
            this.next();
          }.bind(this));

          Event.observe($('previousAsset'), 'click', function() {
            this.previous();
          }.bind(this));
        }

        if($('currentAsset') && $('currentAsset').nodeName == "IMG") {
          Event.observe($('currentAsset'), 'click', function() {
            this.next();
          }.bind(this));
        }
     }

      this.modalWindow.style.top = (document.viewport.getScrollOffsets().top + 48) + "px"
      this.modalWindow.show();
	  this.createCloseButton();
    } else {
      this.cancelView = false;
    }
  },

  showCustomModal: function(content) {
    Event.observe($('modalOverlay'), 'click', function() {
      modalWindow.hide();
    });

    this.modalOverlay.show();
    $('modalWindow').update(content);

    var viewHeight = document.viewport.getHeight() / 2;
    this.modalWindow.style.top = (document.viewport.getScrollOffsets().top + 48) + "px"
    this.modalWindow.show();
	this.createCloseButton();
  },

  loadCustomModal: function(url,params) {
    if( !params ) params = {};
    this.modalOverlay.show();
		this.modalSpinner.show();
    new Ajax.Request(url, {
      method: "get",
      parameters: params,
      onSuccess: function(transport) {
    		modalWindow.modalSpinner.hide();
        var html = transport.responseText;
        modalWindow.showCustomModal('<div id="assetContent" class="customModal" style="overflow:hidden">' + html + '</div>');
      }
    });
  },

  hide: function() {
    document.fire("modal:closed");
    this.cancelView = true;
    this.modalWindow.update("");
    this.modalWindow.hide();
    Event.stopObserving('modalOverlay', 'click');
    clearTimeout(this.loadingTimeout);
    $('modalOverlay').hide();
  },

  next: function() {
		this.setCurrentIndex(this.currentIndex + 1);
		if(this.currentIndex >= this.playlist.length)
		{
			this.setCurrentIndex(0);
		}

		DropioStream.RemoteControl.openAssetInModal(this.playlist[this.currentIndex]);
    new Ajax.Request('/' + BUCKET_URL + '/asset/' + this.playlist[this.currentIndex]  + '/player',{
			  evalJS: false,
				onComplete: function(request){
		        modalWindow.show(request.responseText);
		    }
		});
	},

	previous: function() {
		this.setCurrentIndex(this.currentIndex - 1);
		if(this.currentIndex < 0)
		{
			this.setCurrentIndex(this.playlist.size() - 1);
		}

		DropioStream.RemoteControl.openAssetInModal(this.playlist[this.currentIndex]);
		new Ajax.Request('/' + BUCKET_URL + '/asset/'+ this.playlist[this.currentIndex] +'/player',{
			  evalJS: false,
				onComplete: function(request){
		        modalWindow.show(request.responseText);
		    }
		});
	},

	setBucketURL: function(bucketURL) {
		this.bucketURL = bucketURL;
	},

	setPlaylist: function(playlist) {
		this.playlist = playlist;
	},

	setCurrentIndex: function(index) {
		this.currentIndex = index;
	},

	createCloseButton: function() {
		var closeModal = new Element('img');
		closeModal.id = "closeModal";
		closeModal.src = "/images/closeModalWindow.png";
		Element.insert(this.modalWindow, closeModal, {postition: 'top'});
		Event.observe($('closeModal'), "click", function(){
		  modalWindow.hide();
		});
	},

	refresh: function() {
	  var curAssetID = curModalAssetId();
	  modalWindow.hide();
	  modalWindow.load(curAssetID);
	},

	refreshPlaylist: function() {
		var tempAssetsArray = [];
	    $$(".asset").each(function(asset){
		   tempAssetsArray.push(asset.getAttribute("asset_id"));
		});
	    modalWindow.setPlaylist(tempAssetsArray);
	},

	isDrawerFormActive: function() {
	  activeFound = false;
    $$('#assetDrawer textarea, #assetDrawer input').each(function(input){
      if (input.readAttribute('active') == 'true')
        return activeFound = true;
    });
    return activeFound
  },

  refreshDrawerFormObservers: function() {
    $$('#assetDrawer textarea, #assetDrawer input').each(function(input){
      input.observe('focus',function(){
        this.writeAttribute("active","true");
      });
      input.observe('blur',function(){
        this.writeAttribute("active",false);
      });
    });
  }
});

var modalWindow = new Modal();

var MAX_MODAL_COMMENTS = 5;

function showDrawer(drawerId) {
  DropioStream.RemoteControl.changeModalDrawer(drawerId);
  if(isDrawerOpen()) {
    if(currentModalTab != drawerId) {
      Effect.SlideUp('assetDrawer',
       {
         duration: 0.3,
         afterFinish: function() {
           $(currentModalTab).hide();
           $(drawerId).show();
           Effect.SlideDown('assetDrawer', {duration: 0.3});
           currentModalTab = drawerId;
         }
       });
    } else {
      Effect.SlideUp('assetDrawer',
        {
          duration: 0.3,
          afterFinish: function() {
            $(drawerId).hide();
          }
        });
    }
  } else {
    currentModalTab = drawerId;
    $(drawerId).show();
    Effect.SlideDown('assetDrawer', {duration: 0.3});
  }
}

function isAssetOpenInModal(assetId) {
  return curModalAssetId() == assetId;
}

function modalAssetContainer() {
  tmp = $$("#modalWindow #assetContent .assetContainer")
  if( tmp.length == 0 ) return null;
  else return tmp.first();
}

function curModalAssetId() {
  cont = modalAssetContainer();
  if( cont == null ) return null;
  else return parseInt(cont.readAttribute("asset_id"));
}

function updateModalAsset() {
  n = $("modalAssetUpdatedNotice")
  n.innerHTML = "This asset was updated! Click <a href='#' onclick='modalWindow.refresh()'>here</a> to refresh this window."
  new Effect.Appear(n,{duration:1,from:0,to:1});
}

function removeModalAsset() {
  n = $("modalAssetDeletedNotice")
  n.innerHTML = "This asset was deleted! Click <a href='#' onclick='modalWindow.hide()'>here</a> to close this window."
  if( $("modalAssetUpdatedNotice").style.display == "" ) $("modalAssetUpdatedNotice").hide();
  if( $$("#modalWindow .downloadButton")[0] ) $$("#modalWindow .downloadButton")[0].hide();
  if( $$("#modalWindow .openMenu")[0] ) $$("#modalWindow .openMenu")[0].hide();
  new Effect.Appear(n,{duration:1,from:0,to:1});
}

function modalCommentExists(commentId) {
  return $("modal_comment_" + commentId) != null
}

function showModalComments(assetId) {
  var commentsContainer = $$('#assetDrawer #commentsContainer').first();
  commentsContainer.innerHTML = "<img src='/images/spinner.gif' />";

  new Ajax.Request("/" + BUCKET_URL + "/assets/" + assetId + "/comments.json", {
    method: 'get',
    contentType: 'application/json',
    parameters: {limit: MAX_MODAL_COMMENTS},
    onComplete: function(transport) {
      var response = transport.responseJSON;

      commentsContainer.innerHTML = "";
      commentsList = new Element('ul');
      commentsContainer.insert(commentsList);
      if(response['count'] == 0) {
				theContent = CAN_COMMENT ? "No comments - speak up!" : "No comments"
        addModalComment({contents: theContent, id:0},false,true)
      }
      else {
        response['comments'].reverse().each(function(comment) {
          addModalComment(comment,false,true);
        });
      }
      updateModalCommentsCount(response['count'])
    }
  });
}

function postModalComment(assetId) {
  tempValue = $$('#assetDrawer #comments #commentBox').first().value;
  $$('#assetDrawer #comments #commentBox').first().value = "";
  new Ajax.Request("/" + BUCKET_URL + "/assets/" + assetId + "/post_comment", {
    postBody: '{"comment": {"content": ' + tempValue.toJSON() + '}, "format": "json", "_method": "put"}',
    method: 'put',
    contentType: 'application/json',
    onComplete: function(transport) {
      var response = transport.responseJSON;
      if(response['status'] == 'success') {
        comment_hash = response['comment'];
        comment_hash.comment_count = response['count'];
        addModalComment(comment_hash,true,true);
        addStandardComment(assetId,comment_hash,true);
      }
    }
  });
}

function addModalComment(comment,updateCount,deleteTop) {
  if( modalCommentExists(comment['id']) ) return;

  if( modalCommentExists(0) )
    removeTopModalComment();

  var commentsContainer = $$('#assetDrawer #commentsContainer').first();
  if( commentsContainer == null ) return;
  commentsList = commentsContainer.down();
  if( commentsList == null ) return;

  var html = '<li style="display:none" id="modal_comment_' + comment['id'] + '">' + comment["contents"];
  if( comment['updated_at'] != null )
    html += " - <span class='date'>" + commentDate(comment['updated_at']) + '</span>';

  if( CAN_DELETE && comment['id'] != 0 )
      html += "<div class='inlineModalCommentDelete'><img src='/images/cross.png' onclick='deleteComment("+curModalAssetId()+",\""+getAssetData(curModalAssetId()).name+"\"," + comment['id'] + ")'/></div>";


  html += '</li>'

  commentsList.insert({bottom:html});

  var comments = $$('#assetDrawer #commentsContainer ul li')

  if( (deleteTop && comments.length == MAX_MODAL_COMMENTS+1) )
    removeTopModalComment();

  Effect.Appear(comments.last(),{duration:1,from:0,to:1});

  if( updateCount )
    updateModalCommentsCount(currentModalCommentCount() + 1);
}

function removeModalComment(commentId,updateCount,checkForMore) {
  if( !modalCommentExists(commentId) ) return;
  cM = $("modal_comment_" + commentId)
  if( updateCount ) updateModalCommentsCount(currentModalCommentCount() - 1);
  cM.setAttribute("id","removing_comment_"+commentId);
  new Effect.Fade(cM,{duration:1,from:1,to:0,afterFinish:function(){
    cM.remove();
    if( checkForMore ) {
      if( numModalComments() == 0 )
        showModalComments(curModalAssetId());
    }
  }});
}

function numModalComments() {
  var comments = $$('#assetDrawer #commentsContainer ul li')
  return comments.length
}


function updateModalComment(comment) {
  removeModalComment(comment.id,false,false);
  addModalComment(comment,false,false)
}

function removeTopModalComment() {
  var comments = $$('#assetDrawer #commentsContainer ul li')
  if( comments.length == 0 ) return;
  c = comments.first()
  c.remove();

}

function currentModalCommentCount() {
  if( $("comments_count") == null ) return 0;
  curCount = $("comments_count").innerHTML
  if(curCount == "")
    curCount = 0
  else
    curCount = parseInt(curCount);
  return curCount;
}

function updateModalCommentsCount(count) {
  var html = "Comment";
  if( count > 0 )
    html += " (<span id=\"comments_count\">" +count + "</span>)"
  $("assetContent").setAttribute("comment_count",count);
}

function isDrawerOpen() {
  if($('assetDrawer').style.display != 'none') {
    return true;
  }
  return false;
}

modalLeftRightHandler =  function(event) {
  if( curModalAssetId() != null ) {
    if (!modalWindow.isDrawerFormActive()) {
      if(event.keyCode == Event.KEY_LEFT)
        modalWindow.previous();
      else if( event.keyCode == Event.KEY_RIGHT)
        modalWindow.next();
    }
  }
  else
    Event.stopObserving(window,'keydown',modalLeftRightHandler)
}




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

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

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

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

    if(typeof(this.options.tokens) == 'string')
      this.options.tokens = new Array(this.options.tokens);
    if (!this.options.tokens.include('\n'))
      this.options.tokens.push('\n');

    this.observer = null;

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

    Element.hide(this.update);

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

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

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

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

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

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

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

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

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

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

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

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

  onBlur: function(event) {
    setTimeout(this.hide.bind(this), 250);
    this.hasFocus = false;
    this.active = false;
  },

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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';
    var maxLength = this.options.maxLength || 0;
    if (maxLength != 0) {
      fld.maxLength = maxLength;
    }
    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;
  },
  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();
    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);
  }
});


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: '',
    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) {
      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...'
};


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

/*
Event.observe(window, "load", function() {
  FB_RequireFeatures(["XFBML"], function(){
    FB.Facebook.init(window.api_key, "xd_receiver.htm");
  });
});
*/

var fb_perm_timer = null;                        // timer to retrieve permissions
var fb_is_initialized = false;                   // is window initialized

function ensure_init(callback) {
  if(!window.api_key) {
    window.alert("api_key is not set");
  }

  if(window.fb_is_initialized) {
    callback();
  } else {
    FB_RequireFeatures(["XFBML", "CanvasUtil"], function() {
        FB.FBDebug.logLevel = 4;
        FB.FBDebug.isEnabled = true;
        FB.Facebook.init(window.api_key, "xd_receiver.htm");

        window.fb_is_initialized = true;
        callback();
      });
  }
}

function fb_wait_until_connect() {
  if( $$('#modalWindow .fbconnect_status').first() ) {
    window.fbconnect_perm_type = "publish_stream" // publish_stream is a superset of status_update permission, so we only need this  // $$('#modalWindow .fbconnect_status').first().checked ? "status_update" : "offline_access"

    ensure_init(function() {
        FB.Connect.requireSession(true);
        FB.Facebook.get_sessionWaitable().waitUntilReady(function() {
          FB.Facebook.apiClient.users_hasAppPermission(fbconnect_perm_type, function(result, exception) {
            if( result == "0" ) {
              FB.Connect.showPermissionDialog(fbconnect_perm_type);
              check_permission_granted();
            } else {
              register_fbconnect_account()
            }
          })
        })
    })
  }
}

function fbconnect_unsubscribe() {
  ensure_init(function() {
      FB.Connect.requireSession(true);
      FB.Facebook.get_sessionWaitable().waitUntilReady(function() {
        FB.Facebook.get_sessionWaitable().waitUntilReady(function() {
          var session_key = FB.Facebook.apiClient.get_session() ?
                            FB.Facebook.apiClient.get_session().session_key :
                            null;
          if (session_key) {
            $('fbconnect_unsubscribe_session_key').value = session_key;
            $("unsubscribe_fbconnect_account_form").submit();
          } else
            alert("There was an error retrieving your facebook session. Please try again.");
        })
      })
  })
}

function check_permission_granted() {
  if(fb_perm_timer != null) clearTimeout(fb_perm_timer);

  FB.Facebook.apiClient.users_hasAppPermission(fbconnect_perm_type, function(result, exception) {
      if( result == "1" )
        register_fbconnect_account()
      else
        fb_perm_timer = setTimeout("check_permission_granted()",2000);
  })

}

function register_fbconnect_account() {
  ensure_init(function() {
      FB.Facebook.get_sessionWaitable().waitUntilReady(function() {
          var session_key = FB.Facebook.apiClient.get_session() ?
                            FB.Facebook.apiClient.get_session().session_key :
                            null;
          if (session_key) {
            var session_input = $$("#modalWindow .fb_sessionkey").first();
            var message_input = $$("#modalWindow .fb_message").first();
            var type_input = $$("#modalWindow .fb_type").first();
            var status_radio = $$('#modalWindow .fbconnect_status').first();
            var status_message_tb = $$("#modalWindow .connect_status_message").first();
            var feed_message_tb = $$("#modalWindow .connect_news_feed_message").first();

            if( session_input && message_input && type_input && status_radio ) {
              session_input.value = session_key;

              if( status_radio.checked ) {
                message_input.value = status_message_tb.value;
                type_input.value = "status"
              } else {
				message_input.value = feed_message_tb.value;
                type_input.value = "feed"
              }

              submitSubscriptionForm("subscribeViaFacebook");
            }
          } else {
            alert("There was an error retrieving your facebook session. Please try again.");
          }
        });
    });
}



