/*
 * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
 * in FIPS PUB 180-1
 * Version 2.1a Copyright Paul Johnston 2000 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for details.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}

/*
 * Perform a simple self-test to see if the VM is working
 */
function sha1_vm_test()
{
  return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
}

/*
 * Calculate the SHA-1 of an array of big-endian words, and a bit length
 */
function core_sha1(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << (24 - len % 32);
  x[((len + 64 >> 9) << 4) + 15] = len;

  var w = Array(80);
  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;
  var e = -1009589776;

  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;
    var olde = e;

    for(var j = 0; j < 80; j++)
    {
      if(j < 16) w[j] = x[i + j];
      else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
      var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
                       safe_add(safe_add(e, w[j]), sha1_kt(j)));
      e = d;
      d = c;
      c = rol(b, 30);
      b = a;
      a = t;
    }

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
    e = safe_add(e, olde);
  }
  return Array(a, b, c, d, e);

}

/*
 * Perform the appropriate triplet combination function for the current
 * iteration
 */
function sha1_ft(t, b, c, d)
{
  if(t < 20) return (b & c) | ((~b) & d);
  if(t < 40) return b ^ c ^ d;
  if(t < 60) return (b & c) | (b & d) | (c & d);
  return b ^ c ^ d;
}

/*
 * Determine the appropriate additive constant for the current iteration
 */
function sha1_kt(t)
{
  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
         (t < 60) ? -1894007588 : -899497514;
}

/*
 * Calculate the HMAC-SHA1 of a key and some data
 */
function core_hmac_sha1(key, data)
{
  var bkey = str2binb(key);
  if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz);

  var ipad = Array(16), opad = Array(16);
  for(var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
  return core_sha1(opad.concat(hash), 512 + 160);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert an 8-bit or 16-bit string to an array of big-endian words
 * In 8-bit function, characters >255 have their hi-byte silently ignored.
 */
function str2binb(str)
{
  var bin = Array();
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < str.length * chrsz; i += chrsz)
    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
  return bin;
}

/*
 * Convert an array of big-endian words to a string
 */
function binb2str(bin)
{
  var str = "";
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < bin.length * 32; i += chrsz)
    str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
  return str;
}

/*
 * Convert an array of big-endian words to a hex string.
 */
function binb2hex(binarray)
{
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8  )) & 0xF);
  }
  return str;
}

/*
 * Convert an array of big-endian words to a base-64 string
 */
function binb2b64(binarray)
{
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i += 3)
  {
    var triplet = (((binarray[i   >> 2] >> 8 * (3 -  i   %4)) & 0xFF) << 16)
                | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 )
                |  ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
    for(var j = 0; j < 4; j++)
    {
      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
    }
  }
  return str;
}
var Livesite = function (win, doc) {
  this.doc = doc;
  this.win = win;
  this.MASK = undefined;
  this.MASK_showCount = 0;

  this.STATUS_ID       = 'lsn_status';
  this.MESSAGE_BOX_ID  = 'lsn_message_box';
  this.TRACE_BOX_ID    = 'lsn_trace_box';
  this.LAYER_MASK      = 101;
  this.LAYER_DIALOG    = 102;
  this.LAYER_TRACE     = 103;
  this.LAYER_MSGBOX    = 104;
  this.LAYER_STATUS    = 104;

  this.LAYER_TOP = 100;
  this.SCROLLBAR_WIDTH = -1;

  this.GLOBAL_CSS = new this.Style();
};

