/*  Prototype JavaScript framework, version 1.5.0
 *  (c) 2005-2007 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://prototype.conio.net/
 *
/*--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.5.0',
  BrowserFeatures: {
    XPath: !!document.evaluate
  },

  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
  emptyFunction: function() {},
  K: function(x) { return x }
}

var Class = {
  create: function() {
    return function() {
      this.initialize.apply(this, arguments);
    }
  }
}

var Abstract = new Object();

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 === undefined) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : object.toString();
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  },

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

Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}

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

Object.extend(Number.prototype, {
  toColorPart: function() {
    var digits = this.toString(16);
    if (this < 16) return '0' + digits;
    return digits;
  },

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

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

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

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

var PeriodicalExecuter = Class.create();
PeriodicalExecuter.prototype = {
  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);
  },

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

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.callback(this);
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
}
String.interpret = function(value){
  return value == null ? '' : String(value);
}

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 = count === undefined ? 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 this;
  },

  truncate: function(length, truncation) {
    length = length || 30;
    truncation = truncation === undefined ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : 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 div = document.createElement('div');
    var text = document.createTextNode(this);
    div.appendChild(text);
    return div.innerHTML;
  },

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

  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 name = decodeURIComponent(pair[0]);
        var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;

        if (hash[name] !== undefined) {
          if (hash[name].constructor != Array)
            hash[name] = [hash[name]];
          if (value) hash[name].push(value);
        }
        else hash[name] = 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);
  },

  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.replace(/\\/g, '\\\\');
    if (useDoubleQuotes)
      return '"' + escapedString.replace(/"/g, '\\"') + '"';
    else
      return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  }
});

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

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

var Template = Class.create();
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
Template.prototype = {
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern  = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    return this.template.gsub(this.pattern, function(match) {
      var before = match[1];
      if (before == '\\') return match[2];
      return before + String.interpret(object[match[3]]);
    });
  }
}

var $break    = new Object();
var $continue = new Object();

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

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

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

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

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

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

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

  grep: function(pattern, iterator) {
    var results = [];
    this.each(function(value, index) {
      var stringValue = value.toString();
      if (stringValue.match(pattern))
        results.push((iterator || Prototype.K)(value, index));
    })
    return results;
  },

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

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

  inject: function(memo, iterator) {
    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) {
    var result;
    this.each(function(value, index) {
      value = (iterator || Prototype.K)(value, index);
      if (result == undefined || value >= result)
        result = value;
    });
    return result;
  },

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

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

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

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

  sortBy: function(iterator) {
    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 (typeof args.last() == 'function')
      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,
  member:  Enumerable.include,
  entries: Enumerable.toArray
});
var $A = Array.from = function(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0, length = iterable.length; i < length; i++)
      results.push(iterable[i]);
    return results;
  }
}

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(value && value.constructor == Array ?
        value.flatten() : [value]);
    });
  },

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

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

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

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

  uniq: function() {
    return this.inject([], function(array, value) {
      return array.include(value) ? array : array.concat([value]);
    });
  },

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

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

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

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

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

if(window.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(arguments[i].constructor == Array) {
        for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
          array.push(arguments[i][j]);
      } else {
        array.push(arguments[i]);
      }
    }
    return array;
  }
}
var Hash = function(obj) {
  Object.extend(this, obj || {});
};

Object.extend(Hash, {
  toQueryString: function(obj) {
    var parts = [];

	  this.prototype._each.call(obj, function(pair) {
      if (!pair.key) return;

      if (pair.value && pair.value.constructor == Array) {
        var values = pair.value.compact();
        if (values.length < 2) pair.value = values.reduce();
        else {
        	key = encodeURIComponent(pair.key);
          values.each(function(value) {
            value = value != undefined ? encodeURIComponent(value) : '';
            parts.push(key + '=' + encodeURIComponent(value));
          });
          return;
        }
      }
      if (pair.value == undefined) pair[1] = '';
      parts.push(pair.map(encodeURIComponent).join('='));
	  });

    return parts.join('&');
  }
});

Object.extend(Hash.prototype, Enumerable);
Object.extend(Hash.prototype, {
  _each: function(iterator) {
    for (var key in this) {
      var value = this[key];
      if (value && value == Hash.prototype[key]) continue;

      var pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  },

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

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

  merge: function(hash) {
    return $H(hash).inject(this, function(mergedHash, pair) {
      mergedHash[pair.key] = pair.value;
      return mergedHash;
    });
  },

  remove: function() {
    var result;
    for(var i = 0, length = arguments.length; i < length; i++) {
      var value = this[arguments[i]];
      if (value !== undefined){
        if (result === undefined) result = value;
        else {
          if (result.constructor != Array) result = [result];
          result.push(value)
        }
      }
      delete this[arguments[i]];
    }
    return result;
  },

  toQueryString: function() {
    return Hash.toQueryString(this);
  },

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

function $H(object) {
  if (object && object.constructor == Hash) return object;
  return new Hash(object);
};
ObjectRange = Class.create();
Object.extend(ObjectRange.prototype, Enumerable);
Object.extend(ObjectRange.prototype, {
  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 (typeof responder[callback] == 'function') {
        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 = function() {};
Ajax.Base.prototype = {
  setOptions: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   ''
    }
    Object.extend(this.options, options || {});

    this.options.method = this.options.method.toLowerCase();
    if (typeof this.options.parameters == 'string')
      this.options.parameters = this.options.parameters.toQueryParams();
  }
}

Ajax.Request = Class.create();
Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
  _complete: false,

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

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

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

    params = Hash.toQueryString(params);
    if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='

    // when GET, append parameters to URL
    if (this.method == 'get' && params)
      this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params;

    try {
      Ajax.Responders.dispatch('onCreate', this, this.transport);

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

      if (this.options.asynchronous)
        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);

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

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

      this.transport.send(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';
    }

    // user-defined headers
    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (typeof extras.push == 'function')
        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() {
    return !this.transport.status
        || (this.transport.status >= 200 && this.transport.status < 300);
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState];
    var transport = this.transport, json = this.evalJSON();

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

      if ((this.getHeader('Content-type') || 'text/javascript').strip().
        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
          this.evalResponse();
    }

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

    if (state == 'Complete') {
      // avoid memory leak in MSIE: clean up
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

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

  evalJSON: function() {
    try {
      var json = this.getHeader('X-JSON');
      return json ? eval('(' + json + ')') : null;
    } catch (e) { return null }
  },

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

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

Ajax.Updater = Class.create();

Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
  initialize: function(container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    }

    this.transport = Ajax.getTransport();
    this.setOptions(options);

    var onComplete = this.options.onComplete || Prototype.emptyFunction;
    this.options.onComplete = (function(transport, param) {
      this.updateContent();
      onComplete(transport, param);
    }).bind(this);

    this.request(url);
  },

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

    if (!this.options.evalScripts) response = response.stripScripts();

    if (receiver = $(receiver)) {
      if (this.options.insertion)
        new this.options.insertion(receiver, response);
      else
        receiver.update(response);
    }

    if (this.success()) {
      if (this.onComplete)
        setTimeout(this.onComplete.bind(this), 10);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create();
Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
  initialize: function(container, url, options) {
    this.setOptions(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(request) {
    if (this.options.decay) {
      this.decay = (request.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = request.responseText;
    }
    this.timer = setTimeout(this.onTimerEvent.bind(this),
      this.decay * this.frequency * 1000);
  },

  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 (typeof element == 'string')
    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(query.snapshotItem(i));
    return results;
  };
}

document.getElementsByClassName = function(className, parentElement) {
  if (Prototype.BrowserFeatures.XPath) {
    var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
    return document._getElementsByXPath(q, parentElement);
  } else {
    var children = ($(parentElement) || document.body).getElementsByTagName('*');
    var elements = [], child;
    for (var i = 0, length = children.length; i < length; i++) {
      child = children[i];
      if (Element.hasClassName(child, className))
        elements.push(Element.extend(child));
    }
    return elements;
  }
};

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

if (!window.Element)
  var Element = new Object();

Element.extend = function(element) {
  if (!element || _nativeExtensions || element.nodeType == 3) return element;

  if (!element._extended && element.tagName && element != window) {
    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;

    if (element.tagName == 'FORM')
      Object.extend(methods, Form.Methods);
    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
      Object.extend(methods, Form.Element.Methods);

    Object.extend(methods, Element.Methods.Simulated);

    for (var property in methods) {
      var value = methods[property];
      if (typeof value == 'function' && !(property in element))
        element[property] = cache.findOrStore(value);
    }
  }

  element._extended = true;
  return element;
};

Element.extend.cache = {
  findOrStore: function(value) {
    return this[value] = this[value] || function() {
      return value.apply(null, [this].concat($A(arguments)));
    }
  }
};

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, html) {
    html = typeof html == 'undefined' ? '' : html.toString();
    $(element).innerHTML = html.stripScripts();
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  },

  replace: function(element, html) {
    element = $(element);
    html = typeof html == 'undefined' ? '' : html.toString();
    if (element.outerHTML) {
      element.outerHTML = html.stripScripts();
    } else {
      var range = element.ownerDocument.createRange();
      range.selectNodeContents(element);
      element.parentNode.replaceChild(
        range.createContextualFragment(html.stripScripts()), element);
    }
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  },

  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 $A($(element).getElementsByTagName('*'));
  },

  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 (typeof selector == 'string')
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    return Selector.findElement($(element).ancestors(), expression, index);
  },

  down: function(element, expression, index) {
    return Selector.findElement($(element).descendants(), expression, index);
  },

  previous: function(element, expression, index) {
    return Selector.findElement($(element).previousSiblings(), expression, index);
  },

  next: function(element, expression, index) {
    return Selector.findElement($(element).nextSiblings(), expression, index);
  },

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

  getElementsByClassName: function(element, className) {
    return document.getElementsByClassName(className, element);
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (document.all && !window.opera) {
      var t = Element._attributeTranslations;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name])  name = t.names[name];
      var attribute = element.attributes[name];
      if(attribute) return attribute.nodeValue;
    }
    return element.getAttribute(name);
  },

  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;
    if (elementClassName.length == 0) return false;
    if (elementClassName == className ||
        elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
      return true;
    return false;
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    Element.classNames(element).add(className);
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    Element.classNames(element).remove(className);
    return element;
  },

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

  observe: function() {
    Event.observe.apply(Event, arguments);
    return $A(arguments).first();
  },

  stopObserving: function() {
    Event.stopObserving.apply(Event, arguments);
    return $A(arguments).first();
  },

  // removes whitespace-only text node children
  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.match(/^\s*$/);
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);
    while (element = element.parentNode)
      if (element == ancestor) return true;
    return false;
  },

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

  getStyle: function(element, style) {
    element = $(element);
    if (['float','cssFloat'].include(style))
      style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat');
    style = style.camelize();
    var value = element.style[style];
    if (!value) {
      if (document.defaultView && document.defaultView.getComputedStyle) {
        var css = document.defaultView.getComputedStyle(element, null);
        value = css ? css[style] : null;
      } else if (element.currentStyle) {
        value = element.currentStyle[style];
      }
    }

    if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none'))
      value = element['offset'+style.capitalize()] + 'px';

    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
    if(style == 'opacity') {
      if(value) return parseFloat(value);
      if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if(value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }
    return value == 'auto' ? null : value;
  },

  setStyle: function(element, style) {
    element = $(element);
    for (var name in style) {
      var value = style[name];
      if(name == 'opacity') {
        if (value == 1) {
          value = (/Gecko/.test(navigator.userAgent) &&
            !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0;
          if(/MSIE/.test(navigator.userAgent) && !window.opera)
            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
        } else if(value == '') {
          if(/MSIE/.test(navigator.userAgent) && !window.opera)
            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
        } else {
          if(value < 0.00001) value = 0;
          if(/MSIE/.test(navigator.userAgent) && !window.opera)
            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
              'alpha(opacity='+value*100+')';
        }
      } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat';
      element.style[name.camelize()] = 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};

    // All *Width and *Height properties give 0 on elements with display none,
    // so enable the element temporarily
    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';
      // Opera returns the offset relative to the positioning context, when an
      // element is position relative but top and left have not been defined
      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.style.overflow || 'auto';
    if ((Element.getStyle(element, 'overflow') || 'visible') != '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;
  }
};

Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf});

Element._attributeTranslations = {};

Element._attributeTranslations.names = {
  colspan:   "colSpan",
  rowspan:   "rowSpan",
  valign:    "vAlign",
  datetime:  "dateTime",
  accesskey: "accessKey",
  tabindex:  "tabIndex",
  enctype:   "encType",
  maxlength: "maxLength",
  readonly:  "readOnly",
  longdesc:  "longDesc"
};

Element._attributeTranslations.values = {
  _getAttr: function(element, attribute) {
    return element.getAttribute(attribute, 2);
  },

  _flag: function(element, attribute) {
    return $(element).hasAttribute(attribute) ? attribute : null;
  },

  style: function(element) {
    return element.style.cssText.toLowerCase();
  },

  title: function(element) {
    var node = element.getAttributeNode('title');
    return node.specified ? node.nodeValue : null;
  }
};

Object.extend(Element._attributeTranslations.values, {
  href: Element._attributeTranslations.values._getAttr,
  src:  Element._attributeTranslations.values._getAttr,
  disabled: Element._attributeTranslations.values._flag,
  checked:  Element._attributeTranslations.values._flag,
  readonly: Element._attributeTranslations.values._flag,
  multiple: Element._attributeTranslations.values._flag
});

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

// IE is missing .innerHTML support for TABLE-related elements
if (document.all && !window.opera){
  Element.Methods.update = function(element, html) {
    element = $(element);
    html = typeof html == 'undefined' ? '' : html.toString();
    var tagName = element.tagName.toUpperCase();
    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
      var div = document.createElement('div');
      switch (tagName) {
        case 'THEAD':
        case 'TBODY':
          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
          depth = 2;
          break;
        case 'TR':
          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
          depth = 3;
          break;
        case 'TD':
          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
          depth = 4;
      }
      $A(element.childNodes).each(function(node){
        element.removeChild(node)
      });
      depth.times(function(){ div = div.firstChild });

      $A(div.childNodes).each(
        function(node){ element.appendChild(node) });
    } else {
      element.innerHTML = html.stripScripts();
    }
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  }
};

Object.extend(Element, Element.Methods);

var _nativeExtensions = false;

if(/Konqueror|Safari|KHTML/.test(navigator.userAgent))
  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
    var className = 'HTML' + tag + 'Element';
    if(window[className]) return;
    var klass = window[className] = {};
    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
  });

Element.addMethods = function(methods) {
  Object.extend(Element.Methods, methods || {});

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    var cache = Element.extend.cache;
    for (var property in methods) {
      var value = methods[property];
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = cache.findOrStore(value);
    }
  }

  if (typeof HTMLElement != 'undefined') {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
    copy(Form.Methods, HTMLFormElement.prototype);
    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
      copy(Form.Element.Methods, klass.prototype);
    });
    _nativeExtensions = true;
  }
}

var Toggle = new Object();
Toggle.display = Element.toggle;

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

Abstract.Insertion = function(adjacency) {
  this.adjacency = adjacency;
}

Abstract.Insertion.prototype = {
  initialize: function(element, content) {
    this.element = $(element);
    this.content = content.stripScripts();

    if (this.adjacency && this.element.insertAdjacentHTML) {
      try {
        this.element.insertAdjacentHTML(this.adjacency, this.content);
      } catch (e) {
        var tagName = this.element.tagName.toUpperCase();
        if (['TBODY', 'TR'].include(tagName)) {
          this.insertContent(this.contentFromAnonymousTable());
        } else {
          throw e;
        }
      }
    } else {
      this.range = this.element.ownerDocument.createRange();
      if (this.initializeRange) this.initializeRange();
      this.insertContent([this.range.createContextualFragment(this.content)]);
    }

    setTimeout(function() {content.evalScripts()}, 10);
  },

  contentFromAnonymousTable: function() {
    var div = document.createElement('div');
    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
    return $A(div.childNodes[0].childNodes[0].childNodes);
  }
}

var Insertion = new Object();

Insertion.Before = Class.create();
Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
  initializeRange: function() {
    this.range.setStartBefore(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.parentNode.insertBefore(fragment, this.element);
    }).bind(this));
  }
});

Insertion.Top = Class.create();
Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
  initializeRange: function() {
    this.range.selectNodeContents(this.element);
    this.range.collapse(true);
  },

  insertContent: function(fragments) {
    fragments.reverse(false).each((function(fragment) {
      this.element.insertBefore(fragment, this.element.firstChild);
    }).bind(this));
  }
});

Insertion.Bottom = Class.create();
Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
  initializeRange: function() {
    this.range.selectNodeContents(this.element);
    this.range.collapse(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.appendChild(fragment);
    }).bind(this));
  }
});

Insertion.After = Class.create();
Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
  initializeRange: function() {
    this.range.setStartAfter(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.parentNode.insertBefore(fragment,
        this.element.nextSibling);
    }).bind(this));
  }
});

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

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);
var Selector = Class.create();
Selector.prototype = {
  initialize: function(expression) {
    this.params = {classNames: []};
    this.expression = expression.toString().strip();
    this.parseExpression();
    this.compileMatcher();
  },

  parseExpression: function() {
    function abort(message) { throw 'Parse error in selector: ' + message; }

    if (this.expression == '')  abort('empty expression');

    var params = this.params, expr = this.expression, match, modifier, clause, rest;
    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
      params.attributes = params.attributes || [];
      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
      expr = match[1];
    }

    if (expr == '*') return this.params.wildcard = true;

    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
      modifier = match[1], clause = match[2], rest = match[3];
      switch (modifier) {
        case '#':       params.id = clause; break;
        case '.':       params.classNames.push(clause); break;
        case '':
        case undefined: params.tagName = clause.toUpperCase(); break;
        default:        abort(expr.inspect());
      }
      expr = rest;
    }

    if (expr.length > 0) abort(expr.inspect());
  },

  buildMatchExpression: function() {
    var params = this.params, conditions = [], clause;

    if (params.wildcard)
      conditions.push('true');
    if (clause = params.id)
      conditions.push('element.readAttribute("id") == ' + clause.inspect());
    if (clause = params.tagName)
      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
    if ((clause = params.classNames).length > 0)
      for (var i = 0, length = clause.length; i < length; i++)
        conditions.push('element.hasClassName(' + clause[i].inspect() + ')');
    if (clause = params.attributes) {
      clause.each(function(attribute) {
        var value = 'element.readAttribute(' + attribute.name.inspect() + ')';
        var splitValueBy = function(delimiter) {
          return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
        }

        switch (attribute.operator) {
          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
          case '|=':      conditions.push(
                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
                          ); break;
          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
          case '':
          case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break;
          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
        }
      });
    }

    return conditions.join(' && ');
  },

  compileMatcher: function() {
    this.match = new Function('element', 'if (!element.tagName) return false; \
      element = $(element); \
      return ' + this.buildMatchExpression());
  },

  findElements: function(scope) {
    var element;

    if (element = $(this.params.id))
      if (this.match(element))
        if (!scope || Element.childOf(element, scope))
          return [element];

    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');

    var results = [];
    for (var i = 0, length = scope.length; i < length; i++)
      if (this.match(element = scope[i]))
        results.push(Element.extend(element));

    return results;
  },

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

Object.extend(Selector, {
  matchElements: function(elements, expression) {
    var selector = new Selector(expression);
    return elements.select(selector.match.bind(selector)).map(Element.extend);
  },

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

  findChildElements: function(element, expressions) {
    return expressions.map(function(expression) {
      return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) {
        var selector = new Selector(expr);
        return results.inject([], function(elements, result) {
          return elements.concat(selector.findElements(result || element));
        });
      });
    }).flatten();
  }
});

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}
var Form = {
  reset: function(form) {
    $(form).reset();
    return form;
  },

  serializeElements: function(elements, getHash) {
    var data = elements.inject({}, function(result, element) {
      if (!element.disabled && element.name) {
        var key = element.name, value = $(element).getValue();
        if (value != undefined) {
          if (result[key]) {
            if (result[key].constructor != Array) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return getHash ? data : Hash.toQueryString(data);
  }
};

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

  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().each(function(element) {
      element.blur();
      element.disabled = 'true';
    });
    return form;
  },

  enable: function(form) {
    form = $(form);
    form.getElements().each(function(element) {
      element.disabled = '';
    });
    return form;
  },

  findFirstElement: function(form) {
    return $(form).getElements().find(function(element) {
      return element.type != 'hidden' && !element.disabled &&
        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
    });
  },

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

Object.extend(Form, Form.Methods);

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

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 Hash.toQueryString(pair);
      }
    }
    return '';
  },

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

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

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

  activate: function(element) {
    element = $(element);
    element.focus();
    if (element.select && ( element.tagName.toLowerCase() != 'input' ||
      !['button', 'reset', 'submit'].include(element.type) ) )
      element.select();
    return element;
  },

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

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

Object.extend(Form.Element, Form.Element.Methods);
var Field = Form.Element;
var $F = Form.Element.getValue;

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

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

  inputSelector: function(element) {
    return element.checked ? element.value : null;
  },

  textarea: function(element) {
    return element.value;
  },

  select: function(element) {
    return this[element.type == 'select-one' ?
      'selectOne' : 'selectMany'](element);
  },

  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) {
    // extend element because hasAttribute may not be native
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
}

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

Abstract.TimedObserver = function() {}
Abstract.TimedObserver.prototype = {
  initialize: function(element, frequency, callback) {
    this.frequency = frequency;
    this.element   = $(element);
    this.callback  = callback;

    this.lastValue = this.getValue();
    this.registerCallback();
  },

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

  onTimerEvent: function() {
    var value = this.getValue();
    var changed = ('string' == typeof this.lastValue && 'string' == typeof value
      ? this.lastValue != value : String(this.lastValue) != String(value));
    if (changed) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
}

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

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

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

Abstract.EventObserver = function() {}
Abstract.EventObserver.prototype = {
  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.bind(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();
Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create();
Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
if (!window.Event) {
  var Event = new Object();
}

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,

  element: function(event) {
    return event.target || event.srcElement;
  },

  isLeftClick: function(event) {
    return (((event.which) && (event.which == 1)) ||
            ((event.button) && (event.button == 1)));
  },

  pointerX: function(event) {
    return event.pageX || (event.clientX +
      (document.documentElement.scrollLeft || document.body.scrollLeft));
  },

  pointerY: function(event) {
    return event.pageY || (event.clientY +
      (document.documentElement.scrollTop || document.body.scrollTop));
  },

  stop: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
      event.stopPropagation();
    } else {
      event.returnValue = false;
      event.cancelBubble = true;
    }
  },

  // find the first node with the given tagName, starting from the
  // node the event was triggered on; traverses the DOM upwards
  findElement: function(event, tagName) {
    var element = Event.element(event);
    while (element.parentNode && (!element.tagName ||
        (element.tagName.toUpperCase() != tagName.toUpperCase())))
      element = element.parentNode;
    return element;
  },

  observers: false,

  _observeAndCache: function(element, name, observer, useCapture) {
    if (!this.observers) this.observers = [];
    if (element.addEventListener) {
      this.observers.push([element, name, observer, useCapture]);
      element.addEventListener(name, observer, useCapture);
    } else if (element.attachEvent) {
      this.observers.push([element, name, observer, useCapture]);
      element.attachEvent('on' + name, observer);
    }
  },

  unloadCache: function() {
    if (!Event.observers) return;
    for (var i = 0, length = Event.observers.length; i < length; i++) {
      Event.stopObserving.apply(this, Event.observers[i]);
      Event.observers[i][0] = null;
    }
    Event.observers = false;
  },

  observe: function(element, name, observer, useCapture) {
    element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.attachEvent))
      name = 'keydown';

    Event._observeAndCache(element, name, observer, useCapture);
  },

  stopObserving: function(element, name, observer, useCapture) {
    element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.detachEvent))
      name = 'keydown';

    if (element.removeEventListener) {
      element.removeEventListener(name, observer, useCapture);
    } else if (element.detachEvent) {
      try {
        element.detachEvent('on' + name, observer);
      } catch (e) {}
    }
  }
});

/* prevent memory leaks in IE */
if (navigator.appVersion.match(/\bMSIE\b/))
  Event.observe(window, 'unload', Event.unloadCache, false);
var Position = {
  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,

  // must be called before calling withinIncludingScrolloffset, every time the
  // page is scrolled
  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  realOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return [valueL, valueT];
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return [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 == 'relative' || p == 'absolute') break;
      }
    } while (element);
    return [valueL, valueT];
  },

  offsetParent: 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;
  },

  // caches x/y coordinate pair to use with overlap
  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = this.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 = this.realOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = this.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);
  },

  // within must be called directly before
  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;
  },

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

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

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

    } while (element = element.offsetParent);

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

    return [valueL, valueT];
  },

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

    // find page position of source
    source = $(source);
    var p = Position.page(source);

    // find coordinate system to use
    target = $(target);
    var delta = [0, 0];
    var parent = null;
    // delta [0,0] will do fine with position: fixed elements,
    // position:absolute needs offsetParent deltas
    if (Element.getStyle(target,'position') == 'absolute') {
      parent = Position.offsetParent(target);
      delta = Position.page(parent);
    }

    // correct by body offsets (fixes Safari)
    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

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

  absolutize: function(element) {
    element = $(element);
    if (element.style.position == 'absolute') return;
    Position.prepare();

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

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

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

  relativize: function(element) {
    element = $(element);
    if (element.style.position == 'relative') return;
    Position.prepare();

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

// Safari returns margins on body which is incorrect if the child is absolutely
// positioned.  For performance reasons, redefine Position.cumulativeOffset for
// KHTML/WebKit only.
if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
  Position.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 [valueL, valueT];
  }
}

Element.addMethods();/*
JSONstring v 1.0
copyright 2006 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.


Based on Steve Yen's implementation:
http://trimpath.com/project/wiki/JsonLibrary
*/

JSONstring={
	compactOutput:false, 		
	includeProtos:false, 	
	includeFunctions: false,
	detectCirculars:false,
	restoreCirculars:true,
	make:function(arg,restore) {
		this.restore=restore;
		this.mem=[];this.pathMem=[];
		return this.toJsonStringArray(arg).join('');
	},
	toObject:function(x){
		eval("this.myObj="+x);
		if(!this.restoreCirculars || !alert){return this.myObj};
		this.restoreCode=[];
		this.make(this.myObj,true);
		var r=this.restoreCode.join(";")+";";
		eval('r=r.replace(/\\W([0-9]{1,})(\\W)/g,"[$1]$2").replace(/\\.\\;/g,";")');
		eval(r);
		return this.myObj
	},
	toJsonStringArray:function(arg, out) {
		if(!out){this.path=[]};
		out = out || [];
		var u; // undefined
		switch (typeof arg) {
		case 'object':
			this.lastObj=arg;
			if(this.detectCirculars){
				var m=this.mem; var n=this.pathMem;
				for(var i=0;i<m.length;i++){
					if(arg===m[i]){
						out.push('"JSONcircRef:'+n[i]+'"');return out
					}
				};
				m.push(arg); n.push(this.path.join("."));
			};
			if (arg) {
				if (arg.constructor == Array) {
					out.push('[');
					for (var i = 0; i < arg.length; ++i) {
						this.path.push(i);
						if (i > 0)
							out.push(',\n');
						this.toJsonStringArray(arg[i], out);
						this.path.pop();
					}
					out.push(']');
					return out;
				} else if (typeof arg.toString != 'undefined') {
					out.push('{');
					var first = true;
					for (var i in arg) {
						if(!this.includeProtos && arg[i]===arg.constructor.prototype[i]){continue};
						this.path.push(i);
						var curr = out.length; 
						if (!first)
							out.push(this.compactOutput?',':',\n');
						this.toJsonStringArray(i, out);
						out.push(':');                    
						this.toJsonStringArray(arg[i], out);
						if (out[out.length - 1] == u)
							out.splice(curr, out.length - curr);
						else
							first = false;
						this.path.pop();
					}
					out.push('}');
					return out;
				}
				return out;
			}
			out.push('null');
			return out;
		case 'unknown':
		case 'undefined':
		case 'function':
			out.push(this.includeFunctions?arg:u);
			return out;
		case 'string':
			if(this.restore && arg.indexOf("JSONcircRef:")==0){
				this.restoreCode.push('this.myObj.'+this.path.join(".")+"="+arg.split("JSONcircRef:").join("this.myObj."));
			};
			out.push('"');
			var a=['\n','\\n','\r','\\r','"','\\"'];
			arg+=""; for(var i=0;i<6;i+=2){arg=arg.split(a[i]).join(a[i+1])};
			out.push(arg);
			out.push('"');
			return out;
		default:
			out.push(String(arg));
			return out;
		}
	}
}
//+ Jon Raoni Soares Silva
//@ http://jsfromhell.com/dhtml/drag-library [v1.1]

//=============================================================
// Modified by HubPages to not require EventListener and fixed some bugs
// I learned a lot from this strange strange style
//=============================================================

