/**
 * Provides Confluence-specific overrides of AJS.Dialog defaults
 */
AJS.ConfluenceDialog = function(options) {
    var dialog;
    options = options || {};
    options = jQuery.extend({}, options, {
        keypressListener: function(e) {
            if (e.keyCode === 27) {
                // if dropdown is currently showing, leave the dialog and let the dropdown close itself
                if (!jQuery(".aui-dropdown", dialog.popup.element).is(":visible")) {
                    if (typeof options.onCancel == "function") {
                        options.onCancel();
                    } else {
                        dialog.hide();
                    }
                }
            }
        }
    });
    dialog = new AJS.Dialog(options);
    return dialog;
};
/*  Prototype JavaScript framework, version 1.4.0_pre11
 *  (c) 2005 Sam Stephenson <sam@conio.net>
 *
 *  THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff
 *  against the source tree, available from the Prototype darcs repository.
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *
 *  For details, see the Prototype web site: http://prototype.conio.net/
 *
/*--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.4.0_pre11',

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

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

var Abstract = new Object();

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

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

Function.prototype.bind = function(object) {
  var __method = this;
  return function() {
    return __method.apply(object, arguments);
  }
}

Function.prototype.bindAsEventListener = function(object) {
  var __method = this;
  return function(event) {
    return __method.call(object, event || window.event);
  }
}

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

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

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

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

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

    return returnValue;
  }
}

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

var PeriodicalExecuter = Class.create();
PeriodicalExecuter.prototype = {
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

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

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.callback();
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
}

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

function $() {
  var elements = new Array();

  for (var i = 0; i < arguments.length; i++) {
    var element = arguments[i];
    if (typeof element == 'string')
      element = document.getElementById(element);

    if (arguments.length == 1)
      return element;

    elements.push(element);
  }

  return elements;
}

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

  escapeHTML: function() {
    var div = document.createElement('div');
    var text = document.createTextNode(this);
    div.appendChild(text);
    return div.innerHTML;
  },

  unescapeHTML: function() {
    var div = document.createElement('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
  },

  toQueryParams: function() {
    var pairs = this.match(/^\??(.*)$/)[1].split('&');
    return pairs.inject({}, function(params, pairString) {
      var pair = pairString.split('=');
      params[pair[0]] = pair[1];
      return params;
    });
  },

  inspect: function() {
    return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'";
  }
});

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  toArray: function() {
    return this.collect(Prototype.K);
  },

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

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

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

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray
});

var $A = Array.from = function(iterable) {
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0; i < iterable.length; i++)
      results.push(iterable[i]);
    return results;
  }
}

Object.extend(Array.prototype, Enumerable);

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

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

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

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

  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(value.constructor == Array ?
        value.flatten() : [value]);
    });
  },

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

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

var Hash = {
  _each: function(iterator) {
    for (key in this) {
      var value = this[key];
      if (typeof value == 'function') continue;

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

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

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

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

  toQueryString: function() {
    return this.map(function(pair) {
      return pair.map(encodeURIComponent).join('=');
    }).join('&');
  },

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

function $H(object) {
  var hash = Object.extend({}, object || {});
  Object.extend(hash, Enumerable);
  Object.extend(hash, Hash);
  return hash;
}

var Range = Class.create();
Object.extend(Range.prototype, Enumerable);
Object.extend(Range.prototype, {
  initialize: function(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  },

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

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

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

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

  activeRequestCount: 0
}

Ajax.Responders = {
  responders: [],

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

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

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

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

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate: function() {
    Ajax.activeRequestCount++;
  },

  onComplete: function() {
    Ajax.activeRequestCount--;
  }
});

Ajax.Base = function() {};
Ajax.Base.prototype = {
  setOptions: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      parameters:   ''
    }
    Object.extend(this.options, options || {});
  },

  responseIsSuccess: function() {
    return this.transport.status == undefined
        || this.transport.status == 0
        || (this.transport.status >= 200 && this.transport.status < 300);
  },

  responseIsFailure: function() {
    return !this.responseIsSuccess();
  }
}

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

Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
  initialize: function(url, options) {
    this.transport = Ajax.getTransport();
    this.setOptions(options);
    this.request(url);
  },

  request: function(url) {
    var parameters = this.options.parameters || '';
    if (parameters.length > 0) parameters += '&_=';

    try {
      this.url = url;
      if (this.options.method == 'get')
        this.url += '?' + parameters;

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

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

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

      this.setRequestHeaders();

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

    } catch (e) {
    }
  },

  setRequestHeaders: function() {
    var requestHeaders =
      ['X-Requested-With', 'XMLHttpRequest',
       'X-Prototype-Version', Prototype.Version];

    if (this.options.method == 'post') {
      requestHeaders.push('Content-type',
        'application/x-www-form-urlencoded');

      /* Force "Connection: close" for Mozilla browsers to work around
       * a bug where XMLHttpReqeuest sends an incorrect Content-length
       * header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType)
        requestHeaders.push('Connection', 'close');
    }

    if (this.options.requestHeaders)
      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);

    for (var i = 0; i < requestHeaders.length; i += 2)
      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
  },

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

  evalJSON: function() {
    try {
      var json = this.transport.getResponseHeader('X-JSON'), object;
      object = eval(json);
      return object;
    } catch (e) {
    }
  },

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

    if (event == 'Complete')
      (this.options['on' + this.transport.status]
       || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
       || Prototype.emptyFunction)(transport, json);

    (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
    Ajax.Responders.dispatch('on' + event, this, transport, json);

    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
    if (event == 'Complete')
      this.transport.onreadystatechange = Prototype.emptyFunction;
  }
});

Ajax.Updater = Class.create();
Ajax.Updater.ScriptFragment = '(?:<script.*?>)((\n|.)*?)(?:<\/script>)';

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

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

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

    this.request(url);
  },

  updateContent: function() {
    var receiver = this.responseIsSuccess() ?
      this.containers.success : this.containers.failure;

    var match    = new RegExp(Ajax.Updater.ScriptFragment, 'img');
    var response = this.transport.responseText.replace(match, '');
    var scripts  = this.transport.responseText.match(match);

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

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

    if (this.options.evalScripts && scripts) {
      match = new RegExp(Ajax.Updater.ScriptFragment, 'im');
      setTimeout((function() {
        for (var i = 0; i < scripts.length; i++)
          eval(scripts[i].match(match)[1]);
      }).bind(this), 10);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create();
Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
  initialize: function(container, url, options) {
    this.setOptions(options);
    this.onComplete = this.options.onComplete;

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

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

    this.start();
  },

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

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

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

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

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

// commented out to fix CONF-16613
//document.getElementsByClassName = function(className, parentElement) {
//  var children = (document.body || $(parentElement)).getElementsByTagName('*');
//  return $A(children).inject([], function(elements, child) {
//    if (Element.hasClassName(child, className))
//      elements.push(child);
//    return elements;
//  });
//}

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

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

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

  toggle: function() {
    for (var i = 0; i < arguments.length; i++) {
      var element = $(arguments[i]);
      Element[Element.visible(element) ? 'show' : 'hide'](element);
    }
  },

  hide: function() {
    for (var i = 0; i < arguments.length; i++) {
      var element = $(arguments[i]);
      element.style.display = 'none';
    }
  },

  show: function() {
    for (var i = 0; i < arguments.length; i++) {
      var element = $(arguments[i]);
      element.style.display = '';
    }
  },

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

  getHeight: function(element) {
    element = $(element);
    return element.offsetHeight;
  },

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

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

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

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

  // removes whitespace-only text node children
  cleanWhitespace: function(element) {
    element = $(element);
    for (var i = 0; i < element.childNodes.length; i++) {
      var node = element.childNodes[i];
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        Element.remove(node);
    }
  },

  empty: function(element) {
    return $(element).innerHTML.match(/^\s*$/);
  },

  scrollTo: function(element) {
    element = $(element);
    var x = element.x ? element.x : element.offsetLeft,
        y = element.y ? element.y : element.offsetTop;
    window.scrollTo(x, y);
  }
});

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

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

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

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

    if (this.adjacency && this.element.insertAdjacentHTML) {
      try {
        this.element.insertAdjacentHTML(this.adjacency, this.content);
      } catch (e) {
        if (this.element.tagName.toLowerCase() == 'tbody') {
          this.fragment = this.contentFromAnonymousTable();
          this.insertContent();
        } else {
          throw e;
        }
      }
    } else {
      this.range = this.element.ownerDocument.createRange();
      if (this.initializeRange) this.initializeRange();
      this.fragment = this.range.createContextualFragment(this.content);
      this.insertContent();
    }
  },

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

var Insertion = new Object();

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

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

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

  insertContent: function() {
    this.element.insertBefore(this.fragment, this.element.firstChild);
  }
});

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

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

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

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

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

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

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

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

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

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set(this.select(function(className) {
      return className != classNameToRemove;
    }));
  },

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

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

var Field = {
  clear: function() {
    for (var i = 0; i < arguments.length; i++)
      $(arguments[i]).value = '';
  },

  focus: function(element) {
    $(element).focus();
  },

  present: function() {
    for (var i = 0; i < arguments.length; i++)
      if ($(arguments[i]).value == '') return false;
    return true;
  },

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

  activate: function(element) {
    $(element).focus();
    $(element).select();
  }
}

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

var Form = {
  serialize: function(form) {
    var elements = Form.getElements($(form));
    var queryComponents = new Array();

    for (var i = 0; i < elements.length; i++) {
      var queryComponent = Form.Element.serialize(elements[i]);
      if (queryComponent)
        queryComponents.push(queryComponent);
    }

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

  getElements: function(form) {
    var form = $(form);
    var elements = new Array();

    for (tagName in Form.Element.Serializers) {
      var tagElements = form.getElementsByTagName(tagName);
      for (var j = 0; j < tagElements.length; j++)
        elements.push(tagElements[j]);
    }
    return elements;
  },

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

    if (!typeName && !name)
      return inputs;

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

    return matchingInputs;
  },

  disable: function(form) {
    var elements = Form.getElements(form);
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      element.blur();
      element.disabled = 'true';
    }
  },

  enable: function(form) {
    var elements = Form.getElements(form);
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      element.disabled = '';
    }
  },

  focusFirstElement: function(form) {
    var form = $(form);
    var elements = Form.getElements(form);
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      if (element.type != 'hidden' && !element.disabled) {
        Field.activate(element);
        break;
      }
    }
  },

  reset: function(form) {
    $(form).reset();
  }
}

Form.Element = {
  serialize: function(element) {
    var element = $(element);
    var method = element.tagName.toLowerCase();
    var parameter = Form.Element.Serializers[method](element);

    if (parameter)
      return encodeURIComponent(parameter[0]) + '=' +
        encodeURIComponent(parameter[1]);
  },

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

    if (parameter)
      return parameter[1];
  }
}

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

  inputSelector: function(element) {
    if (element.checked)
      return [element.name, element.value];
  },

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

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

  selectOne: function(element) {
    var value = '', opt, index = element.selectedIndex;
    if (index >= 0) {
      opt = element.options[index];
      value = opt.value;
      if (!value && !('value' in opt))
        value = opt.text;
    }
    return [element.name, value];
  },

  selectMany: function(element) {
    var value = new Array();
    for (var i = 0; i < element.length; i++) {
      var opt = element.options[i];
      if (opt.selected) {
        var optValue = opt.value;
        if (!optValue && !('value' in opt))
          optValue = opt.text;
        value.push(optValue);
      }
    }
    return [element.name, value];
  }
}

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

var $F = Form.Element.getValue;

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

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

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

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

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

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

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

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

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

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

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

  registerFormCallbacks: function() {
    var elements = Form.getElements(this.element);
    for (var i = 0; i < elements.length; i++)
      this.registerCallback(elements[i]);
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          element.target = this;
          element.prev_onclick = element.onclick || Prototype.emptyFunction;
          element.onclick = function() {
            this.prev_onclick();
            this.target.onElementEvent();
          }
          break;
        case 'password':
        case 'text':
        case 'textarea':
        case 'select-one':
        case 'select-multiple':
          element.target = this;
          element.prev_onchange = element.onchange || Prototype.emptyFunction;
          element.onchange = function() {
            this.prev_onchange();
            this.target.onElementEvent();
          }
          break;
      }
    }
  }
}

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

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


if (!window.Event) {
  var Event = new Object();
}

Object.extend(Event, {
  KEY_BACKSPACE: 8,
  KEY_TAB:       9,
  KEY_RETURN:   13,
  KEY_ESC:      27,
  KEY_LEFT:     37,
  KEY_UP:       38,
  KEY_RIGHT:    39,
  KEY_DOWN:     40,
  KEY_DELETE:   46,

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

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

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

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

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

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

  observers: false,

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

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

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

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

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

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

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

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

/* prevent memory leaks in IE */
Event.observe(window, 'unload', Event.unloadCache, false);

var Position = {

  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,

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

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

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

  // caches x/y coordinate pair to use with overlap
  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = this.cumulativeOffset(element);

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

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = this.realOffset(element);

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

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

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

  clone: function(source, target) {
    source = $(source);
    target = $(target);
    target.style.position = 'absolute';
    var offsets = this.cumulativeOffset(source);
    target.style.top    = offsets[1] + 'px';
    target.style.left   = offsets[0] + 'px';
    target.style.width  = source.offsetWidth + 'px';
    target.style.height = source.offsetHeight + 'px';
  }
}

// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
//  Justin Palmer (http://encytemedia.com/)
//  Mark Pilgrim (http://diveintomark.org/)
//  Martin Bialasinki
// 
// See scriptaculous.js for full license.

var Effect = {
  tagifyText: function(element) {
    var tagifyStyle = "position:relative";
    if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1";
    element = $(element);
    $A(element.childNodes).each( function(child) {
      if(child.nodeType==3) {
        child.nodeValue.toArray().each( function(character) {
          element.insertBefore(
            Builder.node('span',{style: tagifyStyle},
              character == " " ? String.fromCharCode(160) : character), 
              child);
        });
        Element.remove(child);
      }
    });
  },
  multiple: function(element, effect) {
    var elements;
    if(((typeof element == 'object') || 
        (typeof element == 'function')) && 
       (element.length))
      elements = element;
    else
      elements = $(element).childNodes;
      
    var options = Object.extend({
      speed: 0.1,
      delay: 0.0
    }, arguments[2] || {});
    var speed = options.speed;
    var delay = options.delay;

    $A(elements).each( function(element, index) {
      new effect(element, Object.extend(options, { delay: delay + index * speed }));
    });
  }
};

var Effect2 = Effect; // deprecated

/* ------------- transitions ------------- */

Effect.Transitions = {}

Effect.Transitions.linear = function(pos) {
  return pos;
}
Effect.Transitions.sinoidal = function(pos) {
  return (-Math.cos(pos*Math.PI)/2) + 0.5;
}
Effect.Transitions.reverse  = function(pos) {
  return 1-pos;
}
Effect.Transitions.flicker = function(pos) {
  return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
}
Effect.Transitions.wobble = function(pos) {
  return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
}
Effect.Transitions.pulse = function(pos) {
  return (Math.floor(pos*10) % 2 == 0 ? 
    (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
}
Effect.Transitions.none = function(pos) {
  return 0;
}
Effect.Transitions.full = function(pos) {
  return 1;
}

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

Effect.Queue = {
  effects:  [],
  interval: null,
  add: function(effect) {
    var timestamp = new Date().getTime();
    
    switch(effect.options.queue) {
      case 'front':
        // move unstarted effects after this effect  
        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
            e.startOn  += effect.finishOn;
            e.finishOn += effect.finishOn;
          });
        break;
      case 'end':
        // start effect after last queued effect has finished
        timestamp = this.effects.pluck('finishOn').max() || timestamp;
        break;
    }
    
    effect.startOn  += timestamp;
    effect.finishOn += timestamp;
    this.effects.push(effect);
    if(!this.interval) 
      this.interval = setInterval(this.loop.bind(this), 40);
  },
  remove: function(effect) {
    this.effects = this.effects.reject(function(e) { return e==effect });
    if(this.effects.length == 0) {
      clearInterval(this.interval);
      this.interval = null;
    }
  },
  loop: function() {
    var timePos = new Date().getTime();
    this.effects.invoke('loop', timePos);
  }
}

Effect.Base = function() {};
Effect.Base.prototype = {
  position: null,
  setOptions: function(options) {
    this.options = Object.extend({
      transition: Effect.Transitions.sinoidal,
      duration:   1.0,   // seconds
      fps:        25.0,  // max. 25fps due to Effect.Queue implementation
      sync:       false, // true for combining
      from:       0.0,
      to:         1.0,
      delay:      0.0,
      queue:      'parallel'
    }, options || {});
  },
  start: function(options) {
    this.setOptions(options || {});
    this.currentFrame = 0;
    this.state        = 'idle';
    this.startOn      = this.options.delay*1000;
    this.finishOn     = this.startOn + (this.options.duration*1000);
    this.event('beforeStart');
    if(!this.options.sync) Effect.Queue.add(this);
  },
  loop: function(timePos) {
    if(timePos >= this.startOn) {
      if(timePos >= this.finishOn) {
        this.render(1.0);
        this.cancel();
        this.event('beforeFinish');
        if(this.finish) this.finish(); 
        this.event('afterFinish');
        return;  
      }
      var pos   = (timePos - this.startOn) / (this.finishOn - this.startOn);
      var frame = Math.round(pos * this.options.fps * this.options.duration);
      if(frame > this.currentFrame) {
        this.render(pos);
        this.currentFrame = frame;
      }
    }
  },
  render: function(pos) {
    if(this.state == 'idle') {
      this.state = 'running';
      this.event('beforeSetup');
      if(this.setup) this.setup();
      this.event('afterSetup');
    }
    if(this.options.transition) pos = this.options.transition(pos);
    pos *= (this.options.to-this.options.from);
    pos += this.options.from;
    this.position = pos;
    this.event('beforeUpdate');
    if(this.update) this.update(pos);
    this.event('afterUpdate');
  },
  cancel: function() {
    if(!this.options.sync) Effect.Queue.remove(this);
    this.state = 'finished';
  },
  event: function(eventName) {
    if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
    if(this.options[eventName]) this.options[eventName](this);
  }
}

Effect.Parallel = Class.create();
Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
  initialize: function(effects) {
    this.effects = effects || [];
    this.start(arguments[1]);
  },
  update: function(position) {
    this.effects.invoke('render', position);
  },
  finish: function(position) {
    this.effects.each( function(effect) {
      effect.render(1.0);
      effect.cancel();
      effect.event('beforeFinish');
      if(effect.finish) effect.finish(position);
      effect.event('afterFinish');
    });
  }
});

Effect.Opacity = Class.create();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = $(element);
    // make this work on IE on elements without 'layout'
    if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
      this.element.style.zoom = 1;
    var options = Object.extend({
      from: Element.getOpacity(this.element) || 0.0,
      to:   1.0
    }, arguments[1] || {});
    this.start(options);
  },
  update: function(position) {
    Element.setOpacity(this.element, position);
  }
});

Effect.MoveBy = Class.create();
Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), {
  initialize: function(element, toTop, toLeft) {
    this.element      = $(element);
    this.toTop        = toTop;
    this.toLeft       = toLeft;
    this.start(arguments[3]);
  },
  setup: function() {
    // Bug in Opera: Opera returns the "real" position of a static element or
    // relative element that does not have top/left explicitly set.
    // ==> Always set top and left for position relative elements in your stylesheets 
    // (to 0 if you do not need them)
    
    Element.makePositioned(this.element);
    this.originalTop  = parseFloat(Element.getStyle(this.element,'top')  || '0');
    this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
  },
  update: function(position) {
    var topd  = this.toTop  * position + this.originalTop;
    var leftd = this.toLeft * position + this.originalLeft;
    this.setPosition(topd, leftd);
  },
  setPosition: function(topd, leftd) {
    this.element.style.top  = topd  + "px";
    this.element.style.left = leftd + "px";
  }
});

Effect.Scale = Class.create();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
  initialize: function(element, percent) {
    this.element = $(element)
    var options = Object.extend({
      scaleX: true,
      scaleY: true,
      scaleContent: true,
      scaleFromCenter: false,
      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
      scaleFrom: 100.0,
      scaleTo:   percent
    }, arguments[2] || {});
    this.start(options);
  },
  setup: function() {
    var effect = this;
    
    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
    this.elementPositioning = Element.getStyle(this.element,'position');
    
    effect.originalStyle = {};
    ['top','left','width','height','fontSize'].each( function(k) {
      effect.originalStyle[k] = effect.element.style[k];
    });
      
    this.originalTop  = this.element.offsetTop;
    this.originalLeft = this.element.offsetLeft;
    
    var fontSize = Element.getStyle(this.element,'font-size') || "100%";
    ['em','px','%'].each( function(fontSizeType) {
      if(fontSize.indexOf(fontSizeType)>0) {
        effect.fontSize     = parseFloat(fontSize);
        effect.fontSizeType = fontSizeType;
      }
    });
    
    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
    
    this.dims = null;
    if(this.options.scaleMode=='box')
      this.dims = [this.element.clientHeight, this.element.clientWidth];
    if(this.options.scaleMode=='content')
      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
    if(!this.dims)
      this.dims = [this.options.scaleMode.originalHeight,
                   this.options.scaleMode.originalWidth];
  },
  update: function(position) {
    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
    if(this.options.scaleContent && this.fontSize)
      this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType;
    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
  },
  finish: function(position) {
    if (this.restoreAfterFinish) {
      var effect = this;
      ['top','left','width','height','fontSize'].each( function(k) {
        effect.element.style[k] = effect.originalStyle[k];
      });
    }
  },
  setDimensions: function(height, width) {
    var els = this.element.style;
    if(this.options.scaleX) els.width = width + 'px';
    if(this.options.scaleY) els.height = height + 'px';
    if(this.options.scaleFromCenter) {
      var topd  = (height - this.dims[0])/2;
      var leftd = (width  - this.dims[1])/2;
      if(this.elementPositioning == 'absolute') {
        if(this.options.scaleY) els.top = this.originalTop-topd + "px";
        if(this.options.scaleX) els.left = this.originalLeft-leftd + "px";
      } else {
        if(this.options.scaleY) els.top = -topd + "px";
        if(this.options.scaleX) els.left = -leftd + "px";
      }
    }
  }
});

Effect.Highlight = Class.create();
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = $(element);
    var options = Object.extend({
      startcolor:   "#ffff99"
    }, arguments[1] || {});
    this.start(options);
  },
  setup: function() {
    // Disable background image during the effect
    this.oldBgImage = this.element.style.backgroundImage;
    this.element.style.backgroundImage = "none";
    if(!this.options.endcolor)
      this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff');
    if (typeof this.options.restorecolor == "undefined")
      this.options.restorecolor = this.element.style.backgroundColor;
    // init color calculations
    this.colors_base = [
      parseInt(this.options.startcolor.slice(1,3),16),
      parseInt(this.options.startcolor.slice(3,5),16),
      parseInt(this.options.startcolor.slice(5),16) ];
    this.colors_delta = [
      parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0],
      parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1],
      parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]];
  },
  update: function(position) {
    var effect = this; var colors = $R(0,2).map( function(i){ 
      return Math.round(effect.colors_base[i]+(effect.colors_delta[i]*position))
    });
    this.element.style.backgroundColor = "#" +
      colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart();
  },
  finish: function() {
    this.element.style.backgroundColor = this.options.restorecolor;
    this.element.style.backgroundImage = this.oldBgImage;
  }
});

Effect.ScrollTo = Class.create();
Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = $(element);
    this.start(arguments[1] || {});
  },
  setup: function() {
    Position.prepare();
    var offsets = Position.cumulativeOffset(this.element);
    var max = window.innerHeight ? 
      window.height - window.innerHeight :
      document.body.scrollHeight - 
        (document.documentElement.clientHeight ? 
          document.documentElement.clientHeight : document.body.clientHeight);
    this.scrollStart = Position.deltaY;
    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
  },
  update: function(position) {
    Position.prepare();
    window.scrollTo(Position.deltaX, 
      this.scrollStart + (position*this.delta));
  }
});

/* ------------- combination effects ------------- */

Effect.Fade = function(element) {
  var oldOpacity = Element.getInlineOpacity(element);
  var options = Object.extend({
  from: Element.getOpacity(element) || 1.0,
  to:   0.0,
  afterFinishInternal: function(effect) 
    { if (effect.options.to == 0) {
        Element.hide(effect.element);
        Element.setInlineOpacity(effect.element, oldOpacity);
      }  
    }
  }, arguments[1] || {});
  return new Effect.Opacity(element,options);
}

Effect.Appear = function(element) {
  var options = Object.extend({
  from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0),
  to:   1.0,
  beforeSetup: function(effect)  
    { Element.setOpacity(effect.element, effect.options.from);
      Element.show(effect.element); }
  }, arguments[1] || {});
  return new Effect.Opacity(element,options);
}