Livesite.prototype = {

/*------------------------------------------------------------------------------
 * defined - Return true unless the variable type is 'undefined'
 * @param variable
 *----------------------------------------------------------------------------*/

  defined: function (unk) {
    return  unk === null               ? false :
            typeof(unk) == 'undefined' ? false :
                                         true;
  },

/* ------------------------------------------------------------------------------
 * isa - Is the unkown an instance of (or derived from) this class
 * isa unk, klass
 * --------------------------------------------------------------------------- */

  isa: function (unk, klass) {
    while (unk) {
      if (unk instanceof klass) return true;
      unk = unk.superclass;
    }
    return false;
  },

/*------------------------------------------------------------------------------
 * getElement - Cross-browser function for referring to a document element
 * @param unk Element id, Element object, or a function (which ought return an
 * element object)
 *----------------------------------------------------------------------------*/

  getElement: function (unk) {
    return  typeof(unk) == 'object'   ?   unk                                :
            typeof(unk) == 'function' ?   unk()                              :
            this.doc.getElementById   ?   this.doc.getElementById(unk)       :
            this.doc.all              ?   this.doc.all[unk]                  :
            this.doc.layers           ?   this.doc.layers[unk]               :
                                          false;
  },

/* ------------------------------------------------------------------------------
 * argsToArray - Convert arguments to a real Array
 * @param args arguments object
 * --------------------------------------------------------------------------- */

  argsToArray: function (args) {
    if (!args) return [];
    var len = args.len || 0;
    var result = new Array(len);
    while (len--) result[len] = args[len];
    return result;
  },

/* ------------------------------------------------------------------------------
 * getObject - Convenience method for getting a member object
 * @param unk <String> Object specification in dot notation
 * This line:
 *
 *    var lang = LSN.getObject('parent.Globals.config.lang');
 *
 * is eqivalent to:
 *
 *    var lang = parent && parent.Globals && parent.Globals.config
 *      ? parent.Globals.config.lang
 *      : undefined;
 * --------------------------------------------------------------------------- */

  getObject: function (unk) {
    if (!unk || typeof(unk) != 'string') throw 'Provide a String';
    if (!unk.match(/[A-Za-z_\$][A-Za-z_\$\.0-9]+/)) throw 'Illegal object address';
    var result = undefined;
    try {
      result = eval(unk);
    } catch (ex) {
      // all exceptions considered harmless
    }
    return result;
  },

/* ------------------------------------------------------------------------------
 * asInt - Integer representation of the provided unkown
 * @param unk The unknown
 * @param gez true|false, must be a positive integer
 * --------------------------------------------------------------------------- */

  asInt: function (unk, gez) {
    var i = unk;
    if (typeof(unk) == 'string') {
      i = parseInt(unk.replace("\w", ""));
    } else {
      i = parseInt(unk);
    }
    return isNaN(i) ? 0 : gez && i < 0 ? 0 : i;
  },

  grep: function (func, a) {
    if (!a || !(a instanceof Array)) return;
    var result = [];
    for (var i = 0; i < a.length; i++) {
      if (func(a[i])) result.push(a[i]);
    }
    return result;
  },

/* ------------------------------------------------------------------------------
 * randomId - Produce a random identifier
 * @param prefix
 * @param multiplier (default 100000)
 * --------------------------------------------------------------------------- */

  randomId: function (prefix, multiplier) {
    if (!multiplier) multiplier = 100000;
    var n = Math.floor(Math.random()*multiplier);
    return prefix ? prefix + n : n;
  },

/* ------------------------------------------------------------------------------
 * asHyphenatedName - Convert a camel-cased name to hypenated
 * --------------------------------------------------------------------------- */

  asHyphenatedName: function (name) {
    function upperToHyphenLower(match) { return '-' + match.toLowerCase(); }
    return name.replace(/[A-Z]/g, upperToHyphenLower);
  },

/* ------------------------------------------------------------------------------
 * asCamelCaseName - Convert the hyphenated name to camel-cased
 * --------------------------------------------------------------------------- */

  asCamelCaseName: function (name) {
    function ucFirstMatch(str, p1, offest, s) { return p1.toUpperCase(); }
    return name.replace(/-([a-z])/g, ucFirstMatch);
  },

/*------------------------------------------------------------------------------
 * getChildrenByTagName - Get child elements by their tag name
 * @param id of the target element, or an element object
 * @param tagName name of the child tags to return
 * @returns Array
 *----------------------------------------------------------------------------*/

  getChildrenByTagName: function (id, tagName) {
    return this.getChildren(id, {'tagName': tagName});
  },

/*------------------------------------------------------------------------------
 * getChilren - Get child elements by their attributes
 * @param id of the target element, or an element object
 * @param props collection of attributes which must match
 * @returns Array
 *
 * The attribute 'tagName' is handled in a special way, as it is not really a
 * node attribute.
 *
 * This example will return a list of INPUT elements which have a class equal
 * to 'ctrl':
 *
 *  var list = LSN.getChildren(document.body, {
 *    class: 'ctrl',
 *    tagName: 'input',
 *  });
 *----------------------------------------------------------------------------*/

  getChildren: function (id, props) {
    if (!id) throw ("Provide an element or element id");
    if (!props) throw ("Provide an attribute collection");
    var result = new Array();
    var elem = this.getElement(id);
    var len = elem.childNodes.length; // .length increases on read in ie6
    for (var i = 0; i < len; i++) {
      if (!elem.childNodes[i]) break;
      if (elem.childNodes[i].getAttributeNode) {
        var matches = true;
        for (var attr in props) {
          var value = attr == 'tagName'
            ? props[attr].toUpperCase()
            : props[attr];
          try {
            var nodeValue = attr == 'tagName'
              ? elem.childNodes[i].tagName
              : elem.childNodes[i].getAttributeNode(attr).value;
          } catch (err) {
            matches = false;
            continue;
          }
          if (value.match(/^~/)) {
            if (nodeValue) {
              var realValue = value.replace(/^~/,'');
              matches = matches & (nodeValue && (nodeValue.search(realValue) > -1));
            } else {
              matches = false;
            }
          } else {
            matches = matches & (nodeValue && (nodeValue == value));
          }
          if (!matches) break;
        }
        if (matches) result.push(elem.childNodes[i]);
        if (elem.childNodes[i].childNodes.length > 0) {
          var subElements = this.getChildren(elem.childNodes[i], props);
          for (var j = 0; j < subElements.length; j++) {
            result.push(subElements[j]);
          }
        }
      }
    }
    return result;
  },

  createElements: function() {
    var args = $A(arguments);
    var elems = new Array();
    while (args && args.length > 0) {
      var attrs = null;
      var children = null;
      var childNodes = null;
      var tag = args.shift();
      if (!this.defined(tag)) continue;
      if (typeof(tag) != 'string' && tag.nodeType) {
        elem = tag;
      } else {
        if (this.isHash(args[0])) {
          attrs = args.shift();
        }
        if (this.isArray(args[0])) {
          children = args.shift();
        }
        var elem = this.createElement(tag, attrs, children);
      }
      elems.push(elem);
    }
    return elems;
  },

  createElement: function (tagName, props, children) {
    if (!tagName) return;
    var elem = undefined;
    if (tagName.indexOf('#') == 0) {
      if (tagName == '#text') {
        elem = this.doc.createTextNode(props.nodeValue);
      } else {
        throw 'Component not available: ' + tagName;
      }
      if (children) throw 'Cannot append children to a #text node';
      return elem;
    } else {
      elem = props && props.namespace && this.doc.createElementNS
        ? this.doc.createElementNS(props.namespace, tagName.toUpperCase())
        : this.doc.createElement(tagName.toUpperCase());
    }
    if (props) {
      for (var k in props) {
        if (k && k == 'namespace') continue;
        var v = props[k];
        if (k == 'style' && typeof(v) == 'object') {
          for (var k2 in v) {
            var v2 = v[k2];
            this.setStyle(elem, k2, v2);
          }
        } else {
          if (k.indexOf('inner') == 0) {
            // dom property
            elem[k] = v;
          } else {
            // attribute
            this.setAttribute(elem, k, v);
          }
        }
      }
    }
    if (children) {
      this.appendChildren(elem, this.createElements.apply(this, children));
    }
    return elem;
  },

  replaceChildren: function (elem, children) {
    this.removeChildren(elem);
    this.appendChildren(elem, children);
  },

  appendChildren: function (elem, children) {
    elem = this.getElement(elem);
    if (!this.defined(elem)) throw new LSN.Error('elem not defined');
    if (!this.defined(children)) throw new LSN.Error('missing children');
    for (var i = 0; i < children.length; i++) {
      var child = children[i];
      if (!this.defined(child)) throw new LSN.Error('lost child');
      if (child instanceof Array) {
        this.appendChildren(elem, child);
      } else {
        elem.appendChild(children[i]);
      }
    }
  },

  insertChildrenAfter: function (elem, children) {
    elem = this.getElement(elem);
    if (!this.defined(elem)) throw new LSN.Error('elem not defined');
    if (!this.defined(children)) throw new LSN.Error('missing children');
    for (var i = 0; i < children.length; i++) {
      var child = children[i];
      if (!this.defined(child)) throw new LSN.Error('lost child');
      if (child instanceof Array) {
        elem = this.insertChildrenAfter(elem, child);
      } else {
        this.insertAfter(child, elem);
        elem = elem.nextSibling;
      }
    }
    return elem;
  },

  insertChildrenBefore: function (elem, children) {
    elem = this.getElement(elem);
    if (!this.defined(elem)) throw new LSN.Error('elem not defined');
    if (!this.defined(children)) throw new LSN.Error('missing children');
    for (var i = 0; i < children.length; i++) {
      var child = children[i];
      if (!this.defined(child)) throw new LSN.Error('lost child');
      if (child instanceof Array) {
        elem = this.insertChildrenBefore(elem, child);
      } else {
        elem.parentNode.insertBefore(child, elem);
      }
    }
    return elem;
  },

  removeElements: function (elem) {
    var args = $A(arguments);
    while (args && args.length > 0) {
      var elem = this.getElement(args.shift());
      if (!elem) continue;
      var pelem = elem.parentNode;
      if (!pelem) continue;
      pelem.removeChild(elem);
    }
  },

  isArray: function (unk) {
    return this.defined(unk) && (unk instanceof Array);
  },

  isHash: function (unk) {
    return this.defined(unk) && (unk instanceof Object) && !(unk instanceof Array);
  },

/* ------------------------------------------------------------------------------
 * getField - Return a name/value pair for the element
 * XXX Not used?
 * --------------------------------------------------------------------------- */

  getField: function (id) {
    var elem = this.getElement(id);
    if (!elem) throw new LSN.Error('Element not found: ' + id);
    var name = elem.name ? elem.name : elem.id;
    var value = this.getValue(elem);
    return this.defined(value)
      ? {'name': name, 'value': value}
      : undefined;
  },

/* ------------------------------------------------------------------------------
 * resolveValue - Follow '&:id' values which refer to other elements
 * --------------------------------------------------------------------------- */

  resolveValue: function (value) {
    if (value.match(/^&:/)) {
      return this.getValue(value.replace(/^&:/, ''));
    }
    return value;
  },

/* ------------------------------------------------------------------------------
 * getValue - Get the logical value according to element type
 * --------------------------------------------------------------------------- */

  getValue: function (elem) {
    var elem = this.getElement(elem);
    if (!elem) return;
    var value = undefined;
    switch (elem.tagName.toUpperCase()) {
      case 'INPUT':
        switch (elem.type.toUpperCase()) {
          case 'HIDDEN':
            value = this.resolveValue(elem.value);
            break;
          case 'CHECKBOX':
            value = elem.checked
              ? this.defined(elem.value)
                ? this.resolveValue(elem.value)
                : true
              : undefined;
            break;
          case 'RADIO':
            if (elem.checked) {
              value = this.defined(elem.value) ? this.resolveValue(elem.value) : true;
            } else {
              value = undefined;
            }
            break;
          case 'PASSWORD':
          case 'TEXT':
            value = elem.value;
            break;
          default:
            throw new LSN.Error('Unhandled input type: '+elem.type);
        }
        break;
      case 'TEXTAREA':
      case 'SELECT':
        value = elem.value;
        break;
      default:
        if (this.defined(elem.innerHTML)) {
          value = elem.innerHTML;
        } else {
          throw new LSN.Error('Unhandled tag: '+elem.tagName);
        }
    }
    return value;
  },

/*------------------------------------------------------------------------------
 * includeScript - Add a script to the current page DOM
 * @param text Text string containing script
 * @param id (optional) Element ID (will replace existing)
 *----------------------------------------------------------------------------*/

  includeScript: function (text, id) {
    if (!id) id = this.randomId('script');
    var elem = this.getElement(id);
    if (elem) elem.parentNode.removeChild(elem);
    this.headInclude('javascript', { type: 'text/javascript', id: id },text);
  },

/*------------------------------------------------------------------------------
 * includeScriptURI - Add a script from URI to the current page DOM
 * @param uri of the script to be included.
 *----------------------------------------------------------------------------*/

  includeScriptURI: function (uri) {
    /* First check if the js is currently linked */
    var scripts = this.doc.getElementsByTagName('script');
    var isIncluded = false;
    for(var i=0; i < scripts.length; i++) {
      var script = scripts[i]
      /*TODO Intelligent URI matching. */
      if(script.getAttribute('src') == uri) {
        isIncluded = true;
        break;
      }
    }
    if (!isIncluded) this.headInclude('js', {type: 'text/javascript', src: uri});
  },

/*------------------------------------------------------------------------------
 * includeStyle - Add a style or group of styles to the current page DOM
 * @param styles Text string containing styles
 * @param id (optional) Element ID (will replace existing)
 *----------------------------------------------------------------------------*/

  includeStyle: function (text, id) {

    if (!id) id = 'style' + Math.floor(Math.random()*10000);
    var elem = this.getElement(id);
    if (elem) {
      this.removeChildren(elem);
      if (Prototype.Browser.IE) {
        elem.styleSheet.cssText = text;
      } else {
        if (text) elem.appendChild(this.doc.createTextNode(text));
      }
    } else {
      this.headInclude('style', {
        'type':   'text/css',
        'media':  'screen',
        'id':     id
      }, text);
    }
  },

/*------------------------------------------------------------------------------
 * includeStyleURI - Add a stylesheet from URI to the current page DOM
 * @param uri of the stylesheet to be included.
 *----------------------------------------------------------------------------*/

  includeStyleURI: function (uri) {
    /* First check if the css is currently linked */
    var links = this.doc.getElementsByTagName('link');
    var isIncluded = false;
    for(var i=0; i < links.length; i++) {
      var link = links[i]
      /*TODO Intelligent URI matching. */
      if(link.getAttribute('href') == uri) {
        isIncluded = true;
        break;
      }
    }
    if (!isIncluded) this.headInclude('css', { 
      type: 'text/css',
      rel: 'stylesheet',
      href: uri,
      media: 'screen',
      title: 'includeStyleURI'
    });
  },

/*------------------------------------------------------------------------------
 * headInclude - add css or js items to the DOM without checks. For internal use.
 * @param type the type of item to include
 * @param props properties list for item.
 * @param text Contains string of content for populating
 *----------------------------------------------------------------------------*/

  headInclude: function (type, props, text) {
    var head = this.doc.getElementsByTagName('head')[0];
    if (!head) return;
    if(type == 'css') {
      head.appendChild(this.createElement('link', props));
    } else if(type == 'style') {
      var styleElem = this.createElement('style', props);
      if (Prototype.Browser.IE) {
        styleElem.styleSheet.cssText = text;
      } else {
        styleElem.appendChild(this.doc.createTextNode(text));
      }
      head.appendChild(styleElem);
    } else if(type == 'js' || type == 'javascript') {
      var scriptElem = this.createElement('script', props);
      if (Prototype.Browser.IE) {
        scriptElem.text = text;
      } else {
        scriptElem.appendChild(this.doc.createTextNode(text));
      }
      head.appendChild(scriptElem);
    }
  },

/* ------------------------------------------------------------------------------
 * insertAfter - Insert an element after another.
 * @param elem Element to insert
 * @param target Element which is to presceed it
 * --------------------------------------------------------------------------- */

  insertAfter: function (elem, target) {
    if (!(target && elem)) return;
    var p = target.parentNode;
    if (p.lastChild == target) {
      p.appendChild(elem);
    } else {
      p.insertBefore(elem, target.nextSibling);
    }
  },

/* ------------------------------------------------------------------------------
 * makePositioned - Give the element a positioned style
 * @param elem Element or Element ID
 * --------------------------------------------------------------------------- */

  makePositioned: function (elem) {
    elem = this.getElement(elem);
    var pos = this.getStyle(elem, 'position');
    if (pos == 'static' || !pos) {
      elem.style.position = 'relative';
      if (Prototype.Browser.Opera) {
        this.setStyle(elem, 'top', 0);
        this.setStyle(elem, 'left', 0);
      }
    }
    return elem;
  },

/* ------------------------------------------------------------------------------
 * getStyle - Get CSS property
 * --------------------------------------------------------------------------- */

  getStyle: function (elem, propName) {
    if (propName == 'cssFloat' && !Prototype.Browser.IE) propName = 'float';
    if (propName == 'cssFloat' && Prototype.Browser.IE) propName = 'styleFloat';
    if (propName == 'float' && Prototype.Browser.IE) propName = 'styleFloat';
    if (propName == 'float' && Prototype.Browser.Opera) propName = 'cssFloat';
    return this.getCSSAttribute(elem, propName);
  },

/* ------------------------------------------------------------------------------
 * setStyle - Set CSS property
 * --------------------------------------------------------------------------- */

  setStyle: function (elem, propName, propValue, importance) {
    var elem = this.getElement(elem);
    if (!elem || !elem.style) return;
    if (propName == 'cssFloat' && !Prototype.Browser.IE) propName = 'float';
    if (propName == 'cssFloat' && Prototype.Browser.IE) propName = 'styleFloat';
    if (propName == 'float' && Prototype.Browser.IE) propName = 'styleFloat';
    if (propName == 'float' && Prototype.Browser.Opera) propName = 'cssFloat';
    if (this.defined(elem.style.setProperty)) {
      elem.style.setProperty(propName, propValue, importance ? importance : null);
    } else {
      propName = this.asCamelCaseName(propName);
      try {
        elem.style[propName] = propValue;
      } catch (ex) {
        if (ex instanceof Object) {
          ex.description =
            'Cannot set style "' + propName + '" to "' + propValue + '".'
        }
        throw ex;
      }
    }
  },

/* ------------------------------------------------------------------------------
 * addClassNames - Multiplicity for prototype.js::Element.addClassName
 * --------------------------------------------------------------------------- */

  addClassNames: function () {
    var a = $A(arguments);
    var e = $(a.shift());
    if (!e) return;
    a.each(function(v){this.addClassName(v)}, e);
  },

/* ------------------------------------------------------------------------------
 * removeClassNames - Multiplicity for prototype.js::Element.removeClassName
 * --------------------------------------------------------------------------- */

  removeClassNames: function () {
    var a = $A(arguments);
    var e = $(a.shift());
    if (!e) return;
    a.each(function(v){this.removeClassName(v)}, e);
  },

  hasClassName: function (elem, name) {
    var className = this.getAttribute(elem, 'class');
    if (!className) return false;
    var names = className.split(/\s/);
    for (var i = 0; i < names.length; i++) {
      if (names[i] == name) return true;
    }
    return false;
  },

  getAttribute: function (elem, attrName) {
    var elem = this.getElement(elem);
    if (!elem) return;
    if (typeof(attrName) != 'string') return;
    if (attrName.indexOf('_') == 0) {
      // user-defined property
      return elem[attrName];
    } else if (attrName.indexOf('on') == 0) {
      // event
      attrName = attrName.toLowerCase();
      return elem[attrName];
    } else {
      if (attrName == 'className' && !Prototype.Browser.IE) attrName = 'class';
      if (attrName == 'class' && Prototype.Browser.IE) attrName = 'className';
      if (!this.defined(elem.getAttribute) || attrName.indexOf('inner') == 0) {
        return elem[attrName];
      } else {
        return elem.getAttribute(attrName);
      }
    }
  },

/* ------------------------------------------------------------------------------
 * setAttribute - Set element attribute
 *
 *  var eventName = attrName.substr(2).toLowerCase();
 *  Event.observe(elem, eventName,
 *    typeof(attrValue) == 'function' ? attrValue : new Function(attrValue));
 * --------------------------------------------------------------------------- */

  setAttribute: function (elem, attrName, attrValue) {
    var elem = this.getElement(elem);
    if (!elem) return;
    if (typeof(attrName) != 'string') return;
    if (attrName.indexOf('_') == 0) {
      // user-defined property
      elem[attrName] = attrValue;
    } else if (attrName.toLowerCase() == 'value') {
      // When the browser (FF) is remembering values, simply setting the attribute
      // does not reflect the most "current" value of the control.  If you're
      // calling this method from JS, we presume you want the control to reflect
      // this new value.
      elem.value = attrValue;
    } else if (attrName.indexOf('on') == 0) {
      // event
      if (typeof(attrValue) == 'function') {
        var eventName = attrName.substr(2).toLowerCase();
        Event.observe(elem, eventName, attrValue);
      } else {
        attrName = attrName.toLowerCase();
        if (Prototype.Browser.IE) {
          eval("elem." + attrName + " = attrValue;");
        } else {
          elem[attrName] = attrValue;
        }
      }
    } else {
      if (attrName == 'className' && !Prototype.Browser.IE) attrName = 'class';
      if (attrName == 'class' && Prototype.Browser.IE) attrName = 'className';
      if (!this.defined(elem.setAttribute) || attrName.indexOf('inner') == 0) {
        elem[attrName] = attrValue;
      } else {
        elem.setAttribute(attrName, attrValue);
      }
    }
  },

  removeAttribute: function (elem, attrName) {
    elem = this.getElement(elem);
    if (!elem) return;
    elem.removeAttribute(attrName);
  },

/*------------------------------------------------------------------------------
 * getRootElement - Get the document's root element
 *----------------------------------------------------------------------------*/

  getRootElement: function () {
    return this.doc.rootElement || this.getBody() || this.doc.documentElement
      || this.doc.lastChild || this.doc;
  },

/*------------------------------------------------------------------------------
 * getBody - Get our document's body
 *----------------------------------------------------------------------------*/

  getBody: function () {
    var bodies = this.doc.getElementsByTagName('body');
    return bodies && bodies.length > 0 ? bodies[0] : null;
  },

/*------------------------------------------------------------------------------
 * getHead - Get our document's head
 *----------------------------------------------------------------------------*/

  getHead: function () {
    var heads = this.doc.getElementsByTagName('head');
    return heads && heads.length > 0 ? heads[0] : this.getRootElement();
  },

/*------------------------------------------------------------------------------
 * bodyAppend - Tack an element on to the body
 * @param elem html element object
 *----------------------------------------------------------------------------*/

  bodyAppend: function (elem) {
    var body = this.getBody();
    if (body) {
      body.appendChild(elem);
      return elem;
    }
  },

/*------------------------------------------------------------------------------
 * createInlineFrame - Construct an inline frame
 *----------------------------------------------------------------------------*/

  createInlineFrame: function () {
    /* create new iframe element */
    var obj;
    if (this.doc.all) {
      obj = this.doc.createElement('<iframe frameborder="0">');
    } else {
      obj = this.doc.createElement("iframe");
      obj.setAttribute('frameborder', 'none');
    }
    return obj;
  },

/*------------------------------------------------------------------------------
 * createJavaPlugin - Construct a Java plug-in object
 * @param path path to jar file
 * @param classname main class
 * @param params array of k/v parameters
 * @param ver java plugin version (default 1.4)
 *----------------------------------------------------------------------------*/

  createJavaPlugin: function (path,classname,params,ver) {

    /* default argument values */
    if (!params) params = new Array();
    if (!ver) ver = '1.4';

    /* create new object element */
    var obj;
    if (this.doc.all) {
      obj = this.doc.createElement("<object classid='clsid:8AD9C840-044E-11D1-B3E9-00805F499D93'>");
    } else {
      obj = this.doc.createElement("object");
      obj.setAttribute( 'code', classname);
      obj.setAttribute( 'type', 'application/x-java-applet;version=' + ver);
    }

    /* set parameters */
    params['archive'] = path;
    params['code'] = classname;
    params['MAYSCRIPT'] = 'true';
    for (var key in params) {
      var param = this.doc.createElement("param");
      param.setAttribute( 'name', key );
      param.setAttribute( 'value', params[key] );
      obj.appendChild(param);
    }

    return obj;

  },

/*------------------------------------------------------------------------------
 * removeChildren - Remove all child nodes from an element
 * @param element|id Element or Element ID
 *----------------------------------------------------------------------------*/

  removeChildren: function (elem) {
    elem = this.getElement(elem);
    if (!(elem && elem.childNodes)) return;
    for (var idx = elem.childNodes.length - 1; idx >= 0; idx--) {
      elem.removeChild(elem.childNodes[idx]);
    }
  },

  setOpacity: function (elem, opacity) {
    opacity = this.asInt(opacity);
    if (Prototype.Browser.IE) {
      this.setStyle(elem, '-ms-filter', 'alpha(opacity='+opacity+')');
      this.setStyle(elem, 'filter', 'alpha(opacity='+opacity+')');
    } else if (Prototype.Browser.Gecko) {
      this.setStyle(elem, '-moz-opacity', '.'+opacity);
      this.setStyle(elem, 'opacity', '.'+opacity);
    } else if (Prototype.Browser.Safari) {
      this.setStyle(elem, '-khtml-opacity', '.'+opacity);
      this.setStyle(elem, 'opacity', '.'+opacity);
    } else {
      this.setStyle(elem, 'opacity', '.'+opacity);
    }
  },

/*------------------------------------------------------------------------------
 * showMask - Mask document body
 *----------------------------------------------------------------------------*/

  showMask: function (styleOpts) {
    var style = {
      'position':'absolute',
      'cursor':'default',
      'opacity':'35',
      'top':0, 'left':0, 'width':0, 'height':0
    };
    if (styleOpts) Object.extend(style, styleOpts);
    if (!this.MASK) {
      this.MASK = this.createElement('div', {'id': 'lsn_mask', 'style':style});
      this.bodyAppend(this.MASK);
      Event.observe(this.win, 'resize', this.resizeMask);
    }
    var opacity = 
    this.setOpacity(this.MASK, style.opacity);
    this.setStyle(this.MASK, 'cursor', style.cursor);
    this.MASK_showCount++;
    //Fix for ie5/6, mask unable to hide select boxes.
    //The src attribute must be set or IE will complain about both secure
    //and non-secure times being on the page
    this.MASK.innerHTML = '<!--[if lte IE 6.5]><iframe src="/"></iframe><![endif]-->';
    this.resizeMask();
    this.setStyle(this.MASK, 'display', 'block');
  },

  resizeMask: function () {
    if (!this.MASK) return;
    var c = new LSN.Canvas();
    this.setStyle(this.MASK, 'width', c.pageX() + "px");
    this.setStyle(this.MASK, 'height', c.pageY() + "px");
  },

/*------------------------------------------------------------------------------
 * hideMask - Unmask document body
 *----------------------------------------------------------------------------*/

  hideMask: function () {
    if (this.MASK) {
      this.MASK_showCount--;
      if (this.MASK_showCount < 0) this.MASK_showCount = 0;
      if (!this.MASK_showCount) {
        this.setStyle(this.MASK, 'cursor', 'auto');
        this.setStyle(this.MASK, 'display', 'none');
      }
    }
  },

/*------------------------------------------------------------------------------
 * msgbox - Display a message with an OK button
 *----------------------------------------------------------------------------*/

  msgbox: function (text, props) {
    this.showMask();
    var div = this.getElement(this.MESSAGE_BOX_ID);	
    if (div) {
      div.firstChild.innerHTML += '<br/>' + text;
    } else {
      this.appendChildren(this.getBody(), this.createElements(
        'div', {id: this.MESSAGE_BOX_ID,
          style: {
            'position': 'absolute',
            'top': '0',
            'left': '0',
            'background-color': 'rgb(255,255,255)',
            'border': '1px outset black',
            'padding': '.5em',
            'z-index': this.LAYER_MSGBOX
          }}, [
          'div', {innerHTML: text, style: {padding: '1em 2em 1em 2em;'}},
          'div', {style: {'text-align': 'right'}}, [
            'button', {
              id: 'msgbox-btn-ok',
              type: 'button',
              onclick: function () {this.hideMsgbox()}.bind(this),
              style: {width: '75px'},
              innerHTML: 'OK'
            }
          ],
        ]
      ));
      div = this.getElement(this.MESSAGE_BOX_ID);	
    }
    if (!props) props = {position:'center'};
    this.setPosition(div, props); // position in plain view
    if (this.getStyle(div, 'display') != 'block') {
      this.setStyle(div, 'display', 'block');
      this.setStyle(div, 'visibility', 'visible');
    }
    this.getElement('msgbox-btn-ok').focus();
  },

  hideMsgbox: function () {
    var div = this.getElement(this.MESSAGE_BOX_ID);	
    if (div && div.parentNode) div.parentNode.removeChild(div);
    this.hideMask();
  },

/*------------------------------------------------------------------------------
 * setStatus - Display a message in the middle of the screen
 *----------------------------------------------------------------------------*/

  setStatus: function (text, props) {
    var div = this.getElement(this.STATUS_ID);	
    if (!div) {
      div = this.createElement('div');
      div.setAttribute('id', this.STATUS_ID);
      this.bodyAppend(div);
    }
    div.innerHTML = text;
    div.style.display = 'block';
    this.setPosition(div, props);
  },

/*------------------------------------------------------------------------------
 * setStatus - Remove the status message from display
 *----------------------------------------------------------------------------*/

  clearStatus: function () {
    var div = this.getElement(this.STATUS_ID);	
    div.innerHTML = '';
    div.style.display = 'none';
  },

/*------------------------------------------------------------------------------
 * setStatus - Display a status message for a brief time
 *----------------------------------------------------------------------------*/

  flashStatus: function (text) {
    this.setStatus(text);
    setTimeout(function(){this.clearStatus()}.bind(this), 1250);
  },

/*------------------------------------------------------------------------------
 * setPosition - Position an absolute element with respect to the view port
 * setPosition #elem, {props}
 * where:
 *  props.position: 'top-third'|'center'|'bottom-left'
 *----------------------------------------------------------------------------*/

  setPosition: function (elem, props) {
    elem = this.getElement(elem);
    if (!props) props = {'position': 'top-third'}
    /* Backup display values */
    var attrVisibility = elem.style.visibility;
    var attrDisplay = elem.style.display;
    /* Set display values */
    elem.style.visibility = 'hidden'; // don't flicker when positioning
    elem.style.display = 'block'; // so get height/width work

    /* Calculate new position */
    var vp = this.getViewportPosition();
    var xy = this.getCenteredXY(elem, props.contextElem);
    if (props.position == 'top-third') {
      var c = new LSN.Canvas();
      var h = this.getHeight(elem);
      var t = (vp.height - h)/3;
      if (t < 0) t = this.asInt(c.scrollY());
      if (t < c.scrollY()) t += c.scrollY();
      elem.style.left = xy[0] + "px";
      elem.style.top = t + "px";
    } else if (props.position == 'center') {
      elem.style.left = xy[0] + "px";
      elem.style.top = xy[1] + "px";
    } else if (props.position == 'bottom-left') {
      vp['left'] += this.asInt(this.getCSSAttribute(elem, 'padding-left'));
      vp['top'] -= this.asInt(this.getCSSAttribute(elem, 'padding-bottom'));
      elem.style.left = vp['left'] + 'px';
      elem.style.top = (vp['top'] + vp['height'] - this.getHeight(elem)) + 'px';
    } else if (props.position == 'bottom-right') {
      elem.style.right = '0px';
      elem.style.bottom = '0px';
    }
    /* Restore original values */
    elem.style.visibility = attrVisibility;
    elem.style.display = attrDisplay;
  },

/*------------------------------------------------------------------------------
 * traceln - Write parameters to trace box followed by a line break
 *----------------------------------------------------------------------------*/

  traceln: function () {
    try {
      if (!arguments || arguments.length <= 0) {
        this.trace("\n");
        return;
      };
      var text = '';
      $A(arguments).each(function(arg){
        if (typeof(arg) == undefined) return;
        if (Object.isString(arg)) {
          text += arg;
        } else if (Object.isArray(arg)) {
          if (text) {
            this.trace(text, "\n");
            text = '';
          }
          arg.each(function(val){
            this.trace(val, "\n");
          });
        } else if (Object.isHash(arg)) {
          if (text) {
            this.trace(text, "\n");
            text = '';
          }
          arg.each(function (pair) {
            this.trace(pair.key, ': ', pair.value, "\n");
          });
        } else {
          text += arg;
        }
      });
      if (text) this.trace(text, "\n");
    } catch (ex) {
      alert(ex);
    }
  },

/*------------------------------------------------------------------------------
 * trace - Display messages in a cumlative trace box
 *----------------------------------------------------------------------------*/

  trace: function () {
    try {
      var text = $A(arguments).join('');
      if (typeof(console) != 'undefined') {
        console.log(text);
        /*
        console.group(text);
        console.trace();
        console.groupEnd(text);
        */
        return;
      }
      text = text.replace(/\n/, '<br/>', 'g');
      var div = this.getElement(this.TRACE_BOX_ID);	
      if (!div) {
        div = this.doc.createElement('div');
        div.setAttribute('id', this.TRACE_BOX_ID);
        this.bodyAppend(div);
      }
      div.innerHTML += text;
      div.scrollTop = div.scrollHeight;
      this.setPosition(div, {'position': 'bottom-right'});
      div.style.display = 'block';
    } catch (ex) {
      alert(ex);
    }
  },

/*------------------------------------------------------------------------------
 * toggleVisibility - Hide or show an element (using css 'visibility' property).
 * @param id HTML Element ID
 *----------------------------------------------------------------------------*/

  toggleVisibility: function (id) {
    var elem = this.getElement(id);
    var v = this.getCSSAttribute(elem, "visibility");
    elem.style.visibility = !v || v == "visible" ? "hidden" : "visible";
  },

/*------------------------------------------------------------------------------
 * toggleDisplay - Hide or show an element (using css 'display' property).
 * @param id HTML Element ID
 *----------------------------------------------------------------------------*/

  toggleDisplay: function (id) {
    var elem = this.getElement(id);
    var v = this.getCSSAttribute(elem, "display");
    elem.style.display = !v || v == "block" ? "none" : "block";
  },

/*------------------------------------------------------------------------------
 * getWindowSize - Return an array (width, height) describing the browser window
 *----------------------------------------------------------------------------*/

  getWindowSize: function () {
    var w = 0;
    var h = 0;
    if (typeof(this.win.innerWidth) == 'number') {
      //Non-IE
      w = this.win.innerWidth;
      h = this.win.innerHeight;
    } else if (this.doc.documentElement && (this.doc.documentElement.clientWidth 
          || this.doc.documentElement.clientHeight)) {
      //IE 6+ in 'standards compliant mode'
      w = this.doc.documentElement.clientWidth;
      h = this.doc.documentElement.clientHeight;
    } else if (this.doc.body && (this.doc.body.clientWidth || 
          this.doc.body.clientHeight)) {
      //IE 4 compatible
      w = this.doc.body.clientWidth;
      h = this.doc.body.clientHeight;
    }
    return new Array(w, h);
  },

/* ------------------------------------------------------------------------------
 * getPageSize - Get [width, height] of page from head to toe
 * --------------------------------------------------------------------------- */

  getPageSize: function () {
    var c = new LSN.Canvas();
    return [c.pageX(), c.pageY()]
  },

/* -----------------------------------------------------------------------------
 * getViewportPosition - Pixel coordinates and dimensions of the viewport
 * -------------------------------------------------------------------------- */

  getViewportPosition: function () {
    var c = new LSN.Canvas();
    var modX = Prototype.Browser.IE ? 0 : 0; // scroll width
    var modY = 0;
    return {
      'left':   c.scrollX(),
      'top':    c.scrollY(),
      'width':  c.windowX() - modX,
      'height': c.windowY() - modY
    };
  },

/*------------------------------------------------------------------------------
 * getCenteredXY - Get the (x, y) pixel coordinates which will center the
 * element relative to the viewport (or contextElem if it is provided)
 *
 * @param element
 * @param contextElem (optional)
 *----------------------------------------------------------------------------*/

  getCenteredXY: function (elem, contextElem) {
    elem = this.getElement(elem);
    var vp;
    if (contextElem) {
      vp = this.getElementPosition(contextElem);
    } else {
      vp = this.getViewportPosition();
    }
    var pos = this.getElementPosition(elem);
    var x = vp['left'] + (vp['width'] / 2) - (pos['width'] / 2);
    var y = vp['top'] + (vp['height'] / 2) - (pos['height'] / 2);
    if (x < vp['left']) x = vp['left'];
    if (y < vp['top']) y = vp['top'];
    return new Array (x, y);
  },

  getPosition: function (elem) {
    return {
      'top':    this.getTop(elem),
      'left':   this.getLeft(elem),
      'right':  this.getRight(elem),
      'bottom': this.getBottom(elem),
      'width':  this.getWidth(elem),
      'height': this.getHeight(elem)
    };
  },

  getInnerPosition: function (elem) {
    return {
      'top':    this.getInnerTop(elem),
      'left':   this.getInnerLeft(elem),
      'right':  this.getInnerRight(elem),
      'bottom': this.getInnerBottom(elem),
      'width':  this.getInnerWidth(elem),
      'height': this.getInnerHeight(elem)
    };
  },

/*------------------------------------------------------------------------------
 * Element dimensions and positions.
 *----------------------------------------------------------------------------*/

  /* Outer */

  getTop: function (element)    {
    return this.getOffset(element, 'top');
  },

  getBottom: function (element) {
    return this.getTop(element) + this.getHeight(element);
  },

  getLeft: function (element)   {
    return this.getOffset(element, 'left');
  },

  getRight: function (element)  {
    return this.getLeft(element) + this.getWidth(element);
  },

  getWidth: function (element)  {
    var result = this.getDimension(element, 'width');
    if (!Prototype.Browser.IE) {
      result += this.asInt(this.getCSSAttribute(element, 'border-left-width'));
      result += this.asInt(this.getCSSAttribute(element, 'border-right-width'));
    }
    return result;
  },

  getHeight: function (element) {
    var result =  this.getDimension(element, 'height');
    if (!Prototype.Browser.IE) {
      result += this.asInt(this.getCSSAttribute(element, 'border-top-width'));
      result += this.asInt(this.getCSSAttribute(element, 'border-bottom-width'));
    }
    return result;
  },

  /* Inner */

  getInnerTop: function (element)    {
    var result = this.getOffset(element, 'top');
    if (!Prototype.Browser.IE) {
      result += this.asInt(this.getCSSAttribute(element, 'border-top-width'));
    }
    return result;
  },

  getInnerBottom: function (element) {
    return this.getInnerTop(element) + this.getInnerHeight(element);
  },

  getInnerLeft: function (element)   {
    var result = this.getOffset(element, 'left');
    if (!Prototype.Browser.IE) {
      result += this.asInt(this.getCSSAttribute(element, 'border-left-width'));
    }
    return result;
  },

  getInnerRight: function (element)  {
    return this.getInnerLeft(element) + this.getInnerWidth(element);
  },

  getInnerWidth: function (element)  {
    return this.getDimension(element, 'width');
  },

  getInnerHeight: function (element) {
    return this.getDimension(element, 'height');
  },

/* -----------------------------------------------------------------------------
 * getElementPosition - Pixel coordinates and dimensions of the element
 * getElementPosition
 *
 * returns: Object w/members: left, top, width, height
 * -------------------------------------------------------------------------- */

  getElementPosition: function (elem) {
    elem = this.getElement(elem);
    return {
      'left':   this.getLeft(elem),
      'top':    this.getTop(elem),
      'width':  this.getWidth(elem),
      'height': this.getHeight(elem)
    };
  },

/*------------------------------------------------------------------------------
 * getOffset - Return the offset (top or left) for the given element.
 * @param element an element object (or its id)
 * @param attr either 'top' or 'left'
 *----------------------------------------------------------------------------*/

  getOffset: function (elem, attr) {
    elem = this.getElement(elem);
    var offset = 0;
    if (elem) {
      offset = attr == 'top'
        ? elem.offsetTop
          ? elem.offsetTop
          : this.defined(elem.getBBox)
            ? elem.getBBox().y
            : 0
        : elem.offsetLeft
          ? elem.offsetLeft
          : this.defined(elem.getBBox)
            ? elem.getBBox().x
            : 0;
      offset += this.getOffset(elem.offsetParent, attr);
    }
    return isNaN(offset) ? 0 : offset;
  },

/*------------------------------------------------------------------------------
 * getDimension - Return the dimension (width or height) for the given element.
 * @param elem an element object (or its id)
 * @param attr either 'width' or 'height'
 *----------------------------------------------------------------------------*/

  getDimension: function (elem, attr) {
    var result = 0;
    if (typeof(elem) != 'object') {
      elem = this.getElement(elem);
    }
    if (elem) {
      result = attr == 'height'
        ? elem.clientHeight
          ? elem.clientHeight
          : elem.offsetHeight
            ? elem.offsetHeight
            : this.defined(elem.getBBox)
              ? elem.getBBox().height
              : 0
        : elem.clientWidth
          ? elem.clientWidth
          : elem.offsetWidth
            ? elem.offsetWidth
            : this.defined(elem.getBBox)
              ? elem.getBBox().width
              : 0;
      if (isNaN(result) || result <= 0) {
        result = this.getCSSAttribute(elem, attr);
        result = parseInt(result.replace("\w", ""));
      }
      if (isNaN(result)) result = 0;
    }
    return isNaN(result) ? 0 : result;
  },

/*------------------------------------------------------------------------------
 * getCSSAttribute - Get a CSS attribute value.
 * @param elem target element
 * @param propName style key
 * First check the style, and then look at the element's classes, then the id
 * selector.
 *----------------------------------------------------------------------------*/

  getCSSAttribute: function (elem, propName) {
    elem = this.getElement(elem);
    if (!elem) return;
    var hyphenated = this.asHyphenatedName(propName);
    var camelCase = this.asCamelCaseName(propName);
    if (elem.currentStyle) {
      return elem.currentStyle[camelCase]
    } else if(this.win.getComputedStyle) {
        var cs = this.doc.defaultView.getComputedStyle(elem,null)
        return cs ? cs.getPropertyValue(hyphenated) : null;
    } else {
      return;
    }
  },

/* ------------------------------------------------------------------------------
 * getScrollbarWidth - Get the width of the scrollbar
 * getScrollbarWidth
 *
 * Note, does not work on Safari for Mac
 * --------------------------------------------------------------------------- */

  getScrollbarWidth: function () {
      if (this.SCROLLBAR_WIDTH > 0) return this.SCROLLBAR_WIDTH;
      var div = this.createElement('div', {
        'style': {
          'position': 'absolute',
          'top':      '-1000px',
          'left':     '-1000px',
          'width':    '100px',
          'height':   '50px',
          'overflow': 'hidden' 
        }
      });
      var content = this.createElement('div', {
        'style': {
          'width':  '100%',
          'height': '200px'
        }
      });
      div.appendChild(content);
      this.bodyAppend(div);
      var w1 = this.getWidth(content);
      div.style.overflow = 'auto';
      var w2 = this.getWidth(content);
      this.getBody().removeChild(div);
      this.SCROLLBAR_WIDTH = w1 - w2;
      if (this.SCROLLBAR_WIDTH <= 0) this.SCROLLBAR_WIDTH = 15; // default
      return this.SCROLLBAR_WIDTH;
  },

/*------------------------------------------------------------------------------
 * getContentWindow - Returns the inner contentWindow of an IFRAME or FRAME.
 * @param id of the FRAME or IFRAME
 *----------------------------------------------------------------------------*/

  getContentWindow: function (frameid) {
    var iframe = this.getFrame(frameid);
    if (!iframe) return;
    return iframe.contentWindow || iframe.window;
  },

/*------------------------------------------------------------------------------
 * getContentDocument - Returns the inner contentDocument of an IFRAME or FRAME.
 * @param id of the FRAME or IFRAME
 *----------------------------------------------------------------------------*/

  getContentDocument: function (frameid) {
    var iframe = this.getFrame(frameid);
    if (!iframe) return;
    return iframe.contentWindow
      ? iframe.contentWindow.document
      : iframe.contentDocument || iframe.document;
  },

  getFrame: function (id) {
    if (typeof(id) == 'object') return id;
    return frames[id] || this.getElement(id);
  },

/* ------------------------------------------------------------------------------
 * setMoveTarget - Help function to make an element moveable
 * @depricated
 * --------------------------------------------------------------------------- */

  setMoveTarget: function (event, elem) {
    new LSN.Move(event, elem);
  },

/* ------------------------------------------------------------------------------
 * isSameOrigin - Resource is located on the running document's web server.
 * Taken from prototype.js b/c needed outside (before) Ajax.Request is created.
 * --------------------------------------------------------------------------- */

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

  addEventListener: function (target, type, listener, scope) {
    var elem = this.getElement(target);
    var name = type.toLowerCase().replace(/^on/, '');
    if (!elem) return;
    var func = function () {
      if (!arguments[0].target && arguments[0].srcElement) {
        arguments[0].target = arguments[0].srcElement;
      }
      listener.apply(scope || this, arguments)
    };
    if (elem.addEventListener) {
      elem.addEventListener(name, func, false); 
    } else if (elem.attachEvent) {
      elem.attachEvent('on'+name, func);
    }
  },

  addEventListeners: function (target, listeners, scope) {
    for (var name in listeners) {
      this.addEventListener(target, name, listeners[name], scope);
    }
  }

};
/* Container (pure virtual base class) */