Dragger = function(o, a){
	var $ = this;
	o.style.position = "absolute", $.object = o, $.d = {x: 0, y: 0}, $.f = [];
	// TODO two possibilities to stop default drag behaviour in FF (returning
	// false is *not* sufficient):  call Event.stop(e), or write a seperate
	// onmousedown handler returning false.  Both provide less than stellar
	// drag performance on FF, though they don't effect IE.  For now, I'm
	// sticking to onmousedown, because Event.stop() apparently doesn't work
	// on Safari 2.0.3
	//a && (Event.observe(o, "mousedown", function(e){this.start(); Event.stop(e);}.bind($), false),
	a && (Event.observe(o, "mousedown", function(){this.start();}.bind($), false),
		o.onmousedown = function() {return false;},
		Event.observe(document, "mouseup", function(){this.stop();}.bind($), false));

	if (!Dragger.updatingMouse) {
		Event.observe(document, "mousemove", this._updateMouse.bind($), false);
		Dragger.updatingMouse = true;
	}
}
with({p: Dragger.prototype, c: Dragger}){
	p._updateMouse = function(e){
		var w = window, b = document.body;
		p.mouse = {x: e.clientX + (w.scrollX || b.scrollLeft || b.parentNode.scrollLeft || 0),
			y: e.clientY + (w.scrollY || b.scrollTop || b.parentNode.scrollTop || 0)};
	};
	p.mouse = {x: 0, y: 0};
	p.dragging = false;
	p.start = function(center){
		var r, $ = this, m = $.mouse, o = $.object;
		for(var r = {l: o.offsetLeft, t: o.offsetTop, w: o.offsetWidth, h: o.offsetHeight};
			o = o.offsetParent; r.l += o.offsetLeft, r.t += o.offsetTop);
		!$.dragging && ($.dragging = true, o = $.object, $.d = center &&
			(m.x < r.l || m.x > r.l + r.w || m.y < r.t || m.y > r.t + r.h) ?
			{x: r.w / 2, y: r.h / 2} : {x: m.x - o.offsetLeft, y: m.y - o.offsetTop},
			this.dragListener = this.drag.bindAsEventListener(this),
			Event.observe(document, "mousemove", this.dragListener, false),
			this.callEvent("onstart"));
	};
	p.drag = function(e){
		// auto-scroll window with drag.  speeds up as you get closer.
		// TODO dragging should stop at drag filter boundary
		// TODO user has to move the mouse to generate drag events to scroll.
		// would be nice if dragging at very top of page keeps going while
		// mouse button is pressed
		if(browser == "IE") {
			// TODO seems to work with both IE6 and IE7, but it's possible we
			// have to sometimes check document.body.scrollTop instead:
			//var minY = document.body.scrollTop;
			var minY = document.body.parentNode.scrollTop;
			var maxY = minY + document.body.parentNode.clientHeight;
			var eventY = event.clientY + minY;
		} else {
			var minY = window.scrollY;
			var maxY = window.scrollY + window.innerHeight;
			var eventY = e.pageY;
		}
		if(eventY > (maxY - 10)) window.scrollBy(0, 6);
		else if(eventY > (maxY - 25)) window.scrollBy(0, 4);
		else if(eventY > (maxY - 50)) window.scrollBy(0, 2);
		else if(eventY < (minY + 10)) window.scrollBy(0, -6);
		else if(eventY < (minY + 25)) window.scrollBy(0, -4);
		else if(eventY < (minY + 50)) window.scrollBy(0, -2);
		var i, p, $ = this, o = $.object, m = ($._updateMouse(e), (m = $.mouse).x -= $.d.x, m.y -= $.d.y, m);
		for(i = $.f.length; i; $.f[--i] && $.f[i][0].apply(m, $.f[i][1]));
		if (o.style.posLeft) {
			o.style.posLeft = m.x, o.style.posTop = m.y;
		} else {
			o.style.left = m.x + "px", o.style.top = m.y + "px";
		}
		return !!this.callEvent("ondrag", e);
	};
	p.stop = function(){
		this.dragging = false;
		this.dragListener && (Event.stopObserving(document, "mousemove", this.dragListener, false));
		this.callEvent("onstop");
	};
	p.addFilter = function(f, arg0, arg1, arg2, argN){
		this.f[this.f.length] = [f, [].slice.call(arguments, 1)];
	};
	p.callEvent = function(e){
		return this[e] instanceof Function ? this[e].apply(this, [].slice.call(arguments, 1)) : undefined;
	};
}

//Standard Filters
Dragger.filters = new function(){
	function lineLength(x, y, x0, y0){
		return Math.sqrt((x -= x0) * x + (y -= y0) * y);
	}
	function dotLineLength(x, y, x0, y0, x1, y1, o){
		if(o && !(o = function(x, y, x0, y0, x1, y1){
			if(!(x1 - x0)) return {x: x0, y: y};
			else if(!(y1 - y0)) return {x: x, y: y0};
			var left, tg = -1 / ((y1 - y0) / (x1 - x0));
			return {x: left = (x1 * (x * tg - y + y0) + x0 * (x * - tg + y - y1)) /
				(tg * (x1 - x0) + y0 - y1), y: tg * left - tg * x + y};
		}(x, y, x0, y0, x1, y1), o.x >= Math.min(x0, x1) && o.x <= Math.max(x0, x1)
		&& o.y >= Math.min(y0, y1) && o.y <= Math.max(y0, y1))){
			var l1 = lineLength(x, y, x0, y0), l2 = lineLength(x, y, x1, y1);
			return l1 > l2 ? l2 : l1;
		}
		else{
			var a = y0 - y1, b = x1 - x0, c = x0 * y1 - y0 * x1;
			return Math.abs(a * x + b * y + c) / Math.sqrt(a * a + b * b);
		}
	}
	this.SQUARE = function(x, y, w, h){
		this.x = this.x < x ? x : this.x > x + w ? x + w : this.x,
		this.y = this.y < y ? y : this.y > y + h ? y + h : this.y;
	};
	this.CIRCLE = function(x, y, ray){
		var tg;
		lineLength(this.x, this.y, x += ray, y += ray) > ray &&
			(this.x = Math.cos(tg = Math.atan2(this.y - y, this.x - x)) * ray + x,
			this.y = Math.sin(tg) * ray + y);
	};
	this.LINE = function(x, y, angle){
		if(!(angle % 90))
			return this.x = x;
		var tg = Math.tan(-angle * Math.PI / 180);
		Math.sin(45 * Math.PI / 180) >= Math.sin(angle * Math.PI / 180) ?
			this.y = (this.x - x) * tg + y : this.x = (this.y - y) / tg + x;
	};
	this.POLY = function(x0, y0, x1, y1, etc, etc, etc){
		for(var a = [].slice.call(arguments, 0), lines = []; a.length > 3;
			lines[lines.length] = {y1: a.pop(), x1: a.pop(), y0: a.pop(), x0: a.pop()});
		if(!lines.length)
			return;
		for(var l, i = lines.length - 1, o = lines[i],
			lower = {i: i, l: dotLineLength(this.x,	this.y, o.x0, o.y0, o.x1, o.y1, 1)};
			i--; lower.l > (l = dotLineLength(this.x, this.y,
			(o = lines[i]).x0, o.y0, o.x1, o.y1, 1)) && (lower = {i: i, l: l}));
		this.y < Math.min((o = lines[lower.i]).y0, o.y1) ? this.y = Math.min(o.y0, o.y1)
			: this.y > Math.max(o.y0, o.y1) && (this.y = Math.max(o.y0, o.y1));
		this.x < Math.min(o.x0, o.x1) ? this.x = Math.min(o.x0, o.x1)
			: this.x > Math.max(o.x0, o.x1) && (this.x = Math.max(o.x0, o.x1));
		Math.abs(o.x0 - o.x1) < Math.abs(o.y0 - o.y1) ?
			this.x = (this.y * (o.x0 - o.x1) - o.x0 * o.y1 + o.y0 * o.x1) / (o.y0 - o.y1)
			: this.y = (this.x * (o.y0 - o.y1) - o.y0 * o.x1 + o.x0 * o.y1) / (o.x0 - o.x1);
	};
};
/*
moo.fx, simple effects library built with prototype.js (http://prototype.conio.net).
by Valerio Proietti (http://mad4milk.net) MIT-style LICENSE.
for more info (http://moofx.mad4milk.net).
Sunday, March 05, 2006
v 1.2.3
*/

var fx = new Object();
//base
fx.Base = function(){};
fx.Base.prototype = {
	setOptions: function(options) {
	this.options = {
		duration: 500,
		onComplete: '',
		transition: fx.sinoidal
	}
	Object.extend(this.options, options || {});
	},

	step: function() {
		var time  = (new Date).getTime();
		if (time >= this.options.duration+this.startTime) {
			this.now = this.to;
			clearInterval (this.timer);
			this.timer = null;
			if (this.options.onComplete) setTimeout(this.options.onComplete.bind(this), 10);
		}
		else {
			var Tpos = (time - this.startTime) / (this.options.duration);
			this.now = this.options.transition(Tpos) * (this.to-this.from) + this.from;
		}
		this.increase();
	},

	custom: function(from, to) {
		if (this.timer != null) return;
		this.from = from;
		this.to = to;
		this.startTime = (new Date).getTime();
		this.timer = setInterval (this.step.bind(this), 13);
	},

	hide: function() {
		this.now = 0;
		this.increase();
	},

	clearTimer: function() {
		clearInterval(this.timer);
		this.timer = null;
	}
}

//stretchers
fx.Layout = Class.create();
fx.Layout.prototype = Object.extend(new fx.Base(), {
	initialize: function(el, options) {
		this.el = $(el);
		this.el.style.overflow = "hidden";
		this.iniWidth = this.el.offsetWidth;
		this.iniHeight = this.el.offsetHeight;
		this.setOptions(options);
	}
});

fx.Height = Class.create();
Object.extend(Object.extend(fx.Height.prototype, fx.Layout.prototype), {	
	increase: function() {
		this.el.style.height = this.now + "px";
	},

	toggle: function() {
		if (this.el.offsetHeight > 0) this.custom(this.el.offsetHeight, 0);
		else this.custom(0, this.el.scrollHeight);
	}
});

fx.Width = Class.create();
Object.extend(Object.extend(fx.Width.prototype, fx.Layout.prototype), {	
	increase: function() {
		this.el.style.width = this.now + "px";
	},

	toggle: function(){
		if (this.el.offsetWidth > 0) this.custom(this.el.offsetWidth, 0);
		else this.custom(0, this.iniWidth);
	}
});

//fader
fx.Opacity = Class.create();
fx.Opacity.prototype = Object.extend(new fx.Base(), {
	initialize: function(el, options) {
		this.el = $(el);
		this.now = 1;
		this.increase();
		this.setOptions(options);
	},

	increase: function() {
		if (this.now == 1 && (/Firefox/.test(navigator.userAgent))) this.now = 0.9999;
		this.setOpacity(this.now);
	},
	
	setOpacity: function(opacity) {
		if (opacity == 0 && this.el.style.visibility != "hidden") this.el.style.visibility = "hidden";
		else if (this.el.style.visibility != "visible") this.el.style.visibility = "visible";
		if (window.ActiveXObject) this.el.style.filter = "alpha(opacity=" + opacity*100 + ")";
		this.el.style.opacity = opacity;
	},

	toggle: function() {
		if (this.now > 0) this.custom(1, 0);
		else this.custom(0, 1);
	}
});

//transitions
fx.sinoidal = function(pos){
	return ((-Math.cos(pos*Math.PI)/2) + 0.5);
	//this transition is from script.aculo.us
}
fx.linear = function(pos){
	return pos;
}
fx.cubic = function(pos){
	return Math.pow(pos, 3);
}
fx.circ = function(pos){
	return Math.sqrt(pos);
}/*
moo.fx pack, effects extensions for moo.fx.
by Valerio Proietti (http://mad4milk.net) MIT-style LICENSE
for more info visit (http://moofx.mad4milk.net).
Tuesday, March 07, 2006
v 1.2.3
*/

//smooth scroll
fx.Scroll = Class.create();
fx.Scroll.prototype = Object.extend(new fx.Base(), {
	initialize: function(options) {
		this.setOptions(options);
	},

	scrollTo: function(el){
		var dest = Position.cumulativeOffset($(el))[1];
		var client = window.innerHeight || document.documentElement.clientHeight;
		var full = document.documentElement.scrollHeight;
		var top = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
		if (dest+client > full) this.custom(top, dest - client + (full-dest));
		else this.custom(top, dest);
	},

	increase: function(){
		window.scrollTo(0, this.now);
	}
});

//text size modify, now works with pixels too.
fx.Text = Class.create();
fx.Text.prototype = Object.extend(new fx.Base(), {
	initialize: function(el, options) {
		this.el = $(el);
		this.setOptions(options);
		if (!this.options.unit) this.options.unit = "em";
	},

	increase: function() {
		this.el.style.fontSize = this.now + this.options.unit;
	}
});

//composition effect: widht/height/opacity
fx.Combo = Class.create();
fx.Combo.prototype = {
	setOptions: function(options) {
		this.options = {
			opacity: true,
			height: true,
			width: false
		}
		Object.extend(this.options, options || {});
	},

	initialize: function(el, options) {
		this.el = $(el);
		this.setOptions(options);
		if (this.options.opacity) {
			this.o = new fx.Opacity(el, options);
			options.onComplete = null;
		}
		if (this.options.height) {
			this.h = new fx.Height(el, options);
			options.onComplete = null;
		}
		if (this.options.width) this.w = new fx.Width(el, options);
	},
	
	toggle: function() { this.checkExec('toggle'); },

	hide: function(){ this.checkExec('hide'); },
	
	clearTimer: function(){ this.checkExec('clearTimer'); },
	
	checkExec: function(func){
		if (this.o) this.o[func]();
		if (this.h) this.h[func]();
		if (this.w) this.w[func]();
	},
	
	//only if width+height
	resizeTo: function(hto, wto) {
		if (this.h && this.w) {
			this.h.custom(this.el.offsetHeight, this.el.offsetHeight + hto);
			this.w.custom(this.el.offsetWidth, this.el.offsetWidth + wto);
		}
	},

	customSize: function(hto, wto) {
		if (this.h && this.w) {
			this.h.custom(this.el.offsetHeight, hto);
			this.w.custom(this.el.offsetWidth, wto);
		}
	}
}

fx.Accordion = Class.create();
fx.Accordion.prototype = {
	setOptions: function(options) {
		this.options = {
			delay: 100,
			opacity: false
		}
		Object.extend(this.options, options || {});
	},

	initialize: function(togglers, elements, options) {
		this.elements = elements;
		this.setOptions(options);
		var options = options || '';
		elements.each(function(el, i){
			options.onComplete = function(){
				if (el.offsetHeight > 0) el.style.height = '1%';
			}
			el.fx = new fx.Combo(el, options);
			el.fx.hide();
		});

		togglers.each(function(tog, i){
			tog.onclick = function(){
				this.showThisHideOpen(elements[i]);
			}.bind(this);
		}.bind(this));
	},

	showThisHideOpen: function(toShow){
		this.elements.each(function(el, i){
			if (el.offsetHeight > 0 && el != toShow) this.clearAndToggle(el);
		}.bind(this));
		if (toShow.offsetHeight == 0) setTimeout(function(){this.clearAndToggle(toShow);}.bind(this), this.options.delay);

	},

	clearAndToggle: function(el){
		el.fx.clearTimer();
		el.fx.toggle();
	}
}

var Remember = new Object();
Remember = function(){};
Remember.prototype = {
	initialize: function(el, options){
		this.el = $(el);
		this.days = 365;
		this.options = options;
		this.effect();
		var cookie = this.readCookie();
		if (cookie) {
			this.fx.now = cookie;
			this.fx.increase();
		}
	},

	//cookie functions based on code by Peter-Paul Koch
	setCookie: function(value) {
		var date = new Date();
		date.setTime(date.getTime()+(this.days*24*60*60*1000));
		var expires = "; expires="+date.toGMTString();
		document.cookie = this.el+this.el.id+this.prefix+"="+value+expires+"; path=/";
	},

	readCookie: function() {
		var nameEQ = this.el+this.el.id+this.prefix + "=";
		var ca = document.cookie.split(';');
		for(var i=0;c=ca[i];i++) {
			while (c.charAt(0)==' ') c = c.substring(1,c.length);
			if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
		}
		return false;
	},

	custom: function(from, to){
		if (this.fx.now != to) {
			this.setCookie(to);
			this.fx.custom(from, to);
		}
	}
}

fx.RememberHeight = Class.create();
fx.RememberHeight.prototype = Object.extend(new Remember(), {
	effect: function(){
		this.fx = new fx.Height(this.el, this.options);
		this.prefix = 'height';
	},
	
	toggle: function(){
		if (this.el.offsetHeight == 0) this.setCookie(this.el.scrollHeight);
		else this.setCookie(0);
		this.fx.toggle();
	},
	
	resize: function(to){
		this.setCookie(this.el.offsetHeight+to);
		this.fx.custom(this.el.offsetHeight,this.el.offsetHeight+to);
	},

	hide: function(){
		if (!this.readCookie()) {
			this.fx.hide();
		}
	}
});

fx.RememberText = Class.create();
fx.RememberText.prototype = Object.extend(new Remember(), {
	effect: function(){
		this.fx = new fx.Text(this.el, this.options);
		this.prefix = 'text';
	}
});

//Easing Equations (c) 2003 Robert Penner, all rights reserved.
//This work is subject to the terms in http://www.robertpenner.com/easing_terms_of_use.html.

//expo
fx.expoIn = function(pos){
	return Math.pow(2, 10 * (pos - 1));
}
fx.expoOut = function(pos){
	return (-Math.pow(2, -10 * pos) + 1);
}

//quad
fx.quadIn = function(pos){
	return Math.pow(pos, 2);
}
fx.quadOut = function(pos){
	return -(pos)*(pos-2);
}

//circ
fx.circOut = function(pos){
	return Math.sqrt(1 - Math.pow(pos-1,2));
}
fx.circIn = function(pos){
	return -(Math.sqrt(1 - Math.pow(pos, 2)) - 1);
}

//back
fx.backIn = function(pos){
	return (pos)*pos*((2.7)*pos - 1.7);
}
fx.backOut = function(pos){
	return ((pos-1)*(pos-1)*((2.7)*(pos-1) + 1.7) + 1);
}

//sine
fx.sineOut = function(pos){
	return Math.sin(pos * (Math.PI/2));
}
fx.sineIn = function(pos){
	return -Math.cos(pos * (Math.PI/2)) + 1;
}
fx.sineInOut = function(pos){
	return -(Math.cos(Math.PI*pos) - 1)/2;
}
//fx.position extension
fx.Position = Class.create();
fx.Position.prototype = Object.extend(new fx.Base(), {
	initialize: function(el, options) {
		this.el = $(el);
		this.setOptions(options);
		this.now = [0,0];
	},
	
	//override the step function as we need some special handling
	step: function() {
		var time  = (new Date).getTime();
		if (time >= this.options.duration+this.startTime) {
			this.now = this.to;
			clearInterval (this.timer);
			this.timer = null;
			if (this.options.onComplete) setTimeout(this.options.onComplete.bind(this), 10);
		}
		else {
			var Tpos = (time - this.startTime) / (this.options.duration);
			var tmp = [];
			tmp[0] = (this.options.transition(Tpos) * (this.to[0]-this.from[0]) + this.from[0]);
			tmp[1] = (this.options.transition(Tpos) * (this.to[1]-this.from[1]) + this.from[1]);
			this.now = tmp;
		}
		this.increase();
	},

	increase: function() {
		this.el.style["left"] = this.now[0] + "px";
		this.el.style["top"] = this.now[1] + "px";
	},

	move: function(from, to) {
		to = to ? to : this.now;
		this.custom(from, to);
	}
});

/*
fx.Color, simple effect for fading between two colors.
by Tom Jensen (http://neuemusic.com) MIT-style LICENSE.
Wednesday, March 08, 2006
v 1.0.0
*/
fx.Color = Class.create();
fx.Color.prototype = Object.extend(new fx.Base(), {
	initialize: function(el, options) {
		this.el = $(el);
		this.setOptions(options);
		this.now = 0;
		this.regex = new RegExp("#?(..?)(..?)(..?)");
		if (!this.options.fromColor) this.options.fromColor = "#FFFFFF";
		if (!this.options.toColor) this.options.toColor = "#FFFFFF";
		if (!this.options.property) this.props = new Array("backgroundColor");
		else this.props = this.options.property.split(",");
	},
	
	increase: function() {
		var hex = "rgb(" + (Math.round(this.cs[0] + (this.ce[0]-this.cs[0])*this.now))+","+(Math.round(this.cs[1] + (this.ce[1]-this.cs[1])*this.now))+","+ (Math.round(this.cs[2] + (this.ce[2]-this.cs[2])*this.now))+")";
		for (i=0; i < this.props.length; i++) {
			if (this.props[i] == "backgroundColor") this.el.style.backgroundColor = hex;
			else if (this.props[i] == "color") this.el.style.color = hex;
			else if (this.props[i] == "borderColor") this.el.style.borderColor = hex;
		}
	},
	
	toggle: function() {
		this.cs = this.regex.exec(this.options.fromColor);
		this.ce = this.regex.exec(this.options.toColor);
		for (i=1; i < this.cs.length; i++) {
			this.cs[i-1] = parseInt(this.cs[i], 16);
			this.ce[i-1] = parseInt(this.ce[i], 16);
		}
		if (this.now > 0) this.custom(1, 0);
		else this.custom(0, 1);
	},
	
	cycle: function() {
		this.toggle();
		setTimeout(this.toggle.bind(this), this.options.duration + 10);
	},
	
	customColor: function(from, to) {
		this.cs = this.regex.exec(from);
		this.ce = this.regex.exec(to);
		for (i=1; i < this.cs.length; i++) {
			if (this.cs[i].length == 1) { this.cs[i] += this.cs[i]; }
			if (this.ce[i].length == 1) { this.ce[i] += this.ce[i]; }
			this.cs[i-1] = parseInt(this.cs[i], 16);
			this.ce[i-1] = parseInt(this.ce[i], 16);
		}
		this.custom(0, 1);
	},

	customColorRGB: function(from, to) {
		this.rgb_regex = new RegExp('^rgb.([^,]*),\s?([^,]*),\s?([^\)]*)');
		this.cs = this.rgb_regex.exec(from);
		this.ce = this.rgb_regex.exec(to);

		// if we accidentally passed in a hex value, 
		// use the regular customColor func
		if (!this.cs) {
			this.customColor(from, to);
			return;
		}

		for (i=1; i < this.cs.length; i++) {
			this.cs[i-1] = parseInt(this.cs[i]);
			this.ce[i-1] = parseInt(this.ce[i]);
		}
		this.custom(0, 1);
	}
});
// minmax.js: make IE5+/Win support CSS min/max-width/height
// version 1.0, 08-Aug-2003
// written by Andrew Clover <and@doxdesk.com>, use freely

/*@cc_on
@if (@_win32 && @_jscript_version>4 && @_jscript_version<5.7)

var minmax_elements;

minmax_props= new Array(
  new Array('min-width', 'minWidth'),
  new Array('max-width', 'maxWidth'),
  new Array('min-height','minHeight'),
  new Array('max-height','maxHeight')
);

// Binding. Called on all new elements. If <body>, initialise; check all
// elements for minmax properties

function minmax_bind(el) {
  var i, em, ms;
  var st= el.style, cs= el.currentStyle;

  if (minmax_elements==window.undefined) {
    // initialise when body element has turned up, but only on IE
    if (!document.body || !document.body.currentStyle) return;
    minmax_elements= new Array();
    window.attachEvent('onresize', minmax_delayout);
    // make font size listener
    em= document.createElement('div');
    em.setAttribute('id', 'minmax_em');
    em.style.position= 'absolute'; em.style.visibility= 'hidden';
    em.style.fontSize= 'xx-large'; em.style.height= '5em';
    em.style.top='-5em'; em.style.left= '0';
    if (em.style.setExpression) {
      em.style.setExpression('width', 'minmax_checkFont()');
      document.body.insertBefore(em, document.body.firstChild);
    }
  }

  // transform hyphenated properties the browser has not caught to camelCase
  for (i= minmax_props.length; i-->0;)
    if (cs[minmax_props[i][0]])
      st[minmax_props[i][1]]= cs[minmax_props[i][0]];
  // add element with properties to list, store optimal size values
  for (i= minmax_props.length; i-->0;) {
    ms= cs[minmax_props[i][1]];
    if (ms && ms!='auto' && ms!='none' && ms!='0' && ms!='') {
      st.minmaxWidth= cs.width; st.minmaxHeight= cs.height;
      minmax_elements[minmax_elements.length]= el;
      // will need a layout later
      minmax_delayout();
      break;
  } }
}

// check for font size changes

var minmax_fontsize= 0;
function minmax_checkFont() {
  var fs= document.getElementById('minmax_em').offsetHeight;
  if (minmax_fontsize!=fs && minmax_fontsize!=0)
    minmax_delayout();
  minmax_fontsize= fs;
  return '5em';
}

// Layout. Called after window and font size-change. Go through elements we
// picked out earlier and set their size to the minimum, maximum and optimum,
// choosing whichever is appropriate

// Request re-layout at next available moment
var minmax_delaying= false;
function minmax_delayout() {
  if (minmax_delaying) return;
  minmax_delaying= true;
  window.setTimeout(minmax_layout, 0);
}

function minmax_stopdelaying() {
  minmax_delaying= false;
}

function minmax_layout() {
  window.setTimeout(minmax_stopdelaying, 100);
  var i, el, st, cs, optimal, inrange;
  for (i= minmax_elements.length; i-->0;) {
    el= minmax_elements[i]; st= el.style; cs= el.currentStyle;

    // horizontal size bounding
    st.width= st.minmaxWidth; optimal= el.offsetWidth;
    inrange= true;
    if (inrange && cs.minWidth && cs.minWidth!='0' && cs.minWidth!='auto' && cs.minWidth!='') {
      st.width= cs.minWidth;
      inrange= (el.offsetWidth<optimal);
    }
    if (inrange && cs.maxWidth && cs.maxWidth!='none' && cs.maxWidth!='auto' && cs.maxWidth!='') {
      st.width= cs.maxWidth;
      inrange= (el.offsetWidth>optimal);
    }
    if (inrange) st.width= st.minmaxWidth;

    // vertical size bounding
    st.height= st.minmaxHeight; optimal= el.offsetHeight;
    inrange= true;
    if (inrange && cs.minHeight && cs.minHeight!='0' && cs.minHeight!='auto' && cs.minHeight!='') {
      st.height= cs.minHeight;
      inrange= (el.offsetHeight<optimal);
    }
    if (inrange && cs.maxHeight && cs.maxHeight!='none' && cs.maxHeight!='auto' && cs.maxHeight!='') {
      st.height= cs.maxHeight;
      inrange= (el.offsetHeight>optimal);
    }
    if (inrange) st.height= st.minmaxHeight;
  }
}

// Scanning. Check document every so often until it has finished loading. Do
// nothing until <body> arrives, then call main init. Pass any new elements
// found on each scan to be bound   

var minmax_SCANDELAY= 500;

function minmax_scan() {
  var el;
  for (var i= 0; i<document.all.length; i++) {
    el= document.all[i];
    if (!el.minmax_bound) {
      el.minmax_bound= true;
      minmax_bind(el);
  } }
}

var minmax_scanner;
function minmax_stop() {
  window.clearInterval(minmax_scanner);
  minmax_scan();
}

minmax_scan();
minmax_scanner= window.setInterval(minmax_scan, minmax_SCANDELAY);
window.attachEvent('onload', minmax_stop);

@end @*/
// Browser Detect.  Most times unneccesary
var detect = navigator.userAgent.toLowerCase();
var OS,browser,version,total,thestring;

if (checkIt('konqueror'))
{
	browser = "Konqueror";
	OS = "Linux";
}
else if (checkIt('safari')) { browser = "Safari"; }
else if (checkIt('opera')) { browser = "Opera"; }
else if (checkIt('msie')) { browser = "IE"; }
else if (!checkIt('compatible'))
{
	browser = "Netscape Navigator";
	version = detect.charAt(8);
}
else { browser = "An unknown browser"; }

if (!version) { version = detect.charAt(place + thestring.length); }

if (!OS)
{
	if (checkIt('linux')) {		OS = "Linux"; }
	else if (checkIt('x11')) {	OS = "Unix"; }
	else if (checkIt('mac')) {	OS = "Mac"; }
	else if (checkIt('win')) {	OS = "Windows"; }
	else { OS = "an unknown operating system"; }
}

function checkIt(string)
{
	place = detect.indexOf(string) + 1;
	thestring = string;
	return place;
}
// end Browser Check
//.

function ssTo(moduleId) 
{
	var s = new SoftScroll('mod_'+ moduleId);
	return false;
}

// ******************************************
// SoftScroll full featured browser scrolling
// usage:
// var s = new ScoftScroll(element_to_scroll_to, duration_of_scroll_in_ms, delay_before_scroll_in_ms);
var SoftScroll = Class.create();
SoftScroll.prototype = {

	// FEATURE: make this only scroll if the element is out of the viewport
	// otherwise sometimes it will be doing the scroll while the comments
	// are visible and it fights your attempt to scroll.  Annoying.
	initialize: function(ele, duration, delay)
	{
		this.ele = $(ele);
		this.durat = duration || 1000;
		this.delay = delay || 0;

		if (this.delay) {
			setTimeout(this.toggle.bind(this), this.delay);
		} else {
			this.toggle();
		}
	},

	toggle: function()
	{
		this.scroll = new fx.Scroll({duration: this.durat});
		this.scroll.scrollTo(this.ele);
	}
};
// End SoftScroll
//.

// **************************************
// FormHelp context help
// usage:
// 	var myHelp = new FormHelp('topic');
//  <a href="javascript:myHelp.toggle(this)">Get Help Here</a>
// (add some html to the /xml/formhelp.php file for your topic)
function FormHelp(t) {
  this.topic = t;

  this.feedUrl = '/xml/formhelp.php';
  this.effectDone = false;
}
	