Effect.Puff = function(element) {
  element = $(element);
  var oldOpacity = Element.getInlineOpacity(element);
  var oldPosition = element.style.position;
  return new Effect.Parallel(
   [ new Effect.Scale(element, 200, 
      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
     Object.extend({ duration: 1.0, 
      beforeSetupInternal: function(effect) 
       { effect.effects[0].element.style.position = 'absolute'; },
      afterFinishInternal: function(effect)
       { Element.hide(effect.effects[0].element);
         effect.effects[0].element.style.position = oldPosition;
         Element.setInlineOpacity(effect.effects[0].element, oldOpacity); }
     }, arguments[1] || {})
   );
}

Effect.BlindUp = function(element) {
  element = $(element);
  Element.makeClipping(element);
  return new Effect.Scale(element, 0, 
    Object.extend({ scaleContent: false, 
      scaleX: false, 
      restoreAfterFinish: true,
      afterFinishInternal: function(effect)
        { 
          Element.hide(effect.element);
          Element.undoClipping(effect.element);
        } 
    }, arguments[1] || {})
  );
}

Effect.BlindDown = function(element) {
  element = $(element);
  var oldHeight = element.style.height;
  var elementDimensions = Element.getDimensions(element);
  return new Effect.Scale(element, 100, 
    Object.extend({ scaleContent: false, 
      scaleX: false,
      scaleFrom: 0,
      scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
      restoreAfterFinish: true,
      afterSetup: function(effect) {
        Element.makeClipping(effect.element);
        effect.element.style.height = "0px";
        Element.show(effect.element); 
      },  
      afterFinishInternal: function(effect) {
        Element.undoClipping(effect.element);
        effect.element.style.height = oldHeight;
      }
    }, arguments[1] || {})
  );
}

Effect.SwitchOff = function(element) {
  element = $(element);
  var oldOpacity = Element.getInlineOpacity(element);
  return new Effect.Appear(element, { 
    duration: 0.4,
    from: 0,
    transition: Effect.Transitions.flicker,
    afterFinishInternal: function(effect) {
      new Effect.Scale(effect.element, 1, { 
        duration: 0.3, scaleFromCenter: true,
        scaleX: false, scaleContent: false, restoreAfterFinish: true,
        beforeSetup: function(effect) { 
          Element.makePositioned(effect.element); 
          Element.makeClipping(effect.element);
        },
        afterFinishInternal: function(effect) { 
          Element.hide(effect.element); 
          Element.undoClipping(effect.element);
          Element.undoPositioned(effect.element);
          Element.setInlineOpacity(effect.element, oldOpacity);
        }
      })
    }
  });
}

Effect.DropOut = function(element) {
  element = $(element);
  var oldTop = element.style.top;
  var oldLeft = element.style.left;
  var oldOpacity = Element.getInlineOpacity(element);
  return new Effect.Parallel(
    [ new Effect.MoveBy(element, 100, 0, { sync: true }), 
      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
    Object.extend(
      { duration: 0.5,
        beforeSetup: function(effect) { 
          Element.makePositioned(effect.effects[0].element); },
        afterFinishInternal: function(effect) { 
          Element.hide(effect.effects[0].element); 
          Element.undoPositioned(effect.effects[0].element);
          effect.effects[0].element.style.left = oldLeft;
          effect.effects[0].element.style.top = oldTop;
          Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } 
      }, arguments[1] || {}));
}

Effect.Shake = function(element) {
  element = $(element);
  var oldTop = element.style.top;
  var oldLeft = element.style.left;
  return new Effect.MoveBy(element, 0, 20, 
    { duration: 0.05, afterFinishInternal: function(effect) {
  new Effect.MoveBy(effect.element, 0, -40, 
    { duration: 0.1, afterFinishInternal: function(effect) {
  new Effect.MoveBy(effect.element, 0, 40, 
    { duration: 0.1, afterFinishInternal: function(effect) {
  new Effect.MoveBy(effect.element, 0, -40, 
    { duration: 0.1, afterFinishInternal: function(effect) {
  new Effect.MoveBy(effect.element, 0, 40, 
    { duration: 0.1, afterFinishInternal: function(effect) {
  new Effect.MoveBy(effect.element, 0, -20, 
    { duration: 0.05, afterFinishInternal: function(effect) {
        Element.undoPositioned(effect.element);
        effect.element.style.left = oldLeft;
        effect.element.style.top = oldTop;
  }}) }}) }}) }}) }}) }});
}

Effect.SlideDown = function(element) {
  element = $(element);
  Element.cleanWhitespace(element);
  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
  var oldInnerBottom = element.firstChild.style.bottom;
  var elementDimensions = Element.getDimensions(element);
  return new Effect.Scale(element, 100, 
   Object.extend({ scaleContent: false, 
    scaleX: false, 
    scaleFrom: 0,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},    
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      Element.makePositioned(effect.element.firstChild);
      if (window.opera) effect.element.firstChild.style.top = "";
      Element.makeClipping(effect.element);
      element.style.height = '0';
      Element.show(element); 
    },  
    afterUpdateInternal: function(effect) { 
      effect.element.firstChild.style.bottom = 
        (effect.originalHeight - effect.element.clientHeight) + 'px'; },
    afterFinishInternal: function(effect) { 
      Element.undoClipping(effect.element); 
      Element.undoPositioned(effect.element.firstChild);
      effect.element.firstChild.style.bottom = oldInnerBottom; }
    }, arguments[1] || {})
  );
}
  
Effect.SlideUp = function(element) {
  element = $(element);
  Element.cleanWhitespace(element);
  var oldInnerBottom = element.firstChild.style.bottom;
  return new Effect.Scale(element, 0, 
   Object.extend({ scaleContent: false, 
    scaleX: false, 
    scaleMode: 'box',
    scaleFrom: 100,
    restoreAfterFinish: true,
    beforeStartInternal: function(effect) { 
      Element.makePositioned(effect.element.firstChild);
      if (window.opera) effect.element.firstChild.style.top = "";
      Element.makeClipping(effect.element);
      Element.show(element); 
    },  
    afterUpdateInternal: function(effect) { 
     effect.element.firstChild.style.bottom = 
       (effect.originalHeight - effect.element.clientHeight) + 'px'; },
    afterFinishInternal: function(effect) { 
        Element.hide(effect.element);
        Element.undoClipping(effect.element); 
        Element.undoPositioned(effect.element.firstChild);
        effect.element.firstChild.style.bottom = oldInnerBottom; }
   }, arguments[1] || {})
  );
}

Effect.Squish = function(element) {
  // Bug in opera makes the TD containing this element expand for a instance after finish 
  return new Effect.Scale(element, window.opera ? 1 : 0, 
    { restoreAfterFinish: true,
      beforeSetup: function(effect) { 
        Element.makeClipping(effect.element); },  
      afterFinishInternal: function(effect) { 
        Element.hide(effect.element); 
        Element.undoClipping(effect.element); } 
  });
}

Effect.Grow = function(element) {
  element = $(element);
  var options = arguments[1] || {};
  
  var elementDimensions = Element.getDimensions(element);
  var originalWidth = elementDimensions.width;
  var originalHeight = elementDimensions.height;
  var oldTop = element.style.top;
  var oldLeft = element.style.left;
  var oldHeight = element.style.height;
  var oldWidth = element.style.width;
  var oldOpacity = Element.getInlineOpacity(element);
  
  var direction = options.direction || 'center';
  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
  var opacityTransition = options.opacityTransition || Effect.Transitions.full;
  
  var initialMoveX, initialMoveY;
  var moveX, moveY;
  
  switch (direction) {
    case 'top-left':
      initialMoveX = initialMoveY = moveX = moveY = 0; 
      break;
    case 'top-right':
      initialMoveX = originalWidth;
      initialMoveY = moveY = 0;
      moveX = -originalWidth;
      break;
    case 'bottom-left':
      initialMoveX = moveX = 0;
      initialMoveY = originalHeight;
      moveY = -originalHeight;
      break;
    case 'bottom-right':
      initialMoveX = originalWidth;
      initialMoveY = originalHeight;
      moveX = -originalWidth;
      moveY = -originalHeight;
      break;
    case 'center':
      initialMoveX = originalWidth / 2;
      initialMoveY = originalHeight / 2;
      moveX = -originalWidth / 2;
      moveY = -originalHeight / 2;
      break;
  }
  
  return new Effect.MoveBy(element, initialMoveY, initialMoveX, { 
    duration: 0.01, 
    beforeSetup: function(effect) { 
      Element.hide(effect.element);
      Element.makeClipping(effect.element);
      Element.makePositioned(effect.element);
    },
    afterFinishInternal: function(effect) {
      new Effect.Parallel(
        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }),
          new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }),
          new Effect.Scale(effect.element, 100, {
            scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, 
            sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true})
        ], Object.extend({
             beforeSetup: function(effect) {
              effect.effects[0].element.style.height = 0;
              Element.show(effect.effects[0].element);
             },              
             afterFinishInternal: function(effect) {
               var el = effect.effects[0].element;
               var els = el.style;
               Element.undoClipping(el); 
               Element.undoPositioned(el);
               els.top = oldTop;
               els.left = oldLeft;
               els.height = oldHeight;
               els.width = originalWidth;
               Element.setInlineOpacity(el, oldOpacity);
             }
           }, options)
      )
    }
  });
}

Effect.Shrink = function(element) {
  element = $(element);
  var options = arguments[1] || {};
  
  var originalWidth = element.clientWidth;
  var originalHeight = element.clientHeight;
  var oldTop = element.style.top;
  var oldLeft = element.style.left;
  var oldHeight = element.style.height;
  var oldWidth = element.style.width;
  var oldOpacity = Element.getInlineOpacity(element);

  var direction = options.direction || 'center';
  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
  var opacityTransition = options.opacityTransition || Effect.Transitions.none;
  
  var moveX, moveY;
  
  switch (direction) {
    case 'top-left':
      moveX = moveY = 0;
      break;
    case 'top-right':
      moveX = originalWidth;
      moveY = 0;
      break;
    case 'bottom-left':
      moveX = 0;
      moveY = originalHeight;
      break;
    case 'bottom-right':
      moveX = originalWidth;
      moveY = originalHeight;
      break;
    case 'center':  
      moveX = originalWidth / 2;
      moveY = originalHeight / 2;
      break;
  }
  
  return new Effect.Parallel(
    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }),
      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}),
      new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition })
    ], Object.extend({            
         beforeStartInternal: function(effect) { 
           Element.makePositioned(effect.effects[0].element);
           Element.makeClipping(effect.effects[0].element);
         },
         afterFinishInternal: function(effect) {
           var el = effect.effects[0].element;
           var els = el.style;
           Element.hide(el);
           Element.undoClipping(el); 
           Element.undoPositioned(el);
           els.top = oldTop;
           els.left = oldLeft;
           els.height = oldHeight;
           els.width = oldWidth;
           Element.setInlineOpacity(el, oldOpacity);
         }
       }, options)
  );
}

Effect.Pulsate = function(element) {
  element = $(element);
  var options    = arguments[1] || {};
  var oldOpacity = Element.getInlineOpacity(element);
  var transition = options.transition || Effect.Transitions.sinoidal;
  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
  reverser.bind(transition);
  return new Effect.Opacity(element, 
    Object.extend(Object.extend({  duration: 3.0, from: 0,
      afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); }
    }, options), {transition: reverser}));
}

Effect.Fold = function(element) {
  element = $(element);
  var originalTop = element.style.top;
  var originalLeft = element.style.left;
  var originalWidth = element.style.width;
  var originalHeight = element.style.height;
  Element.makeClipping(element);
  return new Effect.Scale(element, 5, Object.extend({   
    scaleContent: false,
    scaleX: false,
    afterFinishInternal: function(effect) {
    new Effect.Scale(element, 1, { 
      scaleContent: false, 
      scaleY: false,
      afterFinishInternal: function(effect) { 
        Element.hide(effect.element);  
        Element.undoClipping(effect.element); 
        effect.element.style.top = originalTop;
        effect.element.style.left = originalLeft;
        effect.element.style.width = originalWidth;
        effect.element.style.height = originalHeight;
      } });
  }}, arguments[1] || {}));
}

// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// 
// Element.Class part Copyright (c) 2005 by Rick Olson
// 
// See scriptaculous.js for full license.

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

var Droppables = {
  drops: [],

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

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

    // cache containers
    if(options.containment) {
      options._containers = [];
      var containment = options.containment;
      if((typeof containment == 'object') && 
        (containment.constructor == Array)) {
        containment.each( function(c) { options._containers.push($(c)) });
      } else {
        options._containers.push($(containment));
      }
    }

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

    this.drops.push(options);
  },

  isContained: function(element, drop) {
    var parentNode = element.parentNode;
    return drop._containers.detect(function(c) { return parentNode == c });
  },

  isAffected: function(pX, pY, element, drop) {
    return (
      (drop.element!=element) &&
      ((!drop._containers) ||
        this.isContained(element, drop)) &&
      ((!drop.accept) ||
        (Element.Class.has_any(element, drop.accept))) &&
      Position.within(drop.element, pX, pY) );
  },

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

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

  show: function(event, element) {
    if(!this.drops.length) return;
    var pX = Event.pointerX(event);
    var pY = Event.pointerY(event);
    Position.prepare();

    var i = this.drops.length-1; do {
      var drop = this.drops[i];
      if(this.isAffected(pX, pY, element, drop)) {
        if(drop.onHover)
           drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
        if(drop.greedy) { 
          this.activate(drop);
          return;
        }
      }
    } while (i--);
    
    if(this.last_active) this.deactivate(this.last_active);
  },

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

    if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
      if (this.last_active.onDrop) 
        this.last_active.onDrop(element, this.last_active.element, event);
  },

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

var Draggables = {
  observers: [],
  addObserver: function(observer) {
    this.observers.push(observer);    
  },
  removeObserver: function(element) {  // element instead of obsever fixes mem leaks
    this.observers = this.observers.reject( function(o) { return o.element==element });
  },
  notify: function(eventName, draggable) {  // 'onStart', 'onEnd'
    this.observers.invoke(eventName, draggable);
  }
}

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

var Draggable = Class.create();
Draggable.prototype = {
  initialize: function(element) {
    var options = Object.extend({
      handle: false,
      starteffect: function(element) { 
        new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); 
      },
      reverteffect: function(element, top_offset, left_offset) {
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
        new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
      },
      endeffect: function(element) { 
         new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
      },
      zindex: 1000,
      revert: false
    }, arguments[1] || {});

    this.element      = $(element);
    if(options.handle && (typeof options.handle == 'string'))
      this.handle = Element.Class.childrenWith(this.element, options.handle)[0];
      
    if(!this.handle) this.handle = $(options.handle);
    if(!this.handle) this.handle = this.element;

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

    this.offsetX      = 0;
    this.offsetY      = 0;
    this.originalLeft = this.currentLeft();
    this.originalTop  = this.currentTop();
    this.originalX    = this.element.offsetLeft;
    this.originalY    = this.element.offsetTop;

    this.options      = options;

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

    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
    this.eventMouseMove = this.update.bindAsEventListener(this);
    this.eventKeypress  = this.keyPress.bindAsEventListener(this);
    
    this.registerEvents();
  },
  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    this.unregisterEvents();
  },
  registerEvents: function() {
    Event.observe(document, "mouseup", this.eventMouseUp);
    Event.observe(document, "mousemove", this.eventMouseMove);
    Event.observe(document, "keypress", this.eventKeypress);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);
  },
  unregisterEvents: function() {
    //if(!this.active) return;
    //Event.stopObserving(document, "mouseup", this.eventMouseUp);
    //Event.stopObserving(document, "mousemove", this.eventMouseMove);
    //Event.stopObserving(document, "keypress", this.eventKeypress);
  },
  currentLeft: function() {
    return parseInt(this.element.style.left || '0');
  },
  currentTop: function() {
    return parseInt(this.element.style.top || '0')
  },
  startDrag: function(event) {
    if(Event.isLeftClick(event)) {
      
      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if(src.tagName && (
        src.tagName=='INPUT' ||
        src.tagName=='SELECT' ||
        src.tagName=='BUTTON' ||
        src.tagName=='TEXTAREA')) return;
      
      // this.registerEvents();
      this.active = true;
      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      var offsets = Position.cumulativeOffset(this.element);
      this.offsetX =  (pointer[0] - offsets[0]);
      this.offsetY =  (pointer[1] - offsets[1]);
      Event.stop(event);
    }
  },
  finishDrag: function(event, success) {
    // this.unregisterEvents();

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

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

    if(success) Droppables.fire(event, this.element);
    Draggables.notify('onEnd', this);

    var revert = this.options.revert;
    if(revert && typeof revert == 'function') revert = revert(this.element);

    if(revert && this.options.reverteffect) {
      this.options.reverteffect(this.element, 
      this.currentTop()-this.originalTop,
      this.currentLeft()-this.originalLeft);
    } else {
      this.originalLeft = this.currentLeft();
      this.originalTop  = this.currentTop();
    }

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

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


    Droppables.reset();
  },
  keyPress: function(event) {
    if(this.active) {
      if(event.keyCode==Event.KEY_ESC) {
        this.finishDrag(event, false);
        Event.stop(event);
      }
    }
  },
  endDrag: function(event) {
    if(this.active && this.dragging) {
      this.finishDrag(event, true);
      Event.stop(event);
    }
    this.active = false;
    this.dragging = false;
  },
  draw: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var offsets = Position.cumulativeOffset(this.element);
    offsets[0] -= this.currentLeft();
    offsets[1] -= this.currentTop();
    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
      style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
      style.top  = (pointer[1] - offsets[1] - this.offsetY) + "px";
    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },
  update: function(event) {
   if(this.active) {
      if(!this.dragging) {
        var style = this.element.style;
        this.dragging = true;
        
        if(Element.getStyle(this.element,'position')=='') 
          style.position = "relative";
        
        if(this.options.zindex) {
          this.options.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
          style.zIndex = this.options.zindex;
        }

        if(this.options.ghosting) {
          this._clone = this.element.cloneNode(true);
          Position.absolutize(this.element);
          this.element.parentNode.insertBefore(this._clone, this.element);
        }

        Draggables.notify('onStart', this);
        if(this.options.starteffect) this.options.starteffect(this.element);
      }

      Droppables.show(event, this.element);
      this.draw(event);
      if(this.options.change) this.options.change(this);

      // fix AppleWebKit rendering
      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 

      Event.stop(event);
   }
  }
}

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

var SortableObserver = Class.create();
SortableObserver.prototype = {
  initialize: function(element, observer) {
    this.element   = $(element);
    this.observer  = observer;
    this.lastValue = Sortable.serialize(this.element);
  },
  onStart: function() {
    this.lastValue = Sortable.serialize(this.element);
  },
  onEnd: function() {
    Sortable.unmark();
    if(this.lastValue != Sortable.serialize(this.element))
      this.observer(this.element)
  }
}

var Sortable = {
  sortables: new Array(),
  options: function(element){
    element = $(element);
    return this.sortables.detect(function(s) { return s.element == element });
  },
  destroy: function(element){
    element = $(element);
    this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
      Draggables.removeObserver(s.element);
      s.droppables.each(function(d){ Droppables.remove(d) });
      s.draggables.invoke('destroy');
    });
    this.sortables = this.sortables.reject(function(s) { return s.element == element });
  },
  create: function(element) {
    element = $(element);
    var options = Object.extend({ 
      element:     element,
      tag:         'li',       // assumes li children, override with tag: 'tagname'
      dropOnEmpty: false,
      tree:        false,      // fixme: unimplemented
      overlap:     'vertical', // one of 'vertical', 'horizontal'
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
      containment: element,    // also takes array of elements (or id's); or false
      handle:      false,      // or a CSS class
      only:        false,
      hoverclass:  null,
      ghosting:    false,
      format:      null,
      onChange:    function() {},
      onUpdate:    function() {}
    }, arguments[1] || {});

    // clear any old sortable with same element
    this.destroy(element);

    // build options for the draggables
    var options_for_draggable = {
      revert:      true,
      ghosting:    options.ghosting,
      constraint:  options.constraint,
      handle:      options.handle };

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

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

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

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

    // build options for the droppables  
    var options_for_droppable = {
      overlap:     options.overlap,
      containment: options.containment,
      hoverclass:  options.hoverclass,
      onHover:     Sortable.onHover,
      greedy:      !options.dropOnEmpty
    }

    // fix for gecko engine
    Element.cleanWhitespace(element); 

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

    // make it so

    // drop on empty handling
    if(options.dropOnEmpty) {
      Droppables.add(element,
        {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
      options.droppables.push(element);
    }

    (this.findElements(element, options) || []).each( function(e) {
      // handles are per-draggable
      var handle = options.handle ? 
        Element.Class.childrenWith(e, options.handle)[0] : e;    
      options.draggables.push(
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
      Droppables.add(e, options_for_droppable);
      options.droppables.push(e);      
    });

    // keep reference
    this.sortables.push(options);

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

  },

  // return all suitable-for-sortable elements in a guaranteed order
  findElements: function(element, options) {
    if(!element.hasChildNodes()) return null;
    var elements = [];
    $A(element.childNodes).each( function(e) {
      if(e.tagName && e.tagName==options.tag.toUpperCase() &&
        (!options.only || (Element.Class.has(e, options.only))))
          elements.push(e);
      if(options.tree) {
        var grandchildren = this.findElements(e, options);
        if(grandchildren) elements.push(grandchildren);
      }
    });

    return (elements.length>0 ? elements.flatten() : null);
  },

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

  onEmptyHover: function(element, dropon) {
    if(element.parentNode!=dropon) {
      dropon.appendChild(element);
    }
  },

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

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

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

  serialize: function(element) {
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag:  sortableOptions.tag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format || /^[^_]*_(.*)$/
    }, arguments[1] || {});
    return $(this.findElements(element, options) || []).collect( function(item) {
      return (encodeURIComponent(options.name) + "[]=" + 
              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
    }).join("&");
  }
} 
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Contributors:
//  Richard Livsey
//  Rahul Bhargava
//  Rob Wills
// 
// See scriptaculous.js for full license.

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

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

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

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

    if (typeof(this.options.tokens) == 'string') 
      this.options.tokens = new Array(this.options.tokens);

    this.observer = null;
    
    this.element.setAttribute('autocomplete','off');

    Element.hide(this.update);

    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
  },

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

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

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

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

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

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

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

  onHover: function(event) {
    var element = Event.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex) 
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    Event.stop(event);
  },
  
  onClick: function(event) {
    var element = Event.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
    this.selectEntry();
    this.hide();
  },
  
  onBlur: function(event) {
    // needed to make click events working
    setTimeout(this.hide.bind(this), 250);
    this.hasFocus = false;
    this.active = false;     
  }, 
  
  render: function() {
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
        this.index==i ? 
          Element.addClassName(this.getEntry(i),"selected") : 
          Element.removeClassName(this.getEntry(i),"selected");
        
      if(this.hasFocus) { 
        this.show();
        this.active = true;
      }
    } else this.hide();
  },
  
  markPrevious: function() {
    if(this.index > 0) this.index--
      else this.index = this.entryCount-1;
  },
  
  markNext: function() {
    if(this.index < this.entryCount-1) this.index++
      else this.index = 0;
  },
  
  getEntry: function(index) {
    return this.update.firstChild.childNodes[index];
  },
  
  getCurrentEntry: function() {
    return this.getEntry(this.index);
  },
  
  selectEntry: function() {
    this.active = false;
    this.updateElement(this.getCurrentEntry());
  },

  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }

    var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
    var lastTokenPos = this.findLastToken();
    if (lastTokenPos != -1) {
      var newValue = this.element.value.substr(0, lastTokenPos + 1);
      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
      if (whitespace)
        newValue += whitespace[0];
      this.element.value = newValue + value;
    } else {
      this.element.value = value;
    }
    this.element.focus();
    
    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
  },

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

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

      this.stopIndicator();

      this.index = 0;
      this.render();
    }
  },

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

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

  getToken: function() {
    var tokenPos = this.findLastToken();
    if (tokenPos != -1)
      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
    else
      var ret = this.element.value;

    return /\n/.test(ret) ? '' : ret;
  },

  findLastToken: function() {
    var lastTokenPos = -1;

    for (var i=0; i<this.options.tokens.length; i++) {
      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
      if (thisTokenPos > lastTokenPos)
        lastTokenPos = thisTokenPos;
    }
    return lastTokenPos;
  }
}

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

  getUpdatedChoices: function() {
    entry = encodeURIComponent(this.options.paramName) + '=' + 
      encodeURIComponent(this.getToken());

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

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

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

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

});

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

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

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

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

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

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

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

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

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

// AJAX in-place editor
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor

Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
  initialize: function(element, url, options) {
    this.url = url;
    this.element = $(element);

    this.options = Object.extend({
      okText: "ok",
      cancelText: "cancel",
      savingText: "Saving...",
      clickToEditText: "Click to edit",
      okText: "ok",
      rows: 1,
      onComplete: function(transport, element) {
        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
      },
      onFailure: function(transport) {
        alert("Error communicating with the server: " + transport.responseText.stripTags());
      },
      callback: function(form) {
        return Form.serialize(form);
      },
      handleLineBreaks: true,
      loadingText: 'Loading...',
      savingClassName: 'inplaceeditor-saving',
      loadingClassName: 'inplaceeditor-loading',
      formClassName: 'inplaceeditor-form',
      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
      highlightendcolor: "#FFFFFF",
      externalControl:	null,
      ajaxOptions: {}
    }, options || {});

    if(!this.options.formId && this.element.id) {
      this.options.formId = this.element.id + "-inplaceeditor";
      if ($(this.options.formId)) {
        // there's already a form with that name, don't specify an id
        this.options.formId = null;
      }
    }
    
    if (this.options.externalControl) {
      this.options.externalControl = $(this.options.externalControl);
    }
    
    this.originalBackground = Element.getStyle(this.element, 'background-color');
    if (!this.originalBackground) {
      this.originalBackground = "transparent";
    }
    
    this.element.title = this.options.clickToEditText;
    
    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
    Event.observe(this.element, 'click', this.onclickListener);
    Event.observe(this.element, 'mouseover', this.mouseoverListener);
    Event.observe(this.element, 'mouseout', this.mouseoutListener);
    if (this.options.externalControl) {
      Event.observe(this.options.externalControl, 'click', this.onclickListener);
      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
    }
  },
  enterEditMode: function() {
    if (this.saving) return;
    if (this.editing) return;
    this.editing = true;
    this.onEnterEditMode();
    if (this.options.externalControl) {
      Element.hide(this.options.externalControl);
    }
    Element.hide(this.element);
    this.createForm();
    this.element.parentNode.insertBefore(this.form, this.element);
    Field.focus(this.editField);
    // stop the event to avoid a page refresh in Safari
    if (arguments.length > 1) {
      Event.stop(arguments[0]);
    }
  },
  createForm: function() {
    this.form = document.createElement("form");
    this.form.id = this.options.formId;
    Element.addClassName(this.form, this.options.formClassName)
    this.form.onsubmit = this.onSubmit.bind(this);

    this.createEditField();

    if (this.options.textarea) {
      var br = document.createElement("br");
      this.form.appendChild(br);
    }

    okButton = document.createElement("input");
    okButton.type = "submit";
    okButton.value = this.options.okText;
    this.form.appendChild(okButton);

    cancelLink = document.createElement("a");
    cancelLink.href = "#";
    cancelLink.appendChild(document.createTextNode(this.options.cancelText));
    cancelLink.onclick = this.onclickCancel.bind(this);
    this.form.appendChild(cancelLink);
  },
  hasHTMLLineBreaks: function(string) {
    if (!this.options.handleLineBreaks) return false;
    return string.match(/<br/i) || string.match(/<p>/i);
  },
  convertHTMLLineBreaks: function(string) {
    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
  },
  createEditField: function() {
    var text;
    if(this.options.loadTextURL) {
      text = this.options.loadingText;
    } else {
      text = this.getText();
    }
    
    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
      this.options.textarea = false;
      var textField = document.createElement("input");
      textField.type = "text";
      textField.name = "value";
      textField.value = text;
      textField.style.backgroundColor = this.options.highlightcolor;
      var size = this.options.size || this.options.cols || 0;
      if (size != 0) textField.size = size;
      this.editField = textField;
    } else {
      this.options.textarea = true;
      var textArea = document.createElement("textarea");
      textArea.name = "value";
      textArea.value = this.convertHTMLLineBreaks(text);
      textArea.rows = this.options.rows;
      textArea.cols = this.options.cols || 40;
      this.editField = textArea;
    }
    
    if(this.options.loadTextURL) {
      this.loadExternalText();
    }
    this.form.appendChild(this.editField);
  },
  getText: function() {
    return this.element.innerHTML;
  },
  loadExternalText: function() {
    Element.addClassName(this.form, this.options.loadingClassName);
    this.editField.disabled = true;
    new Ajax.Request(
      this.options.loadTextURL,
      Object.extend({
        asynchronous: true,
        onComplete: this.onLoadedExternalText.bind(this)
      }, this.options.ajaxOptions)
    );
  },
  onLoadedExternalText: function(transport) {
    Element.removeClassName(this.form, this.options.loadingClassName);
    this.editField.disabled = false;
    this.editField.value = transport.responseText.stripTags();
  },
  onclickCancel: function() {
    this.onComplete();
    this.leaveEditMode();
    return false;
  },
  onFailure: function(transport) {
    this.options.onFailure(transport);
    if (this.oldInnerHTML) {
      this.element.innerHTML = this.oldInnerHTML;
      this.oldInnerHTML = null;
    }
    return false;
  },
  onSubmit: function() {
    // onLoading resets these so we need to save them away for the Ajax call
    var form = this.form;
    var value = this.editField.value;
    
    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
    // to be displayed indefinitely
    this.onLoading();
    
    new Ajax.Updater(
      { 
        success: this.element,
         // don't update on failure (this could be an option)
        failure: null
      },
      this.url,
      Object.extend({
        parameters: this.options.callback(form, value),
        onComplete: this.onComplete.bind(this),
        onFailure: this.onFailure.bind(this)
      }, this.options.ajaxOptions)
    );
    // stop the event to avoid a page refresh in Safari
    if (arguments.length > 1) {
      Event.stop(arguments[0]);
    }
    return false;
  },
  onLoading: function() {
    this.saving = true;
    this.removeForm();
    this.leaveHover();
    this.showSaving();
  },
  showSaving: function() {
    this.oldInnerHTML = this.element.innerHTML;
    this.element.innerHTML = this.options.savingText;
    Element.addClassName(this.element, this.options.savingClassName);
    this.element.style.backgroundColor = this.originalBackground;
    Element.show(this.element);
  },
  removeForm: function() {
    if(this.form) {
      if (this.form.parentNode) Element.remove(this.form);
      this.form = null;
    }
  },
  enterHover: function() {
    if (this.saving) return;
    this.element.style.backgroundColor = this.options.highlightcolor;
    if (this.effect) {
      this.effect.cancel();
    }
    Element.addClassName(this.element, this.options.hoverClassName)
  },
  leaveHover: function() {
    if (this.options.backgroundColor) {
      this.element.style.backgroundColor = this.oldBackground;
    }
    Element.removeClassName(this.element, this.options.hoverClassName)
    if (this.saving) return;
    this.effect = new Effect.Highlight(this.element, {
      startcolor: this.options.highlightcolor,
      endcolor: this.options.highlightendcolor,
      restorecolor: this.originalBackground
    });
  },
  leaveEditMode: function() {
    Element.removeClassName(this.element, this.options.savingClassName);
    this.removeForm();
    this.leaveHover();
    this.element.style.backgroundColor = this.originalBackground;
    Element.show(this.element);
    if (this.options.externalControl) {
      Element.show(this.options.externalControl);
    }
    this.editing = false;
    this.saving = false;
    this.oldInnerHTML = null;
    this.onLeaveEditMode();
  },
  onComplete: function(transport) {
    this.leaveEditMode();
    this.options.onComplete.bind(this)(transport, this.element);
  },
  onEnterEditMode: function() {},
  onLeaveEditMode: function() {},
  dispose: function() {
    if (this.oldInnerHTML) {
      this.element.innerHTML = this.oldInnerHTML;
    }
    this.leaveEditMode();
    Event.stopObserving(this.element, 'click', this.onclickListener);
    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
    if (this.options.externalControl) {
      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
    }
  }
};
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// See scriptaculous.js for full license.


Object.debug = function(obj) {
  var info = [];
  
  if(typeof obj in ["string","number"]) {
    return obj;
  } else {
    for(property in obj)
      if(typeof obj[property]!="function")
        info.push(property + ' => ' + 
          (typeof obj[property] == "string" ?
            '"' + obj[property] + '"' :
            obj[property]));
  }
  
  return ("'" + obj + "' #" + typeof obj + 
    ": {" + info.join(", ") + "}");
}