Livesite.prototype.toXFR = function (obj) {
  var result = '';
  if (!LSN.defined(obj)) return '${}';
  if (obj.toXFR) {
    return obj.toXFR();
  } else if (LSN.isHash(obj)) {
    var result = '%{';
    for (var k in obj) {
      var v = obj[k];
      result += encodeURIComponent(k) + LSN.toXFR(v);
    }
    return result + '}';
  } else if (LSN.isArray(obj)) {
    var result = '@{';
    for (var i = 0; i < obj.length; i++) {
      result += LSN.toXFR(obj[i]);
    }
    return result + '}';
  } else {
    return '${' + encodeURIComponent(obj) + '}';
  }
},

Livesite.prototype.Container = Class.create({

  toXFR: function () {
    var result = '';
    this.iterate(function (k, v) {
      result += this._enck(k);
      result += v.toXFR ? v.toXFR() : '${' + this._encv(v) + '}';
    }, this);
    return result;
  },

  _enck: function (k) {
    return encodeURIComponent(k);
  },

  _encv: function (v) {
    return encodeURIComponent(v);
  },

  _deck: function (k) {
    return decodeURIComponent(k);
  },

  _decv: function (v) {
    return decodeURIComponent(v);
  }

});

/* OrderedHash */

Livesite.prototype.OrderedHash = Class.create(Livesite.prototype.Container, {

  toXFR: function ($super) {
    return '%{' + $super() + '}';
  },

  initialize: function () {
    this.clear();
    for (var i = 0; arguments && i < arguments.length; i += 2) {
      var k = arguments[i];
      var v = arguments[i+1];
      this.indicies.push(k);
      this.data[k] = v;
    }
    this.length = this.indicies.length;
  },

  clear: function () {
    this.indicies = [];
    this.data = {};
    this.length = 0;
  },

  get: function (addr) {
    return LSN.Courier.get(this, addr);
  },

  set: function (addr, value) {
    return LSN.Courier.set(this, addr, value);
  },

  remove: function (addr) {
    return LSN.Courier.remove(this, addr);
  },

  getValue: function (key) {
    return this.data[key];
  },

  setValue: function (key, value) {
    var currentKey = LSN.grep(function(item) {return item == key}, this.indicies);
    if (!currentKey.length) this.indicies.push(key);
    this.length = this.indicies.length;
    return this.data[key] = value;
  },

  removeValue: function (key) {
    for (var i = 0; i < this.indicies.length; i++) {
      if (this.indicies[i] == key) {
        this.indicies.splice(i, 1);
        break;
      }
    }
    this.length = this.indicies.length;
    return delete this.data[key];
  },

  keys: function () {
    return this.indicies;
  },

  values: function () {
    var result = [];
    for (var i = 0; i < this.indicies.length; i++) {
      result.push(this.data[this.indicies[i]])
    }
    return result;
  },

  iterate: function (callback, scope) {
    for (var i = 0; i < this.indicies.length; i++) {
      var key = this.indicies[i];
      callback.apply(scope, [key, this.data[key]]);
    }
  },

  toObject: function () {
    return LSN.Courier.toObject(this);
  },

  walk: function (callback, scope) {
    if (!scope) scope = this;
    return LSN.Courier.walk(this, callback, scope);
  }

});