FormHelp.prototype.toggle = function(ele) {

	this.toggleLink = ele;
	this.div = ele.parentNode;
	this.div.style.position = 'absolute'; // set this here so that other help links on
										  // the page are masked when they should be

	// if we haven't already fetched the response text (like when hiding or re-showing)
	if (!this.helpResponse) {
		this.retrieve();  // fetch the help contents async
	}
	
	// then start animating
	if (!this.effectDone) {
		this.effectDone = true;

		// save starting dimensions and location for restore
		this.startWidth = ele.style.width;
		this.startHeight = ele.offsetHeight;
		this.startX = ele.style.left;
		this.startY = ele.style.top;

		this.div.style.textAlign = 'left';
		this.toggleLink.style.textAlign = 'right';
		this.toggleLink.innerHTML = 'hide help';

		Element.makePositioned(this.div);

		var resizeEffect = new fx.Combo(this.div, {
              width:true,
              height:true,
              opacity:false,
              duration: 500, 
              onComplete: function() {} });

		resizeEffect.resizeTo(this.startHeight + 130, 300);
		resizeEffect.toggle();

		if (this.helpDiv) {
			Element.show(this.helpDiv);
		}

	} else {
		this.toggleLink.innerHTML = 'what\'s this?';

		Element.undoPositioned(this.div);
		Element.hide(this.helpDiv);
		this.div.style.width = this.startWidth; //+'px';
		this.div.style.height = this.startHeight +'px';
		this.div.style.left = this.startX;
		this.div.style.top = this.startY;

		this.div.style.position = 'static';
		this.toggleLink.style.textAlign = 'center';
		this.effectDone = false;
	}
	return false;
};

FormHelp.prototype.insertText = function(req) {
	this.helpResponse = req.responseText; // cache the response

	if (!this.helpDiv)
	{
		this.helpDiv = document.createElement('div');	
		this.helpDiv.className = 'helpResponse';
		this.div.insertBefore(this.helpDiv, this.toggleLink);
		this.helpDiv.innerHTML = this.helpResponse;
	}
};
	
FormHelp.prototype.retrieve = function() {
	var ajax = new Ajax.Request(this.feedUrl +'?h='+ this.topic,
		{ method:'get',
		parameters:'url=' + this.feedUrl,
		onFailure:reportError,
		onComplete:this.insertText.bind(this)});
};
// end FormHelp
// ***************************************************

function insertVideo(type, key, css, style, preview_div)
{
	var player_html = '<div class="video">';
	if (type == 'Google')
	{
		player_html += '<embed style="'+ style +'" class="'+ css +'" '+
			'type="application/x-shockwave-flash" id="VideoPlayback" '+
			'src="http://video.google.com/googleplayer.swf?docId='+ key +'&hl=en"'+
			' flashvars="" wmode="opaque">'+
			'</embed>';
	} 
	else if (type == 'YouTube')
	{
		player_html += '<embed style="'+ style +'" class="'+ css +'" '+
			'type="application/x-shockwave-flash" '+
			'src="http://www.youtube.com/v/'+ key +'" scale="exactFit" '+
			'wmode="opaque">'+
			'</embed>';
	}
	else
	{
		player_html = "No Video Available";
	}
	player_html += '</div>';


	if (preview_div)
	{
		Element.update(preview_div, player_html);
	}
	else
	{
		document.write(player_html);
	}
}

// used to record the recommended or dis on the article page
function recArt(id, val)
{
	var idStr = 'rec_'+ id;
	var uri = $H({a: id, v: val}).toQueryString();

	var ajax = new Ajax.Updater(
					{success: idStr}, 
					'/xml/feedback.php',
					{
						parameters: uri, 
						onFailure: reportError
					});

	return false; // for the onclick
}

function requestStatus(id, tgtId, action)
{
	var uri = $H({a: id, v: action}).toQueryString();

	var ajax = new Ajax.Updater(
					{success: tgtId}, 
					'/xml/request.php',
					{
						parameters: uri, 
						onFailure: reportError
					});

	return false; // for the onclick
}

// used to expand "Share It!" section
function toggleShareIt(id, flg) {
	if(flg) {
		var uri = $H({art_id: id}).toQueryString();

		var ajax = new Ajax.Updater(
						{success: "share_tgt"}, 
						'/xml/shareit.php',
						{
							parameters: uri, 
							onFailure: reportError
						});
	} else {
		$("share_tgt").innerHTML = "";
	}

	return false; // for the onclick
}

function toggleStats(e, id)
{
	var tar = Event.element(e);
	var pane = $('authorcenter_pane');

	if (tar.showing_stats)
	{
		Element.hide(pane);
		Element.update(tar, tar.showing_stats);
		tar.showing_stats = false;
	}
	else
	{
		var onComplete = function(req) {
							Element.show(pane);
							tar.showing_stats = tar.innerHTML;
							Element.update(tar, "&nbsp;HIDE STATS&nbsp;");
						 };

		var ajax = new Ajax.Updater(
						{success: "authorcenter_pane"},
						'/xml/articlestats.php',
						{
							onComplete: onComplete,
							parameters: $H({art_id: id}).toQueryString(),
							onFailure: reportError
						});
	}

	return false;
}

function article_reviewed(id)
{
	var ajax = new Ajax.Updater(
					{success: "article_reviewed"},
					'/xml/article_reviewed.php',
					{
						parameters: $H({art_id: id}).toQueryString(),
						onFailure: reportError
					});
}

function article_flag(id,flag)
{
	var ajax = new Ajax.Updater(
					{success: "flaglink_" + id + "_" + flag},
					'/xml/flaghub.php',
					{
						parameters: $H({aID:id,reason:flag}).toQueryString(),
						onFailure: reportError
					});
}

// this is for article flags page in authorcenter
function article_learn(artId,flagId,key)
{
	var ajax = new Ajax.Updater(
					{success: "learn"},
					'/xml/authorcenter_learn.php',
					{
						parameters: $H({artId: artId, flagId: flagId, key: key}).toQueryString(),
						onFailure: reportError
					});

	$('learn').style.display = 'block';
}

function ellipse(str, max_length) {
	// port of hp_controls_core_Helpers::ellipse, slightly modified so
	// we never return anything longer than max_length (vs. max_length + 3)
	if (str.length > max_length && max_length != 0) {
		str = str.substr(0, max_length-3);
		var pos = str.lastIndexOf(" ");
		if(pos === -1) {
			str = str.substr(0, max_length-3) + "...";
		} else {
			str = str.substr(0, pos) + "...";
		}
	}
	return str;
}


// load a random article into browser window "target"
function loadRandomArt(target, minScore) {
	var ajax = new Ajax.Request('/xml/random.php',
		{
			method:    'post',
			parameters:  'score=' + minScore,
			onFailure:   reportError,
			onComplete:  function(req) { target.location.href = req.responseText;}
		});
}

function toggleCommentEdit(commId, allowEdit) {
    // toggle visibility of various elements associated with editing a
    // comment:
    // - the "edit" link, cedit_
    // - the comment editing box, cbox_
    // - the uneditable copy of the text, ctext_
    // If "allowEdit" is set, turn on the editing elements and turn off the
    // others.  Otherwise, vice-versa.
    if(allowEdit) {
        $('cedit_' + commId).style.display = 'none';
        $('cbox_' + commId).style.display = '';
        $('ctext_' + commId).style.display = 'none';

		// Attempts to apply TinyMCE to comment editting failed
		// scaling back scope of this feature; will convert <p> tags to newlines
		// in the textarea and then back to p tags after save

		/*
		if (tinyMCE) {
			var box = $('cbox_'+ commId);
			var textareas = box.getElementsByTagName('textarea');
			if (textareas) {
				var textarea = textareas[0].id;
				var inst = tinyMCE.getInstanceById(textarea);

				if (!inst) {
					var lastTinyId = tinyMCE.idCounter;
					if (lastTinyId) {
						inst = tinyMCE.getInstanceById('mce_editor_'+ lastTinyId);
						if (inst) { 
							alert('there was inst');
							tinyMCE.execInstanceCommand(inst.id, 'mceRemoveControl', true, inst.id);
							alert('removed');
						}
					}

					tinyMCE.idCounter=0;
					tinyMCE.execCommand('mceAddControl', true, textarea);
				}
			}
		}
		*/
    } else {
        $('cedit_' + commId).style.display = '';
        $('cbox_' + commId).style.display = 'none';
        $('ctext_' + commId).style.display = '';
    }
}

// used to report errors with ajax scripts
function reportError(req)
{
	alert("Something went wrong.  Please try again. And when you get a chance, report this issue using the 'feedback' link.");
	var headers = req.getAllResponseHeaders();

	var ajax = new Ajax.Request('/xml/reporterror.php', { parameters: headers +"&error=1" });
}

// ******************
// TAGS STUFF

// used on the title and tag page.  pretty ghetto
function addTagEntries()
{
	var chunkSize = 4;

	var moreEntries = document.createElement("div");
	moreEntries.id="moreEntryDiv";
	var li = null;
	var startNum = 4 + 1;
	var endNum = startNum + chunkSize;

	for (var i=startNum; i < endNum; i++)
	{
		li = document.createElement("li");
		moreEntries.appendChild(li);

		var moreInput = document.createElement("input");
		moreInput.className="tagEntry";
		moreInput.name="tag_"+i;
		moreInput.type="text";
		moreInput.size=40;
		li.appendChild(moreInput);
	}
	$("tagEntries").appendChild(moreEntries);

	// prevent click from continuing
	return true;
}

function hubtool_add_tag(input_id)
{
	var input = (input_id) ? $(input_id) : $('add_tag_input');
	if (!input) { return; }

	var tag;
	if (Field.present(input) && input.type) { // tag entry input field
		tag = $F(input);
		Field.clear(input);
	} else if (input.innerHTML) { // suggested tag
		tag = input.innerHTML;

		// parentNode is anchor, parentNode of that is li
		Element.remove(Element.findElement(input, 'li'));
	}
	if (!tag) { return; }

	var last_tag_id = 0;
	var tag_id_regex =/^tag_(\d+)$/i;
	var tag_eles = document.getElementsByClassName('tagEntry');
	tag_eles.each(function(ele) {
		if (ele.id) {
			var ms = tag_id_regex.exec(ele.id);
			if (ms && ms.length > 0) {
				var id = parseInt(ms[1], 10);

				if ($F(ele).length && id > last_tag_id) {
					last_tag_id = id;
				}
			}
		}
	});

	last_tag_id++;
	var new_tag_id = 'tag_'+ last_tag_id;
	var parent_li = $('add_tag_input').parentNode;
	var tag_html = '<input class="tagEntry" id="'+ new_tag_id +'" name="'+ new_tag_id +'" value="'+ tag +'" size="30" onFocus="_helpOn(\'help__tags\')" onBlur="_helpOff(\'help__tags\')" />';

	// if a tag ele exists and is filled in, do not replace it
	if ($(new_tag_id)) {
		Element.update($(new_tag_id).parentNode, tag_html);
	} else {
		var pole = new Insertion.Before(parent_li, '<li>'+ tag_html +'</li>');
	}

	return false;
}