String.prototype.toArray = function() {
  var results = [];
  for (var i = 0; i < this.length; i++)
    results.push(this.charAt(i));
  return results;
}

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

var Builder = {
  NODEMAP: {
    AREA: 'map',
    CAPTION: 'table',
    COL: 'table',
    COLGROUP: 'table',
    LEGEND: 'fieldset',
    OPTGROUP: 'select',
    OPTION: 'select',
    PARAM: 'object',
    TBODY: 'table',
    TD: 'table',
    TFOOT: 'table',
    TH: 'table',
    THEAD: 'table',
    TR: 'table'
  },
  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
  //       due to a Firefox bug
  node: function(elementName) {
    elementName = elementName.toUpperCase();
    
    // try innerHTML approach
    var parentTag = this.NODEMAP[elementName] || 'div';
    var parentElement = document.createElement(parentTag);
    parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
    var element = parentElement.firstChild || null;
      
    // see if browser added wrapping tags
    if(element && (element.tagName != elementName))
      element = element.getElementsByTagName(elementName)[0];
    
    // fallback to createElement approach
    if(!element) element = document.createElement(elementName);
    
    // abort if nothing could be created
    if(!element) return;

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

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

     return element;
  },
  _text: function(text) {
     return document.createTextNode(text);
  },
  _attributes: function(attributes) {
    var attrs = [];
    for(attribute in attributes)
      attrs.push((attribute=='className' ? 'class' : attribute) +
          '="' + attributes[attribute].toString().escapeHTML() + '"');
    return attrs.join(" ");
  },
  _children: function(element, children) {
    if(typeof children=='object') { // array can hold nodes and text
      children.flatten().each( function(e) {
        if(typeof e=='object')
          element.appendChild(e)
        else
          if(Builder._isStringOrNumber(e))
            element.appendChild(Builder._text(e));
      });
    } else
      if(Builder._isStringOrNumber(children)) 
         element.appendChild(Builder._text(children));
  },
  _isStringOrNumber: function(param) {
    return(typeof param=='string' || typeof param=='number');
  }
}

/* ------------- element ext -------------- */

// adapted from http://dhtmlkitchen.com/learn/js/setstyle/index4.jsp
// note: Safari return null on elements with display:none; see http://bugzilla.opendarwin.org/show_bug.cgi?id=4125
// instead of "auto" values returns null so it's easier to use with || constructs

String.prototype.camelize = function() {
  var oStringList = this.split('-');
  if(oStringList.length == 1)    
    return oStringList[0];
  var ret = this.indexOf("-") == 0 ? 
    oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) : oStringList[0];
  for(var i = 1, len = oStringList.length; i < len; i++){
    var s = oStringList[i];
    ret += s.charAt(0).toUpperCase() + s.substring(1)
  }
  return ret;
}

Element.getStyle = function(element, style) {
  element = $(element);
  var value = element.style[style.camelize()];
  if(!value)
    if(document.defaultView && document.defaultView.getComputedStyle) {
      var css = document.defaultView.getComputedStyle(element, null);
      value = (css!=null) ? css.getPropertyValue(style) : null;
    } else if(element.currentStyle) {
      value = element.currentStyle[style.camelize()];
    }
  
  // If top, left, bottom, or right values have been queried, return "auto" for consistency resaons 
  // if position is "static", as Opera (and others?) returns the pixel values relative to root element 
  // (or positioning context?)
  if (window.opera && (style == "left" || style == "top" || style == "right" || style == "bottom"))
    if (Element.getStyle(element, "position") == "static") value = "auto";
    
  if(value=='auto') value = null;
  return value;
}

// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
  color = "#";
  if(this.slice(0,4) == "rgb(") {
    var cols = this.slice(4,this.length-1).split(',');
    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
  } else {
    if(this.slice(0,1) == '#') {
      if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
      if(this.length==7) color = this.toLowerCase();
    }
  }
  return(color.length==7 ? color : (arguments[0] || this));
}

Element.makePositioned = function(element) {
  element = $(element);
  var pos = Element.getStyle(element, 'position');
  if(pos =='static' || !pos) {
    element._madePositioned = true;
    element.style.position = "relative";
    // Opera returns the offset relative to the positioning context, when an element is position relative 
    // but top and left have not been defined
    if (window.opera){
      element.style.top = 0;
      element.style.left = 0;
    }  
  }
}
  
Element.undoPositioned = function(element) {
  element = $(element);
  if(typeof element._madePositioned != "undefined"){
    element._madePositioned = undefined;
    element.style.position = "";
    element.style.top = "";
    element.style.left = "";
    element.style.bottom = "";
    element.style.right = "";	  
  }
}

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

Element.undoClipping = function(element) {
  element = $(element);
  if (typeof element._overflow == 'undefined') return;
  element.style.overflow = element._overflow;
  element._overflow = undefined;
}

Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
  var children = $(element).childNodes;
  var text     = "";
  var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");

  for (var i = 0; i < children.length; i++) {
    if(children[i].nodeType==3) {
      text+=children[i].nodeValue;
    } else {
      if((!children[i].className.match(classtest)) && children[i].hasChildNodes())
        text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);
    }
  }

  return text;
}

Element.setContentZoom = function(element, percent) {
  element = $(element);
  element.style.fontSize = (percent/100) + "em";  
  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
}

Element.getOpacity = function(element){
  var opacity;
  if (opacity = Element.getStyle(element, "opacity"))
    return parseFloat(opacity);
  if (opacity = (Element.getStyle(element, "filter") || '').match(/alpha\(opacity=(.*)\)/))
    if(opacity[1]) return parseFloat(opacity[1]) / 100;
  return 1.0;
}

Element.setOpacity = function(element, value){
  element= $(element);
  var els = element.style;
  if (value == 1){
    els.opacity = '0.999999';
    if(/MSIE/.test(navigator.userAgent))
      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'');
  } else {
    if(value < 0.00001) value = 0;
    els.opacity = value;
    if(/MSIE/.test(navigator.userAgent))
      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + 
        "alpha(opacity="+value*100+")";
  }  
}

Element.getInlineOpacity = function(element){
  element= $(element);
  var op;
  op = element.style.opacity;
  if (typeof op != "undefined" && op != "") return op;
  return "";
}

Element.setInlineOpacity = function(element, value){
  element= $(element);
  var els = element.style;
  els.opacity = value;
}

Element.getDimensions = function(element){
  element = $(element);
  // All *Width and *Height properties give 0 on elements with display "none",
  // so enable the element temporarily
  if (Element.getStyle(element,'display') == "none"){
    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    els.visibility = "hidden";
    els.position = "absolute";
    els.display = "";
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = "none";
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};    
  }
  
  return {width: element.offsetWidth, height: element.offsetHeight};
} 

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

Position.positionedOffset = function(element) {
  var valueT = 0, valueL = 0;
  do {
    valueT += element.offsetTop  || 0;
    valueL += element.offsetLeft || 0;
    element = element.offsetParent;
    if (element) {
      p = Element.getStyle(element,'position');
      if(p == 'relative' || p == 'absolute') break;
    }
  } while (element);
  return [valueL, valueT];
}

// Safari returns margins on body which is incorrect if the child is absolutely positioned.
// for performance reasons, we create a specialized version of Position.cumulativeOffset for
// KHTML/WebKit only

if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
  Position.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      
      if (element.offsetParent==document.body) 
        if (Element.getStyle(element,'position')=='absolute') break;
        
      element = element.offsetParent;
    } while (element);
    return [valueL, valueT];
  }
}

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

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

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

  element = forElement;
  do {
    valueT -= element.scrollTop  || 0;
    valueL -= element.scrollLeft || 0;    
  } while (element = element.parentNode);

  return [valueL, valueT];
}

// elements with display:none don't return an offsetParent, 
// fall back to  manual calculation
Position.offsetParent = function(element) {
  if(element.offsetParent) return element.offsetParent;
  if(element == document.body) return element;
  
  while ((element = element.parentNode) && element != document.body)
    if (Element.getStyle(element,'position')!='static')
      return element;
  
  return document.body;
}