/* Array */

Livesite.prototype.Array = Class.create(Livesite.prototype.Container, {

  toXFR: function ($super) {
    var result = '';
    this.iterate(function (k, v) {
      result += v.toXFR ? v.toXFR() : '${' + this._encv(v) + '}';
    }, this);
    return '@{' + result + '}';
  },

  initialize: function () {
    this.clear();
    var args = $A(arguments);
    if (args) {
      this.data = args;
      this.length = this.data.length;
    }
  },

  clear: function () {
    this.data = [];
    this.length = 0;
  },

  get: function (addr) {
    return LSN.Courier.get(this, addr);
  },

  set: function (addr, value) {
    return LSN.Courier.set(this, addr, value);
  },

  remove: function (addr) {
    return LSN.Courier.remove(this, addr);
  },

  getValue: function (key) {
    return this.data[key];
  },

  setValue: function (key, value) {
    if (typeof(key) != 'number') key = LSN.asInt(key);
    var result = this.data[key] = value;
    this.length = this.data.length;
    return result;
  },

  removeValue: function (key) {
    if (typeof(key) != 'number') key = LSN.asInt(key);
    var result = this.data.splice(key, 1);
    this.length = this.data.length;
    return result;
  },

  keys: function () {
    var result = [];
    for (var i = 0; i < this.data.length; i++) {
      result.push(i);
    }
    return result;
  },

  values: function () {
    return this.data;
  },

  iterate: function (callback, scope) {
    for (var i = 0; i < this.data.length; i++) {
      callback.apply(scope, [i, this.data[i]]);
    }
  },

  toObject: function () {
    return LSN.Courier.toObject(this);
  },

  walk: function (callback, scope) {
    if (!scope) scope = this;
    return LSN.Courier.walk(this, callback, scope);
  }

});

/* Courier */

Livesite.prototype.Courier = Object();

Livesite.prototype.Courier.get = function (c, addr) {
  var parts = LSN.Address.split(addr);
  for (var i = 0; i < parts.length; i++) {
    if (typeof(c) == 'undefined' || !c.getValue) return;
    c = c.getValue(parts[i]);
  }
  return c;
}

Livesite.prototype.Courier.set = function (c, addr, value) {
  var parts = LSN.Address.split(addr);
  var lastKey = parts.pop();
  if (LSN.defined(lastKey)) {
    var ptr = c;
    for (var i = 0; i < parts.length; i++) {
      var key = parts[i];
      var node = ptr.getValue(key);
      if (!LSN.defined(node)) {
        node = ptr.setValue(key, new LSN.OrderedHash());
      }
      ptr = node;
    }
    return ptr.setValue(lastKey, value);
  } else if (LSN.isa(value, LSN.Container)) {
    c.clear();
    value.iterate(function (k, v) {
      c.set(k, v);
    });
  }
}

Livesite.prototype.Courier.remove = function (c, addr) {
  var parts = LSN.Address.split(addr);
  var lastKey = parts.pop();
  if (!LSN.defined(lastKey)) return;
  var parent = LSN.Courier.get(c, parts);
  if (LSN.isa(parent, LSN.Container)) parent.removeValue(lastKey);
};

Livesite.prototype.Courier.toObject = function (unk) {
  var result = null;
  if (unk instanceof Object) {
    if (unk instanceof LSN.Array
        || unk instanceof LSN.OrderedHash) {
      result = unk instanceof LSN.Array ? [] : {};
      unk.iterate(function (k, v) {
        result[k] = typeof(v.toObject) == 'function' ? v.toObject() : v;
      });
    } else {
      result = unk;
    }
  } else {
    result = unk;
  }
  return result;
};

Livesite.prototype.Courier.walk = function (c, callback, scope, prefix, depth) {
  if (!depth) depth = 0;
  c.iterate(function (k,v) {
    var addr = LSN.defined(prefix) ? prefix + '/' + k : k;
    callback.apply(scope, [k, v, depth, addr]);
    if (LSN.isa(v, LSN.Container)) {
      LSN.Courier.walk(v, callback, scope, addr, (depth + 1));
    }
  });
};

/* Address */

Livesite.prototype.Address = new Object();