function add_tag(art_id)
{
	if (!$('add_tag_input') || !$F('add_tag_input')) { return; }

	// tag cleanup
	var tag = $F('add_tag_input');
	tag = tag.replace(/[^\w\s\$\-\'\%\&]/g,'');	// strip non-allowable characters

	var tag_del = tag.replace(/'/g, "\\'");		// escape apostrophes in del JS
	var tag_url = tag.replace(/ /g, '+');		// escape spaces in url
	var tag_id = 'tagd_'+ tag.replace(/ /g, '_');
	tag_id = tag_id.toLowerCase();				// make tag id case insensitive

	// if tag exists, draw attention
	if ($(tag_id)) 
	{
		$(tag_id).style.fontWeight = 'bolder'; 
	}
	else
	{
		var parent_li =$('add_tag_input').parentNode;
		var tag_html = "<li><a href=\"javascript:void delete_tag('"+ art_id +"','"+ tag_del +"');\"><img src=\"/x/x_14.gif\" width=\"14\" height=\"14\"/></a>";
		tag_html += '<a id="'+ tag_id +'" href="/tag/'+ tag_url +'">'+ tag +'</a>';
		tag_html += '</li>';

		new Insertion.Before(parent_li, tag_html);
		save_tag(art_id, tag, false /* not delete */);
	}

	Field.clear('add_tag_input');
	return false;
}

function delete_tag(art_id, tag)
{
	if (!art_id || !tag) { return; }
 
	var tag_id = 'tagd_'+ tag.replace(/ /g, '_');
	var tag_link = $(tag_id);
	if (!tag_link) { return; }

	var li = tag_link.parentNode;
	Element.remove(li);

	save_tag(art_id, tag, true /* is delete */);

	return false;
}

// do AJAX call to save the tag to the server
function save_tag(art_id, tag, del)
{
	var del_val = (del) ? 1 : 0;
	var req = {a: art_id, 
				v: tag, 
				d: del_val
			  };

	var params = $H(req).toQueryString();
	var ajax = new Ajax.Request('/xml/tagadd.php',
		{ parameters: params,
        onFailure:reportError,
        onComplete:function() {} });
}

// END TAGS
// ******************

function fireOnReturn(watch, func)
{
	Event.observe(watch, 'keyup', function(event) { 
		event = event || window.event;
		if (event.which) {  // netscape
			if (event.which == Event.KEY_RETURN) {
				event.preventDefault();
				func();
			}
        }
		else if (event.keyCode) { // IE
			if (event.keyCode==Event.KEY_RETURN) {
				Event.stop(event);
				func();
			}
		}
	}, false);
}

function moderate_com(mod_id, comment_id, approve)
{
	if (!mod_id || !comment_id) { return; }
 
	var comment_ele = $(mod_id +'_'+ comment_id +'__comment');
	if (!comment_ele) { return; }

	// do animation callback
	var animateModerate = function() {
		var status_ele = $(comment_id +'_status');
		var status_text = (approve) ? 'Approved' : 'Denied';
		Element.update(status_ele, status_text);

		var balloon_ratio = 1.3;
		var text = new fx.Text(status_ele, {duration:400, onComplete:function() {
						this.custom(balloon_ratio, 1); 
						this.options.onComplete = function() {};
					}});
		text.custom(1, balloon_ratio);
	};

	var req = {mod_id: mod_id, com_id: comment_id, v: approve, moderate: 1};
	var params = $H(req).toQueryString();
	var ajax = new Ajax.Request('/xml/comment.php', { 
					method:'post',
					parameters: params,
					onFailure: reportError,
					onComplete: animateModerate });

	return false;
}

// TODO: make it work
function moderate_fanmail(fan_id, comment_id, approve)
{
	if (!fan_id || !comment_id) { return; }
 
	var comment_ele = $('comment__' + fan_id +'_'+ comment_id);
	if (!comment_ele) { return; }

	var req = {fan_id: fan_id, com_id: comment_id, v: approve, moderate: 1};
	var params = $H(req).toQueryString();
	var ajax = new Ajax.Request('/xml/comment.php', { 
					parameters: params,
					onFailure:reportError,
					onComplete:function() {} 
				});

	// do animation
	if (!approve) { Element.update(comment_id +'_status', 'Denied'); }
	else { Element.update(comment_id +'_status', 'Approved'); }

	return false;
}

function save_aff(aff, val)
{
	if (!aff) { return; }
 
	var req = {aff: aff, v: val};
	var params = $H(req).toQueryString();

	var ajax = new Ajax.Request('/xml/userprofile.php', {
        parameters: params,
        onFailure:reportError,
        onComplete:function() {
			// do save status blink
			var status = $(aff +'_code_status');
			Element.update(status, 'Saved');

			var text = new fx.Text(status, {duration:500, onComplete:function() {
				this.custom(1.6, 1); 
				this.options.onComplete = function() { Element.update(status, 'Active'); }
				}});
			text.custom(1, 1.6);
		}.bind(aff)
		});
	return false;
		
	
}

// ********************************
// InlineEdit: controls the click-to-edit "micro content" areas
//.
// To use:
// InlineEdit.register(eleNameOrReference, function(value) { alert("New text: "+ value); });
//. 
// The class will take care of the rest
function InlineEdit() {}
InlineEdit._registered = [];
InlineEdit._onedit = [];
InlineEdit._ondone = [];
InlineEdit._editting = [];
InlineEdit._setonclick = false;

InlineEdit.register = function(ele, onSave)
{
	var obj = $(ele);
	obj.title = "Click to edit";
	obj.style.backgroundColor = '#ffe';
	obj.empty_text = "<i>Click here to edit</i>";
	InlineEdit._registered[obj.id] = onSave;

	obj.highlight = function() {
		if (this.hide_timer) {
			clearTimeout(this.hide_timer);
		}
		this.style.backgroundColor = '#ffffd3';
        if (this.empty_text && (this.innerHTML == "&nbsp;" || this.innerHTML == " " || this.innerHTML.charCodeAt(0) == 160)) { 
			this.innerHTML = this.empty_text;
        }
	};

	obj.onmouseover = obj.highlight;

	obj.onmouseout = function() { 
		if (this.hide_timer) {
			clearTimeout(this.hide_timer);
		}
		this.hide_timer = setTimeout("var el=$('"+ this.id +"');if (el) {el.unhighlight();}", 1000);
	};

	obj.unhighlight = function() {
		this.style.backgroundColor = '#ffe';
		if (this.empty_text && this.innerHTML == this.empty_text) {
			this.innerHTML = '&nbsp;';
		}
	};

	if (!InlineEdit._setonclick)
	{
		document.onclick = InlineEdit._handleDocClick;
		InlineEdit._setonclick = true;
	}
};

InlineEdit.unregister = function(ele)
{
	var obj = $(ele);
	obj.title = '';
	if (obj.hide_timer) {
		clearTimeout(obj.hide_timer);
	}

	obj.onmouseover = function() {};
	obj.onmouseout = function() {};
	obj.style.backgroundColor = '';

	delete InlineEdit._registered[obj.id];
};

InlineEdit.registerCallbacks = function(ele, onEdit, onDone)
{
	var obj = $(ele);
	InlineEdit._onedit[obj.id] = onEdit;
	InlineEdit._ondone[obj.id] = onDone;
};

InlineEdit._handleDocClick = function(e)
{
	if (!document.getElementById || !document.createElement) { return; }
	var obj;
	if (!e) { obj = window.event.srcElement; }
	else { obj = e.target; }

	while (obj.nodeType != 1)
	{
		obj = obj.parentNode;
	}
	if (obj.tagName == 'TEXTAREA' || obj.tagName == 'A') { return; }
	while (!InlineEdit._registered[obj.id] && obj.nodeName != 'HTML')
	{
		obj = obj.parentNode;
	}
	if (obj.nodeName == 'HTML') { return; }

	InlineEdit.edit(obj);
};

InlineEdit.edit = function(ele)
{
	ele = $(ele);
	if (!InlineEdit._registered[ele.id]) { return false;  }

	if (InlineEdit._onedit[ele.id]) {
		var onedit = InlineEdit._onedit[ele.id];
		onedit(ele);
	}
	
	var text = ele.innerHTML;
	if (ele.empty_text && ele.empty_text == text) {
		text = ' ';
	}

	var input_ele = document.createElement('INPUT');
	input_ele.type = 'text';

	// clone styles over (defined below)
	Element.cloneStyles(ele, input_ele);

	ele.parentNode.insertBefore(input_ele, ele);
	InlineEdit._insertEditSpanBefore(ele);

	input_ele.id = ele.id +'_edit_inplace';
	InlineEdit._editting[input_ele.id] = ele;
	Element.remove(ele);
	input_ele.value = text;

	//input_ele.onblur = InlineEdit._onEditBlur;
	input_ele.focus();
	input_ele.select();
	return false;
};

InlineEdit._onButtonClick = function(event) {
	event = event || window.event;
	var button = event.target || event.srcElement;
	var is_save = (button.innerHTML.search(/CANCEL/) == -1) ? true : false;

	var edit_span = button.parentNode;
	var input_ele = edit_span;
	while (input_ele && !InlineEdit._editting[input_ele.id])
	{
		input_ele = input_ele.previousSibling;
	}
	var orig_ele = InlineEdit._editting[input_ele.id];

	input_ele.hasFocus = false;
	var z = input_ele.parentNode;
	z.insertBefore(orig_ele, input_ele);
	z.removeChild(input_ele);
	z.removeChild(document.getElementsByClassName('buttonSpan', z)[0]);

	delete InlineEdit._editting[input_ele.id];

	if (InlineEdit._ondone[orig_ele.id]) {
		var ondone = InlineEdit._ondone[orig_ele.id];
		ondone(orig_ele);
	}

	// call onSave handler
	if (is_save) {
		orig_ele.innerHTML = (input_ele.value.length > 0) ? input_ele.value : '&nbsp;';
		var onSave = InlineEdit._registered[orig_ele.id];
		onSave(input_ele.value);
	}
};

InlineEdit._insertEditSpanBefore = function(obj) {

	// prepare Save / Cancel buttons
	if (document.getElementById && document.createElement)
	{
		var edit_span = document.createElement('span');
		edit_span.className = 'buttonSpan';
		var butt = document.createElement('button');
		var buttext = document.createTextNode('OK');
		butt.appendChild(buttext);
		edit_span.appendChild(butt);

		var can_butt = document.createElement('button');
		var can_buttext = document.createTextNode('CANCEL');
		can_butt.appendChild(can_buttext);
		edit_span.appendChild(can_butt);
		
		obj.parentNode.insertBefore(edit_span, obj);

		butt.onclick = InlineEdit._onButtonClick;
		can_butt.onclick = InlineEdit._onButtonClick;
	}
};

// end InlineEdit
// ***************************

var SampleDuration = Class.create();
SampleDuration.prototype = {

	initialize: function(articleId) {

		this.art_id = articleId;
		this.t = new Timer();
		this.onleaveListener = this.onleave.bindAsEventListener(this);
		Event.observe(window, 'beforeunload', this.onleaveListener, false);
	},
	
	onleave: function(e) {

		e = e || window.event;
		this.t.stop();

		var params = $H({art_id: this.art_id,
						 dur: this.t.length
						});
					
		var ajax = new Ajax.Request('/xml/duration',
					{ parameters: params.toQueryString() });
	}
};

// ***************************
// provide the 'working...' flag during AJAX requests

var myGlobalHandlers = {
	onCreate: function(){
		this.flag(true);
	},

	onComplete: function() {
		if (Ajax.activeRequestCount == 0) {
			this.flag(false);
		}
	},
	
	onScroll: function() {
		var div = $('ajaxing');
		if (div) {
			div.style.top = Position.getViewportScrollY() +'px';
		}
	},
	
	flagUp: function() { this.flag(true); },
	flagDown: function() { this.flag(false); },

	flag: function(up) {
		var ajaxDiv = $('ajaxing');
		if (ajaxDiv) {
			if (up) {
				ajaxDiv.style.display = 'inline';
				this.onScroll();
				this.scrollListener = this.onScroll.bindAsEventListener(this);
				Event.observe(window, 'scroll', this.scrollListener, false);
			} else {
				ajaxDiv.style.display = 'none';
				Event.stopObserving(window, 'scroll', this.scrollListener, false);
				this.scrollListener = null;
			}
		}
	}
};
Ajax.Responders.register(myGlobalHandlers);

// ****************************

// ****************************
// misc object enhancements
Element.setOpacity = function(ele, opacity) 
{
	ele = $(ele);

	if (window.ActiveXObject) { ele.style.filter = "alpha(opacity="+Math.round(opacity*100)+")"; }
	ele.style.opacity = opacity;
};

Element.getCurrentStyle = function(ele)
{
	ele = $(ele);

	var styles;
	if (document.defaultView) {
		// (Mozilla)
		styles = document.defaultView.getComputedStyle(ele,'');
	} else {
		// (IE)
		styles = ele.currentStyle;
	}

	return styles;
};

Element.cloneStyles = function(ele, clone, that_start_with) 
{
	ele = $(ele);
	clone = $(clone);

	var styles = Element.getCurrentStyle(ele);
	for (var name in styles) {
		// very ugly and probably quite naive -- but the edit field in
		// Opera gets screwed up here because of a couple style settings,
		// so we'll just skip over those particular style names.  Their
		// exclusion doesn't seem to hurt anything.
		if(browser == "Opera") {
			if(name == "height" || name == "pixelHeight" || name == "pixelWidth" || name == "posHeight" ||
				name == "posWidth" || name == "width" || name == "font" || name == "fontSize") {
				continue;
			}
		}
		var value = styles[name];
		if(value !== '' && !(value instanceof Object) && name != "length" && name !="parentRule") {
			if (that_start_with && name.indexOf(that_start_with) !== 0) { continue; }
			clone.style[name] = value;
		}
	}
	return clone;
};

// Find the first node with the given tagName, starting from the
// given element; traverses the DOM upwards
Element.findElement = function(element, tagName) 
{
    element = $(element);
    while (element.parentNode && (!element.tagName || (element.tagName.toUpperCase() != tagName.toUpperCase())))
	{
      element = element.parentNode;
	}
    return element;
};


String.prototype.trim = function() 
{
	var res = this;
	while (res.substring(0,1) == ' ') {
		res = res.substring(1,res.length);
	}
	while (res.substring(res.length-1,res.length) == ' ') {
		res = res.substring(0,res.length-1);
	}
	return res;
};

Element.getWidth = function(ele)
{
	ele = $(ele);
	return ele.offsetWidth;
};

Element.ellipsis = function(ele, len)
{
	len = len || (100);
	var p = $(ele);

	if (p && p.innerHTML) 
	{
		var trunc = p.innerHTML;
		if (trunc.length> len) 
		{
			/* Truncate the content of the P, then go back to the end of the
			previous word to ensure that we don't truncate in the middle of
			a word */
			trunc = trunc.substring(0, len);
			trunc = trunc.replace(/\w+$/, '');

			/* Add an ellipses to the end and make it a link that expands
			the paragraph back to its original size */
			/*
			trunc += '<a href="#wtf" ' +
			'onclick="alert(\'blah\');this.parentNode.innerHTML=' +
			'unescape(\''+escape(p.innerHTML)+'\');alert(\'blah\');return false;">' +
			'...<\/a>';
			*/

			trunc += '...';
			p.innerHTML = trunc;
		}
	}
};

// Position functions I find useful

// gets viewport size in all browsers
Position.getViewportHeight = function() {
	if (window.innerHeight!=window.undefined) { return window.innerHeight; }
	if (document.compatMode=='CSS1Compat') { return document.documentElement.clientHeight; }
	if (document.body) { return document.body.clientHeight; }
	return window.undefined; 
};

Position.getViewportWidth = function() {
	if (window.innerWidth!=window.undefined) { return window.innerWidth; }
	if (document.compatMode=='CSS1Compat') { return document.documentElement.clientWidth; }
	if (document.body) { return document.body.clientWidth; } 
	return window.undefined; 
};

Position.getDocumentHeight = function() {
	return document.documentElement.scrollHeight;
};

Position.getDocumentWidth = function() {
	return document.documentElement.scrollWidth;
};

// gets viewport scrollage in all browsers
Position.getViewportScrollX = function() {
  var scrollX = 0;
  if( document.documentElement && document.documentElement.scrollLeft ) {
    scrollX = document.documentElement.scrollLeft;
  }
  else if( document.body && document.body.scrollLeft ) {
    scrollX = document.body.scrollLeft;
  }
  else if( window.pageXOffset ) {
    scrollX = window.pageXOffset;
  }
  else if( window.scrollX ) {
    scrollX = window.scrollX;
  }
  return scrollX;
};

Position.getViewportScrollY = function() {
	var scrollY = 0;
	if( document.documentElement && document.documentElement.scrollTop ) {
		scrollY = document.documentElement.scrollTop;
	}
	else if( document.body && document.body.scrollTop ) {
		scrollY = document.body.scrollTop;
	}
	else if( window.pageYOffset ) {
		scrollY = window.pageYOffset;
	}
	else if( window.scrollY ) {
		scrollY = window.scrollY;
	}
	return scrollY;
};

Position.withinViewport = function(ele) {

	var off = Position.cumulativeOffset($(ele));

	var topLeftVp = [0 + Position.getViewportScrollX(), Position.getViewportScrollY()];
	var botRightVp = [topLeftVp[0] + Position.getViewportWidth(), topLeftVp[1] + Position.getViewportHeight()];

	return (topLeftVp[0] < off[0] && off[0] < botRightVp[0] && // X is in between the side of the VP
		topLeftVp[1] < off[1] && off[1] < botRightVp[1]);
};

Position.set = function(ele, positionArray) {
	if (ele && positionArray) {
		ele.style.left = positionArray[0] +'px';
		ele.style.top = positionArray[1] +'px';
	}
};

// used on hub edit page to change ad level drop down
function updateAdLevelOptions() {
	isCommercial = document.getElementById('isCommercial');
	adLevelSelect = document.getElementById('adLevelSelect');
	if( isCommercial.checked )
	{
		// disable all but the last option
		for( i = adLevelSelect.length - 2; i >=0; i-- )
		{
			adLevelSelect.options[i].disabled = true;
			adLevelSelect.selectedIndex = adLevelSelect.length - 1;
		}
	} else {
		// enable all options
		for( i = adLevelSelect.length - 1; i >=0; i-- )
		{
			adLevelSelect.options[i].disabled = false;
		}
	}
}

// *****************
// Used on /contacts/ page for importing email addresses
function select_all( name, start, end )
{
	for (var i=start; i <= end; i++)
	{
		var ele = $( name + '_' + i );
		if( ele ) { ele.checked = true; }
	}

	var disp = $(name + '_selected');
	if( disp )
	{
		disp.innerHTML = ( end - start) + 1;
	}
	update_plural(name);
}

function unselect_all( name, start, end )
{
	for (var i=start; i <= end; i++)
	{
		var ele = $(name + '_' + i);
		if( ele ) { ele.checked = false; }
	}

	var disp = $(name + '_selected');
	if( disp )
	{
		disp.innerHTML = 0;
	}
	update_plural(name);
}

function checkbox_onchange(name, num)
{
	var disp = $(name + '_selected');
	if( disp )
	{
		var ele = $(name + '_' + num);
		if ( ele.checked ) { 
			disp.innerHTML = parseInt(disp.innerHTML, 10) + 1;
			update_plural(name);
		} else {
			disp.innerHTML = parseInt(disp.innerHTML, 10) - 1;
			update_plural(name);
		}
	}
}

function update_plural(name)
{
	var ele = document.getElementById( name + '_selected' );
	if( ele ) 
	{
		var disp = document.getElementById( name + '_plural' );
		if( disp )
		{
			if( parseInt(ele.innerHTML, 10) == 1 ) {
				disp.innerHTML = ' is';
			} else {
				disp.innerHTML = 's are';
			}
		}
	}
}

function import_now(target,name,start,end)
{
	var target_ele = self.opener.document.getElementById( target );
	if( target_ele )
	{
		for (var i=start; i <= end; i++)
		{
			var ele = $( name + '_' + i );
				if( ele && ele.checked )
			{
				var email = $( name + '_email_' + i );
				if( target_ele.value.length < 2 ||
					target_ele.value.charAt(target_ele.value.length) == ',' ||
					target_ele.value.charAt(target_ele.value.length - 1) == ',' )
				{
					target_ele.value = target_ele.value + email.innerHTML;
				} else {
					target_ele.value = target_ele.value + ', ' + email.innerHTML;
				}
			}
		}
	} else {
		alert("cannot locate parent (opener) window!");
	}
}

// *******************
// DEBUGGING FUNCTIONS

function dump_divs() {

	var outstr = 'DIV REPORT:<br/>';

	var divs = $A(document.getElementsByTagName('div'));
	divs.each (function(div) {

		if (div.id) {
			outstr += '#'+ div.id +', ';
		}
		outstr += '.'+ div.className +', '+ div.offsetWidth +' x '+ div.offsetHeight +'<br/>';
	});

	if (!$('debug_div')) { out('create'); }

	$('debug_div').innerHTML = outstr;
}

function out(out_string) {

	if (window.console) { console.log(out_string); }
	else 
	{
		var pole;
		var div_html = '<div id="debug_div"></div>';
		if (!$('debug_div')) {
			if ($('footer')) 
			{
				pole = new Insertion.Bottom('footer', div_html);
			} else if ($('sidebar')) {
				pole = new Insertion.Bottom('sidebar', div_html);
			}
		}

		if ($('debug_div'))
		{
			pole = new Insertion.Bottom('debug_div', out_string +'<br/>');
		}
	}
}

function search_escape(str)
{
	newstr = escape(str);
	newstr = newstr.replace(/\%20/g,"+");
	return newstr;
}

// Timer class: used to quickly obtain time spans for benchmarking
var Timer = Class.create();

Timer.prototype = {

	initialize: function() {

		this.start();
	},

	start: function() {
			
		this.startTime = new Date();
	},

	stop: function() {

		this.stopTime = new Date();
		this.length = (this.stopTime - this.startTime); // duration in ms
	},

	inspect: function() {
		
		if (!this.stopTime) { this.stop(); }	

		return "duration: "+ this.length +"ms";
	}
};
// **************************************
// hpFormHandler
function hpFormHandler(formName) {

	this.submitMode = false;
	this.submitUri = '/';
	this.nextUri = '/';

	this.lit = false;
	this.form = $(formName);
	this.errors = $H({});
	this.method = 'post';
	this.errorHeader = '<h2>Please fix these errors before continuing</h2>';

	this.setValidators();
}

hpFormHandler.prototype.handleSubmitServerError = function(req) {}

// validators
hpFormHandler.prototype.validateLengthMax = function(ele, max, msg) {
	var val = $F(ele);
	this.testForError(($F(ele).trim().length > max), ele, msg);
}

hpFormHandler.prototype.validateLengthMin = function(ele, min, msg) {
	var val = $F(ele);
	this.testForError((val.length != 0 && val.length < min), ele, msg);
}

hpFormHandler.prototype.validateLengthExactly = function(ele, len, msg) {
	var val = $F(ele);
	this.testForError((val.length != 0 && val.length != len), ele, msg);
}

hpFormHandler.prototype.validateMandatory = function(ele, msg) {

	var val = false;
	if ($F(ele)) { val = $F(ele).trim(); }
	this.testForError((!val || val.length == 0), ele, msg);
}

hpFormHandler.prototype.validateRegex = function(ele, regex, msg) {
	var val = $F(ele);
	this.testForError((val.length != 0 && val.search(regex) == -1), ele, msg);
}

hpFormHandler.prototype.validateNoRegex = function(ele, regex, msg) {
	var val = $F(ele);
	this.testForError((val.search(regex) != -1), ele, msg);
}

hpFormHandler.prototype.validateNoSpaces = function(ele, msg) {
	var val = $F(ele);
	this.testForError(val.search(/ /) != -1, ele, msg);
}

hpFormHandler.prototype.validateNot = function(ele, not, msg) {
	this.testForError(($F(ele).trim() == not), ele, msg);
}

hpFormHandler.prototype.validateSameAs = function(ele, ele2, msg) {
	this.testForError(($F(ele) != $F(ele2)), ele, msg);
}

hpFormHandler.prototype.validateServerCheck = function(ele, url, msg) {
	var val = $F(ele);

	// there's no point in using this check to verify a non-empty field
	// this check saves roundtrips
	if (val.length == 0) return;

	// check to see if the curent value is the same as the last
	// successful server check, thus saving many roundtrips
	if (ele.lastGoodValue && ele.lastGoodValue == val) return;
	val = encodeURIComponent(val);

    var myAjax = new Ajax.Request(url,
	{ 
		method: 'post',
		parameters: ele.id +'='+ val,
		onComplete: function(req) {
			eval(req.responseText);
			this.testForError(!valid, ele, msg);
			if (valid) { ele.lastGoodValue = val; }
			this._showErrors();
		}.bind(this),
		onException: function() { alert("Something went very wrong.  Please try submitting again."); },
		onFailure: reportError
	});

}

// specialized validators
hpFormHandler.prototype.validateEmailList = function(ele) {
	var MAX_LEN = 800; 
	var MIN_LEN = 6; 

	this.validateLengthMin(ele, MIN_LEN, 'The address you entered is too short. Please use an address at least '+ MIN_LEN +' characters in length.');
	this.validateNoRegex(ele, /\$/, 'Dollar signs are not valid in an email address.');
	this.validateNoRegex(ele, /\\/, 'Backslashes are not valid in an email address.');
	this.validateRegex(ele, /\@/, 'A valid email address must contain an @ symbol.');
}

hpFormHandler.prototype.validateEmail = function(ele) {
	
	this.validateEmailList(ele);

	var MAX_LEN = 200; 
	this.validateLengthMax(ele, MAX_LEN, 'Your email address is too long. Please use a shorter address.');
	this.validateNoSpaces(ele, 'Spaces are not valid characters in an email address.  Please recheck your address.'); 
}

hpFormHandler.prototype.validateEmailName = function(ele) {
	var MIN_LEN = 2;
	var MAX_LEN = 200; 
	this.validateLengthMin(ele, MIN_LEN, 'Your name is too short.  Please enter at least 2 characters.');
	this.validateLengthMax(ele, MAX_LEN, 'Your name is too long. Please use a shorter name.');
}

hpFormHandler.prototype.validatePhone = function(ele) {
	var val = $F(ele);

	var us = /^(?:\([2-9]\d{2}\)\ ?|[2-9]\d{2}(?:\-?|\ ?))[2-9]\d{2}[- ]?\d{4}$/;
	this.testForError(!us.test(val) && val.length > 0, ele, 'Please enter a valid phone number');
}

hpFormHandler.prototype.validatePostal = function(ele) {
	var val = $F(ele).trim();

	var valid = false;
	var us =  /^\d{5}(-\d{4})?$/;
	var ca =  /[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ] \d[ABCEGHJKLMNPRSTVWXYZ]\d/i;
	var gb =  /^[A-Za-z]{1,2}[\d]{1,2}([A-Za-z])?\s?[\d][A-Za-z]{2}$/i;

	if (val.length == 0 || (
		us.test(val) || 
		ca.test(val) || 
		gb.test(val))
	) { 
		valid = true;
	}

	this.testForError(!valid, ele, 'Please enter a valid postal code');
}

hpFormHandler.prototype.validateNewPassword = function(ele1, ele2) {

	ele1 = $(ele1);
	ele2 = $(ele2);

	var MAX_LEN = 40;
	var MIN_LEN = 5;

	this.validateMandatory(ele1, 'Please protect your hubpages account with a password.');
	this.validateLengthMin(ele1, MIN_LEN, 'Your password is too short.  Protect your account by choosing a password that is at least  '+ MIN_LEN +' characters long.  Safety first!');
	this.validateLengthMax(ele1, MAX_LEN, 'Your password is too long; it will be difficult to type.  Please use a shorter password.');

	this.validateMandatory(ele2, 'Please confirm your password.');
	this.validateSameAs(ele1, ele2, 'Your passwords do not match.  Please retype them.');
}

hpFormHandler.prototype.validateTag = function(ele) {

	ele = $(ele);

	var MAX_LEN = 60;
	var MIN_LEN = 3;

	this.validateRegex(ele, /^[\w\s\$\-\'\%\&]*$/, "Please use only alphanumeric and $, ', % or & characters in your tag.");
	this.validateLengthMin(ele, 3, 'A tag should be at least three characters long.');
	this.validateLengthMax(ele, MAX_LEN, 'A tag should not be longer than 60 characters.');
}

hpFormHandler.prototype.validateGroupName = function(ele, groupNames) {
	// "groupNames" is an array of existing group names for this user

	this.validateMandatory(ele, 'Please specify a group name.');
	this.validateLengthMax(ele, 50, 'Group names may be no longer than 50 characters.');
	this.validateRegex(ele, /^[\w\s\$\-\'\%\&\!\?]*$/, "Please use only alphanumeric and $, ', -, %, !, ? or & characters in your group name.");

	// check if group name is already in use by this user
	existingName = groupNames.detect(function(name) {
		return($F(ele) == name);
	});
	this.testForError(existingName, ele, "You already have a group with this name.  Please select it from the list, or enter a new name.");
}


// end validators

// observe()
//.
// Installs the event handlers to perform validation and other actions on the form.
// 	Should typically be called after form close.
hpFormHandler.prototype.observe = function() {
	new Form.EventObserver(this.form, this._elemsChanged.bind(this));
}

// focusFirst()
//.
// Gives the first element in the form focus.  Generally a good idea for pages where
// 	the form is the main point of user interest.
hpFormHandler.prototype.focusFirst = function() {
	Form.focusFirstElement(this.form);
}

// tabOnEnter()
//.
// Causes the form to intercept the enter key and tab the focus to the next field
// 	instead of causing a submit.  Can also be called via the static method: 
// 	hpFormHandler.tabOnEnter('formName');
hpFormHandler.prototype.tabOnEnter = function() {
	hpFormHandler.tabOnEnter(this.form);
}

hpFormHandler.tabOnEnter = function(form) {

	if (!$(form)) return;

	var inputs = $A($(form).getElementsByTagName('input'));
	inputs.each(function(node) {  
		Event.observe(node, 'keydown', _handleInputKeypress, false);
	});
}

// ghostField(fromElement, toElement)
//.
// Handles setting up an handler to copy a value from a field to an element 
// 	(typically a div or span).  
// Has the following non-obvious behavior: 
// 		strips all html tags
// 		copies initial data when first called (beyond the handler)
//		does not currently support drop-downs or radio buttons (nput and textarea only)
hpFormHandler.prototype.ghostField = function(fromEle, toEle, copyFunc) {
	
	if ($(fromEle) && $(toEle)) {
		var gw = new GhostWatcher(fromEle, toEle, copyFunc);
	}
}

// setValidors({}, {})
hpFormHandler.prototype.setValidators = function(onChange, onSubmit) {

	// string will be eval-ed by the named validator
	// format is:
	// 	elem_id (or className): { validatorMethod: [additional args for validator] }
	// the validator will be run in this context and the first arg will be the node which
	// is being execauted.
	this.toValidate = $H(onChange);
	this.toValidateOnsubmit = $H(onSubmit);
}

hpFormHandler.prototype.hasErrors = function() {
	return (this.errors && this.errors.keys() && this.errors.keys().length > 0);
}

hpFormHandler.prototype.cancel = function() {

	this.reset();

}

hpFormHandler.prototype.reset = function() {

	Form.reset(this.form);
	if (this.cancelUri) 
		location.href = this.cancelUri;
}

hpFormHandler.prototype.save = function() {

	this._runValidators(true);
	if (this.hasErrors()) { return false; }

	if (window.tinyMCE && tinyMCE.triggerSave) {
		// can throw an exception sometimes (if tinyMCE isn't actually active)
		// so might as well catch and bury the exception
		try 
		{
			tinyMCE.triggerSave(false, true);
		} catch (e) {}
	}

	// send up form data in AJAX request if not submit
	//	submitMode (default is not submitMode)
	if (!this.submitMode) {

		this.params = 'ajax=1&' + Form.serialize(this.form);

		var myAjax = new Ajax.Request(this.submitUri,
		{ 
			method: this.method,
			parameters:this.params,
			onComplete:this.handleResponse.bind(this),
			onFailure:reportError
		});
	}

	// if not submit mode
	// always return false so the button doesn't do the navigation, 
	// the next page does it
	return (this.submitMode);
}

hpFormHandler.prototype.handleResponse = function(req) {

	eval(req.responseText);

	if (valid == 1)
	{	
		if (this.saveCallback)
		{
			this.saveCallback();
		}

		if (this.nextUri) 
			location.href = this.nextUri;

		return true;
	} else {

		this.handleSubmitServerError(req);
		return false;
	}
}

hpFormHandler.prototype.testForError = function(isError, ele, msg) {
	if (isError) {
		//if (!this.errors[msg]) { this.errors[msg] = msg; }
		var tmp = new Object();
		tmp[ele.id] = msg;
		this.errors = this.errors.merge(tmp);
	} else {
		if (this.errors[ele.id] && this.errors[ele.id] == msg) 
			delete this.errors[ele.id];
	}
}

hpFormHandler.prototype._elemsChanged = function(ele) {
	this._runValidators(false);
}

hpFormHandler.prototype._runValidators = function(isSubmit) {

	var formNodeList = Form.getElements(this.form);
	var nodes = $A(formNodeList);

	nodes.each(function(node) { 

		if (isSubmit) {
			var validator = this.toValidateOnsubmit[node.id];
			if (!validator) {  // if the id doesnt work, try the classname 
				validator = this.toValidateOnsubmit[node.className];
			}

			if (validator) {
				validator(node);
			}
		}

		var validator = this.toValidate[node.id];
		if (!validator) {  // if the id doesnt work, try the classname 
			validator = this.toValidate[node.className];
		}

		if (validator) {
			validator(node);
		}
	}.bind(this));  // so that we get the current context

	this._showErrors();
	return !this.hasErrors();
}

hpFormHandler.prototype._showErrors = function() {

	if (!this.errorDiv && !$('formErrors'))
		new Insertion.Top(this.form, '<div id="formErrors"></div>');

	this.errorDiv = $('formErrors');
		
	if (!this.errFade) {
		this.errFade = new fx.Opacity(this.errorDiv, { duration:500 } );
		this.errFade.now = 0;
	}

	if (!this.hasErrors()) {
		if (this.lit) {

			this.errFade.toggle();

			// remove alert borders from input elements
			var eles = document.getElementsByClassName('alertBorder', this.form);
			eles.each(function(ele) { hpFormHandler.lightEle(ele, false /* off */); });

			if ($('nextB')) {
				$('nextB').src = '/i/next.gif';
			}
			this.lit = false;
		}

		return;
	}

	var ehtml = this.errorHeader; 
	ehtml += '<ul>';
	this.errors.each(function(err) {
		ehtml += "<li>"+ err.value +"</li>";
		
		// light the input elements
		var ele = $(err.key);
		hpFormHandler.lightEle(ele, true /* on */);
	});
	ehtml += '</ul>';

	this.errorDiv.className = 'alert';

	if (!this.lit)
	{
		Element.setOpacity(this.errorDiv, 0);
		this.errFade.toggle();
		
		if ($('nextB')) {
			$('nextB').src = '/i/next_dis.gif';
		}
	}

	this.errorDiv.innerHTML = ehtml;
	this.lit = true;
}

function _handleInputKeypress(event) {
	event = event || window.event;

	if (event.which) {  // netscape
		if (event.which == Event.KEY_RETURN) {
			var newEvent = document.createEvent("KeyboardEvent")
			newEvent.initKeyEvent("keydown", true, true, document.defaultView,
                          event.ctrlKey, event.altKey, event.shiftKey,
                          event.metaKey, Event.KEY_TAB, 0);
			event.preventDefault();
			event.target.dispatchEvent(newEvent);
		}
	}
	else if (event.keyCode) { // IE
		if (event.keyCode==Event.KEY_RETURN) event.keyCode=Event.KEY_TAB;
	}
	return true;
}

hpFormHandler.lightEle = function(ele, on) {
	ele = $(ele);
	if (!ele) { return; }
	if (on) { Element.addClassName(ele, 'alertBorder'); }
	else { Element.removeClassName(ele, 'alertBorder'); }
}

// end hpFormHandler
// ***************************************************

// GhostWatcher helper class
// ***************************************************

var GhostWatcher = Class.create();

GhostWatcher.prototype = {

   initialize: function(fromEle, toEle, copyFunc) {
		this.fromEle = $(fromEle);
		this.toEle = $(toEle);
		this.copyFunction = (copyFunc != null) ? copyFunc : this.copyValue;
		
		if (this.fromEle && this.toEle) {
			// bind-fu
			Event.observe(this.fromEle, 'keyup', this.copyFunction.bind(this), false);
		}

		Event.observe(window, 'focus',this.copyFunction.bind(this), false);
		Event.observe(window, 'load', this.copyFunction.bind(this), false);
   },

	// The default ghoster
	// keep this light so that typing is responsive
	copyValue: function(evt) {
		var text = $F(this.fromEle);
		this.toEle.innerHTML = text.stripTags();
	},

	// BUGBUG: does not synthesize event (no on seems to use the event yet anyway)
	recopy: function() {
		this.copyFunction();
	}

};

// end GhostWatcher
// ***************************************************
// ************
// ModuleEdit: controls an individual Module
function ModuleEdit(id) {

	this.data = { 
		id: id,
		mod_data_id: null,
		div_id: null,
		art_id: null,
		type: null,
		links_json: '',
		images_json: '',
		links_added: 0,
		//color: null,
		position: 0
	};

	this.state = { 
		video_type: null,
		video_current_css: null,
		select_id: null,
		images_removed: [],
		link_cache: [],
		links: null,
		draft_saver: null, 
		origtext_width: 0,
		origtext_height: 0,
		last_hide: null,
		last_position: -1,
		delete_confirm_text: "Are you sure you want to delete this capsule?",

		// all this shit should move to resources
		content_id: 'modcont_'+ id,
		rss_results_id: id +'_rss_results',
		rss_edit_id: id +'_rss',
		amazon_searchindex_id: id +'_searchIndex',
		amazon_searchtype_id: id +'_searchType',
		amazon_asins_input_id: id +'_asins',
		amazon_results_id: id +'_amazon_results',
		comment_edit_id: id +'_comment',
		comment_moderated_id: id +'_moderated',
		comment_emailme_id: id +'_emailme',
		comment_noanonymous_id: id +'_anonymous',
		text_div_id: 'txtd_'+ id,
		link_search_id: id +'_link_search',
		link_search_type: 'del',
		link_results_id: id +'_link_search_results',
		link_edit_id: id +'_link',
		link_display_id: id + '_link_results',
		links_list: id +'_linkitem',
		link_url_id: id +'_url',
		link_title_id: id +'_link_title',
		link_desc_id: id +'_desc',
		link_submit_button: id +'_submit_button',
		link_status_id: 'test_status_'+ id,
		feed_id: id +'_feed',
		keywords_id: id +'_keywords',
		max_id: id +'_max',
		colorbar_id: 'colorbar_'+ id,
		hidebar_id: 'hidebar_'+ id,
		titlebar_id: 'titlebar_'+ id,
		title_id: id +'_title',
		title_input_id: id +'_titleinput',
		modal_id: id +'_modal',
		hide_id: id +'_hide',
		hide_text_id: id +'_hidetext',
		empty_notification_id: 'modempty_'+ id
	};

	this.editting = false;

	// holds positioning data for the layout of the capsule
	this.layout = {
		topleft: [15, 150], 
		bottomright: [15 + 450, 150 + 600],
		width: 450, 
		height: 600
	};

	// not currently used, i'd like to move all the element names here
	this.resources = { 
		// universal
		edit_button_id: 'edit_'+ id,
		discard_button_id: 'discard_'+ id,
		up_button_id: 'up_'+ id,
		down_button_id: 'down_'+ id,
		position_id: id +'_position',
		amazon_edit_id: id +'_amazon', // TODO
		draft_notice_id: 'draft_notice',
		draft_revert_id: 'draft_revert',
		json_links_id: 'json_links_'+ id,
		max_results_id: 'max_'+ id,
		// link
		link_edit_url: 'link_edit_url_'+id,			// edit tab, url
		link_edit_title: 'link_edit_title_'+id,		// edit tab, title
		link_edit_desc: 'link_edit_desc_'+id,		// edit tab, description
		link_drag_section: 'link_drag_'+id,
		link_drag_title: 'link_drag_txt_'+id,
		// image
		image_edit_id: 'image_' + id,				// capsule edit section
		image_caption_input: 'descinput_' + id,		// caption input
		image_size_input: 'sizeinput_' + id,		// max size input
		image_display_style_id: 'dispstyle_' + id,	// display type input
		image_popup_id: 'fullsize_' + id,			// show popup input
		image_upload_gif_id: 'upload_gif_' + id,	// upload animation
		image_upload_status_id: 'upload_status_'+ id,	// holds status iframe
		image_upload_target_id: 'upload_target_'+ id,	// status iframe
		image_load_form_id: 'add_image_form_'+ id,	// import/upload form
		image_import_id: 'url_' + id,				// import URL input
		image_upload_id: 'file_' + id,				// upload URL input
		image_import_section_id: 'image_import_' + id,	// import form subsection
		image_upload_section_id: 'image_upload_' + id,	// upload form subsection
		image_queue_id: 'url_import_list_' + id,	// load queue div
		image_drag_section: "image_drag_" + id,		// hubtool DnD thumbnails
		image_drag_title: 'image_drag_txt_'+id,		// DnD instructions
		image_selection_id: "image_selected_" + id,	// hubtool image edit display
		image_viewer_display: 'slide_display_' + id,	// hubtool slide display
		image_viewer_caption: 'slide_desc_' + id,	// hubtool slide caption
		// video
		video_edit_id: 'video_'+ id,
		video_url_input_id: 'video_url_'+ id,
		video_key_id: 'video_key_'+ id,
		video_type_id: 'video_type_'+ id,
		video_results_id: 'video_results_'+ id,
		// text
		text_edit_id: 'txte_'+ id,
		// ebay
		ebay_edit_id: id +'_ebay', // TODO
		ebay_results_id: id +'_ebay_results', // TODO
		ebay_preview_id: 'preview_'+ id,
		ebay_keywords_id: 'keywords_'+ id,
		ebay_seller_id: 'seller_'+ id
	};
}

// static property holds ModuleManager reference
ModuleEdit.Manager = null;

// ***************************************
// options used in the guts of the HubTool
ModuleEdit.options = { 
		capsule_edit_reposition_duration: 600, // ms
		upload_status_hide_timeout: 8 * 1000, // ms
		drag_enabled: true,
		drag_reorder_enabled: true,
		draft_save_enabled: false,
		draft_save_interval: 150 // s
};
// end options
// ***************************************


ModuleEdit.prototype.cleanUp = function() {

		ModuleEdit.Manager = null;
		this.editting = null;
		this.data = null;
		this.state = null;
};

// statics

ModuleEdit.make = function(type, art_id, add_callback) {

	var ajax = new Ajax.Request('/xml/modules.php',
		{ method:'post',
		parameters: 'new=1&art_id='+ art_id +'&type='+ type,
		onFailure: reportError,
		onComplete: add_callback
		});
};

ModuleEdit.toggleIcon = function(ele, disable) {

	ele = $(ele);	
	if (!ele) { return; }

	if (disable) {
		ModuleEdit.disableIcon(ele);
	} 
	else {
		ModuleEdit.reenableIcon(ele);
	}
};

ModuleEdit.disableIcon = function(ele) {

	ele = $(ele);
	if (!ele) { return; }

	ele.className = 'disabledicon';

	var anchor = ele.firstChild;
	if (!anchor.disabled) {
		var emptyFunction =	function() { return false; };

		if (anchor.href && anchor.href != '#') { anchor.bhref = anchor.href; }
		if (anchor.onclick) { anchor.bonclick = anchor.onclick; }
		if (anchor.alt) { anchor.balt = anchor.alt; }
		if (anchor.title) { anchor.btitle = anchor.title; }
		anchor.alt = '';
		anchor.title = '';
		anchor.href = '#';
		anchor.onclick = emptyFunction;
		anchor.disabled = true;
	}
};

ModuleEdit.reenableIcon = function(ele) {

	ele = $(ele);
	if (!ele) { return; }

	ele.className = 'icon';

	var anchor = ele.firstChild;
	if (anchor.disabled)
	{
		if (anchor.bhref) 		{ anchor.href = anchor.bhref; }
		if (anchor.bonclick) 	{ anchor.onclick = anchor.bonclick; }
		if (anchor.balt) 		{ anchor.alt = anchor.balt; }
		if (anchor.btitle) 		{ anchor.title = anchor.btitle; }

		anchor.disabled = false;
	}
};

// static method for hydrating module from a JSON blob
ModuleEdit.getFromJSON = function(json) {

	var module = new ModuleEdit(json.data.mod_id);

	// copy in the properties from the server (overwriting where already exist)
	Object.extend(module.data, json.data);
	Object.extend(module.state, json.state);

	return module;
};

// static method for hydrating module from a module document element
/*
ModuleEdit.getFromModuleElement = function(module_ele) {

	var module = new ModuleEdit(module_ele.getAttribute('mod_id'));

	module.data.div_id = module_ele.getAttribute('id');
	module.data.mod_id = module_ele.getAttribute('mod_data_id');
	module.data.type = module_ele.getAttribute('type');
	module.data.art_id = module_ele.getAttribute('art_id');
	module.data.horiz_id = parseInt(module_ele.getAttribute('horiz'), 10);
	module.data.hide = (module_ele.getAttribute('hide') == 1);
	
	module.isEmpty = (module_ele.getAttribute('empty') == 1);
	
	return module;
};
*/

ModuleEdit.toggleCurtain = function() 
{
	var curtain = $('modalarea');
	if (!curtain) 
	{
		var pole = new Insertion.Bottom(document.body, '<iframe class="modalarea" style="display:none" frameborder="0" id="addcaps_modalarea" src="about:blank"></iframe>');
	}

	if (curtain && curtain.style.display != 'block') // make visible, stretch to fit (IE)
	{
		curtain.style.display = 'block';
		curtain.style.left = '0px';
		curtain.style.top = '0px';
		ModuleEdit.stretchCurtain();

		this.stretchListener = ModuleEdit.stretchCurtain.bindAsEventListener(this);
		Event.observe(window, 'resize', this.stretchListener, false);
	} 
	else // make invisible
	{
		curtain.style.display = 'none';

		Event.stopObserving(window, 'resize', this.stretchListener, false);
		this.stretchListener = null;
	} 
};

ModuleEdit.stretchCurtain = function()
{
	var curtain = $('modalarea');

	// the logic to the madness is that the modal curtain is inserted at the tail end of
	// the body.  It needs to be outside of the element that we're using to measure
	// otherwise it affects the measurement (eg resize window large and the resized
	// curtain is the widest part of the parent element we're measuring).

	var WRAPPER_PAD = 20;
	
	var wrapHeight = Element.getHeight('container') + Element.getHeight('header_wrap') + Element.getHeight('browse_wrap');
	var navBottom = Element.getHeight('sidebar') + Position.cumulativeOffset($('sidebar'))[1];

	curtain.style.width = (Element.getWidth('wrapper') + WRAPPER_PAD) +'px';

	// strecht the curtain as tall as the content or the absoutely positioned sidebar, 
	// whichever is taller

	curtain.style.height = ((wrapHeight > navBottom) ? wrapHeight : navBottom) + 'px'; 
};

// end statics

ModuleEdit.prototype.toggle = function() {

	this.editting = !this.editting;

	// save original HTML for revert changes discard
	if (this.editting)
	{
		this.state.original_html = $(this.data.div_id).innerHTML;
	}

	this._toggleTitle(); 		// recently moved from render
	this._toggleBorder();		// recently moved from render
	this._toggleEditButton();	// recently moved from render
	this._toggleDiscardButton();// recently moved from render
	this._toggleDeleteButton(); // recently moved from render

	this._toggleZIndex();
	this._toggleSize();
	this._toggleDrag();
	this._toggleEmptyNotification();

	switch (this.data.type) {
		case 'Text':
			this.toggleText();
			break;
		case 'Ebay':
			this.toggleEbay();
			break;
		case 'Amazon':
			this.toggleAmazon();
			break;
		case 'Rss':
			this.toggleRss();
			break;
		case 'News':
			this.toggleNews();
			break;
		case 'Link':
			this.toggleLink();
			break;
		case 'Image':
			this.toggleImage();
			break;
		case 'Video':
			this.toggleVideo();
			break;
		case 'Comment':
			this.toggleComment();
			break;
	}

	if (this.editting) {
		this.setDirty();
	}
	else {
		this.save();
	}

	this.render();
};

ModuleEdit.prototype._toggleZIndex = function()
{
	var thediv = this.state.div_ele;

	// Here we take care of zIndex settings for modules
	// in their various states (editting or not, floated or not)
	// This code is prone to making IE crash outright so it is heavily
	// commented to warn of past misteps

	// About zIndex: please note that an element must have position (relative or absolute)
	// in order to honor zIndex.  Non positioned elements will not honor it.  

	// zIndex layers in the hubtool:
	// 10	the currently editting module
	// 5	modalarea curtain (translucent div), covers up non-editted modules
	// 1	non-editting modules (note they are positioned static anyway)

	// IE crashing: IE6 will crash when elements are set to position relative and
	// they have their zIndex set within a short period of time.  
	// I'm still trying to determine in which cases
	// this occurs exactly.


	if (this.editting)
	{
		// If the module is being editted, it's zIndex must be set high,
		// in order to float about the edit curtain
		thediv.style.zIndex = '10';
	}
	else // not editting
	{
		thediv.style.zIndex = '1';
	}
};

ModuleEdit.prototype._toggleSize = function()
{
	var thediv = $(this.data.div_id);

	// note that height is set in renderSize because it needs to be updated
	// as content flows, not just when toggled

	if (this.editting)
	{
		var MODULES_PADDING = 5;
		var LEFT_MARGIN = 122;
		var TOP_EDIT_SPACING_RATIO = 0.15; // ratio of viewport height edit will be placed
		var move_dur = ModuleEdit.options.capsule_edit_reposition_duration;

		var mods = $('modules');
		var mod_offset = Position.cumulativeOffset(thediv);
		var new_width = 520 + ( MODULES_PADDING * 2 ); // Element.getWidth(mods) - MODULES_PADDING;
		var doc_lay = ModuleEdit.Manager.layout;

		// positioning the element some percent of the viewport down
		var scrollOff = doc_lay.viewportScroll[1] + (doc_lay.viewportSize[1] * TOP_EDIT_SPACING_RATIO);

		thediv.style.width = new_width +'px';

		var editMove = new fx.Position(thediv, {duration: move_dur});

		thediv.style.position = "absolute";
		Position.set(thediv, mod_offset);

		editMove.move(mod_offset, [LEFT_MARGIN, scrollOff]);

		// recompute the layout of the element for fast use by the
		// stretch curtain code
		this.computeState();

		/*
		// animation to change the width of the module
		var changeWidth = new fx.Combo(thediv, {duration: 1000,
			width: true,
			height: true,
			opacity: false,
			onComplete: function() {}.bind(this) } );
		changeWidth.customSize('100%', new_width);
		changeWidth.toggle();
		*/
	}
	else
	{
		thediv.style.position = 'static';
		thediv.style.width = 'auto';
		thediv.style.top = thediv.style.left = '0px';
	}
};

ModuleEdit.prototype._toggleDrag = function()
{
	var thediv = $(this.data.div_id);
	var thebar = $("dragbar_"+ this.data.id);

	if (ModuleEdit.options.drag_enabled && this.editting) {
		var d = new Dragger(thediv, false);
		// , means "evaluate left argument, evaluate right argument then return the right arg
		Event.observe(thebar, "mousedown", (function() { d.start(); return false; }).bind(d), false);
        Event.observe(document, "mouseup", d.stop.bind(d), false);
	}
};

ModuleEdit.prototype._toggleEmptyNotification = function() {

	if (this.editting && this.data.div_id) {
		Element.hide(this.state.empty_notification_id);
	}
};

ModuleEdit.prototype._toggleTitle = function() {

	var title = $(this.state.title_id);
	if (!title) { return; }

	if (this.editting) {
		// populate the title input box with the current title for the module
		$(this.state.title_input_id).value = $(this.state.title_id).innerHTML;
	}
	else {
		// save the current title back into the data object for saving
		// and rendering
		this.data.title = $F(this.state.title_input_id).replace(/<[^>]+(>|$)/g, "");

		if (this.data.title !== null) {
			Element.update(this.state.title_id, this.data.title);

			// turn off display for empty title
			if( this.data.title == '' ) {
				$(this.state.title_id).style.display = 'none';
			} else {
				$(this.state.title_id).style.display = '';
			}
		}
	
	}

	// show or hide the colorbar
	this.toggleDisplay(this.state.colorbar_id);
	this.toggleDisplay(this.state.hidebar_id);
	this.toggleDisplay(this.state.titlebar_id);
	this.toggleDisplay(title, !this.editting);
};

ModuleEdit.prototype.toggleAmazon = function() {

	this.toggleDisplay(this.resources.amazon_edit_id);

	if (!this.editting) 
	{
		if ($(this.state.amazon_searchindex_id)) 
		{
			this.data.searchtype = $F(this.state.amazon_searchtype_id);
			this.data.searchindex = $F(this.state.amazon_searchindex_id);
			this.data.maxitems = $F(this.state.max_id);
			this.data.keywords = $F(this.state.keywords_id);
			this.data.asins = $F(this.state.amazon_asins_input_id);
		}
		this.onclickAmazonPreview();
	}
};

ModuleEdit.prototype.toggleComment = function() {

	this.toggleDisplay(this.state.comment_edit_id);

	if (!this.editting) 
	{
		if ($(this.state.comment_moderated_id)) {
			this.data.moderated = $(this.state.comment_moderated_id).checked;
			this.data.emailme = $(this.state.comment_emailme_id).checked;
			this.data.anonymous = $(this.state.comment_noanonymous_id).checked;
		}
	}
};

ModuleEdit.prototype.toggleEbay = function() {

	this.toggleDisplay(this.resources.ebay_edit_id);

	if (this.editting) 
	{
		// preview on ENTER press in input fields
		fireOnReturn($(this.resources.ebay_keywords_id), this.onclickEbayPreview.bind(this));
		fireOnReturn($(this.resources.ebay_seller_id), this.onclickEbayPreview.bind(this));
		// bind to max results dropdown
		Event.observe($(this.resources.max_results_id), 'change', this.onclickEbayPreview.bindAsEventListener(this), false);
		// bind preview button itself
		Event.observe($(this.resources.ebay_preview_id), 'click', this.onclickEbayPreview.bindAsEventListener(this), false);
	} 
	else 
	{
		this.data.keywords = $F(this.resources.ebay_keywords_id);
		this.data.seller = $F(this.resources.ebay_seller_id);
		this.data.maxitems = $F(this.resources.max_results_id);

		this.onclickEbayPreview();
	}
};

ModuleEdit.prototype.toggleLink = function() {

	this.toggleDisplay(this.state.link_edit_id);

	if (this.editting) 
	{
		this.state.select_id = null;
		Element.hide(this.state.link_display_id);  // hide standard display

		fireOnReturn($(this.state.link_search_id), this.onclickLinkFind.bind(this));

		if (!this.state.links)
		{
			this.loadLinkState();
		}

		// create the reorder item and populate it with JSON data
		this.state.reorder = new ModuleContentReorder(this.resources.link_drag_section, 1, 150, 30);
		$H(this.state.links.links).values().each(function(link) {
			if(!link.id) return;  // continue
			this.addLinkDragElement(link.id, link.name);
		}.bind(this));
		this.state.reorder.relayout();
	}
	else 
	{
		Element.show(this.state.link_display_id);

		// re-render HTML elements by DnD canvas order
		// NOTE:  redraw instead of resort, because IDs for added links may be
		// out of synch between the HTML and JSON
		$(this.state.links_list).innerHTML = "";
		this.state.reorder.items.each(function(item) {
			var link = this.state.linkIndex[item.item_id];

			var url = link.url;
			var desc = link.description;
			var title = link.name;

			var newlink = document.createElement("li");
			newlink.id = 'linkitem_'+item.item_id;
			newlink.innerHTML = '<a class="linkext" target="new" href=' +
				url + '>' + title + '</a><p>' + desc + '</p>';
			$(this.state.links_list).appendChild(newlink);
		}.bind(this));

		// set position in JSON based on drag order
		var linkinfo = this.state.reorder.getitemstateRec();
		for(pos in this.state.links.links) {
			var link = this.state.links.links[pos];
			if(!link.id || !linkinfo[link.id])  continue;
			var newpos = linkinfo[link.id].position;
			if(newpos != pos || link.isnew) {
				link.move = true;
				link.newpos = newpos;
				link.dirty = true;
			}
		}

		// dehydrate link state out into data (sent to server via save())
		this.data.links_json = JSONstring.make(this.state.links);

		// delete reorder item and canvas elements
		$(this.resources.link_drag_section).innerHTML = "";
		// TODO is this sufficient?  do we have to delete drag items?
		delete this.state.reorder;
	}
};

ModuleEdit.prototype.loadLinkState = function() 
{
	if ($(this.resources.json_links_id))
	{
		this.state.links = JSONstring.toObject($(this.resources.json_links_id).innerHTML);

		// construct an index of JSON records for easy access by ID
		this.state.linkIndex = Object();
		var links = $H(this.state.links.links).values();
		links.each(function(link) {
			if(!link.id) return;  // continue
			this.state.linkIndex[link.id] = link;
		}.bind(this));
	}
};

ModuleEdit.prototype.toggleImage = function() {

	this.toggleDisplay(this.resources.image_edit_id);

	if (!this.editting) {

		if (this.data.dirty) {

			// generate JSON state data to pass to server
			var imgState = this.state.reorder.getitemstate();
			imgState.each(function(imgRec) {
				imgRec.caption = this.state.photoData[imgRec.id].caption;
				imgRec.maxSize = this.state.photoData[imgRec.id].maxSize;
			}.bind(this));

			this.state.images_removed.each(function(id) {
				imgState.push({id: id, caption: '', position: -1});
			});

			this.data.images_json = JSONstring.make(imgState);

			// set global display status
			if ($(this.resources.image_display_style_id)) {
				this.data.dispTypeId = $F(this.resources.image_display_style_id);
				var elem = $(this.resources.image_display_style_id);
				this.state.image_ctrl.displayStatus = elem.options[elem.selectedIndex].text;
				// Do we need this?
				// if(this.state.image_ctrl.displayStatus == "Normal")
				//	this.state.image_ctrl.displayStatus = "Default";
			}

			if ($(this.resources.image_popup_id)) {
				this.data.showFullSize = ($F(this.resources.image_popup_id) == "on") ? 1 : 0;
				// TODO update tool?
			}

			// update image viewer control with current data
			this.state.image_ctrl.clear();
			this.state.reorder.items.each(function(item) {
				this.state.image_ctrl.addPhoto(this.state.photoData[item.item_id]);
			}.bind(this));

			// update currently displayed section
			// TODO would it be worthwhile to clear out other sections?
			$(this.resources.image_viewer_display).style.height = this.state.image_ctrl.getMaxDisplayHeight() + "px";
			this.state.image_ctrl.render();
		}

		this.state.image_ctrl.toggleViewer();
		Element.show(this.state.content_id);
		Element.hide(this.resources.image_selection_id);
		$(this.resources.image_selection_id).innerHTML = "";

		// delete reorder item and canvas elements
		$(this.resources.image_drag_section).innerHTML = "";
		// TODO is this sufficient?  do we have to delete drag items?
		delete this.state.reorder;
		delete this.state.photoData;
	} else {

		Element.hide(this.state.content_id);
		Element.show(this.resources.image_selection_id);

		// create a pointer to the image control, if there isn't already one
		if(!this.state.image_ctrl) {
			eval("this.state.image_ctrl = imgCtrl_"+this.data.id);
		}
		this.state.image_ctrl.stopTimer();  // stop slideshow

		// create the reorder and photo data array, populate them
		this.state.reorder = new ModuleContentReorder(this.resources.image_drag_section, 8, 50, 50);
		this.state.photoData = new Object();
		this.state.image_ctrl.photoOrder.each(function(img_id) {
			// clone the photo data, so on DISCARD the state is intact
			var rec = this.state.image_ctrl.photoData[img_id];
			this.state.photoData[img_id] = {};
			for(key in rec) { this.state.photoData[img_id][key] = rec[key]; }
			this.addImageDragElement(img_id, rec.urlThumb, rec.caption);
		}.bind(this));
		this.state.reorder.relayout();

		// load first image into viewer
		this.loadImageIntoEditor(this.state.image_ctrl.photoOrder[0]);
	}
};

ModuleEdit.prototype.toggleVideo = function() {

	this.toggleDisplay(this.resources.video_edit_id);

	if (!this.editting) 
	{
        this.onclickVideoPreview();
		if ($(this.resources.video_url_input_id)) {
			this.data.url = $F(this.resources.video_url_input_id);
		}

	}
};

ModuleEdit.prototype.toggleRss = function() {

	this.toggleDisplay(this.state.rss_edit_id);

	if (!this.editting) {
		if ($(this.state.feed_id)) {
			this.data.feed = $F(this.state.feed_id);
			this.data.maxitems = $F(this.state.max_id);

			this.onclickRssPreview();
		}
	}
};

ModuleEdit.prototype.toggleNews = function() {

	this.toggleDisplay(this.state.rss_edit_id);

	if (!this.editting) {
		if ($(this.state.keywords_id)) {
			this.data.keywords = $F(this.state.keywords_id);
			this.data.maxitems = $F(this.state.max_id);

			this.onclickRssPreview();
		}
	}
};

ModuleEdit.prototype.draftSaverExecuter = function() {

	if (browser == 'Konqueror')
	{
		var text_div = $(this.state.text_div_id);

		// prepare for save
		//tinyMCE.triggerSave(false, true);

		// select our instance incase there are multiple EDITs in progress	
		var params = $H({id: this.data.id, 
						save: 1, 
						art: this.data.art_id,
						content: tinyMCE.getContent()
						}).toQueryString();

		var ajax = new Ajax.Request(
			'/xml/textdraft.php',
			{	
				parameters: params,
				onFailure: reportError,
				onComplete: function() { out('Saved draft'); }
			}
		);

		// keep tinyMCE in paragraph editting mode (avoid the large cursor)
		var inst = tinyMCE.getInstanceById( text_div.id );
		tinyMCE.execInstanceCommand(inst.id, 'FormatBlock', false, 'p');
	}
};

ModuleEdit.prototype.toggleText = function() {

	/* PSEUDO CONSTANTS USED IN EDITOR INITIALIZATION */
	var FUDGE_WIDTH = -22;
	var MIN_HEIGHT = 150;
	var ADDITIONAL_HEIGHT = 75;
	/*******/

	//this.toggleDisplay(this.resources.text_edit_id);

	var text_div = $(this.state.text_div_id);
	var inst; // hold reference to our tinyMCE

	if (this.editting) 
	{
		// show txtd_ div if it was hidden (as it is when empty)
		//Element.show(text_div);

		// this uses the overall div width not the text width because of weirdness when a
		// module is floated, editted, then unfloated
		text_div = $(this.state.text_div_id);

		// renderin and sizing code for tinyMCE
		this.state.origtext_width = parseFloat(Element.getWidth(this.data.div_id)) + FUDGE_WIDTH;
		this.state.origtext_height = parseFloat(Element.getHeight(text_div));

		var edit_height = (this.state.origtext_height < MIN_HEIGHT) ? MIN_HEIGHT : this.state.origtext_height;

		edit_height += ADDITIONAL_HEIGHT;

		//alert("edit_height: "+ edit_height);
		
		text_div.style.height = edit_height +'px';
		text_div.style.width = this.state.origtext_width +'px';
		
		// if the current browser is Konqueror
		// don't use tinyMCE
		if (browser == 'Konqueror')
		{
			Element.update( text_div.id, '<textarea id="txta_konqueror" rows="10" cols="80">'+ text_div.innerHTML +'</textarea>');
		}
		else // if the current browser is anything else
		{
			// not sure what this was for but it seems to break Safari
			//tinyMCE.idCounter = 0;
			tinyMCE.execCommand("mceAddControl", false, text_div.id);
			inst = tinyMCE.getInstanceById( text_div.id );

			// get tiny to start in paragraph editting mode (avoid the large cursor)
			tinyMCE.execInstanceCommand(inst.id, 'FormatBlock', false, 'p');

			this.state.mce_ele_id = inst.targetElement.id;
		}

		// setup autosave code
		if (ModuleEdit.options.draft_save_enabled)
		{
			this.state.draft_saver = new PeriodicalExecuter(this.draftSaverExecuter.bind(this), 
										ModuleEdit.options.draft_save_interval);
		}
	} 
	else 
	{
		// cleanup the textarea for Konqueror
		if (browser == 'Konqueror')
		{

			this.data.content = $("txta_konqueror").value;
			Element.update(this.state.text_div_id, this.data.content);
		}
		else
		{
			// cleanup the tinyMCE for everything else
			inst = tinyMCE.getInstanceById(this.state.text_div_id);
			
			// if the toggle was from a DISCARD CHANGES, there will be no inst there
			if (inst)
			{
				// HACKHACK: bizarre Firefox bug causes the iframe reference to be lost
				// and going to re-attach a new reference points to an empty window object
				// which doesn't reference the rest of the document
				if ( $(this.state.mce_ele_id) ) {
					inst.contentWindow = $(this.state.mce_ele_id).contentWindow;
					inst.contentWindow.document.body.innerHTML = inst.contentDocument.body.innerHTML;
				}

				// prepare for save
				tinyMCE.triggerSave(false, true);
				this.data.content = tinyMCE.getContent();
				// HACKHACK tinyMCE 2.1.1.1 getContent() sometimes returns blank on IE --
				// apparently a timing issue due to an earlier IE bug fix that compares
				// timestamps in _cleanupHTML(), who gets called three (!) times on a
				// SAVE -- sometimes the first two calls get the same timestamp.  So: if
				// we get a blank result, we will pause for a timestamp cycle.  A really
				// crappy cycle-wasting solution, hopefully temporary!
				// (tinyMCE timestamp code is older, btw, so something is
				// either running faster, or one of these cleanup calls is new)
				// (an alternative solution is to grab innerHTML instead, but then
				// there are mce serialization tags to clean up)
				//this.data.content = inst.contentWindow.document.body.innerHTML;
				if(this.data.content == '') {
					var rightthen = new Date();
					while(true) {
						var rightnow = new Date();
						if(rightnow.getTime() > rightthen.getTime())
							break;
					}
					this.data.content = tinyMCE.getContent();
				}

				tinyMCE.execCommand("mceRemoveControl", false, this.state.text_div_id);
			}

			// HACKHACK: either a Firefox bug or a bug with the new tinymce, there is a 
			// leftover hidden input element in the DOM after the editor has been removed
			// delete it
			text_div = $(this.state.text_div_id);
			if (text_div.nodeName.toLowerCase() == 'input')
			{
				text_div.parentNode.removeChild(text_div);
			}
		}

		// NOTE: now this reference is to the correct div (see above)
		text_div = $(this.state.text_div_id);

		// turn off the autosaver
		if (this.state.draft_saver) {
			this.state.draft_saver.callback = function() {};
			this.state.draft_saver.frequency = 0;
			delete this.state.draft_saver;
		}
		
		Element.update(text_div, this.data.content);
		text_div.style.display = '';

		// NOTE: 4/21 just changed this width to auto.  I don't know
		// why I felt the need to set it explicitly but it was blowing
		// out text modules that were floated because it would toggle
		// out of edit while in full width then slide to half-width and
		// the text area would blow out the side.
		text_div.style.width = 'auto';
		text_div.style.height = 'auto';

		delete this.state.origtext_width;
		delete this.state.origtext_height;
	}
};

ModuleEdit.prototype.discard = function() {

	this.setClean();

	// toggle the module back to the preview state, but the contents will be overwritten shortly
	ModuleEdit.Manager.toggle(this.data.id);

	// not using Element.update because it evals scripts (which is not what we want here)
	$(this.data.div_id).innerHTML = this.state.original_html;

	// remove old html reference (can be large)
	this.state.original_html = null;
	delete this.state.original_html;

	// for links, we need to reload JSON
	if(this.data.type == 'Link') {
		this.loadLinkState();
	}
};

ModuleEdit.prototype.computeState = function() 
{ 
	var floatPosition = this.getHoriz();
	this.state.floatleft = (floatPosition < 2) ? true : false;
	this.state.floatright = (floatPosition > 2) ? true : false;
	this.state.floatnone = (floatPosition == 2) ? true : false;

	// recache the div element
	var thediv = this.state.div_ele = $(this.data.div_id);

	if (this.editting)
	{
		var l = this.layout;
		l.topleft = Position.cumulativeOffset(thediv); 
		l.height = Element.getHeight(thediv);
		l.width = Element.getWidth(thediv);
		l.bottomright = [l.topleft[0] + l.width, l.topleft[1] + l.height];
	}
};

// override will cause an element to be shown or not regardless of 
// the current state
ModuleEdit.prototype.toggleDisplay = function(item, override) 
{ 
	item = $(item);

	var show;
	if (override != null) // if parameter is not present, override will be null (not false)
	{
		show = override;
	}
	else 
	{
		show = this.editting;
	}

	if (item) 
	{
		var current_display = item.style.display;
		// don't toggle if toggle would be ineffectual
		if (show && current_display == 'none')
		{
			item.style.display = '';
		}
		else if (!show && current_display != 'none')
		{
			item.style.display = 'none';
		}
	}
};

ModuleEdit.prototype.setDirty = function() { this.data.dirty = true; };
ModuleEdit.prototype.setClean = function(xhttp, json) 
{ 
	this.data.dirty = false; 
	
	// clean up the link module actions
	if (this.data.type == 'Link') 
	{
		// if there's JSON state info, reset the innerHTML with it
		if(json) {
			$(this.resources.json_links_id).innerHTML = JSONstring.make(json);
			this.loadLinkState();
		}

		//this.data.links_added = '';
	}
};

ModuleEdit.prototype.getHoriz = function() 
{ 
	return (this.isLast()) ? 2 : this.data.horiz_id; 
};
ModuleEdit.prototype.setHoriz = function(position) { this.data.horiz_id = position; };
ModuleEdit.prototype.isLast = function() { return this.state.is_last; };
ModuleEdit.prototype.isFloated = function() { return (this.getHoriz() != 2); };


// the most complete re-render called when a float change is made
ModuleEdit.prototype.renderExhaustive = function() {
	if(this.data.type == 'Image') {
		// image height/width must be set explicitly
		this.adjustImageWidth();
	}

	this.computeState();

	this._renderFloat(); 
	this.render();
}

ModuleEdit.prototype.setColorbar = function(pos) {
	// only floated modules have colorbar.  image, video, comments *never*
	// have colorbar
	if(this.data.type == "Image" || this.data.type == "Video" || this.data.type == "Comment") return;
	if(pos == 3) {
		// half width, enable colorbar
		$("nocolor_"+this.data.id).style.display = 'none';
		$("yescolor_"+this.data.id).style.display = '';
	} else {
		// else disable colorbar
		$("nocolor_"+this.data.id).style.display = '';
		$("yescolor_"+this.data.id).style.display = 'none';
	}
}

ModuleEdit.prototype.adjustImageWidth = function() {
	// make sure the width/height of image capsule is correct after
	// horizontal or vertical move
	// TODO can we move most/all of this into J44?
	if(this.data.type != "Image") return;

	if(!this.state.image_ctrl) {
		eval("this.state.image_ctrl = imgCtrl_"+this.data.id);
	}

	if(this.data.horiz_id > 2 && !this.isLast()) {
		if(this.state.image_ctrl.floatStatus == 'none') {
			// need to change to half width
			this.state.image_ctrl.floatStatus = 'right';
			$(this.resources.image_viewer_display).style.height = this.state.image_ctrl.getMaxDisplayHeight() + "px";
			$(this.resources.image_viewer_caption).className = 'caption_half';
		}
	} else {
		if(this.state.image_ctrl.floatStatus == 'right') {
			// need to change to full width
			this.state.image_ctrl.floatStatus = 'none';
			$(this.resources.image_viewer_display).style.height = this.state.image_ctrl.getMaxDisplayHeight() + "px";
			$(this.resources.image_viewer_caption).className = 'caption_full';
		}
	}

	var padstatus = this.state.image_ctrl.displayStatus;
	if(padstatus == 'No Border' || padstatus == 'With Border') {
		// reload inline image list
		this.state.image_ctrl.renderInlineImages();
	} else {
		// slideshow or thumbnail view -- reload viewer image
		this.state.image_ctrl.loadSlide(this.state.image_ctrl.viewer_id);
	}
}

ModuleEdit.prototype.render = function() {

	this.computeState();

	this._renderSize();
	this._renderHide();
	this._renderBackground();	// confusing
	//this._renderPosition();
	this._renderTypeSpecific();

	// buttons
	this._renderMoveButtons();
	this._renderFloatButtons();
};

ModuleEdit.prototype._renderSize = function()
{
	var thediv = this.state.div_ele;

	thediv.style.height = 'auto';

	/*
	if (this.editting) {

		// if the auto height is too tall to comfortably fit 
		// in the current viewport, override the height and shrink it
		var auto_height = Element.getHeight(thediv);
		var comfortable_height = ModuleEdit.Manager.layout.viewportSize[1] * 0.70;
	}
	*/
};

ModuleEdit.prototype._renderFloat = function()
{
	var thediv = this.state.div_ele;

	if (!this.editting)
	{
		// setting the DIV style to inline-block so that the div is not hidden after toggle
		// on the Mac (FF & Safari).  Inline-block for floated content seems to fix this.
		if (!this.state.floatnone)
		{
			thediv.style.display = 'inline-block';
		}
		else
		{
			thediv.style.display = 'block';
		}
	}
};

ModuleEdit.prototype._renderTypeSpecific = function()
{
	switch (this.data.type) {
		case 'Video':
			this._renderVideo();
			break;
	}
};

ModuleEdit.prototype._renderHide = function()
{
	var cont = $(this.state.content_id);
	if (!cont) { return; }

	// only paint the hide related UI when hide state has changed
	if (this.state.last_hide != this.data.hide)
	{
		this.toggleDisplay(cont, !this.data.hide);
		Element.update(this.state.hide_text_id, this.data.hide ? '&nbsp;hidden' : '');

		if ($(this.state.hide_id)) {
			$(this.state.hide_id).checked = this.data.hide;
		}
		this.state.last_hide = this.data.hide;
	}
	
};

ModuleEdit.prototype._toggleBorder = function()
{
	var ele = $(this.data.div_id);
	if (!ele) { return; }

	var border = (this.editting) ? 'solid 1px #ccc' : 'none'; // solid 1px #ccc';
	ele.style.borderRight = border;
	ele.style.borderLeft = border;
	ele.style.borderBottom = border;
};

ModuleEdit.prototype._renderBackground = function()
{
	var ele = $(this.state.content_id);
	if (!ele) { return; }

	var newColor = -1;
	if (this.data.color != null) {
		newColor = ModuleEdit._getBackgroundColor('color'+ this.data.color);
	}

	// if no color state exists, determine the color from the DOM
	if (!this.state.current_color) {
		this.state.current_color = Element.getCurrentStyle(ele).backgroundColor;
	}

	// if a color change has been made, do the effect and change update state
	if (this.state.current_color && newColor != -1 && newColor != this.state.current_color) {
		
		var colorize = new fx.Color(ele, {duration: 750,
			onComplete: function() { Element.addClassName(ele, 'color'+ this.data.color); }.bind(this) 
		});

		colorize.customColorRGB(this.state.current_color, newColor);
		this.state.current_color = newColor;	
	}
};

ModuleEdit._getBackgroundColor = function(cssClass) {

	var eles = document.getElementsByClassName(cssClass);

	if (eles.length > 0) {
		var styles = Element.getCurrentStyle(eles.first());
		return styles.backgroundColor;
	} else {
		return null;
	}
};

ModuleEdit.prototype._renderMoveButtons = function()
{
	ModuleEdit.toggleIcon(this.resources.up_button_id, (this.editting || this.data.position == 0));
	ModuleEdit.toggleIcon(this.resources.down_button_id, (this.editting || this.isLast())); 
};

ModuleEdit.prototype._renderFloatButtons = function()
{
	if(this.data.type == 'Comment') {
		ModuleEdit.toggleIcon('cent_'+this.data.id, true);
		ModuleEdit.toggleIcon('rt_'+this.data.id, true);
		return;
	}

	var disable = (this.editting || this.state.floatnone);
	ModuleEdit.toggleIcon('cent_'+ this.data.id, disable); 

	disable = (this.editting || this.state.floatright || this.isLast());
	ModuleEdit.toggleIcon('rt_'+ this.data.id, disable); 
};

ModuleEdit.prototype._toggleDeleteButton = function()
{
	ModuleEdit.toggleIcon(this.data.id +'_delete', (this.editting));
};

ModuleEdit.prototype._toggleDiscardButton = function()
{
	this.toggleDisplay(this.resources.discard_button_id);
};

ModuleEdit.prototype._toggleEditButton = function()
{
	$(this.resources.edit_button_id).innerHTML = (this.editting) ? '&nbsp;SAVE&nbsp;' : '&nbsp;EDIT&nbsp;';
};

// update the Position: label
ModuleEdit.prototype._renderPosition = function() 
{
	var newPosition = this.data.position + 1;
	if (this.state.last_position != newPosition)
	{
		Element.update(this.resources.position_id, String(newPosition));
		this.state.last_position = newPosition;
	}
};

ModuleEdit.prototype._renderVideo = function() 
{
	// set to the correct classname for floated or otherwise
	var size = (this.state.floatnone) ? 'Big' : 'Small';
	var css = 'video'+ this.state.video_type + size;
	
	if (this.state.video_css_class != css) 
	{
		var res_ele = $(this.resources.video_results_id);
		if (res_ele)
		{
			var embed = res_ele.getElementsByTagName('embed')[0];
		}

		if (embed) 
		{ 
			// one last check to prevent wasteful setting of the CSS class
			if (embed.className != css)
			{
				embed.className = css;
			}

			// cache the class to reduce wasteful DOM churn
			this.state.video_css_class = css;
		}
	}
};

ModuleEdit.prototype.onclickDraftShow = function() {

	var params = 'id='+ this.data.id +'&art='+ this.data.art_id;

	var ajax = new Ajax.Request('/xml/textdraft.php',
	{	
		parameters: params,
		onFailure: reportError,
		onComplete: this._draftShowHandler.bind(this)
	});

	return false;
};

ModuleEdit.prototype._draftShowHandler = function(req)
{
	try {
		var resp = JSONstring.toObject( req.responseText );
	} catch(e) {}
	
	if (resp)
	{
		this.state.original_text = tinyMCE.getContent();
		tinyMCE.setContent(resp.draftText);

		Element.update(this.resources.draft_notice_id, "This is the draft that we saved.  Click 'SAVE' to keep it or <a href=\"#\" id=\""+ this.resources.draft_revert_id +"\">go back to your saved text.</a>");

		$(this.resources.draft_revert_id).onclick = this.onclickDraftRevert.bindAsEventListener(this);
	}
};

ModuleEdit.prototype.onclickDraftRevert = function() 
{
	tinyMCE.setContent(this.state.original_text);
	Element.remove(this.resources.draft_notice_id);

	return false;
};

ModuleEdit.prototype.unselectDragElements = function() {
	// restore the default background color to all DnD elements
	this.state.reorder.items.each(function(item) {
		item.ele.style.backgroundColor = '';
	});
};

ModuleEdit.prototype.wireLinkCtrlHandlers = function()
{
	var eles = document.getElementsByClassName('linkedit', this.data.div_id);
	eles.each(function(ele) {
		ele.onclick = this.onclickLinkFindAdd.bindAsEventListener(this);
	}.bind(this));
};

ModuleEdit.prototype.onclickLinkFindAdd = function(event) {
	event = event || window.event;

	var elem = Event.findElement(event, 'div').parentNode; // result div
	var ext_link = document.getElementsByClassName('linkext', elem)[0];
	this.addLink(ext_link.href, ext_link.innerHTML, '');
	Element.remove(elem);

	return false;
};

ModuleEdit.prototype.onclickLinkAdd = function() {
	var url = $F(this.state.link_url_id).trim().toString();
	var title = $F(this.state.link_title_id).trim().toString();
	var desc = $F(this.state.link_desc_id).trim().toString();
	// TODO pretty hacky all around, but we want fully-qualified URLs.
	// probably we should add a formhandler to this instead, or at least
	// improve this regex (currently accepts ftp, http, https)
	regex = /^(ftp|https?):\/\/.+/;
	if(!regex.test(url.toLowerCase())) {
		url = "http://" + url;
	}
	this.addLink(url, title, desc);
}

ModuleEdit.prototype.addLink = function(url, title, desc) {
	if (url.length === 0) { return; }
	if (title.length === 0) { title = url; }

	var l_id = '';

	// create a new id and increment links_added
	this.data.links_added++;
	l_id = 'n'+ (this.data.links_added - 1);

	this.state.links.links[l_id] = {id: l_id,
		dirty: true,
		isnew: true,
		url: url,
		name: title,
		description: desc
	};
	this.state.linkIndex[l_id] = this.state.links.links[l_id];

	// create drag element
	this.addLinkDragElement(l_id, title.toString());
	this.state.reorder.relayout();

	// revert link edit UI to normal state
	Field.clear(this.state.link_url_id);
	Field.clear(this.state.link_title_id);
	Field.clear(this.state.link_desc_id);

	// if there is more than one link, display the drag instructions
	if(this.state.reorder.items.length > 1)
		Element.show(this.resources.link_drag_title);
	else
		Element.hide(this.resources.link_drag_title);
};

ModuleEdit.prototype.onclickLinkUpdate = function(event) {
	// save edits to a link

	var url = $F(this.resources.link_edit_url).trim();
	var title = $F(this.resources.link_edit_title).trim();
	var desc = $F(this.resources.link_edit_desc).trim();
	
	if (url.length === 0) { return; }
	if (title.length === 0) { title = url; }

	var l_id = this.state.select_id;

	// find the record
	var rec = this.state.linkIndex[l_id];
	rec.dirty = true;
	rec.url = url.toString();
	rec.name = title.toString();
	rec.description = desc.toString();

	// update drag element
	var stripname = rec.name.replace(/\<[^>]*\>/gi, "");
	$('linkdrg_'+l_id).innerHTML = '<b>'+ellipse(stripname, 22)+'</b>';

	// clear tab and switch to Add tab
	this.state.select_id = null;
	Field.clear(this.resources.link_edit_url);
	Field.clear(this.resources.link_edit_title);
	Field.clear(this.resources.link_edit_desc);
	this.unselectDragElements();
	selectTab(this.data.id, 1, 4);
};

ModuleEdit.prototype.onclickLinkSelect = function(link_id) {
	if(this.state.select_id != link_id) {
		this.state.select_id = link_id;

		this.unselectDragElements();
		// TODO this looks really assy for selecting text
		$('linkdrg_' + link_id).style.backgroundColor = '#ffc';

		var link = this.state.linkIndex[link_id];
		this.populateLinkEdit(link.url, link.name, link.description);
	}

	// make sure the "edit" tab is selected
	if(link_id) {
		selectTab(this.data.id, 0, 4);
	}
};

ModuleEdit.prototype.populateLinkEdit = function(href, text, desc) {
	if (href == text) {
		text = '';
	}

	$(this.resources.link_edit_url).value = href;
	$(this.resources.link_edit_title).value = text;
	$(this.resources.link_edit_desc).value = desc;
};

ModuleEdit.prototype.onclickLinkRemove = function() {
	var link_id = this.state.select_id;

	// find & mark the link in the link state
	this.state.linkIndex[link_id].deleted = true;
	this.state.linkIndex[link_id].dirty = true;

	// remove item from reorder
	this.state.reorder.remove(link_id);
	this.state.reorder.relayout();

	// clear tab and switch to Add tab
	// TODO would it be better to select next element?
	this.state.select_id = null;
	Field.clear(this.resources.link_edit_url);
	Field.clear(this.resources.link_edit_title);
	Field.clear(this.resources.link_edit_desc);
	selectTab(this.data.id, 1, 4);

	// if there is more than one link, display the drag instructions
	if(this.state.reorder.items.length > 1)
		Element.show(this.resources.link_drag_title);
	else
		Element.hide(this.resources.link_drag_title);
};

ModuleEdit.prototype.onclickLinkFind = function() {

	if (!$(this.state.link_search_id)) { return; }

	var query = $F(this.state.link_search_id);
	var find_type = this.state.link_search_type;
	var cache_key = query +'_'+ find_type;

	// check state link cache before making request
	if (this.state.link_cache[cache_key]) {
		$(this.state.link_results_id).innerHTML = this.state.link_cache[cache_key];
		return;
	}

	var params = $H({search: query,
					maxitems: 10,
					type: this.state.link_search_type
					}).toQueryString();

	// on completion of the request, cache the results in state.link_cache
	var ajax = new Ajax.Updater(
		{success: this.state.link_results_id},
		'/xml/linkresults.php',
		{	
			method: 'post',
			parameters: params,
			onFailure: reportError,
			onComplete: function() { 
				this.wireLinkCtrlHandlers();
				this.state.link_cache[cache_key] = $(this.state.link_results_id).innerHTML; 
			}.bind(this) 
		}
	);
};

ModuleEdit.prototype.onclickLinkTab = function(provider) {

	if (!provider) { return; }
	
	var eles = document.getElementsByClassName('findtaba');

	if ($(provider +'_findtab')) {
		var tab = $(provider +'_findtab');

		eles.each(function(ele) { ele.className = 'findtab'; });	
		tab.className = 'findtaba';
		this.state.link_search_type = provider;
	}

	if ($F(this.state.link_search_id)) {
		this.onclickLinkFind();
	}
};

ModuleEdit.prototype.onclickLinkTest = function() {

	var params = 'url='+ $F(this.state.link_url_id);

	var ajax = new Ajax.Request(
		'/xml/checklink.php',
		{	
			method: 'get',
			parameters: params,
			onFailure: reportError,
			onComplete: this.renderLinkTest.bind(this)
		}
	);
};

ModuleEdit.prototype.renderLinkTest = function(req) {
	
	try {
		var j = JSONstring.toObject(req.responseText);
	} catch(e) {}

	if (j)
	{
		if (j.valid) 
		{
			(j.title && j.title.length > 0) && ( $(this.state.link_title_id).value = j.title );
			(j.desc && j.desc.length > 0) && ( $(this.state.link_desc_id).value = j.desc );
		}

		if (j.status && j.status.length > 0) 
		{
			Element.update(this.state.link_status_id, j.status);
		}
	}
};

ModuleEdit.prototype.addLinkDragElement = function(id, name) {
	// TODO add a second row to each drag element with Description?
	if(!this.state.reorder) return;

	// limit to 40 characters, and strip out HTML tags
	name = ellipse(name, 40);
	name = name.replace(/\<[^>]*\>/gi, "");

	// create drag canvas element
	var dragElem = document.createElement("div");
	dragElem.id = 'linkdrg_'+id;
	dragElem.className = 'dragbar';
	dragElem.style.padding = '1px';
	dragElem.style.cursor = 'pointer';
	// TODO height/width also is set for the reorder item (in toggleImage),
	// we should use a constant for both
	dragElem.style.width = '300px';
	dragElem.style.height = '20px';
	dragElem.style.zIndex = 1;
	dragElem.style.overflow = 'hidden';
	dragElem.onclick = function () {
		this.onclickLinkSelect(id);
	}.bind(this);
	dragElem.onselectstart = function () { return false; };
	dragElem.innerHTML = '<b>'+name+'</b>';
	$(this.resources.link_drag_section).appendChild(dragElem);

	// create drag item
	this.state.reorder.add(new ModuleContentReorderItem('linkdrg_'+id, id, this.state.reorder));
};

ModuleEdit.prototype.onclickImageImportQueue = function() {
	// add new import entry to the image queue
	if ($(this.resources.image_import_id) && $F(this.resources.image_import_id).length > 0) {
		var url = $F(this.resources.image_import_id);

		var new_input = document.createElement("input");
		new_input.type = "hidden";
		// append the date to the input name to make it unique
		// taken from http://www.experts-exchange.com/Web/Web_Languages/JavaScript/Q_21891398.html
		new_input.name = this.resources.image_import_id + (new Date()).getTime().toString().substr(3);
		new_input.value = url;
		// add to the import queue div, that will make cleanup easier
		$(this.resources.image_queue_id).appendChild(new_input);

		// set field back to blank
		$(this.resources.image_import_id).value = "";

		// create queue element and append to queue
		var newelem = document.createElement("div");
		newelem.innerHTML = '<b>'+ url +'</b><br/>';

		$(this.resources.image_queue_id).appendChild(newelem);
	}
};

ModuleEdit.prototype.onclickImageUploadQueue = function() {
	// add file-system file to the upload queue

	// find the file import field
	// TODO this is a mess, because:
	// - we can't copy information from one FILE input to another, so the
	//   new FILE input doesn't have a fixed ID.  could we change the ID
	//   on the input to a random value, then create the new one with the
	//   original ID?  that would make this cleaner
	// - if the "file" input is in a div section with an ID, it somehow
	//   doesn't get passed to $_FILES!  a div without ID is no problem.
	//   hence file_div below, which really should've had an ID.
	// - I also had trouble with some of the standard DOM methods like
	//   getFirstChild() and getLastChild().  possibly firstChild and
	//   lastChild would have worked?
	var file_div = $(this.resources.image_upload_section_id).getElementsByTagName("div")[1]; // second div, should be inputs
	var file_inputs = file_div.getElementsByTagName("input");
	var old_input = file_inputs[file_inputs.length-1];

	if(old_input.value != '') {
		// hide the old input field
		if( browser == 'Safari' ) {
			// HACK safari doesn't recognize hidden FILE fields
			old_input.style.height = "0px";
			old_input.style.width = "0px";
		} else {
			Element.hide(old_input);
		}

		// create queue element and append to queue div
		var newelem = document.createElement("div");
		newelem.innerHTML = '<b>'+ old_input.value +'</b><br/>';
		$(this.resources.image_queue_id).appendChild(newelem);

		// create and add new file input form element
		var new_input = document.createElement("input");
		new_input.type = "file";
		// append the date to the input name to make it unique
		// taken from http://www.experts-exchange.com/Web/Web_Languages/JavaScript/Q_21891398.html
		new_input.name = this.resources.image_upload_id + (new Date()).getTime().toString().substr(3);
		new_input.style.width = "90%";
		file_div.appendChild(new_input);
	}

	return false;
};

ModuleEdit.prototype.onclickImageLoad = function() 
{
	// load all queued images

	// TODO test if there is at least one import or upload queued.
	// maybe using getElementsByTagName(...)?

	Element.show(this.resources.image_upload_gif_id);
	// HACK Javascript in a hidden iframe can't find parent.window on Safari,
	// so instead of hiding the iframe we'll set the height to 0 (and set it
	// back to 60px in imageLoadComplete)
	//Element.hide(this.resources.image_upload_status_id);
	$(this.resources.image_upload_target_id).style.height = '0px';

	if ($(this.resources.image_upload_target_id)) {
		$(this.resources.image_load_form_id).submit();
	}
};

ModuleEdit.prototype.imageLoadComplete = function() {
	this.clearImageUploadForm();
	Element.hide(this.resources.image_upload_gif_id);
	//Element.show(this.resources.image_upload_status_id);
	$(this.resources.image_upload_target_id).style.height = '60px';

	// if there is more than one image, display the drag instructions
	if(this.state.reorder.items.length > 1)
		Element.show(this.resources.image_drag_title);
	else
		Element.hide(this.resources.image_drag_title);
};

ModuleEdit.prototype.clearImageUploadForm = function() {
	// clears all fields from the image upload form
	// (including the hidden import fields)
	$(this.resources.image_queue_id).innerHTML = "";

	// clear the import input
	$(this.resources.image_import_id).value = "";

	// delete all FILE inputs and add a fresh one
	var file_div = $(this.resources.image_upload_section_id).getElementsByTagName("div")[1]; // second div, should be inputs
	file_div.innerHTML = "";
	var new_input = document.createElement("input");
	new_input.type = "file";
	new_input.name = this.resources.image_upload_id;
	new_input.style.width = "90%";
	file_div.appendChild(new_input);
}

ModuleEdit.prototype.addImageDragElement = function(image_id, tn_url, cap) {
	// add thumbnail to drag canvas for new image
	var newImg = document.createElement("img");
	newImg.id = "image_tn_" + image_id;
	newImg.src = tn_url;
	newImg.style.padding = "0.5em 0.5em 0.5em 0.5em";
	newImg.alt = cap;
	newImg.title = cap;
	newImg.onclick = function () { this.loadImageIntoEditor(image_id);}.bind(this);
	newImg.onselectstart = function () { return false; };

	$(this.resources.image_drag_section).appendChild(newImg);

	// add drag item
	this.state.reorder.add(new ModuleContentReorderItem('image_tn_'+image_id, image_id, this.state.reorder));
}

ModuleEdit.prototype.loadImageIntoEditor = function(image_id) {
	// load image info into the edit pane.

	if(this.state.select_id != image_id) {
		this.state.select_id = image_id;
		var rec = this.state.photoData[image_id];

		// select new thumbnail
		this.unselectImageThumbnails();
		$('image_tn_' + image_id).style.backgroundColor = '#c00';

		// load the caption & maxsize, select edit tab
		$(this.resources.image_caption_input).value = rec.caption;
		$(this.resources.image_size_input).selectedIndex = rec.maxSize;

		// load the full-sized image
		if(rec.maxSize == 2) {
			$(this.resources.image_selection_id).innerHTML = "<img src='"+rec.urlQuarter+"' />";
		} else if(rec.maxSize == 1) {
			$(this.resources.image_selection_id).innerHTML = "<img src='"+rec.urlHalf+"' />";
		} else {
			$(this.resources.image_selection_id).innerHTML = "<img src='"+rec.urlFull+"' />";
		}
	}

	// make sure the "edit" tab is selected
	if( image_id ) {
		selectTab(this.data.id, 1, 4);
	}
};

ModuleEdit.prototype.clearImageEditor = function(image_id) {
	// TODO should this be merged with clearImageEditForm???  pretty muddled
	this.unselectImageThumbnails();
	$(this.resources.image_selection_id).innerHTML = "";
	this.clearImageEditForm();
};

ModuleEdit.prototype.unselectImageThumbnails = function() {
	// restore the default background color to all thumbnail control images
	// TODO replace with unselectDragElements() once 'reorder' becomes a
	// member variable of this
	var elems = $(this.resources.image_drag_section).getElementsByTagName('img');
	$A(elems).each(function(ele) {
		ele.style.backgroundColor = '';
	});
};

ModuleEdit.prototype.clearImageEditForm = function() {
	// clears all fields from the image edit form
	this.state.select_id = null;
	$(this.resources.image_caption_input).value = "";
};

ModuleEdit.prototype.onclickImageDataUpdate = function() {
	// load the caption into the appropriate photo description and
	// resize the image
	// TODO hide the update button after click?  only show it when the user
	// modifies the text?
	if(this.state.select_id == null) return;
	var newcap = $(this.resources.image_caption_input).value.replace(/<[^>]+(>|$)/g, "");
	$('image_tn_'+this.state.select_id).alt = newcap;  // thumbnail
	$('image_tn_'+this.state.select_id).title = newcap;

	if( $(this.resources.image_size_input).selectedIndex != this.state.photoData[this.state.select_id].maxSize) {
		switch($(this.resources.image_size_input).selectedIndex) {
			case 1:  newurl = this.state.photoData[this.state.select_id].urlHalf; break;
			case 2:  newurl = this.state.photoData[this.state.select_id].urlQuarter; break;
			default: newurl = this.state.photoData[this.state.select_id].urlFull; break;
		}
		$(this.resources.image_selection_id).innerHTML = newurl;
		this.state.photoData[this.state.select_id].maxSize = $(this.resources.image_size_input).selectedIndex;
	}

	// add the new caption to photo array
	this.state.photoData[this.state.select_id].caption = newcap;

	// give an "updated" message since there's no other feedback
	// TODO location not quite right, can't figure out why.
	var html = '<div id="btnfeedback"><div><b> Updated</b></div></div>';
	var pole = new Insertion.After('tabcontent_'+this.data.id+'_1', html);
	setTimeout("if ($('btnfeedback')) Element.remove('btnfeedback');", 3500);
};

ModuleEdit.prototype.onclickImageRemove = function() {
	// remove the image
	// TODO this method is inexplicably slow on FF, but not IE or Opera.
	if(this.state.select_id == null) return;

	// add item to remove list
	this.state.images_removed.push(this.state.select_id);

	// remove image from photo array
	delete this.state.photoData[this.state.select_id];

	// calculate the next thumbnail (do *before* removing from reorder!)
	var tnNode = $('image_tn_'+this.state.select_id);
	var nextNode = tnNode.nextSibling? tnNode.nextSibling : tnNode.previousSibling;
	var next_id = -1;
	if(nextNode.id) {
		next_id = nextNode.id.match(/[0-9]+/g)[0];
	}

	// remove image from reorder module
	this.state.reorder.remove(this.state.select_id);
	this.state.reorder.relayout();

	// now load the next image into the editor
	if(next_id != -1) {
		this.loadImageIntoEditor(next_id);
	} else {
		this.clearImageEditor();  // no images left
	}

	// if there is more than one image, display the drag instructions
	if(this.state.reorder.items.length > 1)
		Element.show(this.resources.image_drag_title);
	else
		Element.hide(this.resources.image_drag_title);
};

ModuleEdit.prototype.onclickEbayPreview = function() {

	var params = $H({search: $F(this.resources.ebay_keywords_id),
					seller: $F(this.resources.ebay_seller_id),
					maxitems: $F(this.resources.max_results_id)
					}).toQueryString();

	var ajax = new Ajax.Updater(
		{success: this.resources.ebay_results_id},
		'/xml/ebayresults.php',
		{	
			parameters: params,
			onFailure: reportError,
			onComplete: function() {}
		}
	);
};

ModuleEdit.prototype.onclickAmazonPreview = function() {

	var params = $H({search: $F(this.state.keywords_id),
					maxitems: $F(this.state.max_id),
					searchIndex: $F(this.state.amazon_searchindex_id),
					searchType: $F(this.state.amazon_searchtype_id), /* ? 'Keyword' : 'Asin', */
					asins: $F(this.state.amazon_asins_input_id)
					}).toQueryString();

	var ajax = new Ajax.Updater(
		{success: this.state.amazon_results_id},
		'/xml/amazonresults.php',
		{	
			parameters: params,
			onFailure: reportError,
			onComplete: function() {}
		}
	);
};

ModuleEdit.prototype.onclickRssPreview = function() {

	var params = $H({ maxitems: $F(this.state.max_id) }).toQueryString();

	if ($(this.state.feed_id)) {
		params += '&feed='+ encodeURIComponent($F(this.state.feed_id));
	} else if ($(this.state.keywords_id)) {
		params += '&keywords='+ $F(this.state.keywords_id);
	}

	var ajax = new Ajax.Updater(
		{success: this.state.rss_results_id},
		'/xml/rssresults.php',
		{	
			parameters: params,
			onFailure: reportError,
			onComplete: function() {}
		}
	);
};


ModuleEdit.prototype.onclickVideoPreview = function() {

	var params = $H({ url: $F(this.resources.video_url_input_id) }).toQueryString();

	var ajax = new Ajax.Updater(
		{success: this.resources.video_results_id},
		'/xml/videoresults.php',
		{	
			parameters: params,
			onFailure: reportError,
			onComplete: function(xhttp, json) { 
				if (json) {
					var light = (json.key == 'Unrecognized Url');
					hpFormHandler.lightEle(this.resources.video_key_id, light);	
					hpFormHandler.lightEle(this.resources.video_type_id, light);	

					Element.update(this.resources.video_key_id, json.key);
					Element.update(this.resources.video_type_id, json.type);
					this.state.video_type = json.type;

					if (json.showPreview)
					{
						insertVideo(json.type, json.key, json.cssClass, '' /* style */, this.resources.video_results_id);
					}
				}
			}.bind(this)
		}
	);
};

ModuleEdit.prototype.setHide = function() 
{
	if ($(this.state.hide_id)) {
		this.data.hide = $(this.state.hide_id).checked;
	}

	this.render();
};

ModuleEdit.prototype.setColor = function(color) 
{
	this.data.color = String(color);
	var eles = document.getElementsByClassName('selected', 'colorbar_'+ this.data.id);

	Element.removeClassName(eles.first(), 'selected');
	Element.addClassName(eles.first(), 'unselected');

	var clickedEle = $(this.data.id +'_color'+ color);
	Element.addClassName(clickedEle, 'selected');
	Element.removeClassName(clickedEle, 'unselected');

	this.render();
};

ModuleEdit.prototype.load = function() 
{
	// fetch data from the server to hydrate the module
	var loadHash= $H(this.data).merge($H({load: true}));
	var params = loadHash.toQueryString();

	var ajax = new Ajax.Updater(
		{success: this.data.div_id},
		'/xml/modules.php',
		{
			method: 'post',
			parameters: params,
			evalScripts: true,
			onFailure: reportError,
			onComplete: this.render.bind(this)
		}
	);
};

ModuleEdit.prototype.save = function() {

	if (!this.data.dirty) { return; }

	// save data in the control to the server
	var ajax = new Ajax.Request('/xml/modules.php',
		{ method:'post',
		parameters: $H(this.data).toQueryString(),
		onFailure:this.reportSaveError.bind(this),
		//onException:this.reportSaveError.bind(this),
		onComplete:this.setClean.bind(this)
		});
};

ModuleEdit.prototype.animateHoriz = function() {
		
		if (this.data.horiz_id == 2) {
			var div = $(this.data.div_id);
			var new_width = Element.getWidth(div) * 2;

			var changeWidth = new fx.Combo(div, {duration: 300,
				width: true,
				height: true,
				opacity: false,
				onComplete: function() { this.render(); }.bind(this) } );

			changeWidth.customSize(Element.getHeight(div), new_width);
			changeWidth.toggle();
		} 
		else {
			this.render();
		}
};

ModuleEdit.prototype.scrollTo = function() {
	var scroll = new fx.Scroll({duration: 100});
	scroll.scrollTo(this.data.div_id);
};


ModuleEdit.prototype.remove = function(fromServer) {

	// if the delete is triggered by the delete button do the animation
	// otherwise we'ere just moving stuff around so don't
	// this is hacky and i should move th rendering code out
	// of here
	if (fromServer) {
		this.data.deleted = true;
		this.setDirty();
		this.save();

		var shrink = new fx.Height(this.data.div_id, {
			duration: 500,
			transition: fx.backIn,
			onComplete: function() { Element.remove(this.data.div_id); ModuleEdit.Manager.render(); }.bind(this)
			});
		shrink.toggle();
	}
	else {
		Element.remove(this.data.div_id);
	}
};

ModuleEdit.prototype.reportSaveError = function(req, e)
{
	alert("Ooops!\r\nThere was an error saving this capsule.  Please click EDIT and then SAVE to try again.  This error will be reported to the HubPages engineering team.  We thank you for your patience and understanding.");

	var payload = $H({
		response: req.responseText,
		status: req.status,
		headers: req.getAllResponseHeaders()
		}).toQueryString();
			
	 var myError = new Ajax.Request('/xml/reporterror.php',
        { method:'post',
        parameters: payload +"&hubtoolsave=1"
        });
};
// ************
// ModuleManager: controls the layout and order of modules on the page
// **
var ModuleManager = Class.create();

ModuleManager.prototype = {

	initialize: function(container, art_id, module_json) {

		this.div_id = $(container).id;
		this.art_id = art_id;
		this.modules_by_order = [];
		this.modules_by_id = [];
		this.layout = {};
		this.editting = false;

		// keep reference to module reorder, if enabled
		/*
		if (ModuleEdit.options.drag_reorder_enabled)
		{
			this.reorder = new ModuleReorder(art_id);
			alert('here');
		}
		*/

		// add self to the static ModuleEdit.Manager property
		ModuleEdit.Manager = this;

		// add initial modules
		if (module_json)
		{
			var module_eles = JSONstring.toObject(module_json);
			module_eles.each(function(module_ele) { 

				var module = ModuleEdit.getFromJSON(module_ele);
				this.add(module, false, true); // no position, yes bulk
			}.bind(this));
		}
	
		this._renumber();
		this.render();
	},

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

	add: function(module, position, bulk) {
		module.data.position = (position && position.toLowerCase() == 'top') ? 0 : this.getLength();
		
		if (position && position.toLowerCase() == 'top') {
			this.modules_by_order.unshift(module);
		} else {
			this.modules_by_order.push(module);
		}
		this.modules_by_id[module.data.id] = module;

		if (!bulk) {
			if (ModuleEdit.options.drag_reorder_enabled) {

				// add to module reorder, if enabled
				var ritem = ReorderItem.make(module);
				this.reorder.add(ritem);
				this.reorder.relayout();
			}
			this._renumber();
		}

	},

	addNew: function(type) {
		addTime = new Timer(); // DEBUG
		ModuleEdit.make(type, this.art_id, this._handleAddNewResponse.bind(this));
	},

	getByPosition: function(index) {
		return this.modules_by_order[index];
	},

	getById: function(id) {
		return this.modules_by_id[id];
	},

	getPosition: function(module) {
		return this.modules_by_order.indexOf(module);
	},

	getPositionById: function(id) {
		var module = this.modules_by_id[id];
		return this.getPosition(module);
	},

	removeById: function(id, fromServer) {
		var module = this.modules_by_id[id];
		if (module.editting) { return; }
		// TODO this is 95% good for supressing the confirm for an empty
		// capsule, but if the user clicks EDIT, does nothing, and SAVEs,
		// they will be prompted.  and comments will always be prompted
		// unless the hub is new.
		var module_empty = ($(module.state.empty_notification_id).style.display != 'none' || (module.data.type == 'Comment' && isNewHub)) ? true : false;
		if (!module_empty && !confirm(module.state.delete_confirm_text)) { return; }

		delete this.modules_by_id[id];

		this.modules_by_order = this.modules_by_order.reject(function(o) { 
			return (o.data.id == module.data.id); 
		});

		module.remove(fromServer);
		this._renumber();

		// remove from module reorder if enabled
		if (ModuleEdit.options.drag_reorder_enabled)
		{
			this.reorder.remove(module.data.id);
			this.reorder.relayout();
		}
	},

	removeByPosition: function(index) {
		var module = this.getByPosition(index);
		if (module.editting) { return; }

		module = this.modules_by_order.splice(index, 1);
		delete this.modules_by_id[module.data.id];

		module.remove(false);
		this._renumber();
	},

	renderExhaustive: function() {

		this._renderFloatDivs();

		this.modules_by_order.each(function(module) { 

			// remove any positioning offsets that might be set as part of the
			// float and unfloat
			Position.set($(module.data.div_id), [0, 0]);

			module.renderExhaustive();
		});

		this._sweepDeadWrappers();
	},

	render: function() {
		
		this._renderFloatDivs();

		this.modules_by_order.each(function(module) { 
			module.render();
		});

		this._sweepDeadWrappers();
	},

	// flip a capsule from editting to normal or the other way around
	//.
	// this method has been optimized a fair bit: all the capsules are
	// no longer re-rendered as they once were. the key to good performance
	// is that the capsule being editted just gets positioned absolutely
	// and the modal curtain pulled into place
	toggle: function(id) {
		
		this.editting = !this.editting;

		var mod = this.getById(id);
		
		// install compute state handler if in editting mode
		if (this.editting) {
			this.computeState();
			this.computeListener =  this.computeState.bindAsEventListener(this);
			Event.observe(window, 'resize', this.computeListener, false);
		} else {
			Event.stopObserving(window, 'resize', this.computeListener, false);
			this.computeListener = null;
		}

		mod.toggle();

		this.displayCurtain(mod.state.modal_id);
	},

	moveLeft: function(id) { this.moveHoriz(id, 1); },
	moveCenter: function(id) { this.moveHoriz(id, 2); },
	moveRight: function(id) { this.moveHoriz(id, 3); },
	moveHoriz: function(id, position) {

		var module = this.getById(id);
		if (module.editting) { return; }

		module.setColorbar(position); // colorbar only shown for half-width

		module.setHoriz(position);
		module.setDirty();
		module.data.moveHoriz = true;
		module.save();
		module.data.moveHoriz = false;

		this.renderExhaustive();
		this.callEvent("onmove", this.modules_by_order);
	},

	moveUp: function(id) { this.moveVert(id, 0); },
	moveDown: function(id) { this.moveVert(id, 1); },
	moveVert: function(id, movement) { 
	
		this._sweepDeadWrappers();

		var module = this.getById(id);
		var index = this.getPosition(module);
		var length = this.getLength();
		var direction = (movement * 2) - 1; // direction is 1 or -1

		if (index === 0 && movement === 0) { return; }
		if (index == length - 1 && movement == 1) { return; }
		if (module.editting) { return; }
		
		// do data structure reorganizing
		var index_to_delete = (movement == 1) ? index : index - 1;
		var index_to_insert_at = (movement == 1) ? index + 1 : index;

		var module_to_swap = this.getByPosition(direction + index);
		var deleted = this.getByPosition(index_to_delete);
		this.modules_by_order.splice(index_to_delete, 1);
		this.modules_by_order.splice(index_to_insert_at, 0, deleted);

		// save swap to the server
		this._saveSwap(module, module_to_swap);
		this._renumber();

		// image capsules need to have their height/width set explicitly
		// TODO I don't think this is necessary anymore, as images are fixed
		// half- or full-width
		if(module.data.type == "Image") { module.adjustImageWidth(); }
		if(module_to_swap.data.type == "Image") { module_to_swap.adjustImageWidth(); }

		// draw swap effects
		this._animateSwap(module, module_to_swap, direction);

		// call event to alert any listeners of swap
		// NOTE: this must be last as execution never returns
		//   to this function
		this.callEvent("onmove", this.modules_by_order);
	},

	// this does some cleanup of empty DIVs that are leftover from the layout code.
	// Ignoring the fact that these DIVs could probably be avoided, we can improve
	// perceived rendering performance by triggering their cleanup to occur after
	// rendering is complete
	_sweepDeadWrappers: function() {
		setTimeout(this._sweepDeadWrappersHandler.bind(this), 500);
	},

	_sweepDeadWrappersHandler: function() {
		var eles = document.getElementsByClassName('modfloat', 'modules');
		if(eles.length != 1) {
			// always leave at least one modfloat
			eles.each(function(ele) { 

				if (!ele.hasChildNodes()) {
					Element.remove(ele);
				}
			});
		}

		// remove any unused "floatclear" breaks (the re-render will pile
		// them all up after the modfloats)
		var eles = document.getElementsByClassName('floatclear', 'modules');
		eles.each(function(ele) { 
			if(!ele.nextSibling || ele.nextSibling.nodeName != "DIV") {
				Element.remove(ele);
			}
		});
	},

	load: function() {
		this.modules_by_order.each(function(module) { module.load(); });
	},

	save: function() {
		this.modules_by_order.each(function(module) { module.save(); });
	},

	cleanUp: function() {

		this.modules_by_order.each(function(m) { m.cleanUp(); });

		this.div_id = null;
		this.art_id = null;
		this.modules_by_id = null;
		this.modules_by_order = null;
	},

	computeState: function() {

		this.layout.documentSize = [Position.getDocumentWidth(), Position.getDocumentHeight()];
		this.layout.viewportSize = [Position.getViewportWidth(), Position.getViewportHeight()];
		this.layout.viewportScroll = [Position.getViewportScrollX(), Position.getViewportScrollY()];
	},

	// RANDOM HUBTOOL CRUFT
	displayCurtain: function(not_ele) {

		ModuleEdit.toggleCurtain();
	},

	toggleAutoSummary: function() {

		var summary = $('autoSummary').checked;

		$('articleSummary').readOnly = summary ? 'none' : '';
		$('articleSummary').style.color = summary ? '#aaa' : '#000';
	},
	// END RANDOM CRUFT

	_saveSwap: function(module, module2) {

		var params = $H({swap: 1,
						art_id: module.data.art_id,
						id: module.data.id,
						id2: module2.data.id
					 }).toQueryString();

		var ajax = new Ajax.Request('/xml/modulesswap.php', { 
						parameters: params, 
						onFailure: reportError,
						onComplete: function() {}
					});
	},

	_animateSwap: function(module, module_to_swap, direction) {

		// get references to div elements
		var module_div = $(module.data.div_id);
		var module_to_swap_div = $(module_to_swap.data.div_id);

		// do animation
		Element.makePositioned(module_div);
		Element.makePositioned(module_to_swap_div);

		// save the positioning for the swap animation
		var pri_cum = Position.cumulativeOffset(module_div);
		var sec_cum = Position.cumulativeOffset(module_to_swap_div);

		var pri_delta = [(sec_cum[0] - pri_cum[0]) / 3, 
			(direction) * Element.getHeight(module_to_swap_div)];

		var sec_delta = [(pri_cum[0] - sec_cum[0]) / 3, 
			-1 * (direction) * Element.getHeight(module_div)];

		var moveDuration = 400; // ms
		var movePrimary = new fx.Position(module_div, {duration: moveDuration, 
			transition: fx.backIn,
			onComplete: function() { 
				// important to undo positioning here so that zIndexes will
				// not be screwed up

				Element.undoPositioned(module_div);
				Element.undoPositioned(module_to_swap_div);
				this.render();
			}.bind(this) });

		var moveSecondary = new fx.Position(module_to_swap_div, {duration: moveDuration, 
			transition: fx.backIn,
			onComplete: function() { 
			}.bind(this) });

		// set the zIndex so that the element clicked on will fly in front of 
		// the other one
		module_div.style.zIndex = '2'; // this is set back to 1 in _renderFloat()
		movePrimary.move([0, 0], [pri_delta[0], pri_delta[1]]);
		moveSecondary.move([0, 0], [sec_delta[0], sec_delta[1]]);
	},

	_renumber: function() {

		var length = this.getLength();

		this.modules_by_order.each(function(o, idx) {
			o.data.position = idx;
			o.state.is_last = (idx == length -1); 
		});
	},

	_renderFloatDivs: function() {
	
		var lastModuleFloatPosition = 'first';
		var currentDiv;
		var modules = $('modules');
		var firstOldDiv = modules.firstChild;

		this.modules_by_order.each(function(module, idx) {

			var module_div = $(module.data.div_id);
			var realFloatPosition = module.getHoriz();

			if (module.isLast() ) {
				realFloatPosition = 2;
			}

			if (realFloatPosition != lastModuleFloatPosition)
			{
				// we need to add a div for floating
				var floatDivClasses = ['modfloat'];

				// then we add right or left for floated modules
				switch( realFloatPosition )
				{
					case 1: floatDivClasses.push('left'); break;
					case 3: floatDivClasses.push('right'); break;
					case 2: floatDivClasses.push('full'); break;
				}

				// if there was another modfloat div open, append it
				// before we create a new one (for a different realFloatPosition)
				if (lastModuleFloatPosition != 'first' && currentDiv) 
				{
					// if there are currently float divs (this is a rerender of 
					// some sort) insert the new divs above them (to prevent a weird
					// jump at the top of the article).  should firstOldDiv be null, 
					// insertBefore functions like appendChild

					// if old div is floating, add a clear:both *before* the
					// old div, to force FireFox to render the same as IE7.
					if(lastModuleFloatPosition == 3) {
						breakDiv = document.createElement('br');
						breakDiv.className = "floatclear";
						modules.insertBefore(breakDiv, firstOldDiv);
					}
					modules.insertBefore(currentDiv, firstOldDiv);
				}

				currentDiv = document.createElement('div');
				currentDiv.className = floatDivClasses.join(' ');
			}
			lastModuleFloatPosition = realFloatPosition;

			// add a clear:both before full-width comment, image, video
			// capsules, to force edit controls below any floating capsules
			if((module.data.type == 'Comment' || module.data.type == 'Image' || module.data.type == 'Video') && realFloatPosition == 2) {
				breakDiv = document.createElement('br');
				breakDiv.className = "floatclear";
				currentDiv.appendChild(breakDiv);
			}

			currentDiv.appendChild(module_div);

			if (module.isLast()) 
			{
				modules.insertBefore(currentDiv, firstOldDiv);
			}
		});
	},

	_handleAddNewResponse: function(req, json) {

		var matches = req.responseText.match(/ id="(.*?)" /);
		var mod_id = (matches && matches.length > 1) ? matches[1] : false;

		if (mod_id && json)
		{
			var pole = new Insertion.Bottom('modules', req.responseText);

			var module = ModuleEdit.getFromJSON(json);
			this.add(module, 'bottom');

			if (!Position.withinViewport(mod_id) && !$('ind')) {
				this._drawPointerInd('<b><img width="11" height="10" src="/i/dn-w.gif"/> Added capsule below</b>');
			}	

			module.load();
		}

		//NOTE: this was screwing stuff up when adding a new module.
		// I think because of a race condition where the module
		// stats weren't back from the server before the heavy handed 
		// man render kicked in and lost the reference to the new module
		//
		// DANGERDANGER
		// DEBUG: //var renderTime = new Timer();
		this.render();
		// DEBUG: out("render: "+ renderTime.inspect());
	},

	_drawPointerInd: function(contents) {
	
		var pointerHtml = '<div id="ind"><div>'+ contents + '</div></div>';
		var pole = new Insertion.Bottom('modules', pointerHtml);
		if (!window.ActiveXObject) {
			$('ind').style.position = 'fixed';
		}
		setTimeout("if ($('ind')) Element.remove('ind');", 3500);
	},

	callEvent: function(e) {
        return this[e] instanceof Function ? this[e].apply(this, [].slice.call(arguments, 1)) : undefined;
    }
};

function prompt_navigate_away(event)
{
	return '\n\nAre you sure you want to leave the Hubtool now?\nYou may lose some or all of your work.\n\n';
}

// function for switching between tabs
function selectTab(baseid, index, tabcount)
{
	var thetab, thecontent;
	for (var i=0; i < tabcount; i++)
	{
		thetab = $('tab_' + baseid + '_' + i );
		thecontent = $('tabcontent_' + baseid + '_' + i );

		if (!thetab || !thecontent) {
			alert('Cannot locate element: baseid=' + baseid + ' index=' + index + ' tabcount=' + tabcount);
		}

		if (i == index) {
			Element.addClassName(thetab, 'selected');
			Element.addClassName(thecontent, 'selected');
		} else {
			Element.removeClassName(thetab, 'selected');
			Element.removeClassName(thecontent, 'selected');
		}
	}
	
	return false;
}
var ModuleReorder = Class.create();
ModuleReorder.prototype = {

	initialize: function(art_id) {
		ReorderItem.reorder = this;

		this.art_id = art_id;
		this.canvas = $('caps_reorder_canvas');
		this.items = [];

		// some of these are also in controls/Hubtool.php
		this.options = {
			border_repaint_delay: 800, // ms after drag
			filter_additional_overhang: 40, // px
			empty_space_at_top: 10, // px
			ele_height: 25, // px
			ele_float_bump: 10, // px
			ele_float_width: 100, // px
			ele_full_width: 165 // px
		};

		this.options.ele_space_per = this.options.empty_space_at_top + this.options.ele_height; // px
	},


	add: function(item) {
		this.items.push(item);
	},

	remove: function(mod_id) {

		// find in array
		var to_remove = this.items.findAll(function(o) { return (o.mod_id == mod_id); });

		if (to_remove && to_remove.length > 0)
		{
			to_remove = to_remove[0]; // HACKHACK: detect() was throwing an exception

			// delete from array
			this.items = this.items.reject(function(o) { 
				return (o.mod_id == mod_id); 
			});

			// delete from DOM
			Element.remove(to_remove.ele);
		}
	},
	
	findById: function(mod_id) {
		return this.items.detect(function(item) { 
					return (item.mod_id == mod_id); 
				});
	},

	save: function() {

		this._getorder(); // assure that order is correct (it should be after the drag)

		var items_state = this.items.collect(function(item) {
							return {id: item.mod_id, floated: item.floated};
						});
		var caps_string = JSONstring.make(items_state);

		var myAjax = new Ajax.Request('/xml/modulesreorder.php',
		{ parameters: 'art_id='+ this.art_id +'&json='+ caps_string,
		onFailure: reportError,
		onComplete: this.onsavedone.bind(this)
		});
	},
	
	onsavedone: function() {

		// BUGBUG: save article summary info?
		window.onbeforeunload = Prototype.emptyFunction; // remove warning dialog handler
		location.reload();
	},

	ondragdone: function() {
		this._getorder();
		this.relayout();
	},

	// syncs the ordering back to the ordering on the page
	resetorder: function(modules) {

		var new_order = [];
		new_order = modules.collect(function(mod, i) {
						var reorder_item = this.findById(mod.data.id);
						reorder_item.floated = mod.isFloated(); // resync floated state

						return reorder_item;
					}.bind(this));

		this.items = new_order; 
		this.relayout(true /* do full repaint */);
	},

	// BUGBUG: if you reorder the capsules using the traditional arrow buttons,
	// 	that order will not be reflected in the sidebar
	_getorder: function() {
		this.items = this.items.sortBy(function(node, i) { 
						return parseInt(node.ele.style.top, 10); 
					});
	},

	relayout: function(full_repaint) {

		var i = 1;
		this.items.each(function(cap) {

			var ele = $(cap.ele);
			var floatbump = (cap.floated) ? this.options.ele_float_bump : 0;
			var top = (i * this.options.ele_space_per) + floatbump;

			ele.style.bottom = '';
			ele.style.top = top +'px';

			i++;
		}.bind(this));

		if (full_repaint)
		{
			this.items.each(function(cap) { cap.repaint(); });
		}

		// TODO: don't set this unless it has changed
		var canvas_height = (this.items.length + 2) * (this.options.ele_height + 10);
		$('caps_reorder_canvas').style.height = canvas_height +'px';
	}
};

// ---------------------------------------------------

var ReorderItem = Class.create();
ReorderItem.reorder = null;
ReorderItem.prototype = {

	initialize: function(ele, mod_id, floated, floatable) {
		ele = $(ele);
	
		this.mod_id = mod_id;
		this.ele = ele;
		this.opts = ReorderItem.reorder.options;
		this.floated = floated;
		this.floatable = floatable;
		this.dragged = false;

		var d = new Dragger(ele, true);

		var loc = Position.realOffset(ReorderItem.reorder.canvas);
		var dim = Element.getDimensions(ReorderItem.reorder.canvas);
		var add_overhang = this.opts.filter_additional_overhang;

		d.addFilter(Dragger.filters.SQUARE, loc[0] - add_overhang, loc[1], dim.width - add_overhang, dim.height - this.opts.ele_height);

		d.onstop = this.ondragdone.bind(this);
		d.ondrag = this.ondrag.bind(this);

		this.ondblclickListener = this.ondblclick.bindAsEventListener(this);
		this.ele.ondblclick = this.ondblclickListener;
	},

	ondblclick: function(e) {
		if(!this.floatable) return;

		e = e || window.event;
		var ele = Event.element(e);

		this.floated = !this.floated; // toggle floated

		this.repaint(); // repaint self
		ReorderItem.reorder.ondragdone(); // relayout all RIs to allow for correct placement
	},

	ondrag: function(e) {
		this.dragged = true; 

		this.ele.style.zIndex = '100'; 
		this.ele.style.border = 'solid 2px #c00';
	},

	ondragdone: function() {

		if (this.dragged) {
			this.repaint();

			this.dragged = false;
			ReorderItem.reorder.ondragdone(); // call container ModuleReorder
		}
	},
	
	width: function() {
		return this.floated ? this.opts.ele_float_width : this.opts.ele_full_width;
	},

	height: function() {
		return this.opts.ele_height;
	},

	zindex: function() {
		return this.floated ? '2' : '1';
	},

	repaint: function() {
	
		// deselect text that can be inadvertently selected during drag or dbl click
		// in IE we must use onselectstart="return false", in the div tag
		if (browser != 'IE') { Field.focus(this.ele); } 
		this.ele.style.left = '';
		this.ele.style.right = '5px';
		this.ele.style.zIndex = this.zindex();
		this.ele.style.width = this.width() +'px';

		setTimeout("$('"+ this.ele.id +"').style.border = 'solid 1px #666'", this.opts.border_repaint_delay);
	}
};

ReorderItem.make = function(mod) 
{
		var title = '<b>'+ mod.data.display_type +'</b>';
		var html = '<div class="dragbar" id="caps_reorder_'+ mod.data.id +'" '+
					'style="width:100%; height:25px; bottom:0px; '+
					'border:solid 1px #999; font-size:9px; padding:1px; '+
					'overflow:hidden; right:5px; z-index:1;'+
					'cursor:pointer" onselectstart="return false">'+
					title +'</div>';

		var pole = new Insertion.Bottom('caps_reorder_canvas', html);

		var ritem_div = "caps_reorder_"+ mod.data.id;
		var floatable = mod.data.type == 'Comment' ? false : true;
		var ri = new ReorderItem(ritem_div, mod.data.id, mod.isFloated(), floatable);
		ri.repaint();

		return ri;
};


// ------------------------------------------------------------------------

/*** Module Item Reorder ***/

// generic module item drag-and-drop

var ModuleContentReorder = Class.create();
ModuleContentReorder.prototype = {

	initialize: function(canvas_id, num_cols, col_width, col_height) {
		this.canvas = $(canvas_id);
		this.items = [];

		// TODO review if all these are needed in this module
		this.options = {
			num_cols: num_cols,
			filter_additional_overhang: 40, // px
			ele_buffer_v: 10, // px
			ele_buffer_h: 10, // px
			ele_height: col_height, // px
			ele_width: col_width, // px
			offset_v: 5, // empty space at top of canvas, px
			offset_h: 5  // empty space at left of canvas, px
		};

		this.options.ele_space_per_v = this.options.ele_buffer_v + this.options.ele_height; // px
		this.options.ele_space_per_h = this.options.ele_buffer_h + this.options.ele_width; // px
	},

	add: function(item) {
		this.items.push(item);
	},

	remove: function(item_id) {
		this.items = this.items.reject(function(item) { 
			if(item.item_id == item_id) {
				Element.remove(item.ele);
				return true;
			} else {
				return false;
			}
		});
	},

	ondragdone: function() {
		this._getorder();
		this.relayout();
	},

	_getorder: function() {
		this.items = this.items.sortBy(function(item, i) { 
			if(this.options.num_cols == 1) {
				// we want different (simpler) behaviour with only one column
				return parseInt(item.ele.style.top, 10);
			}

			var vIdx = parseInt(item.ele.style.top, 10); 
			var hIdx = parseInt(item.ele.style.left, 10); 
			var rownum = Math.round((vIdx-this.options.offset_v)/this.options.ele_space_per_v);
			var colnum = Math.round((hIdx-this.options.offset_h)/this.options.ele_space_per_h);

			if(item.dropped) {
				// the dropped item.  nasty logic to put it in the right place.
				if(colnum < 0) colnum = 0;
				if(colnum >= this.options.num_cols) colnum = this.options.num_cols-1;
				var startRow = Math.round((item.startY-this.options.offset_v)/this.options.ele_space_per_v);
				var startCol = Math.round((item.startX-this.options.offset_h)/this.options.ele_space_per_h);
				if((startRow < rownum) || (startRow == rownum && startCol < colnum)) {
					colnum+=0.5;
				} else {
					colnum-=0.5;
				}
			}
			return (rownum * this.options.num_cols) + colnum;
		}.bind(this));
	},

	relayout: function(full_repaint) {
		var i = 0;
		this.items.each(function(item) {
			var ele = $(item.ele);

			var rownum = Math.floor(i/this.options.num_cols);
			var colnum = i % this.options.num_cols;

			// store the position, for use when this item is dragged
			item.startY = (rownum * this.options.ele_space_per_v) + this.options.offset_v;
			item.startX = (colnum * this.options.ele_space_per_h) + this.options.offset_h;

			// set the position
			ele.style.bottom = '';
			ele.style.top = item.startY +'px';
			ele.style.left = item.startX +'px';

			i++;
		}.bind(this));

		if (full_repaint) {
			this.items.each(function(item) { item.repaint(); });
		}

		var rows = Math.ceil(this.items.length/this.options.num_cols);
		var canvas_height = (rows * (this.options.ele_space_per_v)) + 10;
		if(canvas_height != parseInt(this.canvas.style.height, 10)) {
			this.canvas.style.height = canvas_height +'px';
		}
	},

	getitemstate: function() {
		// return array of records describing current order of items
		this._getorder();
		items_state = Array();

		var pos = 0;
		this.items.each(function(item) {
			items_state.push({id : item.item_id, position : pos++});
		});
		return items_state;
	},

	getitemstateRec: function() {
		// return hash of records describing current order of items
		this._getorder();
		items_state = Object();

		var pos = 0;
		this.items.each(function(item) {
			items_state[item.item_id] = {id : item.item_id, position : pos++};
		});
		return items_state;
	}
};



var ModuleContentReorderItem = Class.create();
ModuleContentReorderItem.prototype = {

	initialize: function(elem_id, item_id, reorder) {
		// elem_id - the ID of the HTML element this MCRI represents
		// item_id - graphic element ID
		// reorder - ModuleContentReorder instance

		var ele = $(elem_id);

		this.item_id = item_id;
		this.ele = ele;

		// the ModuleContentReorder instance for this dragspace.
		// TODO make sure that this doesn't create a circular reference
		// delete issue
		this.reorder = reorder;

		this.opts = this.reorder.options;

		// drag and drop information
		// 'dragged' is true while being dragged, while 'dropped' is true
		// after the drop, during redraw.
		// TODO not sure if we need 'dropped', this should be tested -- I
		// was getting some weird behaviour when i was keeping 'dragged'
		// true through the redraw, but it's unclear if 'dragged' was
		// actually the source of those problems
		this.dragged = false;
		this.dropped = false;
		this.startX = 0;
		this.startY = 0;

		var d = new Dragger(ele, true);

		// limit potential drag area
		// TODO currently broken.  dim returns (0,0) initially because the
		// canvas hasn't been drawn.  could either relayout() first, or set
		// initial height/width in PHP
		/*
		var loc = Position.realOffset(this.reorder.canvas);
		var dim = Element.getDimensions(this.reorder.canvas);
		var add_overhang = this.opts.filter_additional_overhang;
		d.addFilter(Dragger.filters.SQUARE, loc[0] - add_overhang, loc[1], dim.width - add_overhang, dim.height - this.opts.ele_height);
		*/

		// bind callback functions
		d.onstart = this.onstart.bind(this);
		//d.ondrag = this.ondrag.bind(this);
		d.onstop = this.ondragdone.bind(this);

		this.ondblclickListener = this.ondblclick.bindAsEventListener(this);
		this.ele.ondblclick = this.ondblclickListener;
	},

	ondblclick: function(e) {
		//alert("dbl-click");
		//e = e || window.event;
		//var ele = Event.element(e);
	},

	// TODO since this stuff is all one-shot, it seems like we shouldn't do
	// it ondrag, just onstart.  is there a problem with this?
	onstart: function(e) {
		this.ele.onclick();
		this.dragged = true; 
		this.ele.style.zIndex = '100'; 
	},

	ondrag: function(e) {
		//this.ele.onclick();
		//this.dragged = true; 
		//this.ele.style.zIndex = '100'; 
	},

	ondragdone: function() {
		if(this.dragged) {
			this.repaint();
			this.dragged = false;
			this.dropped = true;
			this.reorder.ondragdone();
			this.dropped = false;
		}
	},

	repaint: function() {
		// deselect text that can be inadvertently selected during drag or dbl click
		// in IE we must use onselectstart="return false", in the div tag
		if (browser != 'IE') { Field.focus(this.ele); } 
		this.ele.style.zIndex = 1;
	}
};
var GroupReorder = Class.create();
GroupReorder.prototype = {

	initialize: function(firstGroupId) {
		GroupReorderItem.reorder = this;

		this.firstGroupId = firstGroupId;
		this.canvas = $('grp_reorder_canvas');
		this.items = [];

		// options -
		// filter_additional_overhang: amount of space above and below drag
		//   area where items can be dragged
		// empty_space_at_top: space above a drag element
		// ele_height:  height of drag elements
		// ele_space_per: total space taken by a drag element
		this.options = {
			filter_additional_overhang: 40, // px
			empty_space_at_top: 5, // px
			ele_height: 20 // px
		};

		this.options.ele_space_per = this.options.empty_space_at_top + this.options.ele_height; // px
	},


	add: function(item) {
		this.items.push(item);
	},

	remove: function(hp_id) {
		this.items = this.items.reject(function(item) { 
			if(item.hp_id == hp_id) {
				Element.remove(item.ele);
				return true;
			} else {
				return false;
			}
		});
	},
	
	removegroup: function(targetId) {
		targetItem = this.findById(targetId);
		if(targetItem.collapsed) {
			targetItem.hiddenChildren.each(function(hiddenItem) {
				hiddenItem.ele.style.display = '';
				this.items.push(hiddenItem);
			}.bind(this));
		} else {
			flag = false;
			offset = this.items.length + 1;
			this.items = this.items.sortBy(function(item, i) {
				if(!item.is_article) {
					flag = (targetId == item.hp_id);
				}
				return flag? (i + offset) : i;
			});
		}

		if(targetId == this.firstGroupId)
			this.firstGroupId = this.items.first().hp_id;

		this.remove(targetId);
		this.relayout();
	},

	findById: function(hp_id) {
		return this.items.detect(function(item) { 
					return (item.hp_id == hp_id); 
				});
	},

	ondragdone: function() {
		this._getorder();
		this.relayout();
	},

	// TODO should toggleGroupById and toggleGroup be moved to edit.php?
	toggleGroupById: function(targetId) {
		targetItem = this.findById(targetId);
		this.toggleGroup(targetItem);
	},

	toggleGroup: function(targetItem) {
		if(targetItem.collapsed) {
			this.expandGroup(targetItem);
		} else {
			this.hideGroup(targetItem);
		}
	},

	hideGroup: function(targetItem) {
		// hide the children of targetItem
		var lastgrp = -1;
		var res = this.items.partition(function(item) {
			if(!item.is_article) {
				lastgrp = item.hp_id;
			} else if(lastgrp == targetItem.hp_id) {
				item.ele.style.display = "none";
				return false;
			}
			return true;
		});
		this.items = res[0];
		targetItem.hiddenChildren = res[1];
		targetItem.ele.getElementsByTagName("img")[0].src = "/x/drop_right_red.gif";

		targetItem.collapsed = true;
		this.relayout();
	},

	expandGroup: function(targetItem) {
		// expand the children of targetItem
		this.items = this.items.inject(Array(), function(items, item, i) {
			// accumulate all items in "items"
			items.push(item);
			if(item == targetItem) {
				targetItem.hiddenChildren.each(function(hiddenItem) {
					hiddenItem.ele.style.display = '';  // make visible
					items.push(hiddenItem);
				});
			}
			return items;
		});

		targetItem.hiddenChildren = null;
		targetItem.ele.getElementsByTagName("img")[0].src = "/x/drop_down_red.gif";
		targetItem.collapsed = false;
		this.relayout();
	},

	hideAll: function() {
		this.items.each(function(item) {
			if(!item.is_article && !item.collapsed)
				this.hideGroup(item);
		}.bind(this));
	},

	expandAll: function() {
		this.items.each(function(item) {
			if(!item.is_article && item.collapsed)
				this.expandGroup(item);
		}.bind(this));
	},

	_getorder: function() {
		this.items = this.items.sortBy(function(node, i) { 
						return parseInt(node.ele.style.top, 10); 
					});
	},

	relayout: function(full_repaint) {
		// make sure first item is a group
		if(this.items.first().hp_id != this.firstGroupId) {
			if(this.firstGroupId == 0 && !this.items.first().is_article) {
				// we just added our first group, so it goes above the orphan group
				this.firstGroupId = this.items.first().hp_id;
			} else {
				groupItem = this.findById(this.firstGroupId);
				this.items = this.items.reject(function(item) {
					return (item.hp_id == this.firstGroupId);
				}.bind(this));
				this.items.unshift(groupItem);
			}
		}

		var i = 1;
		this.items.each(function(item) {
			ele = $(item.ele);
			ele.style.bottom = '';
			ele.style.top = ((i * this.options.ele_space_per)+10) +'px';
			i++;
		}.bind(this));

		if (full_repaint)
		{
			this.items.each(function(item) { item.repaint(); });
		}

		// TODO: don't set this unless it has changed
		canvas_height = ((this.items.length + 2) * this.options.ele_space_per) + 20;
		this.canvas.style.height = canvas_height +'px';
	},

	getitemstate: function() {
		// return array of records describing current order of items
		// records have keys "id", "group" (group ID), and "pos"

		this._getorder();
		items_state = Array();

		pos = 0;
		this.items.each(function(item) {
			if(!item.is_article) {
				groupId = item.hp_id;
				pos = 0;
				// collapsed group items are not in this.items
				if(item.hiddenChildren) {
					item.hiddenChildren.each(function(hiddenitem) {
						items_state.push({id : hiddenitem.hp_id, group : groupId, pos : pos++});
					});
				}
			} else if(groupId == 0) {
				items_state.push({id : item.hp_id, group : 0, pos : 0});
			} else {
				items_state.push({id : item.hp_id, group : groupId, pos : pos++});
			}
		});
		return items_state;
	}
};


// -------- Start GroupReorderItem here -------------------------- //

var GroupReorderItem = Class.create();
GroupReorderItem.prototype = {

	initialize: function(name, ele, hp_id, is_article, showname) {
		// name - text of element
		// ele - the HTML element referring to this item
		// hp_id - article ID or article group ID
		// is_article - "true" if ele represents an article
		// showname - 0 or 1, only relevant for groups
		// TODO name and showname are stored here more as a convenience,
		// they have nothing to do with DND, they're used in the edit group
		// form, and should be moved out to seperate data structure

		ele = $(ele);

		this.name = name;
		this.hp_id = hp_id;
		this.ele = ele;
		this.is_article = is_article;
		this.showname = showname;

		this.opts = GroupReorderItem.reorder.options;
		this.dragged = false;

		this.draggerObj = new Dragger(ele, true);

		// for collapsing/expanding group
		this.collapsed = false;
		this.hiddenChildren = null;

		// limit potential drag area
		loc = Position.realOffset(GroupReorderItem.reorder.canvas);
		dim = Element.getDimensions(GroupReorderItem.reorder.canvas);
		add_overhang = this.opts.filter_additional_overhang;
		this.draggerObj.addFilter(Dragger.filters.SQUARE, loc[0] - add_overhang, loc[1], dim.width - add_overhang, dim.height - this.opts.ele_height);

		// bind callback functions
		this.draggerObj.onstop = this.ondragdone.bind(this);
		this.draggerObj.ondrag = this.ondrag.bind(this);

		this.ondblclickListener = this.ondblclick.bindAsEventListener(this);
		this.ele.ondblclick = this.ondblclickListener;
	},

	ondblclick: function(e) {
		// expand/collapse group
		GroupReorderItem.reorder.toggleGroup(this);

		//e = e || window.event;
		//var ele = Event.element(e);
	},

	ondrag: function(e) {
		if (!this.is_article) {
			// groups shouldn't be dragged
			this.draggerObj.stop();
			this.repaint();
			GroupReorderItem.reorder.relayout();
		} else {
			this.dragged = true; 
			this.ele.style.zIndex = '100'; 
		}
	},

	ondragdone: function() {
		if(this.dragged) {
			// find the previous entry, and check if it's collapsed
			// defaulting groupItem to first() catches the case of the item
			// being dragged above the canvas
			var groupItem = GroupReorderItem.reorder.items.first();
			var top = parseInt(this.ele.style.top, 10);
			GroupReorderItem.reorder.items.detect(function(item) {
				if(parseInt(item.ele.style.top, 10) > top)
					return true;
				groupItem = item;
			});

			if(groupItem.collapsed) {
				this.ele.style.display = 'none';
				groupItem.hiddenChildren.push(this);
				GroupReorderItem.reorder.items = GroupReorderItem.reorder.items.without(this);
			}

			this.repaint();
			this.dragged = false;
			GroupReorderItem.reorder.ondragdone();
		}
	},

	repaint: function() {
		// deselect text that can be inadvertently selected during drag or dbl click
		// in IE we must use onselectstart="return false", in the div tag
		if (browser != 'IE') { Field.focus(this.ele); } 
		// TODO this should come from constants
		this.ele.style.left = '5px';
		this.ele.style.right = '';
		this.ele.style.zIndex = 1;
	}
};
// image display control class.  This holds all data used to display
// slideshows, thumbnail views, etc, as well as render the "default"
// inline view.

var ImageViewerControl = Class.create();

ImageViewerControl.prototype = {

	initialize: function(moduleId, floatStatus, dispStatus, popupFlg) {
		this.modId = moduleId;
		this.floatStatus = floatStatus;   // 'none', 'right'
		this.displayStatus = dispStatus;  // 'No Border', 'With Border', 'Thumbnail', 'Slideshow'
		this.popupFlg = popupFlg;         // boolean

		this.photoData = new Object(); // indexed by ID
		this.photoOrder = new Array(); // values are IDs

		this.viewer_id = null;  // currently loaded image
		this.timer = null;      // slideshow timer
		this.slide_idx = -1;    // slideshow index

		this.resources = {
			ht_viewer_sect: 'image_viewer_' + this.modId,
			ht_inline_sect: 'image_inline_' + this.modId,
			ht_slideshow_sect: 'image_slideshow_' + this.modId,
			ht_thumbnail_sect: 'image_thumbnail_' + this.modId,

			inline_images: 'imgs_' + this.modId,

			viewer_display: 'slide_display_' + this.modId,
			viewer_photo:   'slide_img_'+ this.modId,
			viewer_caption: 'slide_desc_' + this.modId,

			thumb_tn_section: 'slide_tn_section_' + this.modId,

			slide_play_btn: 'play_' + this.modId,
			slide_pause_btn: 'pause_' + this.modId
		};
	},

	addPhoto: function(rec) {
		this.photoData[rec.id] = rec;
		this.photoOrder.push(rec.id);
	},

	clear: function() {
		// TODO not sure what sort of cleanup is ideal here
		delete this.photoData;
		this.photoData = new Object();
		this.photoOrder.clear();
	},

	render: function() {
		// render self based on displayStatus
		switch(this.displayStatus) {
			case 'No Border':
			case 'With Border':
				this.renderInlineImages();
				break;
			case 'Thumbnail':
				this.renderThumbnails();
				break;
			case 'Slideshow':
				this.viewer_id = -1;
				this.nextSlide(true);
				break;
		}
	},

	toggleViewer: function() {
		// switch which section is visible based on displayStatus
		// NOTE:  should only be used in hubtool.
		switch(this.displayStatus) {
			case 'No Border':
			case 'With Border':
                Element.hide(this.resources.ht_viewer_sect);
                Element.show(this.resources.ht_inline_sect);
                Element.hide(this.resources.ht_thumbnail_sect);
                Element.hide(this.resources.ht_slideshow_sect);
                break;
			case 'Thumbnail':
                Element.show(this.resources.ht_viewer_sect);
                Element.hide(this.resources.ht_inline_sect);
                Element.show(this.resources.ht_thumbnail_sect);
                Element.hide(this.resources.ht_slideshow_sect);
                break;
			case 'Slideshow':
                Element.show(this.resources.ht_viewer_sect);
                Element.hide(this.resources.ht_inline_sect);
                Element.hide(this.resources.ht_thumbnail_sect);
                Element.show(this.resources.ht_slideshow_sect);
                break;
		}
	},

	/*** Image Viewer ***/

	loadSlide: function(id) {
		this.viewer_id = id;
		rec = this.photoData[id];

		$(this.resources.viewer_photo).innerHTML = this._getDisplayUrl();
		$(this.resources.viewer_caption).innerHTML = rec.caption;

		// now center the image vertically
		var imgHeight = this._getDisplayHeight(this.viewer_id);
		var viewerHeight = parseInt($(this.resources.viewer_display).style.height, 10);
		$(this.resources.viewer_photo).style.top = Math.floor((viewerHeight-imgHeight)/2) + "px";

		// add full-size popup functionality
		if(this.popupFlg) {
			this._addpopup(id, $(this.resources.viewer_photo).firstChild);
		}
	},

	getMaxDisplayHeight: function() {
		// utility: calculate and return the height of the tallest image
		var top = 0;
		this.photoOrder.each(function(id) {
			var hgt = this._getDisplayHeight(id);
			top = hgt > top ? hgt : top;
		}.bind(this));
		return top;
	},

	_getDisplayUrl: function() {
		// utility: fetch the correct URL for selected image
		rec = this.photoData[this.viewer_id];
		if(rec.maxSize == 2 && this.displayStatus == 'With Border') {
			// force small image size
			return this._createImageTag(rec.urlQuarter, "quarter_frame", rec.esc_cap);
		} else if(rec.maxSize == 2) {
			return this._createImageTag(rec.urlQuarter, "quarter", rec.esc_cap);
		} else if((this.floatStatus == 'right' || rec.maxSize == 1) && this.displayStatus == 'With Border') {
			return this._createImageTag(rec.urlHalfPad, "half_frame", rec.esc_cap);
		} else if(this.floatStatus == 'right' || rec.maxSize == 1) {
			return this._createImageTag(rec.urlHalf, "half", rec.esc_cap);
		} else if(this.floatStatus == 'none' && this.displayStatus == 'With Border') {
			return this._createImageTag(rec.urlFullPad, "full_frame", rec.esc_cap);
		} else if(this.floatStatus == 'none') {
			return this._createImageTag(rec.urlFull, "full", rec.esc_cap);
		}
	},

	_createImageTag: function(url, imgClass, caption) {
		return "<img class='"+imgClass+"' title='"+caption+"' alt='"+caption+"' src='"+url+"' />";
	},

	_getDisplayHeight: function(img_id) {
		// utility: calculate and return the height for image
		rec = this.photoData[img_id];
		if(rec.maxSize == 2) {
			return rec.ratio * 120; // 120 regardless of padding
		} else if((this.floatStatus == 'right' || rec.maxSize == 1) && this.displayStatus == 'With Border') {
			return rec.ratio * 248;
		} else if(this.floatStatus == 'right' || rec.maxSize == 1) {
			return rec.ratio * 260;
		} else if(this.floatStatus == 'none' && this.displayStatus == 'With Border') {
			return rec.ratio * 496;
		} else if(this.floatStatus == 'none') {
			return rec.ratio * 520;
		}
	},

	/*** Inline Photo View ***/

	_addInlineImage: function(id) {
		this.viewer_id = id;
		var rec = this.photoData[id];
		var newDiv = document.createElement("div");
		var imgUrl = this._getDisplayUrl();
		if(this.floatStatus == 'none') {
			var capClass = 'caption_full';
		} else {
			var capClass = 'caption_half';
		}
		newDiv.id = "img_" + rec.id;
		newDiv.innerHTML =
			"<div id='img_url_" + rec.id + "'>" +
				imgUrl +
			"</div>" +
			"<div class='" + capClass + "' id='img_desc_" + rec.id + "'>" +
				rec.caption +
			"</div>";
		$(this.resources.inline_images).appendChild(newDiv);

		// add full-size popup functionality
		if(this.popupFlg) {
			this._addpopup(rec.id, $("img_url_"+rec.id).firstChild);
		}
	},

	renderInlineImages: function() {
		$(this.resources.inline_images).innerHTML = "";
		this.photoOrder.each(function(id) {
			this._addInlineImage(id);
		}.bind(this));
	},

	/*** Thumbnail View ***/

	_addThumbnail: function(id) {
		var rec = this.photoData[id];
		var newImg = document.createElement("img");
		newImg.id = "slide_tn_" + rec.id;
		newImg.src = rec.urlThumb;
		newImg.alt = rec.caption;
		newImg.title = rec.caption;
		newImg.onclick = function () {
			this.loadSlide(rec.id);
		}.bind(this);
		$(this.resources.thumb_tn_section).appendChild(newImg);
	},

	renderThumbnails: function() {
		$(this.resources.thumb_tn_section).innerHTML = "";
		this.photoOrder.each(function(id) {
			this._addThumbnail(id);
		}.bind(this));
		if(this.photoOrder.length > 0) {
			$("slide_tn_"+this.photoOrder[0]).onclick(); // select first tn
		}
	},

	/*** Slideshow View ***/

	nextSlide: function(advance) {
		if(this.photoOrder.length == 0) return;
		if(advance) {
			this.slide_idx++;
			if(this.slide_idx >= this.photoOrder.length) {
				this.slide_idx = 0;
			}
		} else {
			this.slide_idx--;
			if(this.slide_idx < 0) {
				this.slide_idx = this.photoOrder.length-1;
			}
		}
		var id = this.photoOrder[this.slide_idx];
		this.loadSlide(id);
	},

	startTimer: function() {
		this.nextSlide(true);
		this.timer = setTimeout(this.startTimer.bind(this), 3000);  // 3 seconds?
		Element.show($("pause_"+this.modId));
		Element.hide($(this.resources.slide_play_btn));
	},

	stopTimer: function() {
		clearTimeout(this.timer);
		Element.show($(this.resources.slide_play_btn));
		Element.hide($(this.resources.slide_pause_btn));
	},

	/*** Image Pop-Up ***/

	_addpopup: function(id, img) {
		// utility - add popup stuff to image
		img.onclick = function () {
			this.popupFullsize(id);
		}.bind(this);
		img.title = "Click to see full-size image."
		img.style.cursor = 'pointer';
	},

	popupFullsize: function(id) {
		// pop up full-size version of image
		// TODO for now just a seperate window -- do we want a litebox?
		rec = this.photoData[id];
		attrs = "width="+rec.origWidth+",height="+rec.origHeight+",resizable=yes,scrollbars=no,toolbar=no,status=no,menubar=no,directories=no,location=no";
		window.open(rec.urlOriginal, '', attrs);
	}

}
/**
 * addEvent written by Dean Edwards, 2005
 * with input from Tino Zijdel
 *
 * http://dean.edwards.name/weblog/2005/10/add-event/
 **/
function addEvent(element, type, handler) {
	// assign each event handler a unique ID
	if (!handler.$$guid) handler.$$guid = addEvent.guid++;
	// create a hash table of event types for the element
	if (!element.events) element.events = {};
	// create a hash table of event handlers for each element/event pair
	var handlers = element.events[type];
	if (!handlers) {
		handlers = element.events[type] = {};
		// store the existing event handler (if there is one)
		if (element["on" + type]) {
			handlers[0] = element["on" + type];
		}
	}
	// store the event handler in the hash table
	handlers[handler.$$guid] = handler;
	// assign a global event handler to do all the work
	element["on" + type] = handleEvent;
};
// a counter used to create unique IDs
addEvent.guid = 1;

function removeEvent(element, type, handler) {
	// delete the event handler from the hash table
	if (element.events && element.events[type]) {
		delete element.events[type][handler.$$guid];
	}
};

function handleEvent(event) {
	var returnValue = true;
	// grab the event object (IE uses a global event object)
	event = event || fixEvent(window.event);

	// fixes problem in IE where this.events is null somehow
	if( this.events == null ) { return false; }

	// get a reference to the hash table of event handlers
	var handlers = this.events[event.type];
	// execute each event handler
	for (var i in handlers) {
		this.$$handleEvent = handlers[i];
		if (this.$$handleEvent(event) === false) {
			returnValue = false;
		}
	}
	return returnValue;
};

function fixEvent(event) {
	// add W3C standard event methods
	event.preventDefault = fixEvent.preventDefault;
	event.stopPropagation = fixEvent.stopPropagation;
	return event;
};
fixEvent.preventDefault = function() {
	this.returnValue = false;
};
fixEvent.stopPropagation = function() {
	this.cancelBubble = true;
};

// end from Dean Edwards


/**
 * Creates an Element for insertion into the DOM tree.
 * From http://simon.incutio.com/archive/2003/06/15/javascriptWithXML
 *
 * @param element the element type to be created.
 *				e.g. ul (no angle brackets)
 **/
function createElement(element) {
	if (typeof document.createElementNS != 'undefined') {
		return document.createElementNS('http://www.w3.org/1999/xhtml', element);
	}
	if (typeof document.createElement != 'undefined') {
		return document.createElement(element);
	}
	return false;
}

/**
 * "targ" is the element which caused this function to be called
 * from http://www.quirksmode.org/js/events_properties.html
 **/
function getEventTarget(e) {
	var targ;
	if (!e) {
		e = window.event;
	}
	if (e.target) {
		targ = e.target;
	} else if (e.srcElement) {
		targ = e.srcElement;
	}
	if (targ.nodeType == 3) { // defeat Safari bug
		targ = targ.parentNode;
	}

	return targ;
}
/**
 * Written by Neil Crosby. 
 * http://www.workingwith.me.uk/
 *
 * Use this wherever you want, but please keep this comment at the top of this file.
 *
 * Copyright (c) 2006 Neil Crosby
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy 
 * of this software and associated documentation files (the "Software"), to deal 
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
 * SOFTWARE.
 **/
var css = {
	/**
	 * Returns an array containing references to all elements
	 * of a given tag type within a certain node which have a given class
	 *
	 * @param node		the node to start from 
	 *					(e.g. document, 
	 *						  getElementById('whateverStartpointYouWant')
	 *					)
	 * @param searchClass the class we're wanting
	 *					(e.g. 'some_class')
	 * @param tag		 the tag that the found elements are allowed to be
	 *					(e.g. '*', 'div', 'li')
	 **/
	getElementsByClass : function(node, searchClass, tag) {
		var classElements = new Array();
		var els = node.getElementsByTagName(tag);
		var elsLen = els.length;
		var pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)");
		
		
		for (var i = 0, j = 0; i < elsLen; i++) {
			if (this.elementHasClass(els[i], searchClass) ) {
				classElements[j] = els[i];
				j++;
			}
		}
		return classElements;
	},


	/**
	 * PRIVATE.  Returns an array containing all the classes applied to this
	 * element.
	 *
	 * Used internally by elementHasClass(), addClassToElement() and 
	 * removeClassFromElement().
	 **/
	privateGetClassArray: function(el) {
		return el.className.split(' '); 
	},

	/**
	 * PRIVATE.  Creates a string from an array of class names which can be used 
	 * by the className function.
	 *
	 * Used internally by addClassToElement().
	 **/
	privateCreateClassString: function(classArray) {
		return classArray.join(' ');
	},

	/**
	 * Returns true if the given element has been assigned the given class.
	 **/
	elementHasClass: function(el, classString) {
		if (!el) {
			return false;
		}
		
		var regex = new RegExp('\\b'+classString+'\\b');
		if (el.className.match(regex)) {
			return true;
		}

		return false;
	},

	/**
	 * Adds classString to the classes assigned to the element with id equal to
	 * idString.
	 **/
	addClassToId: function(idString, classString) {
		this.addClassToElement(document.getElementById(idString), classString);
	},

	/**
	 * Adds classString to the classes assigned to the given element.
	 * If the element already has the class which was to be added, then
	 * it is not added again.
	 **/
	addClassToElement: function(el, classString) {
		var classArray = this.privateGetClassArray(el);

		if (this.elementHasClass(el, classString)) {
			return; // already has element so don't need to add it
		}

		classArray.push(classString);

		el.className = this.privateCreateClassString(classArray);
	},

	/**
	 * Removes the given classString from the list of classes assigned to the
	 * element with id equal to idString
	 **/
	removeClassFromId: function(idString, classString) {
		this.removeClassFromElement(document.getElementById(idString), classString);
	},

	/**
	 * Removes the given classString from the list of classes assigned to the
	 * given element.  If the element has the same class assigned to it twice, 
	 * then only the first instance of that class is removed.
	 **/
	removeClassFromElement: function(el, classString) {
		var classArray = this.privateGetClassArray(el);

		for (x in classArray) {
			if (classString == classArray[x]) {
				classArray[x] = '';
				break;
			}
		}

		el.className = this.privateCreateClassString(classArray);
	}
}
/**
 * Written by Neil Crosby. 
 * http://www.workingwith.me.uk/articles/scripting/standardista_table_sorting
 *
 * This module is based on Stuart Langridge's "sorttable" code.  Specifically, 
 * the determineSortFunction, sortCaseInsensitive, sortDate, sortNumeric, and
 * sortCurrency functions are heavily based on his code.  This module would not
 * have been possible without Stuart's earlier outstanding work.
 *
 * Use this wherever you want, but please keep this comment at the top of this file.
 *
 * Copyright (c) 2006 Neil Crosby
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy 
 * of this software and associated documentation files (the "Software"), to deal 
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
 * SOFTWARE.
 **/

var standardistaTableSorting = {

	that: false,

	sortColumnIndex : -1,
	lastAssignedId : 0,
	newRows: -1,
	lastSortedTable: -1,

	/**
	 * Initialises the Standardista Table Sorting module
	 **/
	init : function() {
		// first, check whether this web browser is capable of running this script
		if (!document.getElementsByTagName) {
			return;
		}
		
		this.that = this;
		
		this.run();
		
	},
	
	/**
	 * Runs over each table in the document, making it sortable if it has a class
	 * assigned named "sortable" and an id assigned.
	 **/
	run : function() {
		var tables = document.getElementsByTagName("table");
		
		for (var i=0; i < tables.length; i++) {
			var thisTable = tables[i];
			
			if (css.elementHasClass(thisTable, 'sortable')) {
				this.makeSortable(thisTable);
			}
		}
	},
	
	/**
	 * Makes the given table sortable.
	 **/
	makeSortable : function(table) {
	
		// first, check if the table has an id.  if it doesn't, give it one
		if (!table.id) {
			table.id = 'sortableTable'+this.lastAssignedId++;
		}
		
		// if this table does not have a thead, we don't want to know about it
		if (!table.tHead || !table.tHead.rows || 0 == table.tHead.rows.length) {
			return;
		}
		
		// we'll assume that the last row of headings in the thead is the row that 
		// wants to become clickable
		var row = table.tHead.rows[table.tHead.rows.length - 1];
		
		for (var i=0; i < row.cells.length; i++) {
		
			var linkEl = row.cells[i].firstChild;
			// linkEl.onclick = this.headingClicked;
			linkEl.onclick = this.headingClicked;
			linkEl.setAttribute('columnId', i);
		}
	},
	
	sortTheTable : function(e) {

		var that = standardistaTableSorting.that;
		
		// linkEl is the hyperlink that was clicked on which caused
		// this method to be called
		var linkEl = getEventTarget(e);
		
		// directly outside it is a td, tr, thead and table
		var td     = linkEl.parentNode;
		var tr     = td.parentNode;
		var thead  = tr.parentNode;
		var table  = thead.parentNode;
		
		// if the table we're looking at doesn't have any rows
		// (or only has one) then there's no point trying to sort it
		if (!table.tBodies || table.tBodies[0].rows.length <= 1) {
			return false;
		}

		// the column we want is indicated by td.cellIndex
		var column = linkEl.getAttribute('columnId') || td.cellIndex;
		//var column = td.cellIndex;
		
		// find out what the current sort order of this column is
		var arrows = css.getElementsByClass(td, 'tableSortArrow', 'span');
		var previousSortOrder = '';
		if (arrows.length > 0) {
			previousSortOrder = arrows[0].getAttribute('sortOrder');
		}
		
		// work out how we want to sort this column using the data in the first cell
		// but just getting the first cell is no good if it contains no data
		// so if the first cell just contains white space then we need to track
		// down until we find a cell which does contain some actual data
		var itm = ''
		var rowNum = 0;
		while ('' == itm && rowNum < table.tBodies[0].rows.length) {
			itm = that.getInnerText(table.tBodies[0].rows[rowNum].cells[column]);
			rowNum++;
		}
		var sortfn = that.determineSortFunction(itm);
		var newRows;

		// if the last column that was sorted was this one, then all we need to 
		// do is reverse the sorting on this column
		if (table.id == that.lastSortedTable && column == that.sortColumnIndex) {
			newRows = that.newRows;
			newRows.reverse();
		// otherwise, we have to do the full sort
		} else {
			that.sortColumnIndex = column;
			newRows = new Array();

			for (var j = 0; j < table.tBodies[0].rows.length; j++) { 
				newRows[j] = table.tBodies[0].rows[j]; 
			}
			newRows.sort(sortfn);
		}

		that.moveRows(table, newRows);
		that.newRows = newRows;
		that.lastSortedTable = table.id;
		
		// now, give the user some feedback about which way the column is sorted
		
		// first, get rid of any arrows in any heading cells
		var arrows = css.getElementsByClass(tr, 'tableSortArrow', 'span');
		for (var j = 0; j < arrows.length; j++) {
			if( j == column )
			{
				if (null == previousSortOrder || '' == previousSortOrder || 'DESC' == previousSortOrder) {
					arrows[j].innerHTML = '\u25bc';
					arrows[j].setAttribute('sortOrder', 'ASC');
				} else {
					arrows[j].innerHTML = '\u25b2';
					arrows[j].setAttribute('sortOrder', 'DESC');
				}
			} else {
				arrows[j].innerHTML = '&nbsp;';
			}
		}

		return false;
	},

	headingClicked: function(e) {

		var that = standardistaTableSorting.that;
		// myGlobalHandlers.flagUp();
		that.sortTheTable(e);
		// myGlobalHandlers.flagDown();

		return false;
	},

	getInnerText : function(el) {
		
		if ('string' == typeof el || 'undefined' == typeof el) {
			return el;
		}
		
		if (el.innerText) {
			return el.innerText;  // Not needed but it is faster
		}

		var str = el.getAttribute('standardistaTableSortingInnerText');
		if (null != str && '' != str) {
			return str;
		}
		str = '';

		var cs = el.childNodes;
		var l = cs.length;
		for (var i = 0; i < l; i++) {
			// 'if' is considerably quicker than a 'switch' statement, 
			// in Internet Explorer which translates up to a good time 
			// reduction since this is a very often called recursive function
			if (1 == cs[i].nodeType) { // ELEMENT NODE
				str += this.getInnerText(cs[i]);
				break;
			} else if (3 == cs[i].nodeType) { //TEXT_NODE
				str += cs[i].nodeValue;
				break;
			}
		}
		
		// set the innertext for this element directly on the element
		// so that it can be retrieved early next time the innertext
		// is requested
		el.setAttribute('standardistaTableSortingInnerText', str);
		
		return str;
	},

	determineSortFunction : function(itm) {
		
		var sortfn = this.sortCaseInsensitive;
		
		if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) {
			sortfn = this.sortDate;
		}
		if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) {
			sortfn = this.sortDate;
		}
		if (itm.match(/^[£$]/)) {
			sortfn = this.sortCurrency;
		}
		if (itm.match(/^\d?\.?\d+$/)) {
			sortfn = this.sortNumeric;
		}
		if (itm.match(/^[+-]?\d*\.?\d+([eE]-?\d+)?$/)) {
			sortfn = this.sortNumeric;
		}
    		if (itm.match(/^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$/)) {
        		sortfn = this.sortIP;
   		}

		return sortfn;
	},
	
	sortCaseInsensitive : function(a, b) {
		var that = standardistaTableSorting.that;
		
		var aa = that.getInnerText(a.cells[that.sortColumnIndex]).toLowerCase();
		var bb = that.getInnerText(b.cells[that.sortColumnIndex]).toLowerCase();
		if (aa==bb) {
			return 0;
		} else if (aa<bb) {
			return -1;
		} else {
			return 1;
		}
	},
	
	sortDate : function(a,b) {
		var that = standardistaTableSorting.that;

		// y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
		var aa = that.getInnerText(a.cells[that.sortColumnIndex]);
		var bb = that.getInnerText(b.cells[that.sortColumnIndex]);
		
		var dt1, dt2, yr = -1;
		
		if (aa.length == 10) {
			dt1 = aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2);
		} else {
			yr = aa.substr(6,2);
			if (parseInt(yr) < 50) { 
				yr = '20'+yr; 
			} else { 
				yr = '19'+yr; 
			}
			dt1 = yr+aa.substr(3,2)+aa.substr(0,2);
		}
		
		if (bb.length == 10) {
			dt2 = bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2);
		} else {
			yr = bb.substr(6,2);
			if (parseInt(yr) < 50) { 
				yr = '20'+yr; 
			} else { 
				yr = '19'+yr; 
			}
			dt2 = yr+bb.substr(3,2)+bb.substr(0,2);
		}
		
		if (dt1==dt2) {
			return 0;
		} else if (dt1<dt2) {
			return -1;
		}
		return 1;
	},

	sortCurrency : function(a,b) { 
		var that = standardistaTableSorting.that;

		var aa = that.getInnerText(a.cells[that.sortColumnIndex]).replace(/[^0-9.]/g,'');
		var bb = that.getInnerText(b.cells[that.sortColumnIndex]).replace(/[^0-9.]/g,'');
		return parseFloat(aa) - parseFloat(bb);
	},

	sortNumeric : function(a,b) { 
		var that = standardistaTableSorting.that;

		var aa = parseFloat(that.getInnerText(a.cells[that.sortColumnIndex]));
		if (isNaN(aa)) { 
			aa = 0;
		}
		var bb = parseFloat(that.getInnerText(b.cells[that.sortColumnIndex])); 
		if (isNaN(bb)) { 
			bb = 0;
		}
		return aa-bb;
	},

	makeStandardIPAddress : function(val) {
		var vals = val.split('.');

		for (x in vals) {
			val = vals[x];

			while (3 > val.length) {
				val = '0'+val;
			}
			vals[x] = val;
		}

		val = vals.join('.');

		return val;
	},

	sortIP : function(a,b) { 
		var that = standardistaTableSorting.that;

		var aa = that.makeStandardIPAddress(that.getInnerText(a.cells[that.sortColumnIndex]).toLowerCase());
		var bb = that.makeStandardIPAddress(that.getInnerText(b.cells[that.sortColumnIndex]).toLowerCase());
		if (aa==bb) {
			return 0;
		} else if (aa<bb) {
			return -1;
		} else {
			return 1;
		}
	},

	moveRows : function(table, newRows) {
		// We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
		for (var i=0;i<newRows.length;i++) { 
			var rowItem = newRows[i];

			table.tBodies[0].appendChild(rowItem); 
		}
	}
}

function standardistaTableSortingInit() {
	standardistaTableSorting.init();
}

addEvent(window, 'load', standardistaTableSortingInit)