Position.clone = function(source, target) {
  var options = Object.extend({
    setLeft:    true,
    setTop:     true,
    setWidth:   true,
    setHeight:  true,
    offsetTop:  0,
    offsetLeft: 0
  }, arguments[2] || {})
  
  // find page position of source
  source = $(source);
  var p = Position.page(source);

  // find coordinate system to use
  target = $(target);
  var delta = [0, 0];
  var parent = null;
  // delta [0,0] will do fine with position: fixed elements, 
  // position:absolute needs offsetParent deltas
  if (Element.getStyle(target,'position') == 'absolute') {
    parent = Position.offsetParent(target);
    delta = Position.page(parent);
  }
  
  // correct by body offsets (fixes Safari)
  if (parent==document.body) {
    delta[0] -= document.body.offsetLeft;
    delta[1] -= document.body.offsetTop; 
  }

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

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

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

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

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

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

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

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

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

Element.Class = {
    // Element.toggleClass(element, className) toggles the class being on/off
    // Element.toggleClass(element, className1, className2) toggles between both classes,
    //   defaulting to className1 if neither exist
    toggle: function(element, className) {
      if(Element.Class.has(element, className)) {
        Element.Class.remove(element, className);
        if(arguments.length == 3) Element.Class.add(element, arguments[2]);
      } else {
        Element.Class.add(element, className);
        if(arguments.length == 3) Element.Class.remove(element, arguments[2]);
      }
    },

    // gets space-delimited classnames of an element as an array
    get: function(element) {
      return $(element).className.split(' ');
    },

    // functions adapted from original functions by Gavin Kistner
    remove: function(element) {
      element = $(element);
      var removeClasses = arguments;
      $R(1,arguments.length-1).each( function(index) {
        element.className = 
          element.className.split(' ').reject( 
            function(klass) { return (klass == removeClasses[index]) } ).join(' ');
      });
    },

    add: function(element) {
      element = $(element);
      for(var i = 1; i < arguments.length; i++) {
        Element.Class.remove(element, arguments[i]);
        element.className += (element.className.length > 0 ? ' ' : '') + arguments[i];
      }
    },

    // returns true if all given classes exist in said element
    has: function(element) {
      element = $(element);
      if(!element || !element.className) return false;
      var regEx;
      for(var i = 1; i < arguments.length; i++) {
        if((typeof arguments[i] == 'object') && 
          (arguments[i].constructor == Array)) {
          for(var j = 0; j < arguments[i].length; j++) {
            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");
            if(!regEx.test(element.className)) return false;
          }
        } else {
          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");
          if(!regEx.test(element.className)) return false;
        }
      }
      return true;
    },

    // expects arrays of strings and/or strings as optional paramters
    // Element.Class.has_any(element, ['classA','classB','classC'], 'classD')
    has_any: function(element) {
      element = $(element);
      if(!element || !element.className) return false;
      var regEx;
      for(var i = 1; i < arguments.length; i++) {
        if((typeof arguments[i] == 'object') && 
          (arguments[i].constructor == Array)) {
          for(var j = 0; j < arguments[i].length; j++) {
            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");
            if(regEx.test(element.className)) return true;
          }
        } else {
          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");
          if(regEx.test(element.className)) return true;
        }
      }
      return false;
    },

    childrenWith: function(element, className) {
      var children = $(element).getElementsByTagName('*');
      var elements = new Array();

      for (var i = 0; i < children.length; i++)
        if (Element.Class.has(children[i], className))
          elements.push(children[i]);

      return elements;
    }
}
// ============================
// = Search field placeholder =
// ============================
AJS.toInit(function ($) {
    var search = $(".quick-search-query");
    if (!search.length) {
        return;
    }

    search.each(function() {
        $searchBox = $(this);
        $searchBox.data("quicksearch", {
            placeholder: $searchBox.closest("form").find("input[type='submit']").val(),
            placeholded: true
        });
    });

    if (!$.browser.safari) {
        search.val(search.data("quicksearch").placeholder);

        search.addClass("placeholded");

        search.focus(function () {
            var $this = $(this);
            if ($this.data("quicksearch").placeholded) {
                $this.data("quicksearch").placeholded = false;
                $this.val("");
                $this.removeClass("placeholded");
            }
        });

        search.blur(function () {
            var $this = $(this);
            if ($this.data("quicksearch").placeholder && (/^\s*$/).test($this.val())) {
                $this.val($this.data("quicksearch").placeholder);
                $this.data("quicksearch").placeholded = true;
                $this.addClass("placeholded");
            }
        });
        
    }
    else {
        search.each(function () {
            // don't use jQuery because of http://dev.jquery.com/ticket/1957
            // we know we're in Safari, so the assignment will work
            this.type = "search";
        });
        search.attr("results", 10);
        search.attr("placeholder", search.data("quicksearch").placeholder);
        search.val("");
    }

    // Moved out of macros.vm displayGlobalMessages
    $("#messageContainer .confluence-messages").each(function () {
        var message = this;
        if (!getCookie(message.id)) {
            $(message).show();
            $(".message-close-button", message).click(function () {
                $(message).slideUp();
                setCookie(message.id, true);
            });
        }
    });
});

AJS.General =  {
    getContextPath : function() {
        return AJS.$("#confluence-context-path").attr('content') || "";
    }        
};

/**
 * Returns the i18n string for the provided key.
 *
 * This function is the complement of the VM macro "i18n" which puts the i18n string where this function can find it.
 * @param i18nKey an i18n key. Should be the same as entered in an i18n .properties file.
 */
AJS.I18n = AJS.I18n || {};
AJS.I18n.getText = function (i18nKey) {
    return AJS.params["i18n." + i18nKey];
};
/**
 * Wrap this function around values which should not be auto-HTML escaped in template substitution.
 *
 * @param value the String value which should not be escaped
 */
AJS.html = function (value) {
    var str = new String(value);
    str.isHtml = true;
    return str;
};

AJS.toInit(function ($) {
    var templates = {};
    $("script").each(function () {
        if (this.type == "text/x-template") {
            templates[this.title] = AJS.html(this.text);
        }
    });

    AJS.getTemplate = function (name) {
        return templates[name];
    };

    var entities = {
        "&": "&amp;",
        "<": "&lt;",
        ">": "&gt;",
        "'": "&#39;",
        '"': "&quot;"
    };

    AJS.escapeEntities = function (str) {
        if (str == null) {
            return str;
        }
        if (str.isHtml) {
            return "" + str;
        }
        return ("" + str).replace(/[&<>'"]/g, function (c) { return entities[c] || c; });
    };

    function format(message) {
        var args = arguments;
        return message.replace(/\{(\d+)\}/g, function (str, i) {
            var replacement = args[parseInt(i, 10) + 1];
            return replacement != null ? replacement : str;
        });
    };

    /**
     * Retrieves a template with a given name from the page body (in the form
     * <script type="text/x-template" title="name">...</script>) and formats it
     * using AJS.format. The arguments are automatically HTML-encoded, so that
     * you cannot accidentally introduce XSS vulnerabilities with this templating
     * mechanism.
     *
     * @method renderTemplate
     * @param templateName the title of a script tag in the document which contains a template
     * @param args an array or list of arguments which will be the replacement values for tokens {0}, {1}, etc.
     * @return {String} the template with the tokens replaced or empty string if there is no matching template
     * @usage AJS.renderTemplate("someTemplate", "first", "second", "third");
     * @usage AJS.renderTemplate("someTemplate", ["first", "second", "third"]);
    */
    AJS.renderTemplate = function (templateName, args) {
        if (!AJS.getTemplate(templateName)) {
            return "";
        }
        if (!$.isArray(args)) {
            args = Array.prototype.slice.call(arguments, 1); // arguments is not a proper Array
        }
        var template = AJS.getTemplate(templateName).toString();
        var formatArgs = [ template ];
        for (var i = 0; i < args.length; i++) {
            formatArgs.push(AJS.escapeEntities(args[i]));
        }
        return format.apply(this, formatArgs);
    };
});
// ==================
// = Drop-down menu =
// ==================
AJS.menuShowCount = 0;

jQuery.fn.ajsMenu = function (options) {
    options = options || {};
    var $ = jQuery;
    var shownDropDown = null;
    var hideDropDown = function (e) {
        if (typeof AJS.dropDownTimer != "undefined" && AJS.dropDownHider) {
            clearTimeout(AJS.dropDownTimer);
            delete AJS.dropDownTimer;
            AJS.dropDownHider();
            AJS.dropDownHider = null;
        }
    };
    $(".ajs-button", this).each(function () {
        $(this).mouseover(hideDropDown);
    });
    $(".ajs-menu-item", this).each(function () {
        var it = this, $it = $(this),
            dd = $(".ajs-drop-down", it);
        if (!dd.length) return;

        dd = dd[0];
        dd.hidden = true;
        dd.focused = -1;
        dd.hide = function () {
            if (!this.hidden) {
                $it.toggleClass("opened");
                $(it.parentNode).toggleClass("menu-bar-open");
                var as = $("a", this);
                $(this).toggleClass("hidden");
                this.hidden = true;
                $(document).unbind("click", this.fhide).unbind("keydown", this.fmovefocus).unbind("keypress", this.blocker);
                if (this.focused + 1) {
                    $(as[this.focused]).removeClass("active");
                }
                this.focused = -1;
            }
        };
        dd.show = function () {
            if (typeof this.hidden == "undefined" || this.hidden) {
                var dd = this, $dd = $(this);
                $dd.toggleClass("hidden");
                $it.toggleClass("opened");
                $(it.parentNode).toggleClass("menu-bar-open");
                this.hidden = false;
                this.timer = setTimeout(function () {$(document).click(dd.fhide);}, 1);
                $(document).keydown(dd.fmovefocus).keypress(dd.blocker);
                var as = $("a", dd);
                as.each(function (i) {
                    var grandpa = this.parentNode.parentNode;
                    $(this).hover(function (e) {
                        if (grandpa.focused + 1) {
                            $(as[grandpa.focused].parentNode).removeClass("active");
                        }
                        $(this.parentNode).addClass("active");
                        grandpa.focused = i;
                    }, function (e) {
                        if (grandpa.focused + 1) {
                            $(as[grandpa.focused].parentNode).removeClass("active");
                        }
                        grandpa.focused = -1;
                    });
                });
                var topOfViewablePage = (window.pageYOffset || document.documentElement.scrollTop);
                var bottomOfViewablePage = topOfViewablePage + $(window).height();
                $dd.removeClass("above");
                if (!options.isFixedPosition) {
                    if ($dd.offset().top + $dd.height() > bottomOfViewablePage) {
                        $dd.addClass("above");
                        if ($dd.offset().top < topOfViewablePage) {
                            $dd.removeClass("above");
                        }
                    }
                }
            }
        };
        dd.fmovefocus = function (e) {dd.movefocus(e);};
        dd.fhide = function (e) {dd.hide(e);};
        dd.blocker = function (e) {
            var c = e.which;
            if (c == 40 || c == 38) {
                return false;
            }
        };
        dd.movefocus = function (e) {
            var c = e.which,
                a = this.getElementsByTagName("a");
            if (this.focused + 1) {
                $(a[this.focused].parentNode).removeClass("active");
            }
            switch (c) {
                case 40:
                case 9: {
                    this.focused++;
                    break;
                }
                case 38: {
                    this.focused--;
                    break;
                }
                case 27: {
                    this.hide();
                    return false;
                }
                default: {
                    return true;
                }
            }
            if (this.focused < 0) {
                this.focused = a.length - 1;
            }
            if (this.focused > a.length - 1) {
                this.focused = 0;
            }
            a[this.focused].focus();
            $(a[this.focused].parentNode).addClass("active");
            e.stopPropagation();
            e.preventDefault();
            return false;
        };
        dd.show();
        clearTimeout(dd.timer);
        var $dd = $(dd),
            offset = $dd.offset();
        dd.hide();
        if (offset.left + $dd.width() > $(window).width()) {
            $dd.css("margin-left", "-" + (($dd.width()) - ($it.width())) + "px");
        }
        var a = $(".trigger", it);
        if (a.length) {
            var killHideTimerAndShow = function() {
                clearTimeout(AJS.dropDownTimer);
                delete AJS.dropDownTimer;
                AJS.dropDownHider();
                AJS.dropDownHider = null;
                dd.show();
            };

            var showMenu = function (millis) {
                var changingMenu = typeof AJS.dropDownTimer != "undefined";
                shownDropDown = dd;
                if (changingMenu) {
                    killHideTimerAndShow();
                }
                else {
                    AJS.dropDownShower = function () {dd.show(); delete AJS.dropDownShowerTimer;};
                    AJS.dropDownShowerTimer = setTimeout(AJS.dropDownShower, millis);
                }
            };
            var hideMenu = function (millis) {
                var passingThrough = typeof AJS.dropDownShowerTimer != "undefined";
                if (passingThrough) {
                    clearTimeout(AJS.dropDownShowerTimer);
                    delete AJS.dropDownShowerTimer;
                }
                if (typeof AJS.dropDownTimer != "undefined") {
                    clearTimeout(AJS.dropDownTimer);
                    delete AJS.dropDownHider;
                }
                AJS.dropDownHider = function () {dd.hide(); delete AJS.dropDownTimer;};
                AJS.dropDownTimer = setTimeout(AJS.dropDownHider, millis);
            };

            var overHandler = function (e) {
                showMenu(500);
            };

            var outHandler = function (e) {
                hideMenu(300);
            };
            a.click(function (e) { return false; });
            $it.mouseover(overHandler);
            $it.mouseout(outHandler);

            var keypressHandler = function (e) {
                if (shownDropDown)
                    shownDropDown.hide();
                if (e.which == 27) {
                    dd.hide();
                } else {
                    showMenu(0);
                }
            };
            a.keypress(keypressHandler);
        }
    });
};


AJS.toInit(function ($) {

    /* TODO: Restore this once JQuery is integrated and HTMLUnit is upgraded to work with JQuery. */
    /*jQuery(function ($) {
        $(".popup-link").bind("click", function() {
            window.open(this.href, this.id + '-popupwindow', 'width=600, height=400, scrollbars, resizable');
            return false;
        });
    });*/

    var ids = ["action-view-source-link", "view-user-history-link"];
    for (var i = ids.length; i--;) {
        $("#" + ids[i]).click(function (e) {
            window.open(this.href, (this.id + "-popupwindow").replace(/-/g, "_"), "width=600, height=400, scrollbars, resizable");
            e.preventDefault();
            return false;
        });
    }

    /* TODO: Extract this logic out into a common js file */
    var errorHandler = function (errorMessage, item) {
        var errorDiv = $("#ajax-error");
        if (errorDiv.length == 0) {
            $("#com-atlassian-confluence").prepend("<div id='ajax-error'></div>");
            errorDiv = $("#ajax-error");
        }

        errorDiv.append("<span class='error'>" + errorMessage +  "<a class='close'>Close</a></span>");

        errorDiv.find("a.close").click(function () {
            var parent = $(this).parent();
            $(parent).slideUp(1000, function () {
                $(parent).remove();
                if ($("#ajax-error").children(".error").length == 0)
                {
                    $("#ajax-error").remove();
                }
            });
            return false;
        });
        item.removeClass("waiting");
    };

    $("#page-favourite").click(function (e) {
        var menuItem = $(this);
        if (menuItem.hasClass("waiting")) {
            // already waiting
            return AJS.stopEvent(e);
        }
        menuItem.addClass("waiting");
        var url = contextPath + "/json/addfavourite.action";
        if (menuItem.hasClass("selected")) {
            url = contextPath + "/json/removefavourite.action";
        }
        AJS.safeAjax({
            url: url,
            type: "POST",
            dataType: "json",
            data: {
                "entityId": AJS.params.pageId
            },
            success: function(data) {
                if(data.actionErrors) {
                    for (var i = 0; i < data.actionErrors.length; i++) {
                        errorHandler(data.actionErrors, menuItem);
                    }
                    return;
                }
                if (data.errorMessage) {
                    errorHandler(data.errorMessage, menuItem);
                    return;
                }

                menuItem.removeClass("waiting");
                menuItem.toggleClass("selected");
                menuItem.toggleClass("ie-page-favourite-selected");
            },
            error: function(data) {
                errorHandler("Server error while updating favourite", menuItem);
            }
        });
        return AJS.stopEvent(e);
    });

    var watch = $("#page-watch");
    var unwatch = $("#page-unwatch");
    var watchParent = $(watch.parent("li"));
    var unwatchParent = $(unwatch.parent("li"));

    if (watch.hasClass("inactive")) {
        watchParent.addClass("hidden");
    }

    if (unwatch.hasClass("inactive")) {
        unwatchParent.addClass("hidden");
    }

    var watchOrUnwatch = function (url, item, opposite) {
        item.addClass("waiting");
        AJS.safe.ajax({
            url: url,
            type: "POST",
            dataType: "json",
            data: {
                "entityId": AJS.params.pageId
            },
            success: function(data) {
                if(data.actionErrors) {
                    for (var i = 0; i < data.actionErrors.length; i++) {
                        errorHandler(data.actionErrors, item);
                    }
                    return;
                }
                if (data.errorMessage) {
                    errorHandler(data.errorMessage, item);
                    return;
                }

                item.removeClass("waiting");
                item.toggleClass("inactive");
                opposite.toggleClass("inactive");

                item.parent("li").toggleClass("hidden");
                opposite.parent("li").toggleClass("hidden");
            },
            error: function(data) {
                item.removeClass("waiting");
                errorHandler("Server error while updating favourite", menuItem);
            }
        });
    };

    watch.click(function(e) {
        watchOrUnwatch(contextPath + "/pages/startwatching.action", watch, unwatch);
        watch.addClass("waiting");
        return AJS.stopEvent(e);
    });

    unwatch.click(function(e) {
        watchOrUnwatch(contextPath + "/pages/stopwatching.action", unwatch, watch);
        unwatch.addClass("waiting");
        return AJS.stopEvent(e);
    });

    $("#action-menu-link").next().addClass("most-right-menu-item");

    $(".ajs-menu-bar").ajsMenu({isFixedPosition: true});
});

/**
 * Simple scroll-to jQuery plugin.
 * Written by Atlassian, October 2009.
 *
 * Typical usage: $("#container").simpleScrollTo($("#container > .some-item"))
 *
 * The container must be the nearest ancestor of the item with "position: relative" or "position: absolute".
 * This is the only way that the item's position can be calculated relative to it.
 */
jQuery.fn.simpleScrollTo = function (element) {
    var $ = jQuery;
    var container = $(this[0]);
    var topOffset = $(element).position().top;
    var bottomOffset = topOffset + $(element).outerHeight() - container.outerHeight();
    if (topOffset < 0) { // top of element is above the top of the container
        container.scrollTop(container.scrollTop() + topOffset);
    } else if (bottomOffset > 0) { // bottom of element is below the bottom of the container
        container.scrollTop(container.scrollTop() + bottomOffset);
    }
    return this;
};
(function($){
    /**
     * Options are:
     *   dropdownPostprocess - a function that will be supplied with the list created by the dropDown and can manipulate or modify it 
     *                         as necessary.
     *   dropdownPlacement - a function that will be called with the drop down and which should place it in the correct place on the page.
     *                       The supplied arguments are 1) the input that issued the search, 2) the dropDown to be placed.
     *   ajsDropDownOptions - any options the underlying dropDown component can handle expects  
     */
    $.fn.quicksearch = function(path, onShow, options) {
        options = options || {};
        var attr = {
            cache_size: 30,
            max_length: 1,
            effect: "appear"
        };
        var dd,
            cache = {},
            cache_stack = [],
            timer;

        if (typeof path == "function") {
            var oldPath = path();
            var getPath = function () {
                var newPath = path();
                if (newPath != oldPath) {
                    // reset the cache if the path has changed
                    cache = {};
                    cache_stack = [];

                    oldPath = newPath;
                }
                return newPath;
            };
        } else {
            var getPath = function () {
                return path;
            };
        }

        var searchBox = $(this);
        
        var jsonparser = function (json, resultStatus) {
            var hasErrors = json.statusMessage ? true : false; // right now, we are overloading the existence of a status message to imply an error
            var matches = hasErrors ? [[{html: json.statusMessage, className: "error"}]] : json.contentNameMatches;

            if (!hasErrors) {
                var query = json.query;
                if (!cache[query] && resultStatus == "success") {
                    cache[query] = json;
                    cache_stack.push(query);
                    if (cache_stack.length > attr.cache_size) {
                        delete cache[cache_stack.shift()];
                    }
                }
            }

            // do not update drop down for earlier requests. We are only interested in displaying the results for the most current search
            if (json.query != searchBox.val()) {
                return;
            }

            var old_dd = dd;
            
            // Standard dropDown handling of JSON object is to extract name, href, etc and then store the rest of the properties
            // as a jQuery "data" property on the name span called properties.
            dd = AJS.dropDown(matches, options.ajsDropDownOptions)[0];
            dd.jsonResult = json;
            // place the created drop down using the configured dropdownPlacement function
            // if there is none then use a default behaviour            
            if (options.dropdownPlacement) {
                options.dropdownPlacement(searchBox, dd.$);
            } else {
                searchBox.closest("form").find(".quick-nav-drop-down").append(dd.$);
            }

            dd.onhide = function (causer) {
                if (causer == "escape") {
                    searchBox.focus();
                }
            };
            var spans = $("span", dd.$);
            for (var i = 0, ii = spans.length - 1; i < ii; i++) {
                (function () {
                    var $this = $(this),
                    html = $this.html();
                    // highlight matching tokens
                    html = html.replace(new RegExp("(" + json.queryTokens.join("|") + ")", "gi"), "<strong>$1</strong>");
                    $this.html(html);
                }).call(spans[i]);
            }
            
            if (options.dropdownPostprocess) {
                options.dropdownPostprocess(dd.$);
                dd.hider = function () {
                    options.dropdownPostprocess(dd.$);
                };                
            }
            
            /**
             * Check that all items in the drop down can be displayed - show ellipses at the end of any that
             * are too long. Also remove any unused properties that the dropDown may have stored for each 
             * item in the list.
             */
            $("a span", dd.$).each(function () {
                var $a = $(this),
                    elpss = AJS("var", "&#8230;"),
                    elwidth = elpss[0].offsetWidth,
                    width = this.parentNode.parentNode.parentNode.parentNode.offsetWidth,
                    isLong = false,
                    rightPadding = 20; // add some padding so the ellipsis doesn't run over the edge of the box            
            
                AJS.dropDown.removeAllAdditionalProperties($a);
                
                $a.wrapInner($("<em>"));
                $a.append(elpss);
                this.elpss = elpss;
                $("em", $a).each(function () {
                    var $label = $(this);

                    $label.show();
                    if (this.offsetLeft + this.offsetWidth + elwidth > width - rightPadding) {

                        var childNodes = this.childNodes;
                        var success = false;

                        for (var j = childNodes.length - 1; j >= 0; j--) {
                            var childNode = childNodes[j];
                            var truncatedChars = 1;
    
                            var valueAttr = (childNode.nodeType == 3) ? "nodeValue" : "innerHTML";
                            var nodeText = childNode[valueAttr];
    
                            do {
                                if (truncatedChars <= nodeText.length) {
                                    childNode[valueAttr] = nodeText.substr(0, nodeText.length - truncatedChars++);
                                } else { // if we cannot fit even one character of the next word, then try truncating the node just previous to this
                                    break;
                                }
                            } while (this.offsetLeft + this.offsetWidth + elwidth > width - rightPadding);
    
                            if (truncatedChars <= nodeText.length) {
                                // we've managed truncate part of the word and fit it in
                                success = true;
                                break;
                            }
                        }

                        if (success) {
                            isLong = true;
                        } else {
                            $label.hide();
                        }
                    }
                });
                if (!isLong) {
                    elpss.hide();
                }
            });

            if (old_dd) {
                dd.show();
                dd.method = attr.effect;
                old_dd.$.remove();
            } else {
                dd.show(attr.effect);
            }
            if(typeof onShow == "function") {
                onShow.apply(dd);
            }            
        };
        
        searchBox.oldval = searchBox.val();
        searchBox.keyup(function (e) {
            // Don't open the search box on <enter> or <tab>
            if (e.which == 13 || e.which == 9) {
                return;
			}

            var val = searchBox.val();

            if (val != searchBox.oldval) {
                searchBox.oldval = val;

                if (!searchBox.hasClass("placeholded")) {
                    clearTimeout(timer);

                    if (AJS.params.quickNavEnabled && (/[\S]{2,}/).test(val)) {
                        if (cache[val]) {
                            jsonparser(cache[val]);
                        } else {
                            timer = setTimeout(function () { // delay sending a request to give the user a chance to finish typing their search term(s)
                                return AJS.$.ajax({
                                    type: "GET",
                                    url: contextPath + getPath() + AJS.escape(val),
                                    data: null,
                                    success: jsonparser,
                                    dataType: "json",
                                    global: false,
                                    timeout: 5000,
                                    error: function ( xml, status, e ) { // ajax error handler
                                        if (status == "timeout") {
                                            jsonparser({statusMessage: "Timeout", query: val}, status);
                                        }
                                    }
                                });

                            }, 200);
                        }
                    } else {
                        dd && dd.hide();
                    }
                }
            }
        });

        return this;
    };
})(jQuery);

///*--------------------------------------------------------------------------
// Behaviour for page-permissions.vm
// --------------------------------------------------------------------------*/
AJS.PagePermissions = AJS.PagePermissions || {};

// TODO - send to AUI.
AJS.$.fn.disable = function(element) {
    return this.each(function() {
        var el = AJS.$(this);
        var id = el.attr("disabled", "disabled").addClass("disabled").attr("id");
        if (id) {
            // Only search in the parent - element might not exist in the DOM yet.
            AJS.$("label[for=" + id + "]", el.parent()).addClass("disabled");
        }
    });
};
AJS.$.fn.enable = function(element) {
    return this.each(function() {
        var el = AJS.$(this);
        var id = el.attr("disabled", "").removeClass("disabled").attr("id");
        if (id) {
            AJS.$("label[for=" + id + "]", el.parent()).removeClass("disabled");
        }
    });
};

AJS.toInit(function($) {

    var USER = "user",
        GROUP = "group";

    var contextPath = $("#confluence-context-path").attr("content");
    var isEditPage = !!$("#permissions-show-hide-link").length;

    var popup = null;
    var controls = null;
    var table = null;

    /**
     * Handles the AJAX calls to check for added users and groups, calling PermissionsTable.addEntry if found.
     */
    var permissionManager = {

        // Queries the server for whether an entityName represents a user or group
        // perform subsequent group check inside the callback of the user check so it occurs after the user check completes
        addNames : function (entityNames, entityType) {
            var pm = this;
            var entityNamesArray = entityNames.replace(/\s*,\s*/g, ",").split(",");
            var throbber = $("#waitImage");
            throbber.show();
            // TODO - use the parentPageId / space whither this page may have been MOVED.
            var params = {
                name: entityNamesArray,
                type: entityType || "",
                pageId: AJS.params.parentPageId,
                spaceKey: AJS.params.spaceKey
            };
            $.getJSON(contextPath + "/pages/getentities.action", params, function(results) {
                throbber.hide();
                for (var i = 0, len = results.length; i < len; i++) {
                    var entity = results[i].entity;
                    var report = results[i].report;
                    // 1. Add permission row for entity
                    pm.addEntity(results[i]);

                    // 2. Remove from submitted names list
                    var index = $.inArray(entity.name, entityNamesArray);
                    entityNamesArray.splice(index, 1);
                };
                // 3. Didn't find anything for names - should only occur for names via the form
                controls.validator.handleNonExistentEntityNames(entityNamesArray);
            });
        },

        // Note - dupe validation can't be done before looking up the entity from a name because it depends on the entity type.
        addEntity : function(entityResult) {
            if (!entityResult)
                return;

            var entity = entityResult.entity;
            var report = entityResult.report;

            var currentPermissionType = controls.getPermissionType();
            if (controls.validator.isDuplicateEntityForType(entity, currentPermissionType)) {
                table.highlightEntityRow(entity, currentPermissionType);
                return;
            }

            var entry = {
                entity : entity,
                view : true,     // always give added users/groups both permissions
                edit : true,
                report : report
            };
            table.addRow(entry, currentPermissionType);
            table.changedByUser();
            table.highlightEntityRow(entity, currentPermissionType);
            controls.nameField.removeFromNameInput(entity.name);
        },

        makePermissionStrings : function () {
            var permissions = table.makePermissionMap(false);
            return {
                viewPermissionsUsers : permissions.user.view.join(","),
                editPermissionsUsers : permissions.user.edit.join(","),
                viewPermissionsGroups : permissions.group.view.join(","),
                editPermissionsGroups : permissions.group.edit.join(",")
            };
        },

        /**
         * Calculates the correct height for the dialog divs, needed for the scroll bar.
         * TODO - remove this when DB writes AJS.Dialog2.
         */
        refreshLayout : function() {
            var tablesDiv = $("#page-permissions-tables");
            var dialog = $("#update-page-restrictions-dialog");

            var dialogHeight = dialog.outerHeight();
            var headerHeight = dialog.find("h2").outerHeight();
            var buttonBarHeight = dialog.find(".button-panel").outerHeight();
            var panelHeight = dialogHeight - (headerHeight + buttonBarHeight);
            var formHeight = $("#page-permissions-editor-form").outerHeight();
            var tablesHeight = panelHeight - formHeight;

            $("#update-page-restrictions-dialog .panel-body").height(panelHeight);
            tablesDiv.height(tablesHeight);
        }
    };

    /*--------------------------------------------------------------------------
        Public methods called by pop ups and page-editor.js
    --------------------------------------------------------------------------*/
    $.extend(AJS.PagePermissions, {
        // Callback from Choose Users popup
        addUserPermissions : function (commaDelimitedUserNames) {
            permissionManager.addNames(commaDelimitedUserNames, USER);
        },

        // Callback from Choose Groups popup
        addGroupPermissions : function (commaDelimitedGroupNames) {
            permissionManager.addNames(commaDelimitedGroupNames, GROUP);
        },

        makePermissionStrings : permissionManager.makePermissionStrings
    });

    /**
     * Adds rows to the permission table based on JSON data received from the back end. The data should have three
     * parts :
     *      1. permissions - An array of permission arrays, containing :
     *          a. permissionType
     *          b. entityType
     *          c. entity name (username or groupname)
     *          d. owning content name, if not the current page
     *      2. users - A map of usernames to User objects
     *      3. groups - A map of groupnames to Group objects
     */
    function loadTableFromJson (data) {
        table.allowEditing(data.userCanEditRestrictions);
        table.resetInherited();
        if (!permissionManager.permissionsEdited)
            table.resetDirect();

        if (!data) return;

        // 1. First, build up map of permissions for entity. // UI-973
        // TODO - If this design stays, build the map at the back-end. dT
        for (var i = 0, len = data.permissions.length; i < len; i++) {
            var permission = data.permissions[i];
            var permissionType = permission[0].toLowerCase();   // will come in as "View", "Edit"
            var entityType = permission[1];
            var entityName = permission[2];
            var wrappedEntity = (entityType == USER) ? data.users[entityName] : data.groups[entityName];
            var owningContentId = permission[3];
            var owningContentTitle = permission[4];

            var inherited = +owningContentId && owningContentId != AJS.params.pageId;
            if (permissionManager.permissionsEdited && !inherited)
                continue;

            var entryForEntityForPage = {
                owningId: owningContentId,
                entity: wrappedEntity.entity,
                report: wrappedEntity.report
            };
            entryForEntityForPage[permissionType] = true;
            entryForEntityForPage.owningTitle = owningContentTitle;
            entryForEntityForPage.inherited = inherited;

            table.addRow(entryForEntityForPage, permissionType);
        };

        table.saveBackup();
        table.refresh();
    };

    /**
     * Updates the Restrictions summary on the Page Edit screen with the full names of permitted users and the names of
     * permitted groups.
     *
     * Also synchronizes the hidden permission fields from the permissions table and notifies the user if they are
     * changed from the originals.
     */
    function updateEditPage () {

        var nameMap = table.makePermissionMap(true);

        // todo: refactor these doubled 4 lines of code.
        // todo: refactor this doubled line of comments.
        var viewSummaryDiv = $("#permissions-view-summary");
        var viewNames = [].concat(nameMap.group.view).concat(nameMap.user.view);
        if (viewNames.length) viewSummaryDiv.find(".summary-content").text(viewNames.join(", "));
        AJS.setVisible(viewSummaryDiv, viewNames.length);

        var editSummaryDiv = $("#permissions-edit-summary");
        var editNames = [].concat(nameMap.group.edit).concat(nameMap.user.edit);
        if (editNames.length) editSummaryDiv.find(".summary-content").text(editNames.join(", "));
        AJS.setVisible(editSummaryDiv, editNames.length);

        /**
         * Updates the hidden fields that submit the edited permissions in the form. The fields are updated with the
         * data in the Permissions table.
         */
        permissionManager.permissionsEdited = false;
        var permissionStrs = permissionManager.makePermissionStrings();
        for (var key in permissionStrs) {
            var updatedPermStr = permissionStrs[key];
            $("#" + key).val(updatedPermStr);

            if (permissionManager.originalPermissions[key] != updatedPermStr) {
                permissionManager.permissionsEdited = true;
            }
        }
    };

    /**
     * Closes the dialog after saving or cancelling, scrolling the web page to where it was prior to opening.
     */
    function closeDialog () {
        controls.validator.resetValidationErrors();
        table.clearHighlight();
        popup.hide();
        window.scrollTo(permissionManager.bookmark.scrollX, permissionManager.bookmark.scrollY);
    };

    /**
     * Called when the user saves the permissions. If creating/editing a page, just updates the hidden permission inputs.
     * If on any other screen, saves the permissions to the backend.
     */
    function saveClicked () {
        // TODO - the disabling of the submit button should be in AJS.Dialog.
        var submitButton = $(".permissions-update-button").disable();
        if (isEditPage) {
            updateEditPage();

            // Notify the user that the changes are not yet saved to the back-end.
            AJS.setVisible("#page-permissions-unsaved-changes-msg", permissionManager.permissionsEdited);
            submitButton.enable();
            closeDialog();
        } else {
            var post = permissionManager.makePermissionStrings();
            post.pageId = AJS.params.pageId;
            $("#waitImage").show();

            AJS.safe.post(contextPath + "/pages/setpagepermissions.action", post, function(data) {
                $("#waitImage").hide();

                // If any permissions set, show padlock
                AJS.setVisible("#content-metadata-page-restrictions", data.hasPermissions);
                submitButton.enable();
                closeDialog();
            }, "json");
        }
    };

    /**
     * Called when the user cancels the dialog via Cancel button or escape.
     */
    function cancel () {
        closeDialog();
        if (isEditPage) {
            table.restoreBackup();
        }
    };

    /**
     * Creates the permissions dialog with the main panel coming from a template, then initializes the Controls and Table
     * handlers.
     */
    function initPopup () {
        if (popup) return;

        popup = AJS.ConfluenceDialog({
            width : 865,
            height: 530,
            id: "update-page-restrictions-dialog",
            onCancel: cancel
        });

        popup.addHeader(AJS.I18n.getText("page.perms.dialog.heading"));
        popup.addPanel("Page Permissions Editor", AJS.renderTemplate("page-permissions-div"));
        popup.addButton(AJS.params.statusDialogUpdateButtonLabel || AJS.I18n.getText("update.name"), saveClicked, "permissions-update-button");
        popup.addButton(AJS.params.statusDialogCancelButtonLabel || AJS.I18n.getText("cancel.name"), cancel, "permissions-cancel-button");
        popup.popup.element.find(".button-panel").append(AJS.renderTemplate("page-restrictions-help-link"));
        //    $("#update-page-restrictions-dialog .button-panel").append($("#permissions-inherited-warning"));

        controls = AJS.PagePermissions.Controls(permissionManager);
        table = AJS.PagePermissions.Table($("#page-permissions-table"));
        permissionManager.table = table;
    }

    /**
     * Makes final changes to the popup and then displays it.
     */
    function showPopup (data) {
        permissionManager.bookmark = {
            scrollX : document.documentElement.scrollLeft,
            scrollY : document.documentElement.scrollTop
        };

        $(".permissions-update-button").disable();
        
        controls.setVisible(data.userCanEditRestrictions);

        var cancelButtonText = AJS.I18n.getText(data.userCanEditRestrictions ? "cancel.name" : "close.name");
        $(".permissions-cancel-button").text(cancelButtonText);
        AJS.setVisible(".permissions-update-button", data.userCanEditRestrictions);

        popup.show();
        permissionManager.refreshLayout();
    };

    /**
     * Gets page restrictions (direct and inherited), plus group/user details from the server. Also gets a flag if the 
     * user has permission to change restrictions or not.
     */
    function getPermissionsFromServer (callback, editingPage) {
        // If editingPage, Space and Parent Page may have changed due to the Location editor on the edit screen.
        var spaceKey = (editingPage && $("#newSpaceKey").val()) || AJS.params.spaceKey;
        var parentPageTitle = (editingPage && $("#parentPageString").val()) || "";
        var params = {
            pageId: AJS.params.pageId,
            parentPageId: AJS.params.parentPageId,
            parentPageTitle: parentPageTitle,
            spaceKey: spaceKey
        };
        if (AJS.params.newPage) {
            params.draftId = AJS.params.contentId;
        }

        $("#waitImage").show();
        $.getJSON(contextPath + "/pages/getpagepermissions.action", params, function(data) {
            $("#waitImage").hide();

            loadTableFromJson(data);
            callback(data);
        });
    };

    /**
     * Called when the user opens the popup from the view or edit screens.
     * 
     * @param isEditingAPage true if the popup is being called from a create/edit page screen, false otherwise
     */
    function openPopup (isEditingAPage) {
        initPopup();
        getPermissionsFromServer(showPopup, isEditingAPage);
    };

    $("#content-metadata-page-restrictions, #action-page-permissions-link").click(function (e) {
        openPopup(false);
        e.preventDefault();
        return AJS.stopEvent(e);
    });

    /**
     * Show dialog link for the Create/Edit page screen.
     */
    $("#permissions-show-hide-link").click(function (e) {
        openPopup(true);
        e.preventDefault();
        return AJS.stopEvent(e);
    });

    if (isEditPage) {
        // Store original values of hidden permission fields for comparison on dialog save, to show "Unsaved changes"
        permissionManager.originalPermissions = {
            viewPermissionsUsers : $("#viewPermissionsUsers").val(),
            editPermissionsUsers : $("#editPermissionsUsers").val(),
            viewPermissionsGroups : $("#viewPermissionsGroups").val(),
            editPermissionsGroups : $("#editPermissionsGroups").val()
        };
    }
});

/**
 * Controls the Table component of the Page Permissions dialog.
 */
AJS.PagePermissions.Table = function ($table) {

    var $ = AJS.$;
    var t = this;

    // TODO - remove this if the "Everyone" row design stays (or goes).
    var useEveryOne = false;

    /**
     * Determines if the user can edit the permissions in the table.
     */
    var canEdit = false;

    /**
     * Called when permissions are added or removed, updates the no view/edit rows, markers, and row highlights.
     */
    this.refresh = function () {

        var directViewRows = $table.find(".view-permission-row");
        var editRows = $table.find(".edit-permission-row");

        var hasViewPermissions = directViewRows.length > 0;
        var hasEditPermissions = editRows.length > 0;

        // Display "No view/edit restrictions" message row if no restrictions.
        AJS.setVisible("#page-permissions-no-views", !hasViewPermissions);
        AJS.setVisible("#page-permissions-no-edits", !hasEditPermissions);

        $table.each(function() {
            // Only display "Viewing restricted to" message on first view/edit row.
            $(".view-permission-row", this).removeClass("first-of-type").filter(":first").addClass("first-of-type");
            $(".edit-permission-row", this).removeClass("first-of-type").filter(":first").addClass("first-of-type");
        });

//        // If any entries have warning markers, show the warning legend at the bottom of the dialog.
//        var hasWarnings = !!$("#page-permissions-table .permission-entity-result .icon:not(.hidden)").length;
//        AJS.setVisible("#permissions-inherited-warning", hasWarnings);

        t.clearHighlight();
    };

    /**
     * Saves a copy of the last-changed table in case the user makes changes and then cancels.
     */
    this.saveBackup = function () {
        this.copy = $table.children().clone(true);
    };

    /**
     * Restores the saved copy
     */
    this.restoreBackup = function() {
        $table.children().remove();
        $table.append(this.copy);
    };

    var setupUserHover = function(userlink, username) {
        // Ensure no "popup" title will clash with the user hover.
        $("span, img", userlink).attr("title", "");

        // Find/set the index of this user in the user list
        var users = AJS.contentHover.users;
        var arrayIndex = $.inArray(username, users);
        if (arrayIndex == -1) {
            users.push(username);
            arrayIndex = $.inArray(username, users);
        }
        userlink.addClass("content-hover");

        AJS.contentHover(userlink, arrayIndex, contextPath + "/users/userinfopopup.action?username=" + username, AJS.contentHover.confluencePostProcess);
    };

    // Tracks the number of added rows to use as a unique id.
    this.addCount = 0;

    /**
     * Turns the table rows into a map of entity name arrays.
     * 
     * @param getDisplayNames if true, the display names of the entities are returned instead of the internal names.
     */
    this.makePermissionMap = function (getDisplayNames) {
        var permissions = {
            user : {
                view: [],
                edit: []
            },
            group : {
                view: [],
                edit: []
            }
        };

        $table.find("tr.view-permission-row, tr.edit-permission-row").each(function () {
            var row = $(this);
            var entityType = row.is(".user-permission") ? "user" : "group";
            var permissionType = row.is(".view-permission-row") ? "view" : "edit";

            // For summary get the "full name", else the "name"
            var nameType = (getDisplayNames && (entityType == "user")) ? "display-name" : "name";
            var entityName = row.find(".permission-entity-" + nameType).text();

            permissions[entityType][permissionType].push(entityName);
        });

        return permissions;
    };

    /** Not used unless we move on to checkboxes again */
    this.makePermissionMapForCheckboxes = function (forSummary) {
        var permissions = {
            user : {
                view: [],
                edit: []
            },
            group : {
                view: [],
                edit: []
            }
        };

        $table.find("tr.view-permission-row").each(function () {
            var row = $(this);
            var viewChecked = !!row.find(".view-permission-cell input").attr("checked");
            var editChecked = !!row.find(".edit-permission-cell input").attr("checked");

            if (viewChecked || editChecked) {
                var entityType = row.hasClass("user-permission") ? "user" : "group";

                // For summary get the "full name", else the "name"
                var nameType = (forSummary && (entityType == "user")) ? "display-name" : "name";
                var entityName = row.find(".permission-entity-" + nameType).text();

                // Don't count inherited views unless for summary
                if (viewChecked && (forSummary || !row.hasClass("readonly-permission"))) {
                    permissions[entityType].view.push(entityName);
                }
                if (editChecked) {
                    permissions[entityType].edit.push(entityName);
                }
            }
        });

        return permissions;
    };

    var setupEntity = function (row, entity) {
        var nameColumn = row.find("td.permission-entity");
        var imgSrc = contextPath + (entity.profilePictureDownloadPath || "/images/icons/" + entity.type + "_16.gif");
        nameColumn.find("img").attr("src", imgSrc);

        nameColumn.find(".permission-entity-name").text(entity.name);
        if (entity.type == "group") { // || entity.fullName == entity.name || !entity.name) {
            nameColumn.find(".permission-entity-name-wrap").hide();
        }
        nameColumn.find(".permission-entity-display-name").text(entity.fullName || entity.name);

        var userBox = nameColumn.find("span.entity-container");
        if (entity.type == "user") {
            setupUserHover(userBox, entity.name);
        }
    };

    this.addRow = function(entry, permissionType) {

        var entity = entry.entity;

        var newRowElement = $(AJS.renderTemplate("permissions-row-template"));
        newRowElement.addClass(entity.type + "-permission");
        newRowElement.addClass(permissionType + "-permission-row");

        // Change marker row text to match permission type.
        if (permissionType == "edit") {
            newRowElement.find(".page-permissions-marker-cell span").text(AJS.I18n.getText("page.perms.editing.restricted.to"));
        }

        // 1. User or Group with permission
        setupEntity(newRowElement, entity);

        var readOnlyRow = !canEdit || entry.inherited || entry.readOnly;

        if (readOnlyRow) {
            newRowElement.addClass("readonly-permission");
        }

        // 2. Remove col
        var removeLink = newRowElement.find(".remove-permission-link");
        if (readOnlyRow || !canEdit) {
            removeLink.remove();
        } else {
            removeLink.attr("id", "remove-permission-" + this.addCount++);
            removeLink.click(function (e) {
                $(this).closest("tr").remove();
                t.changedByUser();
                return AJS.stopEvent(e);
            });
        }

        if (entry.inherited) {
            // If there is already a table for the owning content, add this row to that
            var inheritedTable = $(".page-permissions-table[owningTitle='" + entry.owningTitle + "']");
            if (!inheritedTable.length) {
                // Else clone a new table
                var newTableDiv= $(AJS.renderTemplate("page-inherited-permissions-table-div-template"));
                inheritedTable = newTableDiv.find("table");
                inheritedTable.attr("owningTitle", entry.owningTitle);

                var desc = newTableDiv.find(".page-inherited-permissions-table-desc");

                // Title/link for the parent page
                var link = desc.find("a");
                var href = contextPath + "/pages/viewpage.action?pageId=" + entry.owningId;
                link.attr("href", href).attr("target", "_blank").text(entry.owningTitle).addClass("page-perms-owningTitle");

                // Title of the current page
                var editorPageTitle = $("#content-title");  // use the title field if page create or edit 
                var title = editorPageTitle.length ? editorPageTitle.val() : AJS.params.pageTitle;
                desc.find("span").addClass(".page-perms-inherited-this-page").text(title);

                $("#page-inherited-permissions-tables").append(newTableDiv);
            }
            inheritedTable.append(newRowElement);


            $("#page-inherited-permissions-table-div").removeClass("hidden");

        } else {
            // Insert the new row either a) after the last row for the same entity or b) at the end of the table.
            if (permissionType == "view") {
                $("#page-permissions-no-edits").before(newRowElement);
            } else {
                $table.append(newRowElement);
            }
        }
    };

//    /**
//     * Adds a row to the table with permission data for a single entity (user or group) for a single owning page.
//     * TODO - this method unused for non-checkbox/radio version
//     */
//    this.addEntryXX = function(entry) {
//
//        var rowId = this.addCount++;
//
//        var entity = entry.entity;
//        var newRowElement = AJS.clone("#permissions-row-template");
//        newRowElement.addClass(entity.type + "-permission");
//
//        if (entry.report) {
//            $(".permission-entity-result .icon", newRowElement).text(entry.report).removeClass("hidden").attr("title", entry.report);
//        }
//        // 1. User or Group with permission
//        setupEntity(newRowElement, entity);
//
//        var inherited = +entry.owningId && entry.owningId != AJS.params.pageId;
//        var readOnly = !canEdit || inherited || entry.readOnly;
//
//        // 2. View permission
//        var viewCheck = newRowElement.find(".view-permission-cell input:checkbox");
//        var checkId = "view-permission-check-" + rowId;
//        viewCheck.attr("id", checkId);
//        viewCheck.next("label").attr("for", checkId);
//        if (readOnly) {
//            viewCheck.disable();
//        }
//
//        // 3. Edit permission
//        var editCheck = newRowElement.find(".edit-permission-cell input:checkbox");
//        var editLabel = editCheck.next("label");
//        if (inherited) {
//            editCheck.remove();
//            editLabel.remove();
//        } else {
//            checkId = "edit-permission-check-" + rowId;
//            editCheck.attr("id", checkId);
//            editLabel.attr("for", checkId);
//            if (readOnly) {
//                editCheck.disable();
//            }
//        }
//
//        // 4a. Inheritence status
//        var inheritDiv = newRowElement.find(".permission-inherited-div");
//        if (inherited) {
//            // i18n string has <a></a> where the link should go
//            inheritDiv.html(AJS.I18n.getText("page.perms.inherited.detail"));
//
//            var href = contextPath + "/pages/viewpage.action?pageId=" + entry.owningId;
//            var link = inheritDiv.find("a");
//            link.attr("href", href).attr("target", "_blank").text(entry.owningTitle);
//        } else {
//            inheritDiv.remove();
//        }
//
//        if (readOnly) {
//            newRowElement.addClass("readonly-permission");
//        }
//
//        // 4b. Remove col
//        var removeLink = newRowElement.find(".remove-permission-link");
//        if (readOnly) {
//            removeLink.remove();
//        } else if (!canEdit) {
//            removeLink.hide();
//        } else {
//            removeLink.click(function (e) {
//                $(this).parents("tr:first").remove();
//                t.permissionsChanged();
//                t.changedByUser();
//                return AJS.stopEvent(e);
//            });
//        }
//
//        if (inherited) {
//            // If there is already a table for the owning content, add this row to that
//            var inheritedTable = $(".page-permissions-table[owningTitle='" + entry.owningTitle + "']");
//            if (!inheritedTable.length) {
//                // Else clone a new table
//                var newTableDiv= AJS.clone("#page-inherited-permissions-table-div-template");
//                inheritedTable = newTableDiv.find("table");
//                inheritedTable.attr("owningTitle", entry.owningTitle);
//
//                newTableDiv.find(".page-perms-owningTitle").text(entry.owningTitle);
//
//                $("#page-inherited-permissions-tables").append(newTableDiv);
//            }
//            inheritedTable.append(newRowElement);
//
//
//            $("#page-inherited-permissions-table-div").removeClass("hidden");
//
//        } else {
//            // Insert the new row either a) after the last row for the same entity or b) at the end of the table.
//            var entityRows = table.find("tr").filter(function () {
//                return $(".permission-entity-name", this).text() == entity.name;
//            });
//            var prevEntityRow = entityRows.length && $(entityRows[entityRows.length - 1]);
//            if (prevEntityRow) {
//                newRowElement.addClass("user-continued");
//                prevEntityRow.after(newRowElement);
//            } else {
//                table.append(newRowElement);
//            }
//        }
//
//        // Have to check boxes after added to DOM for IE.
//        if (entry.view) {
//            viewCheck.attr("checked", "checked");
//        }
//        if (entry.edit) {
//            editCheck.attr("checked", "checked");
//        }
//        t.permissionsChanged();
//    },

    /**
     * Called when the user interacts with the dialog, adding, removing rows or changing checkboxes.
     */
    this.changedByUser = function () {
        $(".permissions-update-button").attr("disabled", "");
        t.clearHighlight();
        t.refresh();
    };

    /**
     * Resets the table when new JSON data populated. Creates a fake "Everyone" row at the top (if enabled).
     */
    this.resetDirect = function () {
        $table.find("tr:not(.marker-row)").remove();
        t.addCount = 0;

        if (useEveryOne) {
            var everyoneEntry = {
                entity : {
                    name : "",      // Guaranteed to not match any user or group names
                    fullName : AJS.I18n.getText("page.perms.everyone.user"),
                    type : "group"
                },
                readOnly : true,
                view : true,
                edit : true
            };
            this.addEntry(everyoneEntry);
        }
    };

    /**
     * Resets the inherited permissions tables when new JSON data populated.
     */
    this.resetInherited = function () {
        $("#page-inherited-permissions-tables div").remove();
    };

    /**
     * Clear any highlighted rows when the user clicks the direct permissions table.
     */
    $table.click(function (e) {
        t.clearHighlight();
    });

    $("#page-inherited-permissions-table-desc").click(function() {
        $(".page-inheritance-togglable").toggleClass("hidden");
        $(".icon", this).toggleClass("twisty-open").toggleClass("twisty-closed");
    });
    /**
     * Finds the uninherited table row for the given entity and highlights it, e.g. if the user tries to reenter the
     * entity name.
     */
    this.highlightEntityRow = function(entity, permissionType) {
        var highlightedRow = $table.find("." + permissionType + "-permission-row").filter(function() {
            return $(".permission-entity-name", this).text() == entity.name;
        });
        $("#page-permissions-tables").simpleScrollTo(highlightedRow);
        highlightedRow.addClass("highlighted-permission");
    };

    this.clearHighlight = function() {
        $("tr.highlighted-permission").removeClass("highlighted-permission");
    };

    this.allowEditing = function(allowEditing) {
        canEdit = allowEditing;
    };

    return this;
};

/**
 * Controls the Form component of the Page Permissions dialog.
 */
AJS.PagePermissions.Controls = function (permissionManager) {
    var $ = AJS.$;

    /**
     * Adds validation error messages for unknown or duplicate names.
     */
    var validator = {

        handleNonExistentEntityNames : function (entityNames) {
            if (!entityNames || !entityNames.length)
                return;

            var commaDelimitedNames = entityNames.join(", ");

            var errorMsg = AJS.I18n.getText("page.perms.error.invalid.entity.names") + " " + commaDelimitedNames;
            $("#page-permissions-error-div").find("div").text(errorMsg).end().removeClass("hidden");
            permissionManager.refreshLayout();
        },

        isDuplicateEntityForType : function (entity, permissionType) {
            var matches = $("#page-permissions-table ." + permissionType + "-permission-row .permission-entity-name").filter(function() {
                return $(this).text() == entity.name;
            });

            return matches.length > 0;
        },

        // TODO - currently unused because duplicated entity row is now highlighted. If design sticks, remove this method.
//        handleDuplicateEntityName : function(entityName) {
//            this.duplicateNames.push(entityName);
//            var commaDelimitedNames = this.duplicateNames.join(", ");
//            this.validationErrors["duplicateNames"] = AJS.params["page-perms-duplicate-names"] + " " + commaDelimitedNames;
//
//            this.updateAndShowValidationErrors();
//        },

        resetValidationErrors : function () {
            $("#page-permissions-error-div").addClass("hidden");
            permissionManager.refreshLayout();
        }
    };

    /**
     * Handles typing of user/names and groups with autocomplete and placeholder text.
     */
    var nameField = (function() {
        var input = $("#page-permissions-names-input");
        var autocompleted = $("#page-permissions-names-hidden");

        // The placeholder will be set as the initial value of the input.
        var placeholder = input.val();
        var placeholderClass = "input-placeholder";

        input.keypress(function (e) {
            if (e.keyCode == Event.KEY_RETURN) {
                namesEntered();
                input.focus();
                return false;
            }
            return true;
        });

        // TODO(dtaylor) Merge this with username from macro-browser-fields.js when time allows.
        input.quicksearch("/json/contentnamesearch.action?type=userinfo&query=", null,
            {
                dropdownPostprocess : function (list) {
                    // remove the "search for" at the bottom of the list
                    $("ol.last", list).remove();

                    // check if there are items in the drop down. If none then display a
                    // message telling the user this
                    if ($("ol", list).length == 0) {
                        list.append(AJS.renderTemplate("permissions-username-no-suggestion-template"));
                    }
                },
                dropdownPlacement : function (input, dropDown) {
                    var placer = AJS("div");
                    placer.addClass("page-perms-name-dropdown-wrapper aui-dd-parent");
                    placer.append(dropDown);
                    input.after(placer);
                    // dropDownEscapeHandler(input, dropDown);
                },
                ajsDropDownOptions : {
                    selectionHandler: function (e, selection) {
                        // if the user selected the "no matches" message then do nothing
                        if (selection.hasClass("message")) return;

                        var contentProps = $.data(selection.find("span")[0], "properties");
                        var username = contentProps.href.substr(contentProps.href.lastIndexOf("/") + 2);
                        autocompleted.val(unescape(username));
                        input.val("");
                        namesEntered();
                        this.hide();
                        e.preventDefault();
                    }
               }
            }
        );
        input.focus(function() {
            if (input.hasClass(placeholderClass)) {
                input.removeClass(placeholderClass).val("");
            }
            // Reset the position of the autocomplete list each time the input gets focus. This allows for the window
            // being resized (and for the input being hidden when the position is originally calculated).
            var ol = input.next(".aui-dd-parent");
            ol.show();
            var expectedLeftOffset = input.offset().left;
            if (ol.offset().left != expectedLeftOffset) {
                ol.css("margin-left", 0);       // "reset" the offset.
                var olMarginLeft = expectedLeftOffset - ol.offset().left;
                ol.css("margin-left", olMarginLeft + "px");
            }
            var expectedTopOffset = input.offset().top + input.outerHeight();
            if (ol.offset().top != expectedTopOffset) {
                ol.css("margin-top", 0);       // "reset" the offset.
                var olMarginTop = expectedTopOffset - ol.offset().top;
                ol.css("margin-top", olMarginTop + "px");
            }
            ol.css({
                "width" : input.outerWidth()
            });
            ol.hide();
        });
        input.blur(function() {
            if (input.val() == "") {
                input.addClass(placeholderClass).val(placeholder);
            }
        });
        return {
            getValue : function() {
                var names = autocompleted.val();
                if (names) {
                    autocompleted.val("");
                } else {
                    names = input.val();
                    if (names == placeholder) {
                        names = "";
                    }
                }
                return names;
            },

            addPlaceholder : function() {
                input.addClass(placeholderClass).val(placeholder);
            },

            /**
             * Removes a name from the input field (called after the name is found at the back end)
             */
            removeFromNameInput : function (nameToRemove) {
                if (!nameToRemove)
                    return;

                var value = input.val();
                if (!value)
                    return;

                var entityNames = value.split(",");
                for (var i = 0; i < entityNames.length; i++) {
                    entityNames[i] = $.trim(entityNames[i]);
                }

                // remove all empty strings and the entity name that's just been added
                entityNames = $.grep(entityNames, function (name) {
                    return name != "" && name != nameToRemove;
                });

                if (entityNames.length) {
                    input.val(entityNames.join(", "));
                } else {
                    if (document.activeElement == input[0]) {
                        input.val("");
                    } else {
                        this.addPlaceholder();
                    }
                }
            }
        };
    })();

    /**
     * Called when the user hits Enter or clicks the Add button.
     */
    var namesEntered = function () {
        validator.resetValidationErrors();
        permissionManager.table.clearHighlight();
        var names = nameField.getValue();
        if (!names)
            return;

        permissionManager.addNames(names);
    };

    // Choose Me button (User and Group button are wired with VM component
    $("#page-permissions-choose-me").click(function(e) {
        validator.resetValidationErrors();
        permissionManager.addNames($(this).find(".remote-user-name").text());
        return AJS.stopEvent(e);
    });

    $("#permissions-error-div-close").click(function(e) {
        validator.resetValidationErrors();
        return AJS.stopEvent(e);
    });

    // Typed user list submit
    $("#add-typed-names").click(namesEntered);

    return {
        validator: validator,
        
        nameField: nameField,

        setVisible : function (show) {
            AJS.setVisible("#page-permissions-editor-form", show);
            AJS.setVisible(".remove-permission-link", show);

            // Set the user/group name field placeholder
            if (show) nameField.addPlaceholder();
        },

        isShowing : function() {
            return !$("#page-permissions-editor-form").hasClass("hidden");
        },

        getPermissionType : function() {
            return !!$("#restrictViewRadio:checked").length ? "view" : "edit";
        }
    };
};


AJS.toInit(function($) {
    var dialog = new AJS.Dialog(600, 210, "link-page-popup")
        .addHeader(AJS.params.linkToThisPageHeading)
        .addPanel(AJS.params.linkToThisPageHeading, "<form id='link-page-popup-form' class='aui'>" +
                               "<fieldset>" +
                               "</fieldset>" +
                               "</form>")
        .addButton(AJS.params.linkToThisPageClose, function(e) { hideDialog(e); }, "link-page-close-button");
    
    var keydownHandler = function(e) {
        if (e.which == 27) { // ESC
            hideDialog(dialog);
        }
    };

    var links = [
        {
            label: AJS.params.linkToThisPageLink,
            id: "link",
            value: $("link[rel=canonical]").attr("href")
        },
        {
            label: AJS.params.linkToThisPageTinyLink,
            id: "tiny-link",
            value: $("link[rel=shortlink]").attr("href")
        },
        {
            label: AJS.params.linkToThisPageWikiMarkup,
            id: "wiki-markup",
            value: $("meta[name=wikilink]").attr("content")
        }
    ];

    $.each(links, function() {
        $("#link-page-popup-form fieldset").append(AJS.format(
                "<div>" +
                    "<label for=''link-popup-field-{0}''>{1}:</label>" +
                    "<input id=''link-popup-field-{0}'' readonly=''readonly'' value='''' class=''text'' type=''text''>" +
                "</div>", this.id, this.label)).find("input:last").val(this.value);
    });

    var hideDialog = function(dialog) {
        dialog.hide();
        $(document).unbind("keydown", keydownHandler);
    };

    $("#link-to-page-link").click(function(e) {
        // If the fieldset isn't available skip the dialoglogin
        if (!$("#link-to-page-fields")[0]) {
            alert("Due to customisations made to this space's layout, Confluence is unable to load the links dialog. " +
                  "For details on how to correct this, please visit the 'Upgrading Custom Layouts' page in our Confluence " +
                  "Documentation, or contact your administrators.");
            return;
        }
        $(this).parents(".ajs-drop-down")[0].hide();
        dialog.show();
        $(document).keydown(keydownHandler);
        $("#link-page-popup-form #link-popup-field-tiny-link").select().focus();
        return AJS.stopEvent(e);
    });

    var linkText = $("#link-page-popup-form fieldset input.text");
    linkText.focus(function() {
        $(this).select();
    });

    // On Safari the mouse up event deselects the text
    linkText.mouseup(function(e){
        e.preventDefault();
    });

});

AJS.toInit(function ($){
    var editInWord = $('#edit-in-word');
    var pathAuth = false;
    if (!editInWord.length){
       editInWord = $('#edit-in-word-pathauth');
       pathAuth = true;
    }
    if (editInWord.length){
    	 var originalHref = editInWord.attr('href'); 
    	 editInWord.click(function (e) {
              e.preventDefault();
              return doEditInOffice(contextPath, originalHref, 'Word.Document', pathAuth);
          })    	
    }
});

AJS.toInit(function ($){

    var contextPath = document.getElementById('confluence-context-path').content;

    $("a.office-editable").each(function() {
        var $this = $(this);
        $this.click(function() {
            var href = $this.attr('href')
            return doEditInOffice(contextPath, href, getProgID(href), false);
        })
    });
    
    $("a.office-editable-pathauth").each(function() {
        var $this = $(this);
        $this.click(function() {
            var href = $this.attr('href')
            return doEditInOffice(contextPath, href, getProgID(href), true);
        })
    });
});

function getProgID(href)
{
    var ext = href.substring(href.lastIndexOf('.') + 1);

    switch(ext)
    {
        case "ppt":
        case "pptx":
        case "ppsx":
        case "pot":
        case "potx":
        case "pptm":
            return "PowerPoint.Show";
        case "doc":
        case "docx":
        case "dot":
        case "dotx":
            return "Word.Document";
        case "xls":
        case "xlt":
        case "xlsx":
        case "xlst":
        case "xlsm":
            return "Excel.Sheet";
        default:
            return ''
    }
}

function filterPath(urlPath)
{
    var jsession = getCookie('jsessionid');
    if (!jsession){
        jsession = getCookie('JSESSIONID');
    }
    if (jsession){
        var splitPath = urlPath.split('/');
        var newPath = '';
        for (var i = 0; i < splitPath.length - 1; i++){
            
            if (splitPath[i].length){
                newPath = newPath + '/' + splitPath[i];
            }            
        }
        newPath = newPath + '/ocauth/' + jsession + '/' + splitPath[splitPath.length - 1];
        return newPath        
     }
     else{
        return urlPath
     }
}

function getCookie( check_name ) {
    // first we'll split this cookie up into name/value pairs
    // note: document.cookie only returns name=value, not the other components
    var a_all_cookies = document.cookie.split( ';' );
    var a_temp_cookie = '';
    var cookie_name = '';
    var cookie_value = '';
    var b_cookie_found = false; // set boolean t/f default f

    for ( i = 0; i < a_all_cookies.length; i++ ) {
        // now we'll split apart each name=value pair
        a_temp_cookie = a_all_cookies[i].split( '=' );

        // and trim left/right whitespace while we're at it
        cookie_name = a_temp_cookie[0].replace(/^\s+|\s+$/g, '');

        // if the extracted name matches passed check_name
        if ( cookie_name == check_name ){
            b_cookie_found = true;
            // we need to handle case where cookie has no value but exists (no = sign, that is):
            if ( a_temp_cookie.length > 1 ){
                cookie_value = unescape( a_temp_cookie[1].replace(/^\s+|\s+$/g, '') );
            }
            // note that in cases where cookie is initialized but no value, null is returned
            return cookie_value;
            break;
        }
        a_temp_cookie = null;
        cookie_name = '';
    }
    if ( !b_cookie_found ){
        return null;
    }
}

function getBaseUrl(){    
	return location.protocol + "//" + location.host;	
}

function doEditInOffice(contextPath, webDavUrl, progID, usePathAuth)
{
	    var baseUrl = getBaseUrl();
    	if (window.ActiveXObject)// this is IE
    	{
    		var ed; 
    		try
    		{
    			ed = new ActiveXObject('SharePoint.OpenDocuments.1');
    		}
    		catch(err)
    		{
    			window.alert('Unable to create an ActiveX object to open the document. This is most likely because of the security settings for your browser.');
    			return false;
    		}
    		if (ed)
    		{
    			if (usePathAuth)
    			{
    				webDavUrl = filterPath(webDavUrl);
    			}
    			ed.EditDocument(baseUrl + webDavUrl, progID);
    			return false;
    		}
    		else
    		{
    			window.alert('Cannot instantiate the required ActiveX control to open the document. This is most likely because you do not have Office installed or you have an older version of Office.');
    			return false;
    		}  
    	}
    	else if (window.URLLauncher)// this means the OC firefox plugin is installed
    	{
    		var isMac = navigator.appVersion.indexOf("Mac") != -1;
    		if (usePathAuth && !isMac)
			{
				webDavUrl = filterPath(webDavUrl);
			}
    		var wdFile = new URLLauncher();
    		// check and see if they have the function we want.
    		if (wdFile.open2)
    		{
	    		//grok the webdav root path and the remaining path after the root
	    		var regex = new RegExp(contextPath + '/plugins/servlet/[^\/]+/');
	    		var rootPath = webDavUrl.match(regex);
	    		var remainingPath = webDavUrl.substring(rootPath[0].length);	
	    		
	    		wdFile.open2(encodeURI(rootPath[0]), encodeURI(remainingPath));
    		}
    		else
    		{
    			wdFile.open(webDavUrl);
    		}
    	}
    	else if(window.InstallTrigger)
    	{
    		if(window.confirm('A plugin is required to use this feature. Would you like to download it?'))
    		{
    			InstallTrigger.install({'WebDAV Launcher': 'https://update.atlassian.com/office-connector/URLLauncher/latest/webdavloader.xpi'});
    		}
    	}
    	else
    	{
    		window.alert('Firefox or Internet Explorer is required to use this feature.');
    	}
    	return false;
}
function enableEdit(node)
{
	node.style.cursor='pointer';
	node.style.backgroundColor='#cccccc';
	node.style.color='';
	//node.parentNode.style.border='1px solid #cccccc';
}
function disableEdit(node)
{
    node.style.cursor='';
	node.style.backgroundColor='#ffffff';
	node.style.color='#ffffff';
	//node.parentNode.style.border='';	
}
AJS.moreLinkClickHandler=function(B){var A=AJS.$(this),C=A.attr("href"),D=A.closest(".more-link-container");if(!D.length){throw new Error("Could not find more link container when one was expected.")}AJS.$(".waiting-image",D).removeClass("hidden");AJS.$(".more-link-text",A).hide();AJS.$.get(C,function(E){$context=AJS.$(E);D.replaceWith($context);AJS.$(".more-link",$context).click(AJS.moreLinkClickHandler)});return AJS.stopEvent(B)};
AJS.toInit(function(A){A(".more-link").click(AJS.moreLinkClickHandler)});
AJS.toInit(function(A){A("select.content-filter").change(function(){A(".filter-control .waiting-image").removeClass("hidden");var B=A(this);A.get(AJS.params.changesUrl,{contentType:A(this).val()},function(E){var D=A(E);var C=B.parent();C.parent().siblings(".results-container").children("ul").html(D);A(".waiting-image",C).addClass("hidden");A(".more-link",D).click(AJS.moreLinkClickHandler)})})});
AJS.toInit(function($) {
    var hideSidebarCookie = "com.atlassian.confluence.sidebar.hide",
        sidebar = $("#personal-info-sidebar"),
        height = sidebar.height(),
        content = $("#content");

    function toggleSidebar() {
        sidebar.toggleClass("collapsed");
        content.toggleClass("sidebar-collapsed");
    }

    if (getCookie(hideSidebarCookie) == "true") {
        toggleSidebar();
    }

    $(".sidebar-collapse").click(function(e) {
        toggleSidebar();
        setCookie(hideSidebarCookie, sidebar.hasClass("collapsed"));
        return AJS.stopEvent(e);
    }).height(height);
    // sidebar.height(height);
});
AJS.toInit(function($) {
    $(".confluence-thumbnail-link").click(function(e) {
        /* the thumbnail javacode will produce a link with a class specifying the image size widthxheight */
        var size = this.className.match(/(^|\s)(\d+)x(\d+)(\s|$)/);
        window.open(this.href, "thumbnail_popup", "width=" + (+size[2] + 20) + ",height=" + (+size[3] + 20) + ",menubar=no,status=no,toolbar=no");
        return AJS.stopEvent(e);
    });
});

/**
 * Code for toggling version metadata.
 */
AJS.toInit(function ($) {
    var comment = $("#version-comment");
    if (comment.length) {
        var showLink = $("#show-version-comment");
        var hideLink = $("#hide-version-comment");
        showLink.click(function (e) {
            showLink.hide();
            hideLink.show();
            comment.show();
            return AJS.stopEvent(e);
        });
        hideLink.click(function (e) {
            hideLink.hide();
            showLink.show();
            comment.hide();
            return AJS.stopEvent(e);
        });
    }
});

AJS.toInit(function ($) {
    if (!$("#add-labels-form").length) {
        return;
    }

    $(".show-labels-editor").click(function () {
        SuggestedLabelsForEntity.viewLabels(AJS.params.pageId, AJS.Labels.suggestedLabelsCallback);
        // reset the value of this field, just in case the browser wants to become helpful and insert the old value
        $("#labelsString").val("");
        $("#labels-section").addClass("open");

        // update the links
        AJS.setVisible(".show-labels-editor", false);
        AJS.setVisible("a.hide-labels-editor", true);
        AJS.setVisible("#labels-section-title", true);

        $("#labelsString").get(0).focus();
        return false;
    });
    $(".hide-labels-editor").click(function () {
        // clear out any error messages
        AJS.Labels.labelOperationError("");
        $("#errorSpan").html("");
        $("#labels-section").removeClass("open");

        // update the links
        AJS.setVisible("a.hide-labels-editor", false);
        AJS.setVisible(".show-labels-editor", true);
        if ($("#labelsList").children().length == 0 && $("#labelsString").val() == "") { // no labels
            $(".show-labels-editor").addClass("add").text(AJS.params.addLabel);
            AJS.setVisible("#labels-section-title", false);
        }
        else {
            $(".show-labels-editor").removeClass("add").text(AJS.params.editLabel);
            AJS.setVisible("#labels-section-title", true);
        }

        // add label if any user input
        AJS.Labels.addLabelFromInput();
        return false;
    });

    $("#add-labels-form").submit(AJS.Labels.addLabelFromInput);
    $(".labels-editor .add-labels").click(AJS.Labels.addLabelFromInput);
    $(".labels-editor .remove-label").click(AJS.Labels.removeLabel);

    // add return key handling to the label field
    $("#labelsString").keydown(function (event) {
        if (event.which == 13 && !AJS.dropDown.current) {
            AJS.Labels.addLabelFromInput();
            return AJS.stopEvent(event);
        }
    });
});


(function () {
    var originalAjax = jQuery.ajax;

    AJS.safe = {
        ajax: function (options) {
            if (options.data && typeof options.data == "object") {
                options.data.atl_token = jQuery("#atlassian-token").attr("content");
                return originalAjax(options);
            }
        },

        get: function () {
            jQuery.ajax = AJS.safe.ajax;
            try {
                return jQuery.get.apply(jQuery, arguments);
            } finally {
                jQuery.ajax = originalAjax;
            }
        },

        getScript: function(url, callback) {
            return AJS.safe.get(url, null, callback, "script");
        },

        getJSON: function(url, data, callback) {
            return AJS.safe.get(url, data, callback, "json");
        },

        post: function(url, data, callback, type) {
            jQuery.ajax = AJS.safe.ajax;
            try {
                return jQuery.post.apply(jQuery, arguments);
            } finally {
                jQuery.ajax = originalAjax;
            }
        }
    };
})();

// DEPRECATED: Use AJS.safe.ajax() directly
AJS.safeAjax = function(options) {
    return AJS.safe.ajax(options);
};

// An extension to Raphael to draw a spinner (useful to show user when waiting for something to load).
// It returns a function that you can execute later to remove the spinner.
Raphael.spinner = function (holderId, radius, colour) {
    var color = colour || "#fff",
        width = radius * 13 / 60,
        r1 = radius * 35 / 60,
        r2 = radius,
        cx = r2 + width,
        cy = r2 + width,
        r = Raphael(holderId, r2 * 2 + width * 2, r2 * 2 + width * 2),

        sectors = [],
        opacity = [],
        beta = 2 * Math.PI / 12,

        pathParams = {stroke: color, "stroke-width": width, "stroke-linecap": "round"};
    for (var i = 0; i < 12; i++) {
        var alpha = beta * i - Math.PI / 2,
            cos = Math.cos(alpha),
            sin = Math.sin(alpha);
        opacity[i] = i / 12;
        sectors[i] = r.path([["M", cx + r1 * cos, cy + r1 * sin], ["L", cx + r2 * cos, cy + r2 * sin]]).attr(pathParams);
    }
    var tick;
    (function ticker() {
        opacity.unshift(opacity.pop());
        for (var i = 0; i < 12; i++) {
            sectors[i].attr("opacity", opacity[i]);
        }
        r.safari();
        tick = setTimeout(ticker, 80);
    })();
    return function () {
        clearTimeout(tick);
        r.remove();
    };
};
(function ($) {

    $.ui = $.ui || {};

    $.fn.extend({
        spinner: function (options) {
            if (!this.is(".ui-spinner")) {
                return new $.ui.spinner(this, options || {});
            }
        }
    });


    $.ui.spinner = function (anchor, options) {
        this.anchor = anchor;
        this.images = options.images || 
            [
                contextPath + "/images/ddtree/black spinner/1.png", 
                contextPath + "/images/ddtree/black spinner/2.png", 
                contextPath + "/images/ddtree/black spinner/3.png",
                contextPath + "/images/ddtree/black spinner/4.png", 
                contextPath + "/images/ddtree/black spinner/5.png", 
                contextPath + "/images/ddtree/black spinner/6.png", 
                contextPath + "/images/ddtree/black spinner/7.png", 
                contextPath + "/images/ddtree/black spinner/8.png", 
                contextPath + "/images/ddtree/black spinner/9.png", 
                contextPath + "/images/ddtree/black spinner/10.png", 
                contextPath + "/images/ddtree/black spinner/11.png",
                contextPath + "/images/ddtree/black spinner/12.png"
            ];
        this.width = options.width || "16px";
        this.height = options.height || options.width || "16px";
        this.hide = function () {
            this.anchor.hide();
            this.stop();
        };
        this.show = function () {
            this.start();
            this.anchor.show();
        };
        this.fadeIn = function () {
            this.anchor.fadeIn.apply(this.anchor, arguments);
        };
        this.fadeOut = function () {
            this.anchor.fadeOut.apply(this.anchor, arguments);
        };
        this.moveTo = function (x, y) {
            this.anchor.css("top", y);
            this.anchor.css("left", x);
        };
        this.putInBox = function (box) {
            var x = box.x || box.x1,
                y = box.y || box.y1,
                width = (typeof box.width == "undefined") ? box.x2 - box.x1 : box.width,
                height = (typeof box.height == "undefined") ? box.y2 - box.y1 : box.height;
            this.moveTo(x + Math.round((width - this.offsetWidth) / 2), y + Math.round((height - this.offsetHeight) / 2));
        };
        this.start = function () {
            if (!this.timer) {
                this.timer = setInterval(spin, 100);
            }
            return this.timer;
        };
        this.stop = function () {
            clearInterval(this.timer);
            this.timer = null;
        };
        this.divs = [];
        var isIE = /*@cc_on1@*/0;
        for (var i = 0, ii = this.images.length; i < ii; i++) {
            var div = document.createElement("div");
            if (isIE) {
                div.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.images[i] + "', sizingMethod='scale')";
            } else {
                var img = document.createElement("img");
                img.src = this.images[i];
                img.style.width = this.width;
                img.style.height = this.height;
                div.appendChild(img);
            }
            div.style.width = this.width;
            div.style.height = this.height;
            this.anchor.append(div);
            if (!this.offsetWidth) {
                this.offsetWidth = div.offsetWidth;
                this.offsetHeight = div.offsetHeight;
            }
            this.divs.push($(div).hide());
        };
        this.frame = 0;
        this.direction = 1;
        var spinner = this;
        var spin = function () {
            spinner.divs[spinner.frame].hide();
            spinner.frame += spinner.direction;
            if (spinner.frame >= spinner.divs.length) {
                spinner.frame = 0;
            }
            if (spinner.frame < 0) {
                spinner.frame = spinner.divs.length - 1;
            }
            spinner.divs[spinner.frame].show();
        };
        this.anchor.css("position", "absolute");
    };
})(jQuery);
// See http://confluence.atlassian.com/pages/viewpage.action?pageId=155813439 for documentation
(function ($) {

    // Check for ui namespace in jQuery
    $.ui = $.ui || {};

    // Adding tree function to jQuery
    // $("#tree").tree();
    $.fn.extend({
        tree: function (options){
            if (!this.is(".ui-tree")) {
                return new $.ui.tree(this, options);
            }
        }
    });

    var preventDefault = function (e) {
        e.preventDefault();
    };

    // The tree object
    $.ui.tree = function (element, options) {
        var el = element,
            tree = this,
            elCreated = false,
            args = arguments;
        // if element is not a list, create one and add it into the element, in which case
        // set a flag to enable removal when tree reloaded
        if (!(/^[ou]l$/i.test(el[0].tagName))) {
            elCreated = true;
            if (!options.url) {
                return false;
            }
            el.html("<ul></ul>");
            el = $("ul", el);
        }
        var root = el[0];   // either a <ul> or an <ol>
        el.addClass("ui-tree");

        // private variables
        var obj = {
            list: el,
            // array of the items as they appear on the screen
            visibleNodes: [],
            dim: el.offset(),
            // array of the pixels assosiated with items from visibleNodes array
            points: [],
            win: $(window),
            timer: null,
            prev: 0,
            events: {
                grab: function () {},
                click: function () {},
                drag: function () {},
                drop: function () {},
                append: function () {},
                insertabove: function () {},
                insertbelow: function () {},
                load: function () {},
                nodeover: function () {},
                nodeout: function () {},
                onready: function () {},
                order: function() {},
                orderUndo: function() {},
                remove: function() {},
                preview: function() {}
            }
        };

        this.options = options;

        // These functions could be useful for debug
        // this.getLis = function () {
        //     return obj.visibleNodes;
        // };
        // this.getPoints = function () {
        //     return obj.points;
        // };
        // this.update = function () {
        //     updatePoints();
        // };

        /**
        * expands nodes by names provided
        * @method expandPath
        * @param {String} attr set of names to be expanded or/and callback function
        * @param {function} callback function which is called after all the nodes are expanded
        * @return {void}
        */
        this.expandPath = function (attr, callback) {
            callback = callback || function () {};
            if (attr.length) {
                var n = 1, node;
                for (var name in attr[0]) {
                    node = this.findNodeBy(name, attr[0][name]);
                    break;
                }
                // silently return if we can't find the node to expand
                if(!node) {
                    return;
                }
                node.open(function () {
                    if (n < attr.length) {
                        for (name in attr[n]) {
                            node = tree.findNodeBy(name, attr[n][name]);
                            if (node) {
                                break;
                            }
                        }
                        n++;
                        node.open(arguments.callee);
                    } else {
                        callback();
                    }
                });
            } else {
                callback();
            }
        };
        /**
        * reloads the tree
        * @method reload
        * @param {Object} options for recreating tree
        * @return {Object} new tree object
        */
        this.reload = function (options) {
            if (elCreated) {
                el.remove();
            }
            for (var i in options) {
                this.options[i] = options[i];
            }
            return new args.callee(element, this.options);
        };
        /**
        * appends node to the tree
        * @method append
        * @param {Object} node object
        * @return {void}
        */
        this.append = function (node) {
            var newnode = createLI(node);
            el.append(newnode);
            prepareLI.call(newnode);
            updateVisibleNodes();
        };

        /**
         * Remove the highlight from any nodes in the tree that are currently highlighted.
         */
        this.unhighlight = function () {
            el.find("li.highlighted").each(function (index, domElement) {
               $(this).removeClass("highlighted");
            });
        };

        function alphanum(a, b) {
            a = (a + "").toLowerCase();
            b = (b + "").toLowerCase();
            
            var chunks = /(\d+|\D+)/g,
                am = a.match(chunks),
                bm = b.match(chunks),
                len = Math.max(am.length, bm.length);
            for (var i = 0; i < len; i++) {
                if (i == am.length) {
                    return -1;
                }
                if (i == bm.length) {
                    return 1;
                }
                var ad = parseInt(am[i], 10),
                    bd = parseInt(bm[i], 10);
                if (ad == am[i] && bd == bm[i] && ad != bd) {
                    return (ad - bd) / Math.abs(ad - bd);
                }
                if ((ad != am[i] || bd != bm[i]) && am[i] != bm[i]) {
                    return am[i] < bm[i] ? -1 : 1;
                }
            }
            return 0;
        }



        function node($li) {
            this[0] = $li[0];
            this.$ = $li;
            this.text = $li.find("span").text();
            this.href = $li.find("a").attr("href");
            this.linkClass = $li.find("a").attr("class");
            this.nodeClass = $li.attr("class");
            this.open = function (callback) {
                return obj.visibleNodes[this[0].num].open(callback);
            };
            this.insertChild = function (nodeorli) {
                nodeorli.$ && (nodeorli = nodeorli[0]);
                obj.visibleNodes[this[0].num].append(nodeorli);
            };
            this.reorder = function () {
                obj.visibleNodes[this[0].num].order(alphanum);
            };
            this.close = function () {
                obj.visibleNodes[this[0].num].close();
            };
            this.getAttribute = function (attribute){
                return this[0][attribute];
            };
            this.setAttribute = function (attribute, value){
                this[0][attribute] = value;
            };
            this.highlight = function () {
                this.$.addClass("highlighted");
            };
            this.unhighlight = function () {
                this.$.removeClass("highlighted");
            };
            this.makeDraggable = function () {
                this.setAttribute("undraggable", false);
                this.$.removeClass("undraggable");
            };
            this.makeUndraggable = function () {
                this.setAttribute("undraggable", true);
                this.$.addClass("undraggable");
            };
            
            /**
             * The no propagation flag specifies that the addition of click handling will not
             * also apply to the children of the node.
             */            
            this.makeClickable = function (nopropagation) {
                this.setAttribute("unclickable", false);
                this.$.removeClass("unclickable");
                var alllinks = this[0].getElementsByTagName("a");
                var a;
                if (nopropagation) {
                    a = $(alllinks[0]);
                } else {
                    a = $(alllinks);
                }
                
                a.unbind("click", preventDefault);
                a.click(obj.events.click);
            };
            
            /**
             * The no propagation flag specifies that the prevention of click handling will not
             * also apply to the children of the node.
             */
            this.makeUnclickable = function (nopropagation) {
                this.setAttribute("unclickable", true);
                this.$.addClass("unclickable");
                var alllinks = this[0].getElementsByTagName("a");
                var a;
                if (nopropagation) {
                    a = $(alllinks[0]);
                } else {
                    a = $(alllinks);
                }
                
                a.click(preventDefault);
                a.unbind("click", obj.events.click);
            };
            this.setText = function(nodeText) {
                this.text = nodeText;
                this[0].text = nodeText;
                this.$.find("span").text(nodeText);
            };
            /**
             * Return the parent node object for this current node.
             * If the node has no parent then null will be returned.
             */
            this.getParent = function () {
                if (this.$.parent(":not(.ui-tree)").length) {
                    // parent node is not the root ul of the tree
                    var parentEl = this.$.parent().parent();
                    if (parentEl.length) {
                        return new node($(parentEl[0]));
                    }
                }
                
                return null;
            };
            
            this.append = function (node) {
                var uls = this.$.find("ul");
                if (!uls.length) {
                    if (this[0].toBeLoaded) {
                        var theNode = this;
                        this.open(function () {theNode.append(node);});
                        return false;
                    }
                    this.$.append("<ul></ul>");
                    uls = this.$.find("ul");
                }
                var newli = createLI(node);
                uls.append(newli);
                prepareLI.call(newli);
                if (typeof this[0].closed == "undefined") {
                    this.$.addClass("closed");
                    this[0].closed = true;
                    uls.hide();
                }
                updateVisibleNodes();
            };
            this.below = function (node) {
                var newnode = createLI(node);
                this.$.after(newnode);
                prepareLI.call(newnode);
                updateVisibleNodes();
            };
            this.above = function (node) {
                var newnode = createLI(node);
                this.$.before(newnode);
                prepareLI.call(newnode);
                updateVisibleNodes();
            };
            this.remove = function () {
                this.$.remove();
                updateVisibleNodes();
            };
            this.reload = function () {
                if (this[0].getElementsByTagName("ul").length) {
                    this[0].removeChild(this[0].getElementsByTagName("ul")[0]);
                    this.$.removeClass("opened").addClass("closed");
                    this[0].closed = true;
                    obj.visibleNodes[this[0].num].open();
                }
            };
            this.order = function (orderer) {
                var $ul = $("ul", this.$),
                    li = this[0];
                li.ordered = true;
                if ($ul.length) {
                    var oldorder = [];
                    li.oldorder = [];
                    $("li", this.$).each(function () {
                        oldorder.push(this);
                        li.oldorder.push(this);
                    });
                    function sorter(a, b) {
                        return orderer($(a).find("span").html(), $(b).find("span").html());
                    }
                    oldorder.sort(sorter);
                    li.order = oldorder;
                    for (var i = 0, ii = oldorder.length; i < ii; i++) {
                        $ul.append(oldorder[i]);
                    }
                }
                updateVisibleNodes();
            };
            this.orderUndo = function () {
                this[0].ordered = false;
                var $ul = $("ul", this.$);
                if (this[0].oldorder && $ul.length) {
                    for (var i = 0, ii = this[0].oldorder.length; i < ii; i++) {
                        $ul.append(this[0].oldorder[i]);
                    }
                }
                this[0].oldorder = null;
                updateVisibleNodes();
            };
            this.setOrdered = function (isOrdered) {
                this[0].ordered = isOrdered;
                $("a.abc:first", this).css("display", isOrdered ? "none" : "block");
                $("a.rollback:first", this).css("display", "none");
            };
            // Copy any other custom parameters to this object
             if (tree.options.parameters && tree.options.parameters.length) {
                 for (var j = 0, jj = tree.options.parameters.length; j < jj; j++) {
                     if ($li[0][tree.options.parameters[j]]) {
                         this[tree.options.parameters[j]] = $li[0][tree.options.parameters[j]];
                     }
                 }
             }
        }
        /**
        * finds a node in the tree by its attribute
        * @method findNodeBy
        * @param {String} attributeName name of the atribute
        * @param {String} attributeValue value of the atribute
        * @return {Object | Array} node object or array of nodes
        */
        this.findNodeBy = function (attributeName, attributeValue) {
            var results = [], lis = root.getElementsByTagName("li");
            for (var i = 0, ii = lis.length; i < ii; i++) {
                if (lis[i][attributeName] == attributeValue) {
                    // Here we creating an object to be used as a return value
                    results.push(new node($(lis[i])));
                }
            }

            if (results.length == 0) {
                return null;
            } else if (results.length == 1) {
                return results[0];
            } else {
                return results;
            }
        };

        if (options.url) {
            // Spinner
            var div = document.createElement("div");
            div.className = "tree-spinner";
            $("body").append(div);
            obj.spinner = $(div).spinner();
            obj.spinner.hide();
        }

        // copy event handlers from options to obj.events
        for (var i in obj.events) {
            if (typeof options[i] == "function") {
                obj.events[i] = options[i];
            }
        }

        /**
         * Return whether the supplied node is a folder.
         */
        function isFolder(grandpa) {
            return !(grandpa.tagName.toLowerCase() == "li" && $("li:not(.tree-helper)", grandpa).length < 1)
        }

        // Object container for nodes in the tree
        function VisibleNode(li) {
            this.$li = $(li);
            this.height = this.$li.height();
        }
        VisibleNode.prototype.append = function (li) {
            if (this.$li[0] == li) {
                return false;
            }
            if (this.$li[0].toBeLoaded) {
                var item = this;
                this.load(function () {item.append(li);});
                return false;
            }
            if (this.$li[0].tagName.toLowerCase() == "li") {
                var ul = $("ul:first", this.$li);
                var grandpa = li.parentNode.parentNode;
                $(".rollback:first", grandpa).css("display", "none");
                if (ul.length) {
                    ul.append(li);
                    if (this.$li[0].ordered) {
                        this.order(alphanum);
                    }
                } else {
                    ul = document.createElement("ul");
                    ul.appendChild(li);
                    this.$li[0].appendChild(ul);
                    this.$li.addClass("opened");
                    $(".click-zone:first", this.$li).css("display", "inline");
                    $(".rollback:first", this.$li).css("display", "none");
                }
                
                if (!isFolder(grandpa)) {
                    obj.visibleNodes[grandpa.num].notaFolderAnymore();                    
                }
                
                setTimeout(updateVisibleNodes, 0);
                obj.events.append.call({source: li, target: this.$li[0]});
            }
        };
        VisibleNode.prototype.below = function (li) {
            var grandpa = li.parentNode.parentNode;
            this.$li.after(li);
            $(".rollback:first", grandpa).css("display", "none");
            if (isFolder(grandpa)) {
                if (!$(li.parentNode).hasClass("ui-tree") && !li.parentNode.parentNode.undraggable) {
                    li.parentNode.parentNode.ordered = false;
                    $(".abc:first", li.parentNode.parentNode).css("display", "block");
                    $(".rollback:first", li.parentNode.parentNode).css("display", "none");
                }                
            } else {
                obj.visibleNodes[grandpa.num].notaFolderAnymore();
            }
            setTimeout(updateVisibleNodes, 0);
            obj.events.insertbelow.call({source: li, target: this.$li[0]});
        };
        VisibleNode.prototype.above = function (li) {
            var grandpa = li.parentNode.parentNode;
            this.$li.before(li);
            $(".rollback:first", grandpa).css("display", "none");
            if (isFolder(grandpa)) {
                if (!$(li.parentNode).hasClass("ui-tree") && !li.parentNode.parentNode.undraggable) {
                    li.parentNode.parentNode.ordered = false;
                    $(".abc:first", li.parentNode.parentNode).css("display", "block");
                    $(".rollback:first", li.parentNode.parentNode).css("display", "none");
                }                
            } else {
                obj.visibleNodes[grandpa.num].notaFolderAnymore();
            }
            setTimeout(updateVisibleNodes, 0);
            obj.events.insertabove.call({source: li, target: this.$li[0]});
        };
        VisibleNode.prototype.order = function (orderer) {
            var li = this.$li[0];
            li.ordered = true;
            var $ul = $("ul:first", this.$li);
            if ($ul.length) {
                var oldorder = [];
                li.oldorder = [];
                $("li", this.$li).each(function () {
                    if (this.parentNode.parentNode == li) {
                        oldorder.push(this);
                        li.oldorder.push(this);
                    }
                });
                function sorter(a, b) {
                    var atext = $("span", a).text().replace(/^\s+|\s+$/g, ""),
                        btext = $("span", b).text().replace(/^\s+|\s+$/g, "");
                    return orderer(atext, btext);
                }
                oldorder.sort(sorter);
                li.order = oldorder;
                for (var i = 0, ii = oldorder.length; i < ii; i++) {
                    $ul.append(oldorder[i]);
                }
            }
            updateVisibleNodes();
        };
        VisibleNode.prototype.orderUndo = function () {
            var li = this.$li[0];
            li.ordered = false;
            var $ul = $("ul:first", this.$li);
            if (li.oldorder && $ul.length && $ul[0].parentNode == li) {
                for (var i = 0, ii = li.oldorder.length; i < ii; i++) {
                    $ul.append(li.oldorder[i]);
                }
            }
            li.oldorder = null;
            li.oldor = null;
            updateVisibleNodes();
        };
        VisibleNode.prototype.open = function (callback) {
            callback = callback || function () {};
            if (this.$li.hasClass("closed")) {
                var ul = $("ul:has(li)", this.$li);
                if (ul.length) {
                    ul.show();
                    this.closed = false;
                    this.$li.removeClass("closed").addClass("opened");
                    updateVisibleNodes();
                    callback(true);
                    return true;
                } else {
                    return this.load(callback);
                }
            }
            callback(false);
            return false;
        };
        VisibleNode.prototype.close = function (callback) {
            callback = callback || function () {};
            var ul = this.$li.contents().filter("ul:has(li)");
            if (ul.length) {
                ul.hide();
                this.closed = true;
                this.$li.removeClass("opened").addClass("closed");
                obj.visibleNodes.splice(this.$li[0].num + 1, ul[0].getElementsByTagName("li").length);
                updateVisibleNodes();
                callback();
            }
        };
        VisibleNode.prototype.load = function (callback) {
            var url = tree.options.url;
            if (!url) {
                return false;
            }
            callback = callback || function () {};
            this.$li[0].toBeLoaded = false;
            this.$li[0].closed = true;
            var params = {};
            if (options.parameters && options.parameters.length) {
                for (var i = 0, ii = options.parameters.length; i < ii; i++) {
                    params[options.parameters[i]] = (this.$li[0][options.parameters[i]] || "");
                }
            }
            var node = this,
                span = this.$li[0].getElementsByTagName("span")[0],
                spanWidth = span.offsetWidth,
                spanLeft = Math.round($(span).offset().left);
            node.loading = true;
            obj.spinner.putInBox({x:spanLeft + spanWidth, y: this.top, width: 25, height: obj.H});
            obj.spinner.show();
            var insertNodes = function (data) {
                var ul = $("ul", node.$li);
                if (!ul.length) {
                    ul = document.createElement("ul");
                    node.$li[0].appendChild(ul);
                    ul = $(ul);
                }
                node.ordered = (typeof data[0].position != "number");
                for (var i = 0, ii = data.length; i < ii; i++) {
                    var li = createLI(data[i]);
                    ul[0].appendChild(li);
                    prepareLI.call(li);
                }
                ul.hide();
                //  this will clear the loading flag
                node.open(callback);
                obj.events.load();
                obj.spinner.hide();

                // Now know which button should be active, based on node.ordered..
                // Never show revert before changes are made, but show ABC if already ordered.
                node.$li[0].ordered = node.ordered;
                $(".abc:first", node.$li[0]).css("display", node.ordered || li.undraggable ? "none" : "block");
                $(".rollback:first", node.$li[0]).css("display", "none");
            };

            $.ajax({
                url: url,
                type: "GET",
                dataType: "json",
                data: params,
                success: insertNodes
            });
            return true;
        };
        VisibleNode.prototype.notaFolderAnymore = function () {
            this.$li.removeClass("closed").removeClass("opened");
            $(".click-zone:first", this.$li).hide();
            $(".abc:first", this.$li).css("display", "none");
            $(".rollback:first", this.$li).css("display", "none");
            var ul = this.$li[0].getElementsByTagName("ul");
            this.closed = false;
            if (ul.length) {
                this.$li[0].removeChild(ul[0]);
            }
        };

        /**
        * finds a node in the tree by its y coordinate and returns it as a bundle
        * @method getItem
        * @param {String} num name of the atribute
        * @return {Object} bundle of visibleNode, where and top
        */
        function getItem(num) {
            var p = obj.points[num];
            if (typeof p != "undefined") {
                return {visibleNode: obj.visibleNodes[p.num], where: p.where, top: p.top};
            } else {
                return {visibleNode: new VisibleNode(root), where: "append", top: obj.dim.top};
            }
        }

        /**
         * Rebuilds the array of tree nodes by y-coordinate.
         */
        function updatePoints() {
            var prev = {y: 0, num: 0};
            obj.points = [];
            for (var j = 0, jj = obj.visibleNodes.length; j < jj; j++) {
                var offset = obj.visibleNodes[j].$li.offset(),
                    y = Math.round(offset.top);
                obj.visibleNodes[j].top = y;
                obj.visibleNodes[j].left = Math.round(offset.left);
                if (prev.y) {
                    var q = (y - prev.y) / 4;
                    for (var i = prev.y; i < y; i++) {
                        var where = (i - prev.y < q)?"above":(i - prev.y < q * 3)?"append":"below";
                        obj.points[i] = {num: prev.num, where: where, top: prev.y};
                    }
                }
                if (j == jj - 1) {
                    var q = (obj.visibleNodes[j].height) / 4;
                    for (var i = y; i < y + obj.visibleNodes[j].height; i++) {
                        var where = (i - y < q)?"above":(i - y < q * 3)?"append":"below";
                        obj.points[i] = {num: j, where: where, top: y};
                    }
                }
                prev.y = y;
                prev.num = j;
            }
        }
        function updateVisibleNodes() {
            obj.visibleNodes = [];
            var lis = $("li:visible", root); //root.getElementsByTagName("li");
            for (var i = 0, ii = lis.length; i < ii; i++) {
                if (!$(lis[i]).hasClass("tree-helper")) {
                    lis[i].num = obj.visibleNodes.length;
                    obj.visibleNodes.push(new VisibleNode(lis[i]));
                }
            }
            updatePoints();
        }
        this.updateVisibleNodes = updateVisibleNodes;

        // Jquery-specific options for passing to "node.draggable" later. Separated for readability.
        var draggableOptions = function() { var options = {
                distance: 3,
                helper: 'clone',
                opacity: 0.7,
                cursorAt: {top: obj.H / 2, left: 30},
                stop: function(e, ui) {
                    clearInterval(obj.timer);
                    clearTimeout(obj.opentimer);
                    obj.opentimer = null;
                    var item = getItem(obj.prev);
                    item.visibleNode.$li.removeClass("over").removeClass("above").removeClass("append").removeClass("below");
                    item.visibleNode.$li.next().removeClass("over").removeClass("above").removeClass("append").removeClass("below");
                    obj.win.unbind("keypress", obj.escape);
                    delete obj.escape;
                    if (options.revert) {
                        options.revert = false;
                        return false;
                    }
                    item = getItem(e.pageY);
                    // check if the target is the source or it's children
                    var ele = item.visibleNode.$li[0], isOk = true;
                    while (ele != root) {
                        if (ele == this) {
                            isOk = false;
                            break;
                        }
                        ele = ele.parentNode;
                    }
                    // don't insert above next element and don't append to element's parent
                    isOk =  isOk && !(item.where == "above" && item.visibleNode.$li.prev()[0] == this) &&
                            !(item.where == "append" && item.visibleNode.$li[0] == this.parentNode.parentNode);
                    if (isOk) {
                        item.visibleNode[item.where](this);
                        obj.events.drop.call({position: item.where, source: this, target: item.visibleNode.$li[0]});
                    }
                },
                start: function (e, ui) {
                    var target = this;
                    ui.helper.append("<strong></strong>")
                        .addClass("tree-helper")
                        .find(".button-panel").remove();
                    obj.events.grab.call(target);
                    if (this.undraggable) {
                        ui.helper.addClass("no");
                        options.revert = true;
                    }
                    obj.escape = function (e) {
                        if (e.keyCode == 27) {
                            var item = getItem(obj.prev);
                            item.visibleNode.$li.removeClass("over").removeClass("above").removeClass("append").removeClass("below");
                            item.visibleNode.$li.next().removeClass("over").removeClass("above").removeClass("append").removeClass("below");
                            var newhelper = ui.helper.clone();
                            ui.helper.before(newhelper);
                            newhelper.animate({
                                left: Math.round($(target).offset().left) + "px",
                                top: Math.round($(target).offset().top) + "px",
                                opacity: 0
                            }, "slow", "swing", function () {newhelper.remove();});
                            ui.helper.css("display", "none");
                            options.revert = true;
                        }
                    };
                    obj.win.keypress(obj.escape);
                },
                drag: function (e, ui) {
                    var olditem = getItem(obj.prev);
                    olditem.visibleNode.$li.removeClass("above").removeClass("append").removeClass("below");
                    olditem.visibleNode.$li.next().removeClass("above").removeClass("append").removeClass("below");
                    if (!options.revert || obj.out) {
                        obj.prev = e.pageY;
                        var item = getItem(obj.prev);
                        if (item.visibleNode.$li[0] == root) {
                            options.revert = true;
                            obj.out = true;
                            return;
                        } else {
                            if (obj.out) {
                                obj.out = false;
                                options.revert = false;
                            }
                        }
                        if (item.visibleNode != olditem.visibleNode) {
                            obj.events.nodeout.call(olditem.visibleNode.$li);
                            if (obj.opentimer) {
                                clearTimeout(obj.opentimer);
                                obj.opentimer = false;
                            }
                        }
                        obj.events.nodeover.call({element: item.visibleNode.$li, position: item.where});
                        var className = item.where,
                            next = item.visibleNode.$li.next();
                        if (className == "below" && next.length && !next.hasClass("tree-helper")) {
                            next.addClass("above");
                        } else {
                            getItem(obj.prev).visibleNode.$li.addClass(className);
                        }
                        // Openning
                        if (item.where == "append" && (item.visibleNode.closed || item.visibleNode.$li[0].toBeLoaded) && !obj.opentimer) {
                            obj.opentimer = (function (item) {
                                return setTimeout(function () {
                                    item.visibleNode.$li.removeClass("append");
                                    item.visibleNode.open(function () {obj.opentimer = false;});
                                }, 500);
                            })(item);
                        }
                        // Scrolling
                        var f = arguments.callee;
                        if (obj.win.height() - e.pageY + obj.win.scrollTop() < 30) {
                            clearInterval(obj.timer);
                            obj.timer = setInterval(function () {
                                window.scrollBy(0, 4);
                                ui.helper.css("top", parseInt(ui.helper.css("top")) + 4 + "px");
                                f({pageY: e.pageY + 4}, ui);
                            }, obj.win.height() - e.pageY + obj.win.scrollTop());
                        } else {
                            if (obj.win.scrollTop() > 0 && (e.pageY - obj.win.scrollTop()) < 30) {
                                clearInterval(obj.timer);
                                obj.timer = setInterval(function () {
                                    window.scrollBy(0, -4);
                                    f({pageY: e.pageY - 4}, ui);
                                    ui.helper.css("top", parseInt(ui.helper.css("top")) - 4 + "px");
                                }, e.pageY - obj.win.scrollTop());
                            } else {
                                if (obj.timer) {
                                    clearInterval(obj.timer);
                                }
                            }
                        }
                        obj.events.drag.call({element: this, left: e.pageX, top: e.pageY});
                    }
                }
            };
            return options;
        };

        // adds necessary event handlers to HTML <li>
        function prepareLI() {
            var nod = $(this);
            if (tree.options.undraggable) {
                nod.mousedown(preventDefault);
            } else {
                nod.draggable(draggableOptions());
                nod[0].undraggable = nod.hasClass("undraggable");
            }
            var a = $(this.getElementsByTagName("a")[0]);
            if (tree.options.unclickable) {
                nod.addClass("unclickable");
                a.click(preventDefault);
            } else {
                a.click(obj.events.click);
            }

            if (tree.options.oninsert) {
                tree.options.oninsert.call(new node(nod), a);
            }
        };

        $.ui.tree.callNumber = 0;
        function createLI(node) {
            var li = document.createElement("li");
            li.className = node.nodeClass;
            if (tree.options.parameters && tree.options.parameters.length) {
                for (var j = 0, jj = tree.options.parameters.length; j < jj; j++) {
                    if (node[tree.options.parameters[j]]) {
                        li[tree.options.parameters[j]] = node[tree.options.parameters[j]];
                    }
                }
            }
            if (tree.options.nodeId) {
                li.id = "node-" + node[tree.options.nodeId];
            }
            var a = document.createElement("a"),
                span = document.createElement("span"),
                dec = document.createElement("i");
            dec.className = "decorator";
            a.href = node.href;
            span.appendChild(document.createTextNode(node.text));
            a.appendChild(span);
            a.appendChild(dec);
            a.className = node.linkClass;
            clickZone = document.createElement("div");
            $(clickZone).addClass("click-zone");
            var getTarget = function (e) {
                if (e) {
                    getTarget = function () {
                        target = e.target;
                        (target.nodeType == 3) && (target = target.parentNode);
                        return target;
                    };
                } else {
                    getTarget = function () {
                        return window.event.srcElement;
                    };
                }
                return getTarget(e);
            };
            clickZone.onclick = (function (e) {
                if (obj.visibleNodes[this.parentNode.num].loading){
                    return;
                }
                if ($(this.parentNode).hasClass("closed")) {
                    obj.visibleNodes[this.parentNode.num].open();
                } else {
                    obj.visibleNodes[this.parentNode.num].close();
                }
                return false;
            });
            li.onmouseover = (function (e) {
                if (!$(getTarget(e)).hasClass("tree-helper")) {
                    $(".button-panel:first", this).addClass("hover");
                }
                return false;
            });
            li.onmouseout = (function (e) {
                if (!$(getTarget(e)).hasClass("tree-helper")) {
                    $(".button-panel:first", this).removeClass("hover");
                }
                return false;
            });
            li.appendChild(clickZone);
            li.appendChild(a);

            var div = document.createElement("div");
            div.className = "button-panel";
            li.appendChild(div);

            var abc = document.createElement("a");
            abc.className = "abc";
            abc.title = "Sort Alphabetically";
            div.appendChild(abc);

            var cba = document.createElement("a");
            cba.className = "rollback";
            cba.title = "Undo Sorting";
            div.appendChild(cba);

            abc.onclick = function (e) {
                var item = obj.visibleNodes[this.parentNode.parentNode.num];
                item.order(alphanum);
                obj.events.order.call({source: item.$li[0]});
                $(this).hide();
                $(cba).show();
                return false;
            };
            cba.onclick = function (e) {
                var item = obj.visibleNodes[this.parentNode.parentNode.num];
                item.orderUndo();
                obj.events.orderUndo.call({source: item.$li[0], orderedChildren: $("ul:first", item.$li[0]).children()});
                $(this).hide();
                $(abc).show();
                return false;
            };
            if (tree.options.isAdministrator) {
                var preview = document.createElement("a");
                preview.className = "preview-node";
                preview.title = "Preview";
                div.appendChild(preview);

                $(preview).click(function (e) {
                    e.preventDefault();
                    var item = obj.visibleNodes[this.parentNode.parentNode.num];
                    obj.events.preview.call({source: preview, node: item.$li[0]});
                });

                var rem = document.createElement("a");
                rem.className = "remove-node";
                rem.title = "Delete";
                div.appendChild(rem);

                $(rem).click(function (e) {
                    e.preventDefault();
                    var item = obj.visibleNodes[this.parentNode.parentNode.num];
                    obj.events.remove.call({source: item.$li[0]});
                });

            }

            $(abc).css("display", "none");
            $(cba).css("display", "none");

            var $li = $(li);
            if ($li.hasClass("opened")) {
                $li.removeClass("opened").addClass("closed");
                li.closed = true;
            } else if ($li.hasClass("closed")) {
                li.toBeLoaded = true;
            } else {
                $(clickZone).css("display", "none");
            }
            return li;
        }

        // initialisation
        var li = el.contents().filter("li");
        if (li.length > 0) {
            // some tree data exists in the DOM already
            obj.H = li.height();
            li.each(prepareLI);
            updateVisibleNodes();
            obj.events.onready.call(this);
        } else {
            // all tree data is to be loaded from back-end
            var url = tree.options.initUrl || tree.options.url;
            if (!url) {
                return false;
            }
            obj.spinner.putInBox({x: obj.dim.left, y: obj.dim.top, width: 16, height: 16});
            obj.spinner.show();
            var call = ++$.ui.tree.callNumber;
            $.getJSON(url, function (data) {
                var dt = +new Date;
                for (var i = 0, ii = data.length; i < ii; i++) {
                    var li = createLI(data[i]);
                    root.appendChild(li);
                    if (i == 0) {
                        obj.H = $(li).height();
                    }
                    prepareLI.call(li);
                }
                updateVisibleNodes();
                obj.spinner.hide();
                // if the data was re-requested again we only call onready for the latest request
                if (call == $.ui.tree.callNumber){
                    obj.events.onready.call(this);
                    $.ui.tree.callNumber = 0;
                }
            });
        }
        obj.offset = root.offsetTop;
        setInterval(function () {
            if (root.offsetTop != obj.offset) {
                updatePoints();
                obj.offset = root.offsetTop;
            }
        }, 10);
        return this;
    };
})(jQuery);
/**
 * Shortens the set of elements by replacing the last character of each with ellipsis
 * until the condition returns true. Typical usage:
 *
 *   $("#some-list li").shortenUntil(function () { return $("#some-list").width() < 500; });
 *
 * @param condition shortening of elements will happen until this function returns true
 */
jQuery.fn.shortenUntil = function (condition) {
    var $ = jQuery;
    var current = 0;
    while (!condition() && current < this.length) {
        var currentText = $(this[current]).text();
        if (currentText == "\u2026") {
            current++;
            continue;
        }
        $(this[current]).text(currentText.replace(/[^\u2026]\u2026?$/, "\u2026"));
    }
    return this;
};

/**
 * Renders a set of breadcrumbs in the specified element. Typical usage:
 *
 *   $("some-element").renderBreadcrumbs([ { title: "Dashboard", url: "/dashboard.action" }, ... ]);
 *
 * @param items an array of objects with 'title' and 'url' properties, representing the breadcrumbs.
 */
jQuery.fn.renderBreadcrumbs = function (items) {
    var $ = jQuery;
    var el = this;
    var html = [];
    var i = 0;
    var last = items.length - 1;
    
    var space = items[i];
    var spaceClass = space.url.indexOf("~") >= 0 ? "personalspacedesc" : "spacedesc";
    html.push(AJS.renderTemplate("movePageBreadcrumb", space.title,
            space.url, (i == last ? "last content-type-" + spaceClass : ""), space.title));

    while (i++ < last) {
        var parent = items[i];
        html.push(AJS.renderTemplate("movePageBreadcrumb", parent.title,
            parent.url, (i == last ? "last content-type-page" : ""), parent.title));
    }

    this.html(html.join(""));
    $('li a span', this).shortenUntil(function () {
        return el.width() < el.closest(".breadcrumbs-container").width();
    });
    return this;
};

AJS.toInit(function ($) {
    var contextPath = $("#confluence-context-path").attr("content");

    // returns false if the breadcrumb contains the current page
    function isValidLocation(breadcrumbs) {
        for (var i=1; i<breadcrumbs.length; i++) { // skip dashboard and space title
            if (breadcrumbs[i].title == AJS.params.pageTitle) {
                return false;
            }
        }
        return true;
    };

    if (!AJS.MoveDialog) AJS.MoveDialog = {};

    var breadcrumbCache = {}; // cached for entire request -- if this isn't okay, move it into Breadcrumbs class below

    /**
     * Handles retrieval of breadcrumbs via AJAX and caching of the responses until the page reloads.
     */
    AJS.MoveDialog.getBreadcrumbs = function (spaceKey, pageTitle, success, error) {
        var cacheKey = spaceKey + ":" + pageTitle;
        if (cacheKey in breadcrumbCache) {
            success(breadcrumbCache[cacheKey], "success");
            return;
        }
        $.ajax({
            type: "GET",
            dataType: "json",
            data: {
                spaceKey: spaceKey,
                title: pageTitle
            },
            url: contextPath + "/pages/breadcrumb.action",
            error: error || function () { },
            success:  function (data, textStatus) {
                if (!data || !data.breadcrumbs) {
                    error(data, textStatus);
                    return;
                }
                var breadcrumbs = $.makeArray(data.breadcrumbs);
                // strip out "Dashboard" and "People"
                while (breadcrumbs[0] && (/dashboard.action$/.test(breadcrumbs[0].url) || /peopledirectory.action$/.test(breadcrumbs[0].url))) {
                    breadcrumbs.shift();
                }
                breadcrumbCache[cacheKey] = breadcrumbs;
                success(breadcrumbs, textStatus);
            }
        });
    };

    /**
     * Returns an object with an 'update' method, which can be called to render a breadcrumb
     * with that location inside the breadcrumbsElement.
     *
     * @param breadcrumbsElement the element (usually a 'ul') where the breadcrumb will be
     * rendered.
     */
    AJS.MoveDialog.Breadcrumbs = function (breadcrumbsElement) {

        function displayBreadcrumbs(spaceKey, breadcrumbs, controls) {
            breadcrumbsElement.renderBreadcrumbs(breadcrumbs);
            var validLocation = spaceKey != AJS.params.spaceKey || isValidLocation(breadcrumbs);
            if (validLocation) {
                controls.clearErrors();
            } else {
                controls.error(AJS.params.movePageDialogInvalidLocation);
                $("li:last-child", breadcrumbsElement).addClass("warning");
            }
        }

        return {
            /**
             * Updates the breadcrumb to the specified location. Any errors are handled by
             * calling 'controls.error' with the message.
             *
             * @param spaceKey the space key of the new breadcrumb
             * @param pageTitle the page title of the new breadcrumb
             * @param controls should contain an 'error' function which is used to pass
             * errors back to the caller, and a 'clearErrors' which indicates no errors
             * occurred
             */
            update: function (spaceKey, pageTitle, controls) {
                breadcrumbsElement.html(AJS.renderTemplate("movePageBreadcrumbLoading"));

                AJS.MoveDialog.getBreadcrumbs(spaceKey, pageTitle,
                    function (breadcrumbs, textStatus) {
                        if (textStatus != "success" || !breadcrumbs) {
                            breadcrumbsElement.html(AJS.renderTemplate("movePageBreadcrumbError", textStatus));
                            return;
                        }
                        displayBreadcrumbs(spaceKey, breadcrumbs, controls);
                    },
                    function (xhr) {
                        breadcrumbsElement.html(AJS.renderTemplate("movePageBreadcrumbError"));
                        if (xhr.status == 404) {
                            controls.error(AJS.params.movePageDialogLocationNotFound);
                        }
                    }
                );
            }
        };
    };
});

jQuery.fn.movePageAutocomplete = function (url, appendTo, notFoundTemplate, selectionHandler) {
    var $ = jQuery;
    var handler = selectionHandler;
    return $(this).quicksearch(url, null, {
        dropdownPostprocess: function (list) {
            $("> ol.last", list).remove();
            if (!$("> ol", list).length) { // empty list
                $(list).append(AJS.renderTemplate(notFoundTemplate));
            }
            $("> ol:last-child", list).addClass("last");
            $("a", list).attr("tabindex", "-1"); // prevent tabbing to links
        },
        dropdownPlacement: function (input, dropDown) {
            $(appendTo).append(dropDown);
        },
        ajsDropDownOptions: {
            selectionHandler: function (e, selected) {
                if (selected) {
                    this.hide("selected");
                    handler(e, selected);
                    e.preventDefault();
                }
            }
        }
    });
};

/**
 * Renders the 'Known Location' tab with autocomplete in the move page dialog.
 *
 * @param controls should contain:
 * - an 'error' function for passing errors back to the caller
 * - a 'clearErrors' function to indicate no problems occurred
 * - a 'select' function to handle selection of a new parent page
 * - a 'moveButton' jQuery-wrapped element, which is the default button in the dialog
 */
jQuery.fn.movePageLocation = function (controls) {
    var $ = jQuery;
    var container = $(this);
    var space = $("#new-space", container);
    var spaceKey = $("#new-space-key", container);
    var parentPage = $("#new-parent-page", container);
    var aboutToChange = function () {
        clearTimeout(aboutToChange.timer);
        // CONF-17608: Added timeout, so blur event handler wouldn’t overwrite mouse click handler
        aboutToChange.timer = setTimeout(fieldChanged, 500);
    };

    var fieldChanged = function () {
        clearTimeout(aboutToChange.timer);
        if (space.val() == "") {
            space.val(AJS.params.spaceName);
            spaceKey.val(AJS.params.spaceKey);
        }
        controls.clearErrors();
        controls.select(spaceKey.val(), space.val(), parentPage.val());
    };

    parentPage.blur(aboutToChange).focus(function () {
        controls.clearErrors();
        AJS.dropDown.current && AJS.dropDown.current.hide();
    });
    space.blur(aboutToChange).focus(function () {
        AJS.dropDown.current && AJS.dropDown.current.hide();
    });

    space.movePageAutocomplete(
        "/json/contentnamesearch.action?type=spacedesc&type=personalspacedesc&query=",
        $(".new-space-dropdown", container),
        "movePageNoMatchingSpaces",
        function (e, selected) {
            var props = selected.find("span").data("properties");
            spaceKey.val(props.spaceKey);
            space.val(AJS("span").html(props.name).text());
            parentPage.val("");
            fieldChanged();
            parentPage.focus();
        }
    );
    parentPage.movePageAutocomplete(
        function () { return "/json/contentnamesearch.action?type=page&spaceKey=" + spaceKey.val() + "&query="; },
        $(".new-parent-page-dropdown", container),
        "movePageNoMatchingPages",
        function (e, selected) {
            var title = AJS("span").html(selected.find("span").data("properties").name).text();
            parentPage.val(title);
            fieldChanged();
            window.setTimeout(function () {
                controls.moveButton.focus(); // focus slightly afterwards, so Firefox 2 doesn't submit the form
            }, 50);
        }
    );
};

/**
 * The search panel of the move page dialog.
 *
 * @param controls should contain:
 * - an 'error' function for passing errors back to the caller
 * - a 'clearErrors' function to indicate no problems occurred
 * - a 'select' function to handle selection of a new parent page
 */
jQuery.fn.movePageSearch = function (controls) {
    var $ = jQuery;
    var contextPath = $("#confluence-context-path").attr("content");
    var container = this;
    var button = $("input[type=button]", container);
    var query = $("input.search-query", container);
    var space = $(".search-space", container);
    var results = $(".search-results", container);

    // enter keypress on space or query should search
    $([ space[0], query[0] ]).keydown(function (e) {
        if (e.which == 13) {
            button.click();
        }
    });

    // arrows in query or results should move selection up or down
    $([ query[0], results[0] ]).keydown(function (e) {
        function moveSelection(delta) {
            var results = $(".search-result", container);
            var selected = $(".search-result.selected", container);
            var index = results.index(selected) + delta;
            if (index < 0) index = results.length - 1;
            if (index >= results.length) index = 0;
            results.eq(index).click();
        }

        if (e.which == 38) {
            moveSelection(-1);
        } else if (e.which == 40) {
            moveSelection(1);
        }
    });

    button.click(function () {
        controls.clearErrors();
        var queryString = query.val();
        if (queryString == "") {
            results.empty();
            return;
        }
        results.html(AJS.getTemplate("movePageSearchResultsLoading").toString());

        $.ajax({
            type: "GET",
            dataType: "json",
            data: {
                queryString: queryString,
                where: space.val(),
                types: ["spacedesc", "personalspacedesc", "page"]
            },
            url: contextPath + "/json/search.action",
            error: function () {
                controls.error(AJS.params.movePageDialogSearchError);
            },
            success: function(data, status) {
                if (status != "success") {
                    controls.error(AJS.params.movePageDialogSearchError);
                    return;
                }
                if (!data.results || data.results.length == 0) {
                    var message = AJS.format(AJS.params.movePageDialogSearchNoResults, AJS.escapeEntities(queryString));
                    results.html("<div class='no-results'>" + message + "</div>");
                    return;
                }

                results.html(AJS.getTemplate("movePageSearchResultsTable").toString());

                var startIndex = data.startIndex + 1;
                var endIndex = data.startIndex + data.results.length;
                var resultsCount = AJS.format(AJS.params.movePageDialogSearchResultCount, startIndex, endIndex,
                    data.total, AJS.escapeEntities(queryString));
                results.prepend(AJS.renderTemplate("movePageSearchResultsCount", AJS.html(resultsCount)));

                $.each(data.results, function () {
                    var item = this;
                    var el = AJS.$(AJS.renderTemplate("movePageSearchResult", [
                        item.title,
                        item.url,
                        item.type,
                        item.spaceName,
                        item.spaceKey,
                        item.friendlyDate,
                        item.date
                    ]));
                    $(el).click(function (e) {
                        if (item.type == "page") {
                            controls.select(item.spaceKey, item.spaceName, item.title, item.id);
                        } else {
                            controls.select(item.spaceKey, item.spaceName);
                        }
                        results.find(".selected").removeClass("selected");
                        $(this).addClass("selected");
                        return AJS.stopEvent(e);
                    });
                    $(el).hover(function () {
                        $(this).addClass("hover");
                    }, function () {
                        $(this).removeClass("hover");
                    });
                    results.find("table").append(el);
                });
                $(".search-result:first", results).click();
            }
        });
    });
};
/**
 * The history (or 'Recently Viewed' in the new vernacular) panel of the move page dialog.
 *
 * @param controls should contain:
 * - an 'error' function for passing errors back to the caller
 * - a 'clearErrors' function to indicate no problems occurred
 * - a 'select' function to handle selection of a new parent page
 */
jQuery.fn.movePageHistory = function (controls) {
    var $ = jQuery;
    var contextPath = $("#confluence-context-path").attr("content");
    var container = this;
    var results = $(".search-results", container);

    // arrows in results should move selection up or down
    $(results).keydown(function (e) {
        function moveSelection(delta) {
            var results = $(".search-result", container);
            var selected = $(".search-result.selected", container);
            var index = results.index(selected) + delta;
            if (index < 0) index = results.length - 1;
            if (index >= results.length) index = 0;
            results.eq(index).click();
        }

        if (e.which == 38) {
            moveSelection(-1);
        } else if (e.which == 40) {
            moveSelection(1);
        }
    });

    results.html(AJS.getTemplate("movePageHistoryLoading").toString());

    $.ajax({
        type: "GET",
        dataType: "json",
        data: {
            types: ["spacedesc", "personalspacedesc", "page"]
        },
        url: contextPath + "/json/history.action",
        error: function () {
            controls.error(AJS.params.movePageDialogHistoryError);
        },
        success: function(data, status) {
            if (status != "success") {
                controls.error(AJS.params.movePageDialogHistoryError);
                return;
            }
            if (!data.history || data.history.length == 0) {
                results.html("<div class='no-results'>" + AJS.params.movePageDialogHistoryNoResults + "</div>");
                return;
            }

            results.html(AJS.getTemplate("movePageSearchResultsTable").toString());
            $.each(data.history, function () {
                var item = this;
                if (item.id == AJS.params.pageId) { // skip current page
                    return;
                }
                var el = AJS.$(AJS.renderTemplate("movePageSearchResult", [
                    item.title,
                    contextPath + item.url,
                    item.type,
                    item.spaceName,
                    item.spaceKey,
                    item.friendlyDate,
                    item.date
                ]));
                $(el).click(function (e) {
                    if (item.type == "page") {
                        controls.select(item.spaceKey, item.spaceName, item.title, item.id);
                    } else {
                        controls.select(item.spaceKey, item.spaceName);
                    }
                    results.find(".selected").removeClass("selected");
                    $(this).addClass("selected");
                    return AJS.stopEvent(e);
                });
                $(el).hover(function () {
                    $(this).addClass("hover");
                }, function () {
                    $(this).removeClass("hover");
                });
                results.find("table").append(el);
            });
            if ($(".search-result", results).length == 0) {
                results.html("<div class='no-results'>" + AJS.params.movePageDialogHistoryNoResults + "</div>");
            }
        }
    });
};
/**
 * The 'Browse' panel with a tree view in the move page dialog.
 *
 * @param controls includes a 'select' and 'error' callback for handling those events on this panel
 * @param newSpaceKey the space key selected by the user in another panel. The tree should change to show this space.
 * @param newSpaceName the name of the space selected by the user
 * @param newParentPage the parent page selected by the user in another panel. The tree should expand to show this page.
 * @param originalParent the original parent page of the current page. The tree should also expand to show this page.
 */
jQuery.fn.movePageBrowse = function (controls, newSpaceKey, newSpaceName, newParentPage, originalParent) {
    var $ = jQuery;
    var contextPath = $("#confluence-context-path").attr("content");
    var container = this;

    var treeContainer = $(".tree", container);
    treeContainer.addClass("loading").html(AJS.renderTemplate("movePageBrowsePanelLoading"));

    var tree;

    var clickHandler = function (e) {
        e.preventDefault(); // don't follow the link that was clicked

        $("a.highlighted", treeContainer).removeClass("highlighted");
        $(this).addClass("highlighted");

        newSpaceKey = $("#chosenSpaceKey").val();
        newSpaceName = $("#chosenSpaceKey option:selected").text();
        newParentPage = $(this).hasClass("root-node")
            ? ""                           // clicked on space
            : $(this).find("span").text(); // clicked on page
        controls.select(newSpaceKey, newSpaceName, newParentPage);
    };

    var initialiseSpacePicker = function () {
        // initialise the space selector (if there is one - if only one space available then it will be a hidden input)
        $("select#chosenSpaceKey").val(newSpaceKey).change(function () {
            var spaceKey = $(this).val();
            var spaceName = $(this).find("option:selected").text();

            $("#tree-root-node-item a").text(spaceName)
                .toggleClass("highlighted", newSpaceKey == spaceKey && newParentPage == "")
                .toggleClass("current-parent", AJS.params.spaceKey == spaceKey && originalParent == "");

            treeContainer.addClass("rendering");
            tree = tree.reload({
                initUrl: contextPath + "/pages/children.action?spaceKey=" + encodeURIComponent(spaceKey) + "&node=root"
            });
        });

        // create and initialise the treeRootDiv
        $("#tree-root-div").html(AJS.renderTemplate("movePageBrowsePanelSpace", newSpaceName))
            .find("a").click(clickHandler)
                .toggleClass("highlighted", newParentPage == "")
                .toggleClass("current-parent", AJS.params.spaceKey == newSpaceKey && originalParent == "");
    };

    var expandToPage = function (spaceKey, pageTitle, callback) {
        if (spaceKey != $("#chosenSpaceKey").val()) {
            callback(false);
            return;
        }
        AJS.MoveDialog.getBreadcrumbs(spaceKey, pageTitle, function (breadcrumbs) {
            var toExpand = $.map(breadcrumbs.slice(1), function (o) {
                return { text: o.title };
            });
            tree.expandPath(toExpand, function () { callback(true); });
        }, function () {
            controls.error("Could not retrieve tree expansion information.");
            callback(false);
        });
    };

    treeContainer.load(
        contextPath + "/panels/browsepagelocation.action",
        {
            panelName: "browse",
            dialogMode: "view",
            spaceKey: newSpaceKey,
            parentPageString: newParentPage,
            pageId: AJS.params.pageId
        },
        function () {
            treeContainer.removeClass("loading").addClass("rendering");

            initialiseSpacePicker();

            tree = $("#parent-selection-tree").tree({
                url: contextPath + "/pages/children.action",
                initUrl: contextPath + "/pages/children.action?spaceKey=" + encodeURIComponent(newSpaceKey) + "&node=root",
                parameters: ["pageId", "text"],
                undraggable: true,
                nodeId: "pageId",
                click: clickHandler,
                onready: function () {
                    treeContainer.removeClass("rendering").addClass("expanding");
                    $("#parent-selection-tree .button-panel").remove();

                    expandToPage(AJS.params.spaceKey, originalParent, function (found) {
                        if (found && originalParent != "") {
                            var parentPageNode = tree.findNodeBy("text", originalParent);
                            if (parentPageNode) parentPageNode.$.find("> a").addClass("current-parent");
                        }
                        if (found && AJS.params.pageTitle != "") {
                            var pageNode = tree.findNodeBy("text", AJS.params.pageTitle);
                            if (pageNode) pageNode.makeUnclickable();
                        }

                        expandToPage(newSpaceKey, newParentPage, function (found) {
                            if (found) {
                                var selectedNode = tree.findNodeBy("text", newParentPage);
                                if (selectedNode) {
                                    selectedNode.$.find("> a").addClass("highlighted");

                                    // scroll into view
                                    var topOffset = selectedNode.$.position().top;
                                    var containerHeight = treeContainer.height();
                                    if (topOffset < 0 || topOffset > containerHeight) {
                                        treeContainer.scrollTop(treeContainer.scrollTop() + topOffset - containerHeight / 3);
                                    }
                                }
                            }

                            treeContainer.removeClass("expanding");
                        });
                    });
                },
                oninsert: function () {
                    var $li = this.$;
                    if ($li.parents("li:first").attr("unclickable")) {
                        this.makeUnclickable();
                    }
                }
            });
            AJS.MoveDialog.tree = tree;
        }
    );

};

AJS.toInit(function ($) {
    var MovePageDialog = function (options) {
        options = $.extend({
            spaceKey: AJS.params.spaceKey,
            spaceName: AJS.params.spaceName,
            parentPageTitle: AJS.params.parentPageTitle,
            title: AJS.params.movePageDialogViewPageTitle, // "Move Page: 'Title'
            buttonName: AJS.params.movePageDialogMoveButton, // "Move"
            moveHandler: function (dialog) {
                AJS.log("No move handler defined. Closing dialog.");
                dialog.remove();
            },
            cancelHandler: function (dialog) {
                dialog.remove();
            }
        }, options);

        var newSpaceKey = options.spaceKey;
        var newSpaceName = options.spaceName;
        var newParentPage = options.parentPageTitle;

        var structure = AJS.ConfluenceDialog({
            width : 800,
            height: 590,
            id: "move-page-dialog"
        });
        structure.addHeader(options.title);
        structure.addPanel(AJS.params.movePageDialogLocationPanel, AJS.renderTemplate("movePageDialog"), "location-panel", "location-panel-id");
        structure.addPanel(AJS.params.movePageDialogSearchPanel, AJS.renderTemplate("movePageSearchPanel"), "search-panel", "search-panel-id");
        structure.addPanel(AJS.params.movePageDialogHistoryPanel, AJS.renderTemplate("movePageHistoryPanel"), "history-panel", "history-panel-id");
        structure.addPanel(AJS.params.movePageDialogBrowsePanel, AJS.renderTemplate("movePageBrowsePanel"), "browse-panel", "browse-panel-id");

        // panel switching logic
        structure.get("panel:0")[0].onselect = function () {
            $("#new-space-key").val(newSpaceKey);
            $("#new-space").val(newSpaceName);
            $("#new-parent-page").val(newParentPage).select();
        };
        structure.get("panel:1")[0].onselect = function () {
            // always clear out the previous selection
            $("#move-page-dialog .search-panel .search-results .selected").removeClass("selected");
            $("#move-page-dialog input.search-query").focus();
        };
        structure.get("panel:0").select();

        structure.addButton(options.buttonName, function (dialog) {
            var space = $("#new-space:visible").val();
            var spaceKey = $("#new-space-key").val();
            var parentPage = $("#new-parent-page:visible").val();
            if (space && (space != newSpaceName || spaceKey != newSpaceKey || parentPage != newParentPage)) {
                AJS.MoveDialog.getBreadcrumbs(spaceKey, parentPage, function () {
                    options.moveHandler(dialog, spaceKey, space, parentPage, setErrors);
                }, function (xhr) {
                        $('#new-parent-breadcrumbs').html(AJS.renderTemplate("movePageBreadcrumbError"));
                        if (xhr.status == 404) {
                            controls.error(AJS.params.movePageDialogLocationNotFound);
                        }
                    });
            } else {
                options.moveHandler(dialog, newSpaceKey, newSpaceName, newParentPage, setErrors);
            }
        }, "move-button");
        structure.addButton("Cancel", options.cancelHandler, "move-cancel-button");
        structure.show();

        var dialog = $("#move-page-dialog");

        // move breadcrumbs to the bottom of all pages
        $(".location-panel .location-info", dialog).appendTo($(".page-body", dialog));

        // error messages next to the buttons
        $(".button-panel", dialog).prepend(AJS.renderTemplate("movePageErrors"));

        var breadcrumbs = new AJS.MoveDialog.Breadcrumbs($('#new-parent-breadcrumbs'));
        var moveButton = structure.get("button:0")[0].item;

        function setErrors(errors) {
            if (!errors || errors.length == 0) {
                $("#move-errors").addClass("hidden");
                $(moveButton).attr("disabled", "");
                return;
            }
            if (!$.isArray(errors)) errors = [ errors ];
            $("#move-errors").text(errors[0]).attr("title", errors.join("\n")).removeClass("hidden");
            $(moveButton).attr("disabled", "disabled");
        }

        var controls = {
            moveButton: moveButton,
            clearErrors: function () {
                setErrors([]);
            },
            error: setErrors,

            // called when a destination is selected on one of the panels
            select: function (spaceKey, spaceName, parentPageTitle) {
                newSpaceKey = spaceKey;
                newSpaceName = spaceName;
                newParentPage = parentPageTitle || "";
                breadcrumbs.update(newSpaceKey, newParentPage, controls);
            }
        };

        // render the current breadcrumbs immediately
        var originalParent = AJS.params.parentPageTitle != "" ? AJS.params.parentPageTitle : AJS.params.fromPageTitle;
        var currentBreadcrumbs = new AJS.MoveDialog.Breadcrumbs($('#current-parent-breadcrumbs'));
        currentBreadcrumbs.update(AJS.params.spaceKey, originalParent, controls);

        $(".location-panel", dialog).movePageLocation(controls);
        $(".search-panel", dialog).movePageSearch(controls);
        $(".history-panel", dialog).movePageHistory(controls);
        structure.get("panel:2")[0].onselect = function () {
            // refresh the history panel every time it loads, in case the user has navigated elsewhere in another tab
            $(".history-panel", dialog).movePageHistory(controls);
        };
        structure.get("panel:3")[0].onselect = function () {
            // always refresh the tree when loading the Browse tab, don't load it initially
            $(".browse-panel", dialog).movePageBrowse(controls, newSpaceKey, newSpaceName, newParentPage, originalParent);
        };

        $("#new-parent-page").select(); // focus the new parent page input

        return dialog;
    };

    var MovePageParams = function (spaceKey, pageTitle) {
        var params = {
            pageId: AJS.params.pageId,
            spaceKey: spaceKey
        };
        if (pageTitle != "") {
            params.targetTitle = pageTitle;
            params.position = "append";
        } else {
            params.position = "topLevel";
        }
        return params;
    };

    function viewPageMoveHandler(dialog, newSpaceKey, newSpaceName, newParentPage, setErrors) {
        dialog = dialog.popup.element;
        dialog.addClass("waiting");
        $("button", dialog).attr("disabled", "disabled");
        var throbber = $("<div class='throbber'></div>");
        dialog.append(throbber);
        var killSpinner = Raphael.spinner(throbber[0], 100, "#666");

        function error(messages) {
            setErrors(messages);
            dialog.removeClass("waiting");
            killSpinner();
            $("button", dialog).attr("disabled", "");
        }

        $.ajax({
            url: contextPath + "/pages/movepage.action",
            type: "GET",
            dataType: "json",
            data: new MovePageParams(newSpaceKey, newParentPage),
            error: function () {
                error(AJS.params.movePageDialogMoveFailed);
            },
            success: function (data) {
                var errors = [].concat(data.validationErrors || []).concat(data.actionErrors || []).concat(data.errorMessage || []);
                if (errors.length > 0) {
                    error(errors);
                    return;
                }
                window.location.href = contextPath + data.page.url + (data.page.url.indexOf("?") >= 0 ? "&" : "?") + "moved=true";
            }
        });
    }

    $("#action-move-page-dialog-link").click(function (e) {
        e.preventDefault();

        if ($("#move-page-dialog").length > 0) {
            $("#move-page-dialog, body > .shadow, body > .blanket").remove();
        }

        new MovePageDialog({
            moveHandler: viewPageMoveHandler
        });

        return false;
    });

    $("#edit-move-page-dialog-link").click(function (e) {
        e.preventDefault();

        if ($("#move-page-dialog").length > 0) {
            $("#move-page-dialog, body > .shadow, body > .blanket").remove();
        }

        new MovePageDialog({
            spaceKey: $("#newSpaceKey").val(),
            spaceName: $("#space_content").text(),
            parentPageTitle: $("#parentPageString").val(),
            buttonName: AJS.params.movePageDialogOkButton,
            title: AJS.params.movePageDialogEditPageTitle,
            moveHandler: function (dialog, newSpaceKey, newSpaceName, newParentPage, setErrors) {
                // TODO: AJAX validation, should use setErrors
                $("#space_content").text(newSpaceName);
                $("#newSpaceKey").val(newSpaceKey);
                $("#parentPageString").val(newParentPage);
                $("#parent_content").text(newParentPage);
                if (newParentPage != "") {
                    $("#position").val("append");
                    $("#parent_info").removeClass("hidden");
                } else {
                    $("#parent_info").addClass("hidden");
                    $("#position").val("topLevel");
                }
                dialog.remove();
            }
        });

        return false;
    });
    
    // animate the header after moving
    if ($("#header").is(".moved")) {
        $("<div></div>").prependTo($("#header")).css({
            backgroundColor: "#69c",
            position: "absolute",
            left: 0,
            top: 0,
            width: "100%",
            height: "100%",
            "z-index": -1
        }).animate({ opacity: 0.001 }, 1500, function () { $(this).remove(); });
    }
});

/**
 * Atlassian JS animation framework. Simple, but not overly so.
 *
 * TODO: Document this, make it an object
 */
var AJS = AJS || {};

AJS.animation = {
    running: [],
    queue: [],
    timer: null,
    duration: 300,
    period: 20,
    add: function(item) {
        this.queue.push(item);
    },
    start: function() {
        if (this.timer != null) return;
        this.running = this.queue;
        this.queue = [];
        jQuery.each(this.running, function () {
            if (this.onStart) {
                this.onStart();
            }
        });
        var animation = this;
        var startTime = new Date().getTime();
        var endTime = startTime + this.duration;
        this.timer = setInterval(function() {
            var time = new Date().getTime();
            var pos = (time - startTime) / (endTime - startTime);
            if (pos <= 1)
                animation.animate(pos);
            if (pos >= 1 && animation.timer != null)
                animation.finish();
        }, this.period);
        return this.timer;
    },
    finish: function() {
        clearInterval(this.timer);
        jQuery.each(this.running, function () {
            if (this.onFinish) {
                this.onFinish();
            }
        });
        this.running = [];
        this.timer = null; // must be last because it's the lock to prevent concurrent executions
        if (this.queue.length > 0) this.start();
    },
    animate: function(pos) {
        jQuery.each(this.running, function () {
            if (this.animate) {
                this.animate(AJS.animation.interpolate(pos, this.start, this.end, this.reverse));
            }
        });
    },
    interpolate: function(pos, start, end, reverse) {
        if (typeof start != "undefined" && typeof end != "undefined") {
            if (reverse) {
                return end + pos * (start - end);
            } else {
                return start + pos * (end - start);
            }
        }
        return pos;
    },
    combine: function(list) {
        return {
            animations: list,
            append: function(animation) {
                this.animations.push(animation);
                return this;
            },
            onStart: function() {
                jQuery.each(this.animations, function () {
                    if (this.onStart) {
                        this.onStart();
                    }
                });
            },
            onFinish: function() {
                jQuery.each(this.animations, function () {
                    if (this.onFinish) {
                        this.onFinish();
                    }
                });
            },
            animate: function(pos) {
                jQuery.each(this.animations, function () {
                    if (this.animate) {
                        this.animate(AJS.animation.interpolate(pos, this.start, this.end, this.reverse));
                    }
                });
            }
        };
    }
};
var IE = /*@cc_on function(){ switch(@_jscript_version){ case 1.0:return 3; case 3.0:return 4; case 5.0:return 5; case 5.1:return 5; case 5.5:return 5.5; case 5.6:return 6; case 5.7:return 7; }}()||@*/0;

jQuery(function($) { 
    
    if (IE && IE < 7) {
        function applyPngFilter(imageSrc) {
            this.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + (imageSrc || this.src) + "', sizingMethod='scale')"
        }
        $(".logo.anonymous").each(function () {
            var div = document.createElement("div");
            div.className = "replacement";
            applyPngFilter.call(div, this.src);
            $(this).replaceWith(div);
        });
        $(".comment-actions .comment-permalink a").each(function () {
            $(this).addClass("filtered");
            var path_light = $(this).css("background-image").replace(/^url\(\"?|\"?\)$/g, ""); // remove url(...) surrounding actual URL
            var path_dark = path_light.replace("light", "dark");
            applyPngFilter.call(this, path_light);
            this.style.cursor = "pointer";
            this.style.background = "none";
            $(this).hover(function () {
                applyPngFilter.call(this, path_dark);
            }, function () {
                applyPngFilter.call(this, path_light);
            });
        });
    }

    var collapseTransition = function (comment) {
        var imageTransition = function (image, reverse) {
            return {
                animate: function (pos) {
                    image.style.height = AJS.animation.interpolate(pos, 48, 24, reverse) + "px";
                    image.style.width = image.style.height;
                    image.style.marginLeft = AJS.animation.interpolate(pos, 0, 12, reverse) + "px";
                },
                onFinish: function () {
                    image.style.height = '';
                    image.style.width = '';
                    image.style.marginLeft = '';
                }
            };
        };
        var opacityTransition = function (el, reverse) {
            return {
                start: 1.0,
                end: 0.0,
                reverse: reverse,
                animate: function (pos) {
                    el.style.opacity = pos;
                    el.style.filter = "alpha(opacity=" + (pos * 100) + ")";
                },
                onFinish: function () {
                    el.style.opacity = "";
                    el.style.filter = "alpha(opacity=" + (reverse ? 100 : 0) + ")";
                }
            };
        };
        var heightTransition = function (el, reverse) {
            if (!reverse)
                el.originalHeight = jQuery(el).height();
            return {
                start: el.originalHeight || 50,
                end: 0,
                reverse: reverse,
                animate: function (pos) {
                    el.style.height = pos + "px";
                },
                onFinish: function () {
                    el.style.height = '';
                }
            };
        };

        var body = jQuery(comment).find('.comment-body')[0];
        var reverse = comment.className.indexOf("collapsed") >= 0;
        return AJS.animation.combine([
            imageTransition(jQuery(comment).parent().find('.comment-user-logo img, .comment-user-logo .replacement')[0], reverse),
            opacityTransition(body, reverse),
            opacityTransition(jQuery(comment).find('.excerpt')[0], !reverse),
            heightTransition(body, reverse),
            {
                onFinish: function () {
                    if (reverse)
                        jQuery(comment).removeClass('collapsed');
                    else
                        jQuery(comment).addClass('collapsed');
                }
            }
        ]);
    };

    /*
     * Alternate colours of comments. Doing this with threaded comments in the backend
     * is painful.
     */
    $('#comments-section .comment:odd').addClass('odd');

    /*
     * Bind collapsing comment functionality to comment-toggle class.
     */
    function commentToggle() {
        var toggle = this;
        $(toggle).unbind('click');
        AJS.animation.add(collapseTransition($(toggle).parent()[0]));
        AJS.animation.add({
            onFinish: function () { $(toggle).click(commentToggle); } /* rebind */
        });
        AJS.animation.start();
    }
    
    var toggle = $('.comment-toggle');
    toggle.css('cursor', 'pointer');
    toggle.attr("title", AJS.params.collapseTooltip);
    toggle.click(commentToggle);

    /*
     * Remove comment pop-up confirmation.
     */
    $('.comment-action-remove a').click(function() {
        if(confirm(AJS.params.deleteCommentConfirmMessage))
        {
            this.href = this.href + '&confirm=yes';
            return true;
        }
        return false;
    });

    /*
     * Toggle links for hiding and showing the comments section.
     */
    $('#comments-hide').click(function() {
        $('#page-comments').addClass("hidden");
        $(this).addClass("hidden");
        $('#comments-show').removeClass("hidden");
        $('#comments-expand-collapse').addClass("hidden");
        return false;
    });
    $('#comments-show').click(function() {
        $('#page-comments').removeClass("hidden");
        $(this).addClass("hidden");
        $('#comments-hide').removeClass("hidden");
        $('#comments-expand-collapse').removeClass("hidden");
        return false;
    });

    /*
     * Collapse- and expand-all functionality.
     *
     * We only actually animate the first 10 comments. This looks much less jerky
     * and you can't tell the difference because the buttons are only at the top.
     */
    $('#comments-collapse').click(function() {
        $(this).addClass("hidden");
        $('#collapse-wait').removeClass("hidden");
        $('.comment:lt(10):not(.collapsed,.add,.reply,.edit)').each(function () {
                AJS.animation.add(collapseTransition(this));
        });
        AJS.animation.add({
            onFinish: function () {
                $('#collapse-wait').addClass("hidden");
                $('#comments-expand').removeClass("hidden");
                $('.comment:not(.add,.reply,.edit)').addClass("collapsed");
            }
        });
        AJS.animation.start();
        return false;
    });
    $('#comments-expand').click(function() {
        $(this).addClass("hidden");
        $('#expand-wait').removeClass("hidden");
        $('.comment:lt(10).collapsed').each(function () {
            AJS.animation.add(collapseTransition(this));
        });
        AJS.animation.add({
            onFinish: function () {
                $('#expand-wait').addClass("hidden");
                $('#comments-collapse').removeClass("hidden");
                $('.comment').removeClass("collapsed");
            }
        });
        AJS.animation.start();
        return false;
    });
});

AJS.toInit(function ($) {
    $("#ellipsis").click(function () {
        try {
            $(".hidden-crumb", $("#breadcrumbs")).removeClass("hidden-crumb");
            $(this).addClass("hidden-crumb");
        } catch(e) {
            AJS.log(e);
        }
    });
});

AJS.toInit(function ($) {

    /**
     * function to add a tooltip showing the space name to each drop down list item
     */
    AJS.quicksearch = AJS.quicksearch || {};
    AJS.quicksearch.dropdownPostprocess = function (list) {
         $("a span", list).each(function () {
            var $a = $(this);
            // get the hidden space name property from the span
            var spaceName = AJS.dropDown.getAdditionalPropertyValue($a, "spaceName") || "";

            // we need to go through html node creation so that all encoded symbols(like &gt;) are displayed correctly
            if (spaceName) {
                spaceName = " (" + AJS("i").html(spaceName).text() + ")";
            }

            $a.attr("title", $a.text() + spaceName);
        });
    };

    /**
     * Append the drop down to the form element with the class quick-nav-drop-down
     */
    var quickNavPlacement = function (input, dropDown) {
        input.closest("form").find(".quick-nav-drop-down").append(dropDown);
    };
        
    $("#quick-search-query").quicksearch("/json/contentnamesearch.action?query=", null, {
        dropdownPostprocess : AJS.quicksearch.dropdownPostprocess,
        dropdownPlacement : quickNavPlacement 
    });
    if ($("#space-blog-search-query").length && ("#confluence-space-key").length) {
	    $("#space-blog-search-query").quicksearch("/json/contentnamesearch.action?type=blogpost&spaceKey=" + 
	    		AJS("i").html($("#confluence-space-key").attr("content")).text() + "&query=", null, {
	        dropdownPostprocess : AJS.quicksearch.dropdownPostprocess,
	        dropdownPlacement : quickNavPlacement 
	    });
    };
});

(function($) {
    /**
     * Creates a new hover popup
     *
     * @param items jQuery object - the items that trigger the display of this popup when the user mouses over.
     * @param identifier A unique identifier for this popup. This should be unique across all popups on the page and a valid CSS class.
     * @param url The URL to retrieve popup contents.
     * @param postProcess A function called after the popup contents are loaded.
     *                    `this` will be the popup jQuery object, and the first argument is the popup identifier.
     * @param options Custom options to change default behaviour. See AJS.contentHover.opts for default values and valid options.
     *
     * @return jQuery object - the popup that was created
     */
    AJS.contentHover = function(items, identifier, url, postProcess, options) {
        var opts = $.extend(false, AJS.contentHover.opts, options);
        var p, contents;
        var mousePosition;
        var initialMousePosition;
        var popup = $("#content-hover-" + identifier);
        if (!popup.length) {
            // This is the first contentHover call for this identifier - create and set up the content container div.
            $(opts.container).append($('<div id="content-hover-' + identifier + '" class="ajs-content-hover"><div class="contents"></div></div>'));
            popup = $("#content-hover-" + identifier);
            contents = popup.find(".contents");
            contents.css("width", opts.width + "px");
            contents.mouseover(function() {
                clearTimeout(p.hideDelayTimer);
                popup.unbind("mouseover");
            }).mouseout(function() {
                hidePopup();
            });
        } else {
            // A popup has already been created for this identifier (ie. this contentHover call is from a newly-created
            // element with an existing identifier).
            contents = popup.find(".contents");
        }

        // The popup HTML element is persisted across contentHover calls so it can hold data.
        p = popup[0];

        var showPopup = function() {
            if (popup.is(":visible")) {
                return;
            }

            p.showTimer = setTimeout(function() {
                if (!p.contentLoaded || !p.shouldShow) {
                    return;
                }
                p.beingShown = true;
                var posx = mousePosition.x - 3;
                var posy = mousePosition.y + 15;

                if (posx + opts.width + 30 > $(window).width()) {
                    popup.css({
                        right: "20px",
                        left: "auto"
                    });
                }
                else {
                    popup.css({
                        left: posx + "px",
                        right: "auto"
                    });
                }

                var bottomOfViewablePage = (window.pageYOffset || document.documentElement.scrollTop) + $(window).height();
                if ((posy + popup.height()) > bottomOfViewablePage) {
                    posy = bottomOfViewablePage - popup.height() - 5;
                    popup.mouseover(function() {
                        clearTimeout(p.hideDelayTimer);
                    }).mouseout(function() {
                        hidePopup();
                    });
                }
                popup.css({
                    top: posy + "px"
                });

                var shadow = $("#content-hover-shadow").appendTo(popup).show();
                // reset position of popup box
                popup.fadeIn(opts.fadeTime, function() {
                    // once the animation is complete, set the tracker variables
                    p.beingShown = false;
                });

                shadow.css({
                    width: contents.outerWidth() + 32 + "px",
                    height: contents.outerHeight() + 25 + "px"
                });
                $(".b", shadow).css("width", contents.outerWidth() - 26 + "px");
                $(".l, .r", shadow).css("height", contents.outerHeight() - 21 + "px");
            }, opts.showDelay);
        };

        var hidePopup = function() {
            p.beingShown = false;
            p.shouldShow = false;
            clearTimeout(p.hideDelayTimer);
            clearTimeout(p.showTimer);
            clearTimeout(p.loadTimer);
            p.contentLoading = false;
            p.shouldLoadContent = false;
            // store the timer so that it can be cleared in the mouseover if required
            p.hideDelayTimer = setTimeout(function() {
                popup.fadeOut(opts.fadeTime);
            }, opts.hideDelay);
        };

        $(items).mousemove(function(e) {
            mousePosition = { x: e.pageX, y: e.pageY };
            
            if (!p.beingShown) {
                clearTimeout(p.showTimer);
            }
            p.shouldShow = true;
            // lazy load popup contents
            if (!p.contentLoaded) {
                if (p.contentLoading) {
                    // If the mouse has moved more than the threshold don't load the contents
                    if (p.shouldLoadContent) {
                        var distance = (mousePosition.x - initialMousePosition.x)*(mousePosition.x - initialMousePosition.x)
                                + (mousePosition.y - initialMousePosition.y) * (mousePosition.y - initialMousePosition.y);
                        if (distance > (opts.mouseMoveThreshold * opts.mouseMoveThreshold)) {
                            p.contentLoading = false;
                            p.shouldLoadContent = false;
                            clearTimeout(p.loadTimer);
                            return;
                        }
                    }
                } else {
                    // Save the position the mouse started from
                    initialMousePosition = mousePosition;
                    p.shouldLoadContent = true;
                    p.contentLoading = true;
                    p.loadTimer = setTimeout(function () {
                        if (!p.shouldLoadContent)
                            return;

                        contents.load(url, function() {
                            p.contentLoaded = true;
                            p.contentLoading = false;
                            postProcess.call(popup, identifier);
                            showPopup();
                        });
                    }, opts.loadDelay);
                }
            }
            // stops the hide event if we move from the trigger to the popup element
            clearTimeout(p.hideDelayTimer);
            // don't trigger the animation again if we're being shown
            if (!p.beingShown) {
                showPopup();
            }
        }).mouseout(function() {
            hidePopup();
        });

        contents.click(function(e) {
            e.stopPropagation();
        });

        $("body").click(function() {
            p.beingShown = false;
            clearTimeout(p.hideDelayTimer);
            clearTimeout(p.showTimer);
            popup.hide();
        });

        return popup;
    };

    AJS.contentHover.opts = {
        fadeTime: 100,
        hideDelay: 500,
        showDelay: 700,
        loadDelay: 50,
        width: 300,
        mouseMoveThreshold: 10,
        container: "body"
    };

    AJS.toInit(function(){
        $("body").append($('<div id="content-hover-shadow"><div class="tl"></div><div class="tr"></div><div class="l"></div><div class="r"></div><div class="bl"></div><div class="br"></div><div class="b"></div></div>'));
        $("#content-hover-shadow").hide();
    });
})(jQuery);

if (typeof AJS.followCallbacks == "undefined") AJS.followCallbacks = [];
if (typeof AJS.favouriteCallbacks == "undefined") AJS.favouriteCallbacks = [];

// if you want to customize the behaviour of the follow button you can add callbacks to the above array
// by adding this code to your javascript page:
//
// if (typeof AJS.followCallbacks == "undefined") AJS.followCallbacks = [];
// AJS.followCallbacks.push(function(user) {
//    alert('favourite added'+user');
// });
//
// these callbacks are called after the post to the server has completed.
//
// You can add to the followCallbacks or the favouriteCallbacks if you want callbacks on the follow functions
// or the favourite functions respectively.

// Confluence specific code
AJS.toInit(function($) {

    AJS.contentHover.confluencePostProcess  = function(id) {
        var username = users[id];
        $(".ajs-menu-bar", this).ajsMenu();
        $(".follow-icon, .unfollow-icon", this).each(function() {
            var $this = $(this).click(function(e) {
                if ($this.hasClass("waiting")) {
                    return;
                }
                var url = $this.hasClass("unfollow-icon") ? "/unfollowuser.action" : "/followuser.action";
                $this.addClass("waiting");
                AJS.safe.post(contextPath + url + "?username=" + username + "&mode=blank", function() {
                    $this.removeClass("waiting");
                    $this.parent().toggleClass("follow-item").toggleClass("unfollow-item");
                    $.each(AJS.followCallbacks, function() {
                        this(username);
                    });
                });
                return AJS.stopEvent(e);
            });
        });

    };

    AJS.contentHover.users = [];
    var users = AJS.contentHover.users;
    $(".confluence-userlink, .userLogoLink").each(function() {
        var userlink = $(this);
        var matched = /username:([^ ]*)/.exec(userlink.attr("class"));
        if (matched == null) {
            return;
        }
        var username = matched[1];

        // Ensure no "popup" title will clash with the user hover.
        userlink.attr("title", "");
        $("img", userlink).attr("title", "");
        
        var arrayIndex = $.inArray(username, users);
        if (arrayIndex == -1) {
            users.push(username);
            arrayIndex = $.inArray(username, users);
        }

        $(this).addClass("userlink-" + arrayIndex);
    });

    $.each(users, function(i) {
        AJS.contentHover($(".userlink-" + i), i, contextPath + "/users/userinfopopup.action?username=" + users[i], AJS.contentHover.confluencePostProcess);
    });
});
AJS.toInit(function($) {
    function trim(string) {
        return string.replace(/(^\s*)|(\s*$)/g, "");
    }
    function isBlank(string) {
        return trim(string).length == 0;
    }

    // One user status popup per page.
    var popup;
    var statusHolder = $("<div id='current-user-status-holder' class='current-user-latest-status'></div>");
    $("body").append(statusHolder);
    statusHolder.hide();
    var maxChars = 140;

    function createPopUp() {
        var popup = new AJS.Dialog(650, 200, "update-user-status-dialog");
        popup.addHeader(AJS.params.statusDialogHeading || "What are you working on?");
        popup.addPanel("Set Status", "<form class='update-status'>" +
                                     "<textarea name='status-text' id='status-text'></textarea>" +
                                     "<span id='update-status-chars-left'>" + maxChars + "</span>" +
                                     "<div id='dialog-current-status' class='current-user-latest-status'>" +
                                     (AJS.params.statusDialogLatestLabel || "Latest:") +
                                     " <span class='status-text'></span></div>" +
                                     "</form>");
        popup.addButton(AJS.params.statusDialogUpdateButtonLabel || "Update", updateStatus, "status-update-button");
        popup.addButton(AJS.params.statusDialogCancelButtonLabel || "Cancel", function (dialog) {dialog.hide();}, "status-cancel-button");
        popup.popup.element.find(".button-panel").append("<span class='error-message'></span>");
        popup.setError = function(html) {
            $("#update-user-status-dialog .error-message").html(html)
        }
        return popup;
    }
    
    function setCurrentStatus(status) {
        $(".current-user-latest-status .status-text").html(status.text);

        $(".current-user-latest-status a[id^=view]").each(function() {
            var href = $(this).attr("href");
            $(this).attr("href", href.replace(/\d+$/, status.id))
                   .text(status.friendlyDate)
                   .attr("title", new Date(status.date).toLocaleString());
        });
    }

    function getLatestStatus() {
        $.getJSON(contextPath + "/status/current.action", function(data) {
            if (data.errorMessage != null) {
                popup.setError(data.errorMessage);
            }
            else {
                setCurrentStatus(data);
            }
        });
    }

    var updateStatus = function() {
        var textarea = $("#update-user-status-dialog #status-text").attr("disabled", "disabled").blur();
        var text = textarea.val();
        // Move focus away from textarea
        $("#update-user-status-dialog #status-text").blur();
        $("#update-user-status-dialog #status-text").attr("readonly", "readonly");
        $(".status-update-button").attr("disabled", "disabled");

        if (text.length > maxChars || isBlank(text)) {
            return false;
        }
        AJS.safe.ajax({
            url: contextPath + "/status/update.action",
            type: "POST",
            dataType: "json",
            data: {
                "text": text
            },
            success: function(data) {
                if (data.errorMessage != null) {
                    popup.setError(data.errorMessage);
                }
                else {
                    setCurrentStatus(data);
                    $("#update-user-status-dialog #status-text").val("");
                    setTimeout(function() { popup.hide(); }, 1000);
                }
            },
            error: function(xhr, text, error) {
                AJS.log("Error updating status: " + text);
                AJS.log(error);
                popup.setError("There was an error - " + error);
            }
        });
    };
    $("#set-user-status-link").click(function(e) {
        var dropDown = $(this).parents(".ajs-drop-down")[0];
        dropDown && dropDown.hide();

        if (typeof popup == "undefined") {
            popup = createPopUp();
            var $charsLeft = $("#update-status-chars-left");
            var $updateButton = $(".status-update-button").attr("disabled", "disabled");
            $("#update-user-status-dialog form.update-status #status-text").keydown(function(e) {
                if (e.which == 27) { // ESC
                    popup.hide();
                }
                else if (e.which == 13) { // Enter
                    updateStatus();
                }
            }).bind("blur focus change " + ($.browser.mozilla ? "paste input" : "keyup"), function() {
                var length = maxChars - $(this).val().length;
                $charsLeft.removeClass("over-limit").removeClass("close-to-limit").text(length);
                $updateButton.removeAttr("disabled");
                
                if (isBlank($(this).val())) {
                    $updateButton.attr("disabled", "disabled");
                }
                if (length < 0) {
                    $charsLeft.addClass("over-limit").html("&minus;" + -length);
                    $updateButton.attr("disabled", "disabled");
                }
                else if (length < 20) {
                    $charsLeft.addClass("close-to-limit");
                }
            });
            $("#update-user-status-dialog form.update-status").submit(function(e) {
                updateStatus();
                return AJS.stopEvent(e);
            });
        }
        popup.setError("");
        getLatestStatus();
        $("#update-user-status-dialog #status-text").removeAttr("readonly");
        popup.show();
        $("#update-user-status-dialog #status-text").removeAttr("disabled").focus();
        return AJS.stopEvent(e);
    });
});

function placeFocus()
{
    // If the page has been loaded with an #anchor, don't place focus because it breaks the anchor
    // If the page contains a page-edit form, don't place focus because it pisses people off too frequently
    if (document.location.hash || document.getElementById("editpageform") || document.getElementById("createpageform") || document.getElementById("upload-attachments"))
    {
        return;
    }

    // allow ability to customize which textfield the focus goes to (by specifying "?autofocus=<id of element>")
    var autoFocusElementId = "";
    var queryString = window.location.search.substring(1);
    // substring to remove the leading "?"
    var parameterPairs = queryString.split("&");
    for (var i = 0; i < parameterPairs.length; i++)
    {
        var key = parameterPairs[i].split("=")[0];
        var value = parameterPairs[i].split("=")[1];
        if (key == "autofocus" && (value != null && value.length > 0))
        {
            autoFocusElementId = "'" + value + "'";
            // necessary single quotes as element ids returned by element.id contain them
        }
    }

    var stopNow = false;
    for (var i = 0; i < document.forms.length; i++)
    {
        var currSet = document.forms[i].elements;
        if (document.forms[i].id != 'quick-search' && document.forms[i].name != 'inlinecommentform')
        {
            for (var j = 0; j < currSet.length; j++)
            {
                if (
                    (currSet[j].type == 'text' || currSet[j].type == 'password' || currSet[j].type == 'textarea')
                        && !currSet[j].disabled
                        && !(currSet[j].style.display == 'none')
                    )
                {
                    try
                    {
                        if (autoFocusElementId != null && autoFocusElementId.length > 0)
                        {
                            if (currSet[j].id == autoFocusElementId)
                            {
                                currSet[j].focus();
                                stopNow = true;
                                break;
                            }
                        }
                        else
                        {
                            currSet[j].focus();
                            stopNow = true;
                            break;
                        }
                    }
                    catch (e)
                    {
                        // ignore
                        // setting focus to input elements inside hidden div's causes an exception on IE
                    }
                }
            }
        }
        if (stopNow)
            break;
    }
}

function setCookie(name, value, exp_y, exp_m, exp_d, path, domain, secure)
{
    var cookie_string = name + "=" + escape(value);

    if (exp_y)
    {
        var expires = new Date(exp_y, exp_m, exp_d);
        cookie_string += "; expires=" + expires.toGMTString();
    }

    if (path)
        cookie_string += "; path=" + escape(path);
    else
        cookie_string += "; path=/";

    if (domain)
        cookie_string += "; domain=" + escape(domain);

    if (secure)
        cookie_string += "; secure";

    document.cookie = cookie_string;
}

function getCookie(cookie_name)
{
    var results = document.cookie.match(cookie_name + '=(.*?)(;|$)');

    if (results)
        return ( unescape(results[1]) );
    else
        return null;
}

function highlight(element)
{
    new Effect.Highlight(element,{endcolor:"#f0f0f0"});
}