Livesite.prototype.Address.split = function (addr) {
  if (!LSN.defined(addr) || addr === "" || addr == '/') return [];
  if (addr instanceof Array) return addr;
  if (typeof(addr) != 'string') return [addr];
  return addr.replace(/^\//, '').split('/');
};

Livesite.prototype.Address.normalize = function (addr) {
  if (!LSN.defined(addr) || addr === "") return '';
  if (typeof(addr) == 'string') {
    addr = addr.replace(/\/{2,}/g, '/');
    if (addr == '/') return addr;
    addr = addr.replace(/\/$/, '');
    // TODO replace '../foo' constructs
    return addr;
  }
};

Livesite.prototype.Address.ext = function (addr) {
  var lastKey = LSN.Address.split(addr).pop();
  if (!lastKey) return;
  if (typeof(lastKey) != 'string') return;
  if (lastKey.indexOf('.') <= 0) return;
  return lastKey.split('.').pop();
};

Livesite.prototype.Address.parent = function (addr) {
  var parts = LSN.Address.split(addr);
  parts.pop();
  var result = parts.length ? parts.join('/') : '';
  return addr.indexOf('/') == 0 ? '/' + result : result;
};

Livesite.prototype.Address.name = function (addr) {
  var parts = LSN.Address.split(addr);
  return parts.pop();
};

Livesite.prototype.Address.join = function (addr) {
  if (!(addr instanceof Array)) throw new LSN.Error('Illegal Argument: addr');
  return LSN.Address.normalize(addr.join('/'));
};
/* Copyright 2007 Livesite Networks, LLC. All rights reserved. */
/* Adopted from: http://examples.oreilly.com/jscript3/text/7-3.txt */
/* Written by: Ryan Gies */

Livesite.prototype.Error = function (msg) {
  if (typeof(msg) == 'string') {
    this.message = msg;
  } else if (msg instanceof Object) {
    for (var key in msg) {
      this[key] = msg[key];
    }
  }
  for(var a = arguments.caller; a != null; a = a.caller) {
    var s = a.callee.toString().match(/function (\w*)/)[1];
    if ((s == null) || (s.length == 0)) s = "anonymous";
    this.message += "\n" + s;
    this.stacktrace.push(s);
    if (a.caller == a) break;
  }
}

Livesite.prototype.Error.prototype = {
  'status':     '',
  'message':    'An error ocurred',
  'stacktrace': []
};
/**
 * LSN.Hub - Client/server bridge data bridge
 */

Livesite.prototype.Hub = function () {
  this.cache = new LSN.Hub_Node('/', 'directory');
  this.listeners = [];
  this.fetchq = {};
};

/* Action events sent to listeners: 'fetched', 'stored', and 'removed' */

Livesite.prototype.Hub.prototype.addListener = function (callback, scope) {
  this.listeners.push([callback, scope]);
};

/* Please remove yourself when you're done so we don't call you back */

Livesite.prototype.Hub.prototype.removeListener = function (callback, scope) {
  for (var i = 0; i < this.listeners.length; i++) {
    var listener = this.listeners[i];
    if (callback === listener[0] && scope === listener[1]) {
      this.listeners.splice(i--, 1);
    }
  }
};

/* Get the cached instance, no server traffic */

Livesite.prototype.Hub.prototype.get = function (addr) {
  return this.cache.get(addr);
}

/* Fetch content from the server */

Livesite.prototype.Hub.prototype.fetch = function (addr, optBranch) {
  addr = LSN.Address.normalize(addr);
  if (!addr) return;
  var reqOpts = {
    method:'POST',
    requestHeaders: {
      'X-Command': 'fetch',
      'Cache-Control': 'max-age=0, must-revalidate'
    }
  };
  var qkey = optBranch ? addr + '+branch' : addr;
  if (this.fetchq[qkey]) return;
  var params = {};
  if (optBranch) params.branch = true;
  var node = this.get(addr);
  if (LSN.defined(node) && node.mtime && node.fetched) {
    var d = new Date((1000*node.mtime)); // secs to millis
    reqOpts.requestHeaders['If-Modified-Since'] = d.toUTCString();
    reqOpts.requestHeaders['Cache-Control'] = 'max-age=0; must-revalidate';
  } else {
    reqOpts.requestHeaders['Cache-Control'] = 'no-fetch';
  }
  reqOpts.onSuccess = function (r) {
    var addr = this._setCache(r);
    this._sendUpdates('fetched', addr, true);
  }.bind(this);
  reqOpts.on304 = function (r) {
    this._sendUpdates('fetched', addr, false);
  }.bind(this);
  reqOpts.on404 = function (r) {
    if (this.get(addr)) {
      this.cache.remove(addr);
      this._sendUpdates('removed', addr, true);
    }
  }.bind(this);
  reqOpts.onFailure = function (r) {
    if (this.get(addr)) {
      this.cache.remove(addr);
      this._sendUpdates('removed', addr, true);
    }
  }.bind(this);
  reqOpts.onComplete = function (r) {
    delete this.fetchq[qkey];
  }.bind(this);
  this.fetchq[qkey] = true;
  new LSN.Request(addr, reqOpts).submit(params);
};

/* Store content to the server */

Livesite.prototype.Hub.prototype.store = function (addr, value) {
  this._xcmd('store', addr, {'value': value},
    function (r) {
      var addr = this._setCache(r);
      this._sendUpdates('stored', addr, true);
    }.bind(this)
  );
}

/* Create a new node on the server */

Livesite.prototype.Hub.prototype.create = function (addr, name, type) {
  this._xcmd('create', addr, {'name': name, 'type': type},
    function (r) {
      var addr = this._setCache(r);
      this._sendUpdates('fetched', addr, true);
    }.bind(this)
  );
};

/* Remove a node from the server */

Livesite.prototype.Hub.prototype.remove = function (addr) {
  this._xcmd('remove', addr, {},
    function (r) {
      this.cache.remove(addr);
      this._sendUpdates('removed', addr, true);
    }.bind(this)
  );
};

/* Copy one node to another on the server */

Livesite.prototype.Hub.prototype.copy = function (addr, dest) {
  this._xcmd('copy', addr, {'dest':dest},
    function (r) {
      var dest = this._setCache(r);
      this._sendUpdates('fetched', dest, true);
    }.bind(this)
  );
};

/* Move one node to another on the server */

Livesite.prototype.Hub.prototype.move = function (addr, dest) {
  this._xcmd('move', addr, {'dest':dest},
    function (r) {
      var dest = this._setCache(r);
      this.cache.remove(addr);
      this._sendUpdates('removed', addr, true);
      this._sendUpdates('fetched', dest, true);
    }.bind(this)
  );
};

/* Internal implementation methods */

Livesite.prototype.Hub.prototype._xcmd = function (cmd, addr, values, cb) {
  if (!cmd) throw 'No command provided';
  if (!addr) throw 'No address provided';
  var reqOpts = {
    method:'POST',
    requestHeaders: {'X-Command': cmd},
    onSuccess: cb
  };
  reqOpts.onFailure = function (r) {
    throw r.statusText + ': ' + r.responseText;
  }.bind(this);
  new LSN.Request(addr, reqOpts).submit(values);
};

Livesite.prototype.Hub.prototype._setCache = function (r) {
  var rh = r.responseHash;
  var type = rh.get('head/meta/type');
  var body = rh.get('body');
  var addr;
  if (type == 'subset') {
    body.iterate(function (key, value) {
      addr = this._merge(value);
    }, this);
  } else {
    addr = this._merge(rh);
  }
  return addr;
};

Livesite.prototype.Hub.prototype._merge = function (struct) {
  var addr = struct.get('head/meta/addr');
  var type = struct.get('head/meta/type');
  var mtime = struct.get('head/meta/mtime');
  var body = struct.get('body');
  var text = struct.get('head/meta/content');
  var node = new LSN.Hub_Node(addr, type, mtime, body, text);
  if ('directory' == node.type) {
    node.iterate(function (k, v) {
      var addr2 = LSN.Address.normalize(node.addr + '/' + k);
      var child = new LSN.Hub_Node(v.get('addr'), v.get('type'), v.get('mtime'));
      child.fetched = false;
      node.set(k, child);
    }, this);
    var value = this.get(node.addr);
    if (LSN.isa(value, LSN.Container)) {
      // Set previously vivified values
      var keys = node.keys();
      for (var i = 0; i < keys.length; i++) {
        var k = keys[i];
        var v = value.get(k);
        if (LSN.isa(v, LSN.Container)) {
          if (v.type == node.get(k).type) {
            if (v.mtime >= node.get(k).mtime) node.set(k, v);
          } else {
            this.cache.remove(v.addr);
            this._sendUpdates('removed', v.addr, true);
          }
        }
      }
      // Notify listeners of removed nodes
      value.iterate(function (k, v) {
        if (!LSN.defined(node.get(k))) {
          this.cache.remove(v.addr);
          this._sendUpdates('removed', v.addr, true);
        }
      }, this);
    }
  } else {
    node.walk(function (k,v,d,a) {
      var addr2 = LSN.Address.normalize(node.addr + '/' + a);
      var type = undefined;
      if (LSN.isa(v, LSN.Container)) {
        type = v instanceof LSN.OrderedHash
          ? 'data-hash'
          : v instanceof LSN.Array
            ? 'data-array'
            : undefined;
      } else {
        type = 'data-scalar';
        var ext = LSN.Address.ext(k);
        if (ext) type += '-' + ext;
      }
      var child = new LSN.Hub_Node(addr2, type, mtime, v);
      node.set(a, child);
    }, this);
  }
  if (addr == '/') {
    this.cache.mtime = node.mtime;
  }
  this.cache.set(addr, node);
  return addr;
};

Livesite.prototype.Hub.prototype._sendUpdates = function (action, addr, modified) {
  var value = action == 'removed' ? {'addr':addr} : this.get(addr);
  for (var i = 0; i < this.listeners.length; i++) {
    var callback = this.listeners[i][0];
    var scope = this.listeners[i][1];
    try {
      // Spawn a new thread for each callback
      setTimeout(callback.bind(scope, action, value, modified), 0);
    } catch (ex) {
      // Do not stop other updates from happening just because
      // one callback failed.  Remove the troubled callback.
      this.listeners.splice(i--, 1);
      //LSN.traceln('Removing data-event listener: ', ex);
    }
  }
};
Livesite.prototype.Hub_Node = Class.create(Livesite.prototype.OrderedHash, {

  initialize: function ($super, addr, type, mtime, body, text) {
    $super();
    if (LSN.isa(body, LSN.Container)) {
      body.iterate(function (k, v) { this.set(k, v); }, this);
      this.content = text;
    } else {
      this.content = body;
    }
    this.mtime = mtime;
    this.addr = addr;
    this.type = type;
    this.icon = LSN.Hub_Icons.getIcon(type);
    this.ccd = LSN.Hub_Util.ccd(type);
    this.fetched = true;
    this.dateStr = '';
    this.timeStr = '';
    this.dateObj = mtime ? new Date(1000 * mtime) : undefined;
    if (this.dateObj) {
      this.dateStr += this.dateObj.getFullYear();
      this.dateStr += '-' + this._zero(this.dateObj.getMonth());
      this.dateStr += '-' + this._zero(this.dateObj.getDate());
      this.timeStr += this._zero(this.dateObj.getHours());
      this.timeStr += ':' + this._zero(this.dateObj.getMinutes());
      this.timeStr += ':' + this._zero(this.dateObj.getSeconds());
    }
  },

  toXFR: function ($super) {
    return this.type.match(/^data-scalar/)
      ? LSN.toXFR(this.content)
      : this.type.match(/^data-array/)
        ? LSN.toXFR(this.values())
        : $super();
  }

});

Livesite.prototype.Hub_Node.prototype._zero = function (num) {
  return num < 10 ? '0' + num : num;
};
Livesite.prototype.Hub_Util = {};

Livesite.prototype.Hub_Util.ccd = function (type) {
  return type == 'directory'
    || type.match(/^file-(data|multipart)/)
    || type.match(/^data-(array|hash)/)
      ? true : false;
}
Livesite.prototype.Hub_List = Class.create({

  /** Public API */

  chroot: function (a) {
    var ra = a || '/';
    if (ra == this.ra) return;
    this.ra = ra;
    this.sel = undefined;
    this.trg = undefined;
    this.exp = undefined;
    this.re = this._getbopt('showRoot')
      ? this._new_dl()
      : this._new_dl(this.ra);
    LSN.replaceChildren(this.ce, [this.re]);
  },

  select: function (a, delay) {
    a = this._defa(a);
    this.sel = a;
    this.trg = a;
    this.exp = undefined;
    var dt = this._e(a, 'dt');
    var n = this.dm.get(a);
    if (delay && dt && n) {
      // When given a delay we highlight the next item (presuming it is an 
      // existing node) and defer the actual selection.  When is does happen, 
      // it only continues if it remains the active selection.  This allows the
      // up & down arrow keys to glide without invoking the entire process.
      this._hl(n);
      setTimeout(function (n) {
        if (n.addr == this.sel) {
          this._fetch(a, true);
        } else {
        }
      }.bind(this, n), delay);
    } else {
      this._fetch(a, true);
    }
  },

  expand: function (a, optSel) {
    a = this._defa(a);
    if (optSel) this.sel = a;
    this.trg = a;
    this.exp = a;
    this._fetch(a, true);
  },

  isExpanded: function (n) {
    return this._isexp(n.addr);
  },

  getSelected: function () {
    if (!LSN.defined(this.sel)) return undefined;
    return this.dm.get(this.sel);
  },

  expandSelected: function () {
    var n = this.getSelected();
    if (n) this._expand(n);
  },

  collapseSelected: function () {
    var n = this.getSelected();
    if (n) this._collapse(n);
  },

  selectPrevious: function () {
    if (!this.sel) return;
    var ui = this._ui(this.sel)
    if (ui.dt.previousSibling) {
      // There is a previous sibling
      var a = this._etoa(ui.dt.previousSibling);
      while (this._isexp(a)) {
        var cui = this._ui(a);
        if (cui.dl.lastChild) {
          a = this._etoa(cui.dl.lastChild);
        } else {
          break;
        }
      }
      this.select(a, 250);
    } else {
      var pui = this._pui(this.sel);
      if (pui && pui.dt) {
        // There is a parent
        this.select(this._etoa(pui.dt), 250);
      }
    }
  },

  selectNext: function () {
    if (!this.sel) return;
    var ui = this._ui(this.sel)
    if (this._isexp(this.sel) && ui.dl.firstChild) {
      // This node is expanded and there is a child
      this.select(this._etoa(ui.dl.firstChild), 250);
    } else if (ui.dd.nextSibling) {
      // There is a next sibling
      this.select(this._etoa(ui.dd.nextSibling), 250);
    } else {
      var pui = this._pui(this.sel);
      while (pui && pui.dt) {
        if (pui.dd.nextSibling) {
          // There is a parent's next sibling
          this.select(this._etoa(pui.dd.nextSibling), 250);
          break;
        }
        pui = this._pui(this._etoa(pui.dt));
      }
    }
  },

  focus: function (a) {
    if (!a) return;
    var dt = this._e(a, 'dt');
    if (!dt) return;
    dt.appendChild(this.kpe);
    this.kpe.focus();
  },

  defaults: {
    /* May be a boolean value or a function (which returns one) */
    showRoot: true,
    autoExpand: false,
    canDisplay: true,
    canExpand: function(n) { return n.ccd; },
    /* Must be functions */
    formatName: function (n, str) { return str; },
    formatDetail: function (n) { return []; },
    onClick: function (event, n) { },
    onSelect: function (n) { }
  },

  /** Object internals */

  initialize: function(e, dm, ra, opts) {
    // parameters
    if (!e instanceof Object) throw 'Illegal argument: e';
    if (!dm instanceof LSN.Hub) throw 'Illegal argument: dm';
    if (!opts instanceof Object) throw 'Illegal argument: opts';
    this.opts = Object.clone(this.defaults);
    if (opts) Object.extend(this.opts, opts);
    // members
    this.id = LSN.randomId('nlist-');   // Unique ID base for UI elements
    this.dm = dm;                       // Data manager (LSN.Hub)
    this.ce = LSN.getElement(e);        // Container element
    this.re = undefined;                // Root element
    this.ra = undefined;                // Root address
    this.sel = undefined;               // Selected item address
    this.lsel = undefined;              // Last selected item address
    this.trg = undefined;               // Action target address
    this.exp = undefined;               // Expansion target address
    this.hl = undefined;                // Hilighted item dt
    this.kp = undefined;                // Keypress event handler
    this.kpe = undefined;               // Element which can receive focus()
    // init
    LSN.includeStyleURI('/res/css/hub_list.css');
    this.dm.addListener(this._devt, this);
    this.chroot(ra);
    this._initkp();
  },

  destroy: function () {
    this.dm.removeListener(this._devt, this);
  },

  _initkp: function () {
    this.kpe = LSN.bodyAppend(LSN.createElement('button', {
      'class':'nlist',
      style:{
        'margin':'0','padding':'0','width':'0','height':'0','border':'none',
        'background':'transparent','color':'transparent',
        'position':'absolute','left':'0',
        'z-index':'-1'
      }
    }));
    this.kp = new LSN.KeyPress({trace:false}).attach(this.kpe);
    this.kp.setHandler('up', this.selectPrevious, this);
    this.kp.setHandler('down', this.selectNext, this);
    this.kp.setHandler('left', this.collapseSelected, this);
    this.kp.setHandler('right', this.expandSelected, this);
  },

  /* Core worker methods */

  _fetch: function (a, branch) {
    this.dm.fetch(a, branch);
  },

  /* devt - Data Event */
  _devt: function (action, n, modified) {
    if (action == 'fetched' || action == 'stored') {
      if (n.addr == this.trg) {
        // The updated node is one which we just performed an action upon
        if (this._e(n.addr, 'dt') && !modified) {
          // We have an element for this node and it is not modified
          if (n.addr == this.exp && !this._isexp(n.addr)) {
            // We need to expand this node
            this._populate(n);
          }
        } else {
          this._populate(n);
        }
        this.exp = undefined;
        this.trg = undefined;
        var tgl = this._e(n.addr, 'tgl');
        if (n.length > 0 && LSN.hasClassName(tgl, 'empty')) {
          var cnodes = n.values();
          for (var i = 0; i < cnodes.length; i++) {
            if (this._canDisplay(cnodes[i])) {
              LSN.removeClassNames(tgl, 'empty');
              break;
            }
          }
        }
        if (n.addr == this.sel) this._sel(n);
      } else if (this._e(n.addr, 'dt') && modified && this._isexp(n.addr)) {
        // The updated node is visible
        this._populate(n);
      } else if (!this._e(n.addr, 'dt') && this._p(n.addr, 'dt') &&
          this._isexp(LSN.Address.parent(n.addr)) && this._canDisplay(n)) {
        // We do not yet have this node although its parent is expanded
        var pn = this._pn(n.addr);
        if (pn) this._populate(pn);
      }
    } else if (action == 'removed') {
      var ui = this._ui(n.addr);
      var sel = undefined;
      if (LSN.defined(this.sel) && this.sel.indexOf(n.addr) == 0) {
        if (ui.dt) {
          var e = ui.dt.nextSibling && ui.dt.nextSibling.nextSibling
            ? ui.dt.nextSibling.nextSibling
            : ui.dt.previousSibling
              ? ui.dt.previousSibling.previousSibling
              : ui.dt.parentNode;
          var sel = this._etoa(e);
        } else {
          sel = LSN.Address.parent(n.addr);
        }
      }
      for (var k in ui) {
        if (!(ui[k] && ui[k].parentNode)) continue;
        ui[k].parentNode.removeChild(ui[k]);
      }
      if (sel) this.select(sel);
      this.focus(sel);
    } else {
    }
  },

  /* Commands */

  _populate: function (n) {
    if (n.addr.indexOf(this.ra) != 0) return;
    var parts = [this.ra];
    if (n.addr != this.ra) {
      var a2 = n.addr.substring(this.ra.length);
      parts = parts.concat(LSN.Address.split(a2));
    }
    var segments = new Array(); // address segments
    for (var i = 0; i < parts.length; i++) {
      segments.push(parts[i]);
      var seg_a = LSN.Address.join(segments);
      var seg_n = this.dm.get(seg_a);
      // Vivify node
      if (this._canDisplay(seg_n)) {
        if (!this._e(seg_n.addr, 'dl')) {
          var nextsib = undefined;
          if ((i+1) < parts.length) {
            nextsib = this._e(LSN.Address.join(segments.concat(parts[i+1])), 'dt');
          }
          this._new(seg_n, nextsib);
        } else {
          this._update(seg_n);
        }
      } else {
        continue;
      }
      if (seg_a == n.addr && n.addr != this.exp && !this._isexp(n.addr)) break;
      if (!this._canexp(n)) break;
      var h = 0; // hidden items
      var cnodes = seg_n.values();
      for (var j = 0; j < cnodes.length; j++) {
        var cn = cnodes[j];
        if (this._canDisplay(cn)) {
          // Vivify child nodes
          if (!this._e(cn.addr, 'dl')) {
            var nextsib = undefined;
            if ((j+1) < cnodes.length) {
              nextsib = this._e(cnodes[j+1].addr, 'dt');
            }
            this._new(cn, nextsib);
          } else {
            this._update(cn);
          }
        } else {
          h++;
        }
      }
      var tgl = this._e(seg_n.addr, 'tgl');
      if (h == seg_n.length) {
        // When there are no all child nodes which can be displayed,
        // disable the can-expand state
        LSN.addClassNames(tgl, 'empty');
        LSN.removeChildren(this._e(seg_n.addr, 'dl'));
      } else {
        LSN.removeClassNames(tgl, 'empty');
      }
    }
  },

  _collapse: function (n) {
    var ui = this._ui(n.addr);
    LSN.removeClassNames(ui.tgl, 'isexp');
    LSN.setStyle(ui.dl, 'display', 'none');
  },

  _expand: function (n) {
    var ui = this._ui(n.addr);
    LSN.setStyle(ui.dl, 'display', 'block');
    this.trg = n.addr;
    this.exp = n.addr;
    this._fetch(n.addr);
  },

  _sel: function (n) {
    this._hl(n);
    this.opts.onSelect.apply(this, [n]);
    this.lsel = this.sel;
  },

  _hl: function (n) {
    var dt = this._e(n.addr, 'dt');
    if (dt === this.hl) return;
    if (this.hl) LSN.removeClassNames(this.hl, 'sel');
    this.hl = dt;
    if (this.hl) {
      LSN.addClassNames(this.hl, 'sel');
      this._scroll(n);
      this.focus(n.addr);
      return true;
    }
    return false;
  },

  _scroll: function (n) {
    var dt = this._e(n.addr, 'dt');
    var pt = (this.ce.scrollTop);
    var ph = LSN.getHeight(this.ce);
    var tt = LSN.getTop(dt) - LSN.getTop(this.ce);
    var tb = LSN.getBottom(dt) - LSN.getTop(this.ce);
    if (tb > (pt + ph) || (tt < pt)) {
      var pm = LSN.asInt(pt + (ph/2));
      var m = LSN.asInt(tt - (ph/2));
      this.ce.scrollTop = m;
    }

  },

  _vis: function (n) {
    var p_tgl = this._p(n.addr, 'tgl');
    if (p_tgl) LSN.addClassNames(p_tgl, 'canexp', 'isexp');
  },

  /* Behavior */

  _getbopt: function (name, n) {
    return typeof(this.opts[name]) == 'function'
      ? this.opts[name].apply(this, [n])
      : this.opts[name];
  },

  _autoExpand: function (n) {
    return this._getbopt('autoExpand', n);
  },

  _canDisplay: function (n) {
    return this._getbopt('canDisplay', n);
  },

  _formatName: function (n) {
    var name = undefined;
    if (n.addr == this.ra) {
      name = n.addr;
    } else {
      name = LSN.Address.name(n.addr);
      if (!LSN.defined(name)) name = n.addr;
    }
    return this.opts.formatName.apply(this, [n, name]);
  },

  /* UI Events */

  _osel: function (event, n) {
    this.focus(n.addr);
    this.opts.onClick.apply(this, [event, n]);
    if (event.stopped) return;
    this.sel = n.addr;
    if (this._canexp(n) && this._autoExpand(n)) {
      this._expand(n);
    } else {
      this.sel = n.addr;
      this.trg = n.addr;
      this._fetch(n.addr);
    }
    Event.stop(event);
  },

  _omover: function (event, n) {
    LSN.addClassNames(this._e(n.addr, 'dt'), 'hl');
  },

  _omout: function (event, n) {
    LSN.removeClassNames(this._e(n.addr, 'dt'), 'hl');
  },

  _toggle: function (event, n) {
    this.focus(n.addr);
    var tgl = this._e(n.addr, 'tgl');
    if (LSN.hasClassName(tgl, 'empty') || !this._canexp(n)) {
      return this._osel(event, n);
    }
    if (this._isexp(n.addr)) {
      this._collapse(n);
    } else {
      this._expand(n);
    }
    Event.stop(event);
  },

  /* Element manipulation */

  _new: function (n, nextsib) {
    var dl = this._new_dl(n.addr);
    var elems = [this._new_dt(n), this._new_dd(n, dl)];
    if (nextsib) {
      LSN.insertChildrenBefore(nextsib, elems);
    } else {
      LSN.appendChildren(this._p(n.addr, 'dl'), elems);
    }
    var ui = this._ui(n.addr);
    if (this._canexp(n)) {
      LSN.addClassNames(ui.tgl, 'canexp');
    } else {
      LSN.removeClassNames(ui.tgl, 'canexp', 'isexp')
    }
    LSN.setAttribute(ui.lbl, 'innerHTML', this._formatName(n));
    this._update(n, ui);
    return dl;
  },

  _update: function (n, ui) {
    if (!ui) ui = this._ui(n.addr);
    var dtlItems = this.opts.formatDetail(n);
    if (dtlItems && dtlItems.length > 0) {
      var list = [];
      for (var i = dtlItems.length - 1; i >= 0; i--) {
        list.push(LSN.createElement('li', {
          'class': 'li' + (i+1),
          innerHTML: dtlItems[i]
        }));
      }
      LSN.replaceChildren(this._e(n.addr, 'dtl'), list);
    }
    this._vis(n);
  },

  _new_dt: function (n) {
    var onDblClick = this._autoExpand(n)
      ? undefined
      : this._toggle.bindAsEventListener(this, n);
    return LSN.createElements(
      'dt', {
        id: this._eid(n.addr, 'dt'),
        onClick: this._osel.bindAsEventListener(this, n),
        onDblClick: onDblClick,
        onMouseOver: this._omover.bindAsEventListener(this, n),
        onMouseOut: this._omout.bindAsEventListener(this, n),
        onSelectStart: function (event) {event.stop();}.bindAsEventListener(this),
        onMouseDown: function (event) {event.stop();}.bindAsEventListener(this)
      }, [
        'ul', {
          id: this._eid(n.addr, 'dtl')
        },
        'div', {
          id: this._eid(n.addr, 'tgl'),
          onClick: this._toggle.bindAsEventListener(this, n),
          'class': 'tgl'
        },
        'img', {
          id: this._eid(n.addr, 'ico'),
          title: n.addr,
          onClick: this._osel.bindAsEventListener(this, n),
          'class': 'ico',
          src: n.icon
        },
        'a', {
          id: this._eid(n.addr, 'lbl'),
          'class': 'name',
          style: {cursor: 'default'}
        }
      ]
    );
  },

  _new_dd: function (n, dl) {
    return LSN.createElements('dd', {id:this._eid(n.addr, 'dd')}, [dl]);
  },

  _new_dl: function (a) {
    if (!LSN.defined(a)) a = '';
    return LSN.createElement('dl', {id:this._eid(a, 'dl'), 'class':'nlist'});
  },

  /* Utility methods */

  _defa: function (a) {
    if (LSN.defined(a)) {
      if (a.indexOf(this.ra) != 0)
        throw 'Address outside of list root: ' + a;
    } else {
      a = this.ra;
    }
    return a;
  },

  _canexp: function (n) {
    return this._getbopt('canExpand', n);
  },

  _isexp: function (a) {
    var tgl = this._e(a, 'tgl');
    if (!tgl) return;
    return LSN.hasClassName(tgl, 'isexp') || LSN.hasClassName(tgl, 'empty')
      ? true
      : LSN.hasClassName(tgl, 'canexp')
        ? false
        : true;
  },

  _etoa: function (e) {
    if (!e) return;
    var id = e.id;
    var a = id.substr(this.id.length);
    return a.substr(0, a.indexOf('?'));
  },

  _eid: function (a, tag) {
    return this.id + a + '?' + tag;
  },

  _pid: function (a, tag) {
    if (a == this.ra) return this._eid('', tag);
    return this._eid(LSN.Address.parent(a), tag);
  },

  _e: function (a, tag) {
    return LSN.getElement(this._eid(a, tag));
  },

  _p: function (a, tag) {
    return LSN.getElement(this._pid(a, tag));
  },

  _pn: function (a) {
    var pa = LSN.Address.parent(a);
    if (pa.indexOf(this.ra) != 0) return undefined;
    if (pa == a) return undefined;
    return this.dm.get(pa);
  },

  _pui: function (a) {
    if (a != this.ra && a.indexOf(this.ra) == 0) {
      return this._ui(LSN.Address.parent(a));
    }
  },

  _ui: function (a) {
    return {
      dt: this._e(a, 'dt'),
      tgl: this._e(a, 'tgl'),
      ico: this._e(a, 'ico'),
      lbl: this._e(a, 'lbl'),
      dd: this._e(a, 'dd'),
      dl: this._e(a, 'dl')
    };
  }

});
Livesite.prototype.Hub_Icons = {

  ICONS: {
    'application-wireframe.png': '/res/icons/16x16/nodes/application-wireframe.png',
    'applications-internet.png': '/res/icons/16x16/nodes/applications-internet.png',
    'bgleft.png': '/res/icons/16x16/nodes/bgleft.png',
    'canexp.png': '/res/icons/16x16/nodes/canexp.png',
    'data-array.png': '/res/icons/16x16/nodes/data-array.png',
    'data-hash.png': '/res/icons/16x16/nodes/data-hash.png',
    'data-scalar.png': '/res/icons/16x16/nodes/data-scalar.png',
    'directories.png': '/res/icons/16x16/nodes/directories.png',
    'directory.png': '/res/icons/16x16/nodes/directory.png',
    'file-bmp.png': '/res/icons/16x16/nodes/file-bmp.png',
    'file-csv.png': '/res/icons/16x16/nodes/file-csv.png',
    'file-data-array.png': '/res/icons/16x16/nodes/file-data-array.png',
    'file-data.png': '/res/icons/16x16/nodes/file-data.png',
    'file-doc.png': '/res/icons/16x16/nodes/file-doc.png',
    'file-flv.png': '/res/icons/16x16/nodes/file-flv.png',
    'file-gif.png': '/res/icons/16x16/nodes/file-gif.png',
    'file-jar.png': '/res/icons/16x16/nodes/file-jar.png',
    'file-jpeg.png': '/res/icons/16x16/nodes/file-jpeg.png',
    'file-jpg.png': '/res/icons/16x16/nodes/file-jpg.png',
    'file-mp3.png': '/res/icons/16x16/nodes/file-mp3.png',
    'file-multipart-html.png': '/res/icons/16x16/nodes/file-multipart-html.png',
    'file-multipart.png': '/res/icons/16x16/nodes/file-multipart.png',
    'file-pdf.png': '/res/icons/16x16/nodes/file-pdf.png',
    'file-pm.png': '/res/icons/16x16/nodes/file-pm.png',
    'file-png.png': '/res/icons/16x16/nodes/file-png.png',
    'file-swf.png': '/res/icons/16x16/nodes/file-swf.png',
    'file-text-cgi.png': '/res/icons/16x16/nodes/file-text-cgi.png',
    'file-text-css.png': '/res/icons/16x16/nodes/file-text-css.png',
    'file-text-ht.png': '/res/icons/16x16/nodes/file-text-ht.png',
    'file-text-html.png': '/res/icons/16x16/nodes/file-text-html.png',
    'file-text-js.png': '/res/icons/16x16/nodes/file-text-js.png',
    'file-text-pl.png': '/res/icons/16x16/nodes/file-text-pl.png',
    'file-text-pm.png': '/res/icons/16x16/nodes/file-text-pm.png',
    'file-text.png': '/res/icons/16x16/nodes/file-text.png',
    'folders.png': '/res/icons/16x16/nodes/folders.png',
    'isempty.png': '/res/icons/16x16/nodes/isempty.png',
    'isexp.png': '/res/icons/16x16/nodes/isexp.png',
    'mpe-bg.png': '/res/icons/16x16/nodes/mpe-bg.png',
    'mpe-data-array.png': '/res/icons/16x16/nodes/mpe-data-array.png',
    'mpe-data-hash.png': '/res/icons/16x16/nodes/mpe-data-hash.png',
    'mpe-data-scalar.png': '/res/icons/16x16/nodes/mpe-data-scalar.png',
    'mpe-end.png': '/res/icons/16x16/nodes/mpe-end.png',
    'noexp.png': '/res/icons/16x16/nodes/noexp.png',
    'unknown.png': '/res/icons/16x16/nodes/unknown.png',
    'user-home.png': '/res/icons/16x16/nodes/user-home.png'
  },

  getIcon: function (type) {
    var parts = type.split('-');
    var icon = undefined;
    while (!icon && parts.length > 0) {
      var name = parts.join('-') + '.png';
      icon = LSN.Hub_Icons.ICONS[name];
      parts.pop();
    }
    return icon || LSN.Hub_Icons.ICONS['unknown.png'];
  }

};
Livesite.prototype.Canvas = Class.create({

  initialize: function() {
    this.root = LSN.getRootElement();
    this.doc = LSN.doc.documentElement;
    this.win = LSN.win;
  },

  windowX: function() {
    var windowX = this.win.innerWidth || (this.doc && this.doc.clientWidth)
      || this.root.clientWidth || (this.doc && this.doc.offsetWidth);
    return LSN.asInt(windowX);
  },

  windowY: function() {
    var windowY = this.win.innerHeight || (this.doc && this.doc.clientHeight)
      || this.root.clientHeight || (this.doc && this.doc.offsetHeight);
    return LSN.asInt(windowY);
  },

  scrollX: function() {
    var scrollX = (this.doc && this.doc.scrollLeft) || this.win.pageXOffset
      || this.root.scrollLeft;
    return LSN.asInt(scrollX);
  },

  scrollY: function() {
    var scrollY = (this.doc && this.doc.scrollTop) || this.win.pageYOffset
      || this.root.scrollTop;
    return LSN.asInt(scrollY);
  },

  pageX: function() {
    var pageX = Math.max(
      LSN.asInt(this.doc.scrollWidth),
      LSN.asInt(this.root.scrollWidth),
      LSN.asInt(this.root.offsetWidth)
    )
    return LSN.asInt(pageX);
  },

  pageY: function() {
    var pageY = Math.max(
      LSN.asInt(this.doc.scrollHeight),
      LSN.asInt(this.root.scrollHeight),
      LSN.asInt(this.root.offsetHeight)
    )
    return LSN.asInt(pageY);
  }

});

/*
  var c = new LSN.Canvas();
  LSN.traceln('-', c.root, ',', c.doc, ',', c.win);
  LSN.traceln('=', c.doc.clientHeight, ',', c.doc.offsetHeight);
  LSN.traceln('=', c.win.innerHeight, ',', c.root.clientHeight);
  LSN.traceln('w', c.windowX(), 'x', c.windowY());
  LSN.traceln('s', c.scrollX(), 'x', c.scrollY());
  LSN.traceln('p', c.pageX(), 'x', c.pageY());
*/
Livesite.prototype.DragHandle = Class.create({

  initialize: function(elem, opts) {
    this.opts = {
      'threshold': 0,
      'onMouseDown': function (event, dh) { event.stop() },
      'onMouseUp': function (event, dh) { event.stop() },
      'onMouseMove': function (event, dh) { event.stop() }
    };
    Object.extend(this.opts, opts);
    this.elem = LSN.getElement(elem);
    if (!this.elem) return;
    this.listenOn = Prototype.Browser.IE ? LSN.doc : LSN.win;
    this.onMouseDownListener = this.onMouseDown.bindAsEventListener(this);
    this.onMouseMoveListener = this.onMouseMove.bindAsEventListener(this);
    this.onMouseUpListener = this.onMouseUp.bindAsEventListener(this);
    Event.observe(this.elem, 'mousedown', this.onMouseDownListener);
    this.reset();
  },

  reset: function () {
    this.orig_mx = 0;
    this.orig_my = 0;
    this.delta_x = 0;
    this.delta_y = 0;
    this.dragging = false;
  },

  onMouseDown: function (event) {
    this.reset();
    this.orig_mx = Event.pointerX(event);
    this.orig_my = Event.pointerY(event);
    Event.observe(this.listenOn, 'mousemove', this.onMouseMoveListener);
    Event.observe(this.listenOn, 'mouseup', this.onMouseUpListener);
    this.opts.onMouseDown(event, this);
  },

  onMouseUp: function (event) {
    Event.stopObserving(this.listenOn, 'mousemove', this.onMouseMoveListener);
    Event.stopObserving(this.listenOn, 'mouseup', this.onMouseUpListener);
    this.dragging = false;
    this.opts.onMouseUp(event, this);
  },

  onMouseMove: function(event) {
    this.delta_x = Event.pointerX(event) - this.orig_mx;
    this.delta_y = Event.pointerY(event) - this.orig_my;
    if (!this.dragging
        && Math.abs(this.delta_x) < this.opts.threshold
        && Math.abs(this.delta_y) < this.opts.threshold) {
      return
    }
    this.dragging = true;
    this.opts.onMouseMove(event, this);
  }

});
Livesite.prototype.Style = Class.create({

  initialize: function() {
    this.style = undefined;
    this.sheet = undefined;
    this.cssRules = undefined;
    this.cssRulesByName = undefined;
  },

  vivify: function () {
    this.style = LSN.createElement('style', {
      id: 'LSN_CSS',
      type: 'text/css',
      rel: 'stylesheet',
      media: 'screen'
    });
    LSN.getHead().appendChild(this.style);
    this.sheet = this.style.sheet || this.style.styleSheet;
    this.cssRules = this.sheet.cssRules || this.sheet.rules;
    this.cssRulesByName = {};
  },

  objToStr: function(obj, opts) {
    var result = '';
    for (var name in obj) {
      if (LSN.defined(opts) && LSN.defined(opts.exclude)) {
        if (name.match(opts.exclude)) {
          continue;
        }
      }
      result += name + ':' + obj[name] + ';' + "\n";
    }
    return result;
  },

  strToObj: function(str) {
    str = str.replace(/\r?\n\r?/g, '');
    var result = {};
    var items = str.split(';');
    for (var i = 0; i < items.length - 1; i++) {
      var key = items[i].split(/:/, 1);
      var value = items[i].substr(key[0].length + 1);
      value = value.replace(/^\s+/, '');
      var name = key[0];
      result[name] = value;
    }
    return result;
  },

  cssNameToJsName: function(name) {
    if (name == 'float') return 'cssFloat';
    if (name == 'class') return 'className';
    return LSN.asCamelCaseName(name);
  },

  jsNameToCssName: function(name) {
    if (name == 'cssFloat') return 'float';
    if (name == 'className') return 'class';
    return LSN.asHyphenatedName(name);
  },

  createRule: function(name, props) {
    if (!this.style) this.vivify();
    var str = this.objToStr(props);
    var rule = null;
    var idx = -1;
    if (LSN.defined(this.sheet.addRule)) {
      /* ie */
      idx = this.sheet.rules.length;
      this.sheet.addRule(name, str);
      rule = this.sheet.rules[idx];
    } else if(LSN.defined(this.sheet.insertRule)) {
      idx = this.sheet.cssRules.length;
      this.sheet.insertRule(name +' {' + str + '}', idx);
      rule = this.sheet.cssRules[idx];
    }
    this.cssRulesByName[name] = rule;
    return rule;
  },

  updateRule: function(name, props) {
    if (!this.style) this.vivify();
    var rule = this.cssRulesByName[name];
    if (rule) {
      for (var propName in props) {
        LSN.setStyle(rule, propName, props[propName]);
      }
    } else {
      rule = this.createRule(name, props);
    }
    return rule;
  }

});
Livesite.prototype.Move = Class.create({

  initialize: function(event, elem) {
    this.elem = $(elem);
    this.listenOn = Prototype.Browser.IE ? LSN.doc : LSN.win;
    if (!this.elem) return;
    this.onMouseMoveListener = this.onMouseMove.bindAsEventListener(this);
    this.onMouseUpListener = this.onMouseUp.bindAsEventListener(this);
    Event.observe(this.listenOn, 'mousemove', this.onMouseMoveListener);
    Event.observe(this.listenOn, 'mouseup', this.onMouseUpListener);
    var vp = LSN.getViewportPosition();
    LSN.makePositioned(elem);
    this.min_x    = vp['left'];
    this.min_y    = vp['top'];
    this.abs_x    = LSN.getLeft(this.elem);
    this.abs_y    = LSN.getTop(this.elem);
    this.orig_x   = LSN.asInt(LSN.getStyle(this.elem, 'left'));
    this.orig_y   = LSN.asInt(LSN.getStyle(this.elem, 'top'));
    this.orig_z   = LSN.getStyle(this.elem, 'z-index');
    this.orig_mx  = Event.pointerX(event);
    this.orig_my  = Event.pointerY(event);
    // Constrain movement to the visible canvas
    this.max_x = vp['left'] + vp['width'] - LSN.getWidth(this.elem);
    this.max_y = vp['top'] + vp['height'] - LSN.getHeight(this.elem);
    // Position element
    LSN.setStyle(this.elem, 'left', this.orig_x.toString(10) + 'px');
    LSN.setStyle(this.elem, 'top', this.orig_y.toString(10) + 'px');
    LSN.setStyle(this.elem, 'z-index', '199');
    Event.stop(event);
  },

  onMouseUp: function (event) {
    LSN.setStyle(this.elem, 'z-index', this.orig_z);
    Event.stopObserving(this.listenOn, 'mousemove', this.onMouseMoveListener);
    Event.stopObserving(this.listenOn, 'mouseup', this.onMouseUpListener);
  },

  onMouseMove: function(event) {
    // Calculate
    var delta_x = Event.pointerX(event) - this.orig_mx;
    var delta_y = Event.pointerY(event) - this.orig_my;
    var new_x   = this.orig_x + delta_x;
    var new_y   = this.orig_y + delta_y;
    // Constrain
    if (this.abs_x + delta_x >= this.max_x) new_x = this.max_x;
    if (this.abs_y + delta_y >= this.max_y) new_y = this.max_y;
    if (this.abs_x + delta_x <  this.min_x) new_x = this.min_x;
    if (this.abs_y + delta_y <  this.min_y) new_y = this.min_y;
    // Apply new position
    if (new_x != null)
      LSN.setStyle(this.elem, 'left', (new_x).toString(10) + 'px');
    if (new_y != null)
      LSN.setStyle(this.elem, 'top', (new_y).toString(10) + 'px');
    Event.stop(event);
  }

});
Livesite.prototype.KeyPress = Class.create({

  initialize: function (options) {
    this.opts = {'trace':false};
    Object.extend(this.opts, options);
    this.handlers = new Hash();
    this.repeat = {};
    return this;
  },

  setHandler: function(sequence, func, scope) {
    func = typeof(func) == 'function' ? func : new Function(func);
    this.handlers.set(sequence, [func, scope]);
    return this;
  },

  attach: function (elem) {
    Event.observe(elem, 'keydown', this.onKeyDown.bindAsEventListener(this));
    Event.observe(elem, 'keypress', this.onKeyPress.bindAsEventListener(this));
    Event.observe(elem, 'keyup', this.onKeyUp.bindAsEventListener(this));
    return this;
  },

  enable: function () {
    return this;
  },

  disable: function () {
    return this;
  },

  onKeyDown: function (event) {
    this.trace(event);
    var seq = this.getSequence(event);
    if (seq.ismod) return;
    var handler = this.getHandler(seq);
    if (handler) {
      this.repeat[seq.ascii] = 0;
      return this.doEvent(event, handler);
    }
  },

  onKeyPress: function (event) {
    var seq = this.getSequence(event);
    if (seq.ismod) return;
    var handler = this.getHandler(seq);
    if (handler) {
      var doEvent = false;
      if (LSN.defined(this.repeat[seq.ascii])) {
        this.repeat[seq.ascii]++;
        doEvent = this.repeat[seq.ascii] > 1;
      } else {
        // keydown not received
        doEvent = true;
      }
      return doEvent ? this.doEvent(event, handler) : event.stop();
    }
  },

  onKeyUp: function (event) {
    var seq = this.getSequence(event);
    if (seq.ismod) return;
    delete this.repeat[seq.ascii];
  },

  doEvent: function (event, handler) {
    if (event.stopped) return;
    Event.stop(event);
    return handler[1] ? handler[0].apply(handler[1], [event]) : handler[0](event);
  },

  getHandler: function (seq) {
    return this.handlers.get(seq.ascii) || this.handlers.get(seq.numeric);
  },

  getSequence: function (event, numeric) {
    var char_code = this.getCharCode(event);
    var ascii_code = String.fromCharCode(char_code).toLowerCase();
    var prefix = event.ctrlKey ? "ctrl+" : "";
    if (event.altKey) prefix += "alt+";
    if (event.shiftKey) prefix += "shift+";
    var seq = {ascii:prefix, numeric:prefix + char_code, ismod:false};
    var is_special = true;
    if (Prototype.Browser.Gecko) {
      // http://developer.mozilla.org/en/docs/DOM:event.keyCode
      is_special = event.keyCode && !event.charCode;
    }
    if (!is_special) {
      seq.ascii += ascii_code;
    } else {
      switch (char_code) {
        case 8  : seq.ascii += 'backspace'; break;
        case 9  : seq.ascii += 'tab'; break;
        case 13 : seq.ascii += 'enter'; break;
        case 16 : seq.ascii  = 'shift'; break;
        case 17 : seq.ascii  = 'ctrl'; break;
        case 18 : seq.ascii  = 'alt'; break;
        case 27 : seq.ascii += 'esc'; break;
        case 37 : seq.ascii += 'left'; break;
        case 38 : seq.ascii += 'up'; break;
        case 39 : seq.ascii += 'right'; break;
        case 40 : seq.ascii += 'down'; break;
        case 46 : seq.ascii += 'delete'; break;
        case 36 : seq.ascii += 'home'; break;
        case 35 : seq.ascii += 'end'; break;
        case 33 : seq.ascii += 'pageup'; break;
        case 34 : seq.ascii += 'pagedown'; break;
        case 45 : seq.ascii += 'insert'; break;
        default : seq.ascii += ascii_code;
      }
    }
    seq.ismod = 'shift' == seq.ascii || 'ctrl' == seq.ascii ||
      'alt' == seq.ascii;
    return seq;
  },

  getCharCode: function (event) {
    return  event.which     ? event.which     :
            event.keyCode   ? event.keyCode   :
            event.charCode  ? event.charCode  :
                              0;
  },

  doesTargetNativelyHandleEvent: function () {
    return
      event.target &&
      event.target.tagName &&
      (
        event.target.tagName.toUpperCase().match(/^(BUTTON|A)$/) ||
        (
          event.target.tagName.toUpperCase() == 'INPUT' &&
          event.target.type &&
          event.target.type.toUpperCase() == 'SUBMIT'
        )
      ) &&
      (
        ascii_code == 'enter' ||
        ascii_code == 'space'
      );
  },

  trace: function (event) {
    if (!this.opts.trace) return;
    var seq = this.getSequence(event);
    LSN.traceln('ASCII seq: ', seq.ascii);
    LSN.traceln('ASCII num: ', seq.numeric);
    LSN.traceln(' charCode: ', event.charCode);
    LSN.traceln('  keyCode: ', event.keyCode);
    LSN.traceln('    which: ', event.which);
    LSN.traceln('     type: ', event.type);
  }

});
/* 
 * LSN.Request - Encode request body as JSON, accept only and decode 
 * 'text/json-hash' responses.
 *
 * Examples which show how the responseHash 'h' is returned.
 *
 *  // Synchronous request
 *  var r = new LSN.Request(url, {asynchronous:false});
 *  var h = r.submit();
 *
 *  // onSuccess is a bound function
 *  foo = function(req) {
 *    var h = req.responseHash;
 *  }
 *  var p = new LSN.Request(url, {onSuccess: foo.bind(this)});
 *  p.submit();
 *
 *  // onSuccess is an unbound anonymous function
 *  var p = new LSN.Request(url, {
 *    onSuccess: function(req) {
 *      if (req.responseHash !== this.responseHash) throw 'Ouch';
 *    }
 *  });
 *  p.submit();
 */

Livesite.prototype.Request = Class.create({

  pendingAuth: false,

  initialize: function (uri, opts) {
    this.uri = uri;
    this.opts = {
      'requestHeaders': {'Accept': 'text/json-hash'},
      'method': 'POST',
      'evalJS': false,
      'evalJSON': false,
      'loginURI': '/res/login/login.dlg',
      'onSuccess': this._onSuccess.bind(this),
      'on401': this._on401.bind(this),
      'on403': this._on403.bind(this),
      'onException': this._onException.bind(this)
    };
    if (opts) {
      for (var k in opts) {
        if (k == 'onSuccess') {
          this.onSuccess = opts.onSuccess;
        } else {
          if (typeof(this.opts[k]) == 'object') {
            Object.extend(this.opts[k], opts[k]);
          } else {
            this.opts[k] = opts[k];
          }
        }
      }
    }
  },

  submit: function(postBody) {
    this.responseHash = null;
    this.response = null;
    this.reqOpts = Object.clone(this.opts);
    if (this.reqOpts.method.match(/^GET$/i) && postBody) {
      this.reqOpts.parameters = postBody;
      delete this.reqOpts.postBody;
    } else {
      this.setPostBody(postBody);
    }
    new Ajax.Request(this.uri, this.reqOpts);
    return this.reqOpts.asynchronous ? this : this.responseHash;
  },

  resubmit: function() {
    new Ajax.Request(this.uri, this.reqOpts);
    return this.reqOpts.asynchronous ? this : this.responseHash;
  },

  addParameter: function(k, v) {
    if (!this.opts.parameters) this.opts.parameters = {};
    this.opts.parameters[k] = v;
  },

  _onSuccess: function(r) {
    this.response = r;
    this._parse(r);
    r.responseHash = this.responseHash;
    if (Object.isFunction(this.onSuccess)) this.onSuccess(r);
  },

  _on401: function (r) {
    if (this.uri != this.opts.loginURI) {
      if (!LSN.Request.pendingAuth) {
        new LSN.Dialog(this.opts.loginURI).show({
          onSuccess: function () {
            LSN.Request.pendingAuth = false;
            this.resubmit();
          }.bind(this)
        });
        LSN.Request.pendingAuth = true;
      } else {
        // TODO Create a pending req's queue, call setInteval to
        // process the queue, create a processQueue funtion
      }
    }
  },

  _on403: function (r) {
    // TODO Log a bug with Opera
    if (Prototype.Browser.Opera) {
      this._on401(r);
    }
  },

  _onException: function(r, ex) {
    // Cannot rethrow ... something to do with prototype?
    if ((typeof(console) != 'undefined') && console.error && console.trace) {
      console.error(ex);
      console.trace();
    } else {
      alert(ex);
    }
  },

  setPostBody: function(body) {
    if (Object.isUndefined(body)) return;
    if (Object.isString(body)) return encodeURIComponent(body);
    if (false) {
      this.reqOpts.postBody = encodeURIComponent(Object.toJSON(body));
      this.reqOpts.requestHeaders['X-Content-Format'] = 'text/json-hash';
    } else {
      this.reqOpts.postBody = LSN.toXFR(body);
      this.reqOpts.requestHeaders['X-Content-Format'] = 'text/data-xfr';
    }
  },

  _parse: function(r) {
    if (!LSN.isSameOrigin(this.uri)) throw "SOP Violation";
    if (r.getHeader('Content-Type') != 'text/json-hash')
      throw "Invalid response content";
    eval('this.responseHash = ' + r.responseText);
  }

});

/*

  setPostBody: function(body) {
    if (Object.isUndefined(body)) return;
    if (Object.isString(body)) return encodeURIComponent(body);
    var result = '';
    LSN.Courier.walk(body, [
      function (k, v) {
        var kk =
        result += encodeURIComponent("'" + k + "':");
        if (v instanceof Array) {

        } else if (v instanceof Object) {
        } else if (!isNaN(v)) {
        } else {
          encodeURIComponent(value)}
        }
      }
    );
    body = Object.toJSON(body);

    {value:encodeURIComponent(value)}
    return Object.toJSON(body);
  },

*/
Livesite.prototype.Sync = Class.create({

  initialize: function(url, options) {
    this.url = url;
    this.period = 2;
    this.onUpdate = function(){};
    this.onRevert = function(){};
    Object.extend(this, options);
    this.timer = null;
    this.clear();
  },

  isRunning: function() {
    return this.timer ? true : false;
  },

  isUpdating: function() {
    return this.inbound.length > 0;
  },

  start: function(bTimerOnly) {
    if (this.isRunning()) return;
    if (!bTimerOnly) this.sync(); // start right away
    this.timer = setInterval(this.sync.bind(this), this.period * 1000);
  },

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

  send: function(body) {
    this.outbound.push(body);
  },

  clear: function() {
    this.inbound = []
    this.outbound = [];
    this.sent = 0;
    this.received = 0;
  },

  revert: function() {
    var doRestart = this.isRunning();
    this.stop();
    this.clear();
    var meta = {'cmd': 'revert'};
    Object.extend(meta, this.meta);
    var req = new LSN.Request(this.url, {'asynchronous': false});
    req.submit({'meta': meta});
    if (doRestart) this.start();
  },

  sync: function() {
    if (this.isUpdating()) return;
    if (!this.url) throw "No URL provided";
    if (!LSN.isSameOrigin(this.url)) {
      this.stop();
      throw "SOP Violation";
    }
    if ((this.sent - this.received) > 0) return;
    var p = new LSN.Request(this.url, {
      'asynchronous': false,
      'onSuccess': this._onSuccess.bind(this, this.outbound.length),
      'onComplete': this._onComplete.bind(this)
    });
    p.submit({'meta': this.meta, 'updates': this.outbound});
    this.sent++;
  },

  _onSuccess: function(sentCount, transport) {
    if (sentCount) this.outbound.splice(0, sentCount);
    var body = transport.responseHash.get('body').toObject();
    this.meta.rev = body.meta.rev;
    if (body.updates && body.updates.length > 0)
      this.inbound = this.inbound.concat(body.updates);
    if (body.meta.cmd)
      this.onCommand(body.meta.cmd);
  },

  _onComplete: function(transport) {
    this.received++;
    var recCount = this.inbound.length;
    if (recCount > 0) {
      for (var i = 0; i < recCount; i++) {
        this.onUpdate(this.inbound[0]);
        this.inbound.shift(); // *after* doUpdate (so 'isUpdating' works)
      }
    }
  }

});
/* ------------------------------------------------------------------------------
 * LSN.Widget - A response comprised of HTML, JS and CSS which is not a full 
 * document.
 *
 *  var w = new LSN.Widget('/my.html', {container: 'mydiv'});
 *  w.show({param:'value'});
 *
 * For flexibility, this widget will integrate its CSS and JS into the head of
 * the running document, but it *won't* append the HTML to the current document.
 * --------------------------------------------------------------------------- */

Livesite.prototype.Widget = Class.create({

  initialize: function (uri, options) {
    if (!LSN.isSameOrigin(uri)) throw "SOP Violation";
    if (!this.id) this.id = LSN.randomId('widget_');
    this.request = new LSN.Request(uri, {
      'onSuccess': this._onSuccess.bind(this),
      'onFailure': this._onFailure.bind(this)
    });
    this.container = undefined;
    this.masked = false;
    this.handleKeypress = {};
    this.refetch = true;
    this.events = {};
    this.includeEvents = {};
    this.modal = false;
    this.props = {};
    this.incstat = {
      varname: this.id + '_is_ready',
      timeout: 10,
      decay: 2
    };
    $H(options).each(this.setOption.bind(this));
    this.reset();
  },

  reset: function() {
    this.nodes = null;
    this.values = {};
    this.ctrls = [];
    this.btns = [];
    this.btn_events = [];
    this.includeEvents = {};
    this.kp = new LSN.KeyPress();
    this._stopEvent = false;
  },

  setOption: function(kvpair) {
    if (kvpair.key.match(/^on[A-Z]/)) {
      this.addEvent(kvpair.key, kvpair.value);
    } else {
      this[kvpair.key] = kvpair.value;
    }
  },

  addEvent: function(name, func) {
    name = name.replace(/^on/, '');
    name = name.toLowerCase();
    if(!this.events[name]) this.events[name] = new Array();
    this.events[name].push(func);
  },

  includeEvent: function(name, func) {
    name = name.replace(/^on/, '');
    name = name.toLowerCase();
    if(!this.includeEvents[name]) this.includeEvents[name] = new Array();
    this.includeEvents[name].push(func);
  },

  stopEvent: function () {
    this._stopEvent = true;
  },

  doEvent: function(name) {
    /* For convenience, gather values if an ok button was clicked */
    if (this._stopEvent) {this._stopEvent = false; return;}
    if (name == 'ok') {
      this.getValues();
    }
    /* Fire events */
    if (this.includeEvents[name]) {
      for (var i = 0; i < this.includeEvents[name].length; i++) {
        this.includeEvents[name][i].apply(this);
        if (this._stopEvent) {this._stopEvent = false; return;}
      }
    }
    /* Fire events */
    if (this.events[name]) {
      for (var i = 0; i < this.events[name].length; i++) {
        this.events[name][i].apply(this);
        if (this._stopEvent) {this._stopEvent = false; return;}
      }
    }
    /* Set focus handlers when the dialog is shown */
    if (name == 'show') {
      try {
        if (this.ctrls.length > 0) {
          this.ctrls[0].focus();
        } else if (this.btns.length > 0) {
          this.btns[0].focus();
        }
      } catch (ex) {
        // swallow exeptions whey trying to set foccus on a hidden element
      }
      // in case dialog increases page size
      if (this.modal && this.masked) LSN.resizeMask();
      // The dialog is ready after it is shown.
      //
      // TODO Find the proper way to detect that the JS has been parsed.
      //
      // The ready event must not happen until the the dialog's JS and CSS have 
      // been processed *and* the elements which have been added to the DOM are 
      // ready.  In all my test cases everything is great except for the 
      // JS--thus the setTimeout hack.
      this.fireWhenReady();
    }
    /* For convenience, close the dialog if an ok or cancel buttons was clicked */
    if (name == 'ok' || name == 'cancel') {
      this.hide();
    }
    /* Remove page mask */
    if (name == 'hide') {
      if (this.modal && this.masked) LSN.hideMask();
    }
  },

  fireWhenReady: function () {
    try {
      if (eval (this.incstat.varname)) {
        this.doEvent('ready');
        return;
      }
    } catch (ex) {
      // ready var is not defined
    }
    //LSN.traceln('waiting ' + this.incstat.timeout + 'ms for: ' + this.incstat.varname);
    setTimeout(this.fireWhenReady.bind(this), this.incstat.timeout);
    this.incstat.timeout *= this.incstat.decay;
  },

  show: function(params) {
    this.params = params ? params : {};
    if (this.modal) {
      LSN.showMask();
      this.masked = true;
    }
    if (this.nodes) {
      if (this.refetch) {
        this.destroy();
        this.request.submit();
      } else {
        this._updateUI();
        this.doEvent('show');
      }
    } else {
      /* Fetch the dialog from the server */
      this.request.submit();
    }
  },

  _onFailure: function() {
    if (this.modal) LSN.hideMask();
  },

  _onSuccess: function(r) {
    var resp = r.responseHash;
    LSN.Widget.includeHead(resp.get('head'), this.id, this);
    /* Create a temporary container for creating inner html */
    var tmpDiv = LSN.createElement('div', {'innerHTML': resp.get('body')});
    this.nodes = $A(tmpDiv.childNodes);
    /* Extract buttons and input controls */
    this.ctrls = LSN.getChildren(tmpDiv, {'name': '~values\/[a-z]+'});
    this.btns = LSN.getChildren(tmpDiv, {'name': '~action\/[a-z]+'});
    /* Extract onshow events */
    var elems = LSN.getChildren(tmpDiv, {'onshow': '~.+'});
    for (var i = 0; i < elems.length; i++) {
      this.includeEvent('show', new Function(elems[i].getAttribute('onshow')));
    }
    this._updateUI();
    /* Fire events */
    this.doEvent('load');
    this.doEvent('show');
  },

  getValues: function() {
    for (var i = 0; i < this.ctrls.length; i++) {
      var ptr = this.values;
      var path = this.ctrls[i].name.split('/');
      for (var j = 1; j < path.length; j++) {
        var val = LSN.defined(path[j+1])
          ? isNaN(path[j+1])
            ? new Object()
            : new Array()
          : LSN.getValue(this.ctrls[i]);
        if (LSN.defined(val)) {
          /* checkboxes/radio buttons will return undef when not checked */
          ptr[path[j]] = val;
        }
        ptr = ptr[path[j]];
      }
    }
    return this.values;
  },

  _updateUI: function () {
    /* Assign event handlers to buttons */
    for (var i = 0; i < this.btns.length; i++) {
      var name = this.btns[i].name;
      var action = name.match(/action\/([a-z]+)/);
      var btn_event = this.doEvent.bind(this, action[1]);
      this.btn_events[i] = btn_event;
      Event.observe(this.btns[i], 'click', btn_event);
      if (this.handleKeypress[action[1]]) {
        if (action[1] == 'ok') this.kp.setHandler('enter', btn_event);
        if (action[1] == 'cancel') this.kp.setHandler('esc', btn_event);
      }
    }
    /* Attach keypress to controls only */
    for (var i = 0; i < this.ctrls.length; i++) {
      if (this.ctrls[i].tagName != 'TEXTAREA') {
        this.kp.attach(this.ctrls[i]);
      }
    }
    /* Append elements to the container */
    var ce = LSN.getElement(this.container || LSN.getBody());
    for (var i = 0; i < this.nodes.length; i++) {
      var node = this.nodes[i];
      if (!node.style) continue; // TODO better check for DOM Elements
      ce.appendChild(node);
    }
  },

  _removeUI: function() {
    var trash = LSN.createElement('div');
    if (this.btns) {
      for (var i = 0; i < this.btns.length; i++) {
        Event.stopObserving(this.btns[i], 'click', this.btn_events[i]);
      }
    }
    if (this.nodes) {
      for (var i = 0; i < this.nodes.length; i++) {
        if (this.nodes[i].nodeParent) {
          this.nodes[i].parentNode.removeChild(this.nodes[i]);
        }
        trash.appendChild(this.nodes[i]);
      }
    }
  },

  hide: function() {
    this.doEvent('hide');
    this._removeUI();
  },

  destroy: function() {
    this.hide();
    this.doEvent('destroy');
    this.reset();
  }

});

Livesite.prototype.Widget.includeHead = function(head, id, caller) {
  if (head) {
    var jsLinks = head.get('links/js');
    if (jsLinks) {
      jsLinks.iterate(function (k,v) {LSN.includeScriptURI(v)});
    }
    var cssLinks = head.get('links/css');
    if (cssLinks) {
      cssLinks.iterate(function (k,v) {LSN.includeStyleURI(v)});
    }
    if (head.get('css')) {
      var css_id = id ? 'css_' + id : null;
      LSN.includeStyle(head.get('css'), css_id);
    }
    if (head.get('js')) {
      var js_id = id ? 'js_' + id : null;
      LSN.includeScript(head.get('js'), js_id);
    }
    var jsEvents = head.get('events/js');
    if (jsEvents) {
      jsEvents.iterate(function (target, events) {
        events.iterate(function (idx, kvpair) {
          var evtFunc = new Function(kvpair.get('value')).bind(caller);
          if (target == 'dialog' || target == 'widget') {
            if (caller) {
              caller.includeEvent(kvpair.get('key'), evtFunc);
            }
          } else {
            Event.observe(eval(target), kvpair.get('key'), evtFunc);
          }
        });
      });
    }
  }
  LSN.includeScript('var ' + id + '_is_ready=true;');
};
/* ------------------------------------------------------------------------------
 * Dialog - An interactive widget
 * @extends LSN.Widget
 *
 *  // Construct a new dialog (http request is not made until first showing).
 *  // Notice the 'this' pointer is == 'dlg' within the event function.
 *  var dlg = new Dialog('/mydialog.html', {
 *    onOk: function() {
 *      alert(join(';',this.values));
 *      dlg.hide();
 *    }
 *  });
 *
 *  dlg.addEvent('cancel', function() {dlg.hide()}; // default behavior
 *
 *  // Display the dialog (fetches from the server, integrates js and css; and
 *  // appends html to the current document's root element (body).
 *  dlg.show();
 * 
 * By default, the dialog is fetched from the server each time show() is called.
 * To re-use the same dialog on subsesquent show() calls, pass the option:
 *
 *  refetch: false
 *
 * to the constructor.
 * --------------------------------------------------------------------------- */

Livesite.prototype.Dialog = Class.create(Livesite.prototype.Widget, {

  initialize: function ($super, uri, userOpts) {
    this.id = LSN.randomId('dlg_');
    var opts = {
      handleKeypress: {'ok':true, 'cancel':true},
      modal: true
    };
    Object.extend(opts, userOpts);
    $super(uri, opts);
  }

});
Livesite.prototype.PageLayout = Class.create({
  initialize: function (opts) {
    Object.extend(this, opts);
    Event.observe(LSN.win, 'load', this.load.bindAsEventListener(this));
    Event.observe(LSN.win, 'resize', this.resize.bindAsEventListener(this));
    Event.observe(LSN.win, 'unload', this.unload.bindAsEventListener(this));
  },
  load: function (event) {this.resize(event)},
  resize: function (event) {},
  unload: function (event) {}
});
Livesite.prototype.DragConstrained = Class.create(Livesite.prototype.DragHandle, {

  initialize: function ($super, elem, options) {
    var vp = LSN.getViewportPosition();
    this.origPos = {};
    this.newPos = {};
    this.hasStarted = false;
    this.hasEnded = false;
    this.props = {
      threshold: 0,
      target: this.elem,
      constraint: 'none',
      mode: 'absolute',
      min_x: vp['left'],
      min_y: vp['top'],
      max_x: vp['left'] + vp['width'] - LSN.getWidth(this.elem),
      max_y: vp['top'] + vp['height'] - LSN.getHeight(this.elem),
      onStart: function () {},
      onDrag: function () {},
      onEnd: function () {}
    }
    Object.extend(this.props, options);
    $super(elem, {
      'threshold': this.props.threshold,
      'onMouseMove': function (event, dh) {
        var startNow = false;
        if (this.props.constraint != 'vertical') {
          this.newPos.left = LSN.asInt(this.origPos.left + dh.delta_x);
          if (this.newPos.left < this.props.min_x) this.newPos.left = this.props.min_x;
          if (this.newPos.left > this.props.max_x) this.newPos.left = this.props.max_x;
          if (this.newPos.left != this.origPos.left && !this.hasStarted) startNow = true;
          LSN.setStyle(this.props.target, 'left', this.newPos.left + 'px');
        }
        if (this.props.constraint != 'horizontal') {
          this.newPos.top = LSN.asInt(this.origPos.top + dh.delta_y);
          if (this.newPos.top < this.props.min_y) this.newPos.top = this.props.min_y;
          if (this.newPos.top > this.props.max_y) this.newPos.top = this.props.max_y;
          if (this.newPos.top != this.origPos.top && !this.hasStarted) startNow = true;
          LSN.setStyle(this.props.target, 'top', this.newPos.top + 'px');
        }
        if (startNow) {
          this.hasStarted = true;
          this.props.onStart.apply(this, [event, dh]);
        }
        this.props.onDrag.apply(this, [event, dh]);
        event.stop();
      }.bind(this),
      'onMouseDown': function (event, dh) {
        this.hasStarted = false;
        this.hasEnded = false;
        this.newPos = {top: this.origPos.top, left: this.origPos.left};
        if (this.props.mode == 'absolute') {
          this.origPos.top = LSN.getTop(this.props.target);
          this.origPos.left = LSN.getLeft(this.props.target);
        } else {
          this.origPos.top = LSN.asInt(LSN.getStyle(this.props.target, 'top'));
          this.origPos.left = LSN.asInt(LSN.getStyle(this.props.target, 'left'));
        }
        event.stop();
      }.bind(this),
      'onMouseUp': function (event, dh) {
        if (this.hasStarted) {
          this.hasEnded = true;
          event.stop();
          this.props.onEnd.apply(this, [event, dh]);
        }
      }.bind(this)
    });
  }

});
Livesite.prototype.ContentEditor = Class.create({

  initialize: function () {
    this.frame = undefined;
    this.doc = undefined;
    this.win = undefined;
    this.lsn = undefined;
  },

  attach: function (frame) {
    this.frame = LSN.getFrame(frame);
    var doc = LSN.getContentDocument(this.frame);
    //doc.designMode = 'On';
    this.doc = doc; // *after* desginMode is set
    this.win = LSN.getContentWindow(this.frame);
    this.lsn = new Livesite(this.win, this.doc);
    //this.exec('useCSS', false);
    return this;
  },

  insert: function (tag) {
    //var elem = LSN.createElement(tag, {contentEditable:'true'});
    var id = this.lsn.randomId(tag);
    var elem = this.lsn.createElement(tag, {
      id: id,
      contentEditable:'false',
      innerHTML: 'Your text here',
      /*
      onMouseDown: function (event) {
        this.win.LSN.setMoveTarget(event, id);
      }.bindAsEventListener(this),
      */
      onBlur: function (event) {
        event.target.contentEditable = 'false';
        this.lsn.setStyle(event.target, 'cursor', 'default');
        event.target.style.border = '';
      }.bindAsEventListener(this),
      onClick: function (event) {
        if (event.target.id != id) return;
        event.target.contentEditable = 'true';
        this.lsn.setStyle(event.target, 'cursor', 'text');
        event.target.style.border = '1px solid red';
        //event.target.focus();
        //this.exec('selectAll');
      }.bindAsEventListener(this),
      style: {
        cursor: 'default'
      }
    });
    if (!elem) return;
    var range = this.getRange();
    var parent = range.commonAncestorContainer;
    if (parent !== this.doc.body) {
      this.lsn.insertAfter(elem, parent);
    } else {
      //range.insertNode(elem);
      parent.appendChild(elem);
    }
    elem.focus();
  },

  exec: function (cmd, args) {
      var sel = this.getSelection();
    if (false) {
      for (var i = 0; i < sel.rangeCount; i++) {
        var range = sel.getRangeAt(i);
        range.surroundContents(LSN.createElement('b'));
      }
    } else {
      sel.execCommand(cmd, false, args);
    }
    //this.doc.execCommand(cmd, false, args);
  },

  getSelection: function () {
     return this.win.getSelection ? this.win.getSelection() : this.doc.selection;
  },

  setSelection: function () {
  },

  getRange: function() {
    var sel = this.getSelection();
    if (!sel) return undefined;
    if (sel.rangeCount > 0) return sel.getRangeAt(0);
    var range = this.doc.createRange();
    range.setStart(this.doc.body, 0);
    range.setEnd(this.doc.body, 0);
    return range;
  },

  destroy: function () {
  }

});

LSN = new Livesite(window, document);

