/* ALL CODE COPYRIGHT ZEHN 2009 */

/* more classes/functions built on top of prototype. This file should probably
 * not ever be overwritten in site_public */

/* MIXINS
 * sets of functions/variables that can be 'inherited' by any class
 */

// a configurable mixin. credit to "Practical Prototype and Scriptaculous",
// Andrew Dupont
var Configurable = {
  set_options: function(options)
  {
    this.options = {};
    var constructor = this.constructor;
    
    // if there's a superclass need to use their options as base
    if(constructor.superclass)
    {
      // work back up ancestors building the ancestor chain
      var chain = [], klass = constructor;
      while(klass = klass.superclass)
        chain.push(klass);
      chain = chain.reverse();
      
      // for each ancestor, starting with eldest, extend options with defaults
      for(var i = 0, len = chain.length; i < len; i++)
      {
       Object.extend(this.options, klass.DEFAULT_OPTIONS || {});
      }
    }
    // extend options first to this class' defaults, then specifics of instance
    Object.extend(this.options, constructor.DEFAULT_OPTIONS);
    return Object.extend(this.options, options || {});
  }
};

// tracks all instances of a class, on a (static) variable of the class. Again
// credit to "Practical Prototype and Scriptaculous", Andrew Dupont
var Trackable = {
  register: function()
  {
    // if class doesn't have tracking object yet, create it
    var c = this.constructor;
    if(!c.instances) c.instances = new Array();
    
    // finally add to tracking object
    c.instances.push(this);
  },
  unregister: function()
  {
    var c = this.constructor;
    c.instances = c.instances.without(this);
  }
};

/* ZEHN
 * zehn namespace holds all my zehn classes and instances of them
 */
var Zehn = {
  editors: new Array()
}; // ensure global reference available
document.observe('dom:loaded', function()
{
  // need to add it here to ensure that the constructors are available
	Zehn.report = new Zehn.StatusReporter();
});

// global stop observing function
Zehn.stop_observing = function(element)
{
  element = $(element);
  element.stopObserving();
  element.select('*').invoke('stopObserving');
}

// returns page of full document as array [x, y]
Zehn.get_page_size = function()
{
  var xScroll, yScroll;
  if(window.innerHeight && window.scrollMaxY)
  {
    xScroll = window.innerWidth + window.scrollMaxX;
    yScroll = window.innerHeight + window.scrollMaxY;
  }
  else if(document.body.scrollHeight > document.body.offsetHeight)
  {
    // all but Explorer Mac
    xScroll = document.body.scrollWidth;
    yScroll = document.body.scrollHeight;
  }
  else
  {
    // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
    xScroll = document.body.offsetWidth;
    yScroll = document.body.offsetHeight;
  }
  
  var windowWidth, windowHeight;
  
  if(self.innerHeight)
  {
    // all except Explorer
    if(document.documentElement.clientWidth)
      windowWidth = document.documentElement.clientWidth;
    else
      windowWidth = self.innerWidth;
    windowHeight = self.innerHeight;
  }
  else if(document.documentElement && document.documentElement.clientHeight)
  {
    // Explorer 6 Strict Mode
    windowWidth = document.documentElement.clientWidth;
    windowHeight = document.documentElement.clientHeight;
  }
  else if(document.body)
  {
    // other Explorers
    windowWidth = document.body.clientWidth;
    windowHeight = document.body.clientHeight;
  }
  
  // for small pages with total height less then height of the viewport
  if(yScroll < windowHeight)
    pageHeight = windowHeight;
  else
    pageHeight = yScroll;
  
  // for small pages with total width less then width of the viewport
  if(xScroll < windowWidth)
    pageWidth = xScroll;
  else
    pageWidth = windowWidth;
  
  return [pageWidth,pageHeight];
}

// reports status of javascript activities to the user
Zehn.StatusReporter = Class.create({
  // constructor
  initialize: function()
  {
    this.registerAjaxResponders();
    this.msgQueue = new Array();
    this.observers();
    
    // make sure we have a 'popup-messages-store' div
    if(!($('popup-messages-store')))
    {
      var div = document.createElement('div');
      div.id = 'popup-messages-store';
      $(div).setStyle({ display: 'none' });
      var body = $$('body');
      body = body[0];
      body.insert(div);
    }
  },
  
  // register responders to act on every ajax request
  registerAjaxResponders: function()
  {
    // register functions to respond to stages of Ajax Requests
    Ajax.Responders.register({
      onCreate: function(request, response)
      {
        request.options.parameters.editorId = 'notright';
        request.url = TO_WEB + 'ajax.php';
        
        this.updateRequestCount();
        // show the ajax loading and highlight
        $('ajax-loading').show().highlight();
      }.bind(this),
      
      onComplete: function(request, response)
      {
        this.updateRequestCount();
        // if number of requests = 0, hide ajax-loading div
        if(Ajax.activeRequestCount == 0)
          $('ajax-loading').hide();
        
        // deal with any errors, should really be onXYZ's but they don't receive
        // a callback by the Ajax.Request (bug in prototype? confusing API?!!)
        var strResponse = response.status.toString(); // ie workaround
        if(strResponse.substr(0,1) == '4')
        {
          if(response.status == 404)
            document.fire("zehnError:created", "Page not found");
          else if(response.status == 419 || response.status == 420)
            document.fire("zehnError:created", response.responseText);
          else
            document.fire("zehnError:created", "Unknown AJAX error");
        }
      }.bind(this)
    });
  },
  
  // close the message at the top of the queue (ie, currently being shown)
  close_shown_message: function()
  {
    this.msgQueue[0].close();
  },
  
  // observe creation and closure of error messages
  observers: function()
  {
    // add listener for the zehn:error event
    document.observe("zehnError:created", function(error)
    {
      this.msgQueue.push(new Zehn.ErrorMessage(error.memo));
      // show this error now if it's the first in the queue (index 0)
      if(this.msgQueue.length == 1)
        this.msgQueue[0].show();
    }.bind(this));
    
    // add listener for the zehnError:closed event
    document.observe("zehnError:closed", function()
    {
      this.msgQueue.shift();
      if(this.msgQueue.length > 0)
        this.msgQueue[0].show();
    }.bind(this));
    
    // add listener for the zehn:error event
    document.observe("zehnMessage:created", function(msg)
    {
      this.msgQueue.push(new Zehn.Message(msg.memo));
      // show this error now if it's the first in the queue (index 0)
      if(this.msgQueue.length == 1)
        this.msgQueue[0].show();
    }.bind(this));
    
    // add listener for the zehnError:closed event
    document.observe("zehnMessage:closed", function()
    {
      this.msgQueue.shift();
      if(this.msgQueue.length > 0)
        this.msgQueue[0].show();
    }.bind(this));
  },
  
  // correctly update the Ajax request count in the ajax-loading element
  updateRequestCount: function()
  {
    $('ajax-request-count').update(Ajax.activeRequestCount);
  }
});

// shows a message, fires an event when it is closed
Zehn.Message = Class.create({
  // accept a string as the actual message or default to 'message'
  initialize: function(message)
  {
    this.message = message || 'message';
    this.rep = 'message';
  },
  
  // show the message (append to dom)
  show: function()
  {
    // fade in the lightbox
    if(!($('lightbox').visible()))
    {
      // resize the lightbox first
      var arrayPageSize = Zehn.get_page_size();
      $('lightbox').setStyle({
        width: arrayPageSize[0] + 'px',
        height: arrayPageSize[1] + 'px'});
      
      $('lightbox').setOpacity(0);
      $('lightbox').show();
      new Effect.Opacity('lightbox', { from: 0.0, to: 0.7, duration: 0.2 });
    }
    
    // clone the popup element
    var div = $$('div#popup-container > .' + this.rep + '-popup')[0];
    div.down('.' + this.rep + '-message').update(this.message);
    
    // set it to be 50 px from top of screen
    //var dOffsets = document.viewport.getScrollOffsets();
    //div.setStyle({ top: (dOffsets.top + 50).toString() + 'px' });
    
    // show and alert to its presence
    div.appear({duration: 0.1});
    
    div.select('.close-popup').each(function(closeLink)
    {
      closeLink.observe('click', this.close_event.bind(this));
    }.bind(this));
  },
  
  // close link observation function
  close_event: function(e)
  {
    e.stop();
    this.close();
  },
  
  // close this popup, fire an event to let zehn know the popup's closed
  close: function()
  {
    // remove observer
    var div = $$('div#popup-container > .' + this.rep + '-popup')[0];
    // hide popup, fire event
    div.hide();
    div.select('a,button,form').invoke('stopObserving');
    div.select('.popup-message').each(function(item)
    {
      $('popup-messages-store').insert(item.remove());
    });
    
    // also fade out then make invisible the lightbox
    new Effect.Opacity('lightbox', { from: 0.7, to: 0.0, duration: 0.2,
      afterFinish: function() { $('lightbox').hide(); }
    });
    
    if(this.rep == 'error') document.fire('zehnError:closed');
    else document.fire('zehnMessage:closed');
  }
});

// shows an error message, fires an event when it is closed
Zehn.ErrorMessage = Class.create(Zehn.Message, {
  // accept a string as the actual message or default to 'error'
  initialize: function(error)
  {
    this.message = error || 'error';
    this.rep = 'error';
  }
});

// handles image hover overs
Zehn.HoverImage = Class.create({
  // accept image or imageId by passing through $, add event observers
  initialize: function(image)
  {
    image = $(image);
    // add two observers
    image.observe('mouseover', this.over.bind(image));
    image.observe('mouseout', this.out.bind(image));
  },
  
  // mouseovered
  over: function()
  {
    var pos = this.src.lastIndexOf('.jpg');
    this.src = this.src.substr(0, pos) + '-over.jpg';
  },
  
  // mouseouted
  out: function()
  {
    var pos = this.src.lastIndexOf('-over');
    this.src = this.src.substr(0, pos) + '.jpg';
  }
});

// handles a set of tabs
Zehn.TabSet = Class.create({
  // accept containing div to whole tab set. This should have a ul.links with
  // <li>'s inside, each one with id of 'tab-id'
  // div.tab-content for each, with corresponding ids of 'tab-content-id'
  initialize: function(container)
  {
    this.container = container;
    this.container.down('ul.links').select('li').each(function(link)
    {
      link.observe('click', this.switch_content.bind(this));
    }.bind(this));
  },
  
  // switch the content of the tab set
  switch_content: function(e)
  {
    e.stop();
    element = e.element();
    if(!element.match('li'))
      element = element.up('li');
    
    // deselect all links
    this.container.down('ul.links').select('li').each(function(li)
    {
      li.removeClassName('selected');
    });
    
    // select this link
    element.addClassName('selected');
    
    // hide all contents
    this.container.select('div.tab-content').each(function(tabContent)
    {
      tabContent.hide();
    });
    
    // show specific content
    var tabContent = $('tab-content-' + element.id.substr(4)); 
    tabContent.show();
    
    // do we need to load?
    if(tabContent.hasClassName('ajax-load'))
    {
      tabContent.fire("tab:load");
      tabContent.removeClassName('ajax-load');
    }
  }
});

// handles toggling of more/less links
Zehn.ToggleContent = Class.create({
  // accept element which contains divs of toggle and content
  initialize: function(container)
  {
    this.container = $(container);
    this.content = this.container.down('.toggle-content');
    this.toggler = this.container.down('.toggle-switch');
    this.moreImage = this.toggler.down('img.more');
    this.lessImage = this.toggler.down('img.less');
    
    // listen to clicks on link in toggle
    this.toggler.down('a').observe('click', this.toggle.bind(this));
  },
  
  // toggle it
  toggle: function(e)
  {
    e.stop(); // stop propagation of click
    if(this.content.visible())
    {
      this.content.hide();
      this.moreImage.show();
      this.lessImage.hide();
      this.content.fire("content:hidden");
    }
    else
    {
      this.content.show();
      this.moreImage.hide();
      this.lessImage.show();
      this.content.fire("content:shown");
    }
  }
});

// new in place editor for zehn
Zehn.InPlaceEditor = Class.create(Configurable, Trackable, {
  initialize: function(element, options)
  {
    this.element = element;
    this.value_element = element.down('.value');
    this.form_element = element.down('.form');
    this.register(); // register with Trackable
    this.set_options(options); // set options with Configurable
    this.add_listeners();
  },
  
  // add event listeners
  add_listeners: function()
  {
    this.value_element.observe('mouseover', this.over.bind(this));
    this.value_element.observe('mouseout', this.out.bind(this));
    this.value_element.observe('click', this.enter_edit.bind(this));
    this.form_element.down('form').observe('submit', this.save.bind(this));
    this.form_element.down('a.cancel').observe('click',
      this.leave_edit.bind(this));
  },
  
  // enter hover
  over: function()
  {
    this.value_element.addClassName(this.options.hoverClassName);
  },
  
  // leave hover
  out: function() 
  {
    this.value_element.removeClassName(this.options.hoverClassName);
  },
  
  // enter edit mode
  enter_edit: function()
  {
    this.constructor.instances.each(function(item) { item.leave_edit(); });
    this.value_element.hide();
    this.form_element.show();
    this.form_element.
      down('input[type=text],input[type=password],textarea,select').activate();
  },
  
  // leave edit mode
  leave_edit: function(e)
  {
    if(e)
      e.stop();
    if(this.form_element.visible())
    {
      this.form_element.hide();
      this.value_element.show();
    }
  },
  
  // save changes to the field
  save: function(e)
  {
    e.stop(); // stop event propagation
    // submit ajax request to the server
    var options = Object.extend(this.options.ajaxOptions, {
      onComplete: this.handle_response.bind(this)
    });
    
    Object.extend(options.parameters, {
      form: Object.toJSON(Form.serialize(this.form_element.down('form'), true))
    });
    new Ajax.Request(this.options.submitURL, options);
    
    // disable form
    this.form_element.down('form').disable();
  },
  
  // handle response from server, having attempted to save
  handle_response: function(response)
  {
    var headerStatus = response.status.toString();
    if(headerStatus == '200')
    {
      // make sure there's no error classes on the form elements
      this.form_element.
        select('input[type=text],input[type=password],textarea,select')
        .each(function(item) { item.removeClassName('error'); });
      this.value_element.update(response.responseText);
      this.leave_edit();
      this.value_element.pulsate();
      
      // fire saved event
      this.value_element.fire("attribute:updated");
    }
    else
    {
      if(headerStatus == '419')
      {
        // if we have error add class of form element
        this.form_element.
          select('input[type=text],input[type=password],textarea,select')
          .each(function(item) { item.addClassName('error'); });
      }
    }
    
    // enable form again
    this.form_element.down('form').enable();
  }
});
// set default options
Zehn.InPlaceEditor.DEFAULT_OPTIONS = {
  hoverClassName: 'editable-hover',
  onSaved: function() {},
  submitURL: '',
  ajaxOptions: {
    method: 'post'
  }
};


// sortable table: table is the table element whose rows need to be sortable,
// sortColumns are hash of:
// columnIdofTh : function which gets value to sort given td of column for a row
Zehn.SortableTable = Class.create({
  // store functions and observe links
  initialize: function(table, sortColumns)
  {
    this.table = $(table);
    this.valueFuncs = $H(sortColumns);
    
    // observe links to sort
    this.valueFuncs.keys().each(function(columnId)
    {
      $(columnId).down('a.sort').observe('click', this.sort.bind(this));
    }.bind(this));
  },
  
  // sort by a column
  sort: function(e)
  {
    e.stop();
    var columnId = e.element().up('th').id;
    var columnNum = e.element().up('th').previousSiblings().size();
    
    // ascending or descending
    var sort = 'asc';
    if($(columnId).down('span.asc').visible())
      sort = 'desc';
    
    // now build value and tr arrays, by removing tds
    var trs = new Array();
    this.table.select('tr').each(function(tr)
    {
      if(tr.down('th')) return; // skip over header
      
      // get relevant td
      var td = tr.down('td', columnNum);
      
      // add value and element (accessing correct value function by using get
      // accessor, then giving that function the td it wants by navigating down)
      trs.push({
        value: this.valueFuncs.get(columnId)(tr.down('td', columnNum)),
        tr: tr.remove()
      });
    }.bind(this));
    
    // sort values
    trs = trs.sort(function(a, b) { return b.value - a.value; });
    
    // put back into table in correct order
    trs.each(function(tr)
    {
      if(sort == 'asc')
        this.table.down('tbody').insert({ top: tr.tr });
      else
        this.table.down('tbody').insert({ bottom: tr.tr });
    }.bind(this));
    trs.undefined; // don't need these anymore
    
    // finally change visibility of spans: all invisible first, then just this
    // one just sorted visible
    this.table.select('span.asc, span.desc').each(function(item)
    { item.hide(); });
    
    $(columnId).down('span.' + sort).show();
  }
  
});


// client side validation for a form
Zehn.FormValidator = Class.create(Trackable, {
  initialize: function(objs, form)
  {
    if(typeof(form) != 'undefined')
      this.form = form;
    
    // add an element for each
    this.elements = [];
    this.funcs = [];
    objs.each(function(obj)
    {
      if(!this.form && obj.element)
        this.form = obj.element.up('form');
      if(obj.element)
        this.elements.push(new Zehn.ElementValidator(obj));
      else if(obj.func)
        this.elements.push(new Zehn.FuncValidator(obj));
    }.bind(this));
    
    // observe submission of the form
    this.form.observe('submit', this.validate.bind(this));
    
    // and finally just check all initial values if we're error reporting
    if($$('.error-report').size() > 0)
      this.validate();
    
    this.register(); // register with Trackable
  },
  
  // validate each element in form, only return true if all elements validate
  validate: function(e)
  {
    var result = true;
    this.elements.each(function(element)
    {
      if(!element.validate())
        result = false;
      element.validate_mark(true);
    });
    if(e && !result)
      e.stop(); // stop event propagation if it didn't validate
    return result;
  }
});
// validate that can be called from anywhere, with form element as parameter
Zehn.FormValidator.static_validate = function(form)
{
  form = $(form);
  
  // set variables accessible via bind in iterated function
  var validator = null;
    
  // iterate over instances of formValidators to find match
  Zehn.FormValidator.instances.each(function(formVal)
  {
    if(formVal.form.identify() == form.identify())
      validator = formVal;
  });
  
  // if no match found return false
  if(validator == null)
    return false;
  
  return validator.validate();
};

// client side validation for a form element
Zehn.ElementValidator = Class.create(Configurable, Trackable, {
  initialize: function(elementObj)
  {
    this.element = elementObj.element;
    this.register();
    if(typeof(elementObj.options) == 'undefined')
      elementObj.options = {};
    this.set_options(elementObj.options);
    
    if(typeof(elementObj.regex) != 'undefined' && elementObj.regex)
    {
      this.re = new RegExp(elementObj.regex);
      this.method = 'regex';
    }
    
    if(typeof(elementObj.match) != 'undefined' && elementObj.match)
    {
      this.matchElement = elementObj.match;
      this.method = 'match';
    }
    
    if(typeof(elementObj.prototip) != 'undefined' && elementObj.prototip)
    {
      new Tip(this.element, $(this.element.id + '-popup').innerHTML, {
        fixed: true,
        stem: this.options.stem,
        hook: this.options.hook,
        style: 'the6yardbox',
        hideOn: false,
        showOn: 'noneexistantevent'
      });
      this.prototip = true;
    }
    else
      this.prototip = false;
    
    if(typeof(elementObj.tinyMCE) != 'undefined' && elementObj.tinyMCE)
      this.tinyMCE = true;
    else
      this.tinyMCE = false;
    
    elementObj.element.observe('change', this.validate_mark.bind(this));
  },
  
  // actually validate
  validate: function()
  {
    if(this.element.hasClassName('optional') && this.element.value == '')
      return true;
    
    if(this.tinyMCE)
      var value = tinyMCE.get(this.element.id).getContent();
    else
      var value = $F(this.element);
    
    if(this.method == 'regex')
    {
      if(value.match(this.re)) return true;
    }
    if(this.method == 'match')
    {
      if(value == $F(this.matchElement)) return true;
    }
    
    return false;
  },
  
  // validate and mark according to pass and fail functions
  validate_mark: function(emptyFail)
  {
    if(!emptyFail)
    {
      if(this.element.value == '')
      {
        this.pass();
        return;
      }
    }
    if(this.validate())
      this.pass();
    else
      this.fail();
  },
    
  // element was validated ok
  pass: function()
  {
    this.element.removeClassName('element-error');
    if(this.prototip)
      this.element.prototip.hide();
    else
      $(this.element.id + '-popup').hide();
  },
  
  // element did not pass validation
  fail: function()
  {
    this.element.addClassName('element-error');
    if(this.prototip)
    {
      this.element.prototip.show();
    }
    else
      $(this.element.id + '-popup').show();
  }
});
Zehn.ElementValidator.DEFAULT_OPTIONS = {
  stem: 'topLeft',
  hook: { target: 'bottomRight', tip: 'topLeft' }
};
//validate that can be called from anywhere, with form element as parameter
Zehn.ElementValidator.static_validate = function(element)
{
  element = $(element);
  
  // set variables accessible via bind in iterated function
  var validator = null;
    
  // iterate over instances of formValidators to find match
  Zehn.ElementValidator.instances.each(function(elVal)
  {
    if(elVal.element.identify() == element.identify())
      validator = elVal;
  });
  // if no match found return false
  if(validator == null)
    return false;
  
  return validator.validate_mark();
};

//client side validation for a form element
Zehn.FuncValidator = Class.create({
  initialize: function(funcObj)
  {
    this.func = funcObj.func;
    this.popup = funcObj.popup;
  },
  
  // actually validate
  validate: function()
  {
    return this.func();
  },
  
  // validate and mark according to pass and fail functions
  validate_mark: function(emptyFail)
  {
    if(this.validate())
      this.popup.hide();
    else
      this.popup.show();
  }
});

// a dynamic list, from which items can be added and removed
Zehn.DynamicList = Class.create({
  initialize: function(ul, count, del, inputName, addPosition)
  {
    if(inputName)
      this.inputName = inputName;
    this.ul = $(ul);
    this.countEl = $(count);
    this.count = this.ul.select('li').size();
    this.countEl.update(this.count);
    this.delEl = $(del);
    this.delElCount = 0;
    this.addPosition = addPosition;
    this.previousValue = '';
  },
  
  // add an element
  add: function(element)
  {
    if($(this.ul.identify() + '-li-' + element.id))
    {
      $(this.ul.identify() + '-li-' + element.id).pulsate();
      return;
    }
    element.id = this.ul.id + '-li-' + element.id;
    if(this.addPosition == 'top')
      this.ul.insert({ top: element });
    else
      this.ul.insert({ bottom: element });
    
    // insert the delete link
    var delLink = this.delEl.cloneNode(true);
    delLink.id = delLink.id + '-' + this.delElCount;
    delLink.setStyle({ 'display': 'inline' });
    this.delElCount = this.delElCount + 1;
    element.insert({ top: delLink });
    delLink.observe('click', this.remove.bind(this));
    
    // add hidden field
    if(this.inputName)
    {
      var hidden = new Element('input', {
        type  : 'hidden',
        value : element.id.substr(this.ul.id.length + 4),
        name  : this.inputName
        });
      element.insert({ bottom: hidden.hide() });
    }
    
    // update the count
    this.count = this.count + 1;
    this.countEl.update(this.count);
  },
  
  // remove an element
  remove: function(e)
  {
    // stop event, remove li and stop observing it completely
    e.stop();
    Zehn.stop_observing(e.element().up('li').remove());
    
    // decrement count
    this.count = this.count - 1;
    this.countEl.update(this.count);
  }
});

// A suggester dropdown list
Zehn.Suggester = Class.create({
  initialize: function(id, module, func, name, max, hideInput)
  {
    this.module = module;
    this.func = func;
    this.max = max;
    this.ul = $(id);
    this.suggestions = $(id + '-suggestions');
    this.help = $(id + '-help');
    this.error = $(id + '-error');
    this.maxError = $(id + '-max-error');
    this.input = $(id + '-suggester');
    this.hideInput = hideInput;
    this.index = -1;
    
    this.hiddenFields = this.ul.next('span.hidden-fields');
    this.name = name;
    
    // position all the elements correctly
    if(!this.suggestions.style.position ||
       this.suggestions.style.position != 'absolute')
      this.suggestions.style.position = 'absolute';
    if(!this.help.style.position ||
        this.help.style.position != 'absolute')
       this.help.style.position = 'absolute';
    if(!this.error.style.position ||
        this.error.style.position != 'absolute')
       this.error.style.position = 'absolute';
    this.position_popups();
    
    this.add_observers();
  },
  
  // position the popups
  position_popups: function()
  {
    Position.clone(this.ul, this.suggestions, {
      setHeight: false, 
      setWidth: false,
      offsetTop: this.ul.offsetHeight
    });
    Position.clone(this.ul, this.help, {
      setHeight: false,
      setWidth: false,
      offsetTop: this.ul.offsetHeight
    });
    Position.clone(this.ul, this.error, {
      setHeight: false,
      setWidth: false,
      offsetTop: this.ul.offsetHeight
    });
    Position.clone(this.ul, this.maxError, {
      setHeight: false,
      setWidth: false,
      offsetTop: this.ul.offsetHeight
    });
  },
  
  // add required observers
  add_observers: function()
  {
    this.ul.observe('click', this.focus_input.bind(this));
    this.input.observe('keydown', function(e)
    {
      switch(e.keyCode)
      {
        case Event.KEY_TAB:
        case Event.KEY_RETURN:
          e.cancel = true;
          e.returnValue = false;
          e.stop();
          if(this.index != -1)
          {
            this.suggestions.select('li')[this.index].stopObserving();
            this.element_added(this.suggestions.select('li')[this.index]);
          }
          else if($F(this.input) != '')
          {
            this.original_entry($F(this.input));
          }
      }
    }.bind(this));
    this.input.observe('keyup', this.process_key_press.bind(this));
    this.input.observe('blur', function()
    {
      this.help.hide();
      this.error.hide();
      this.maxError.hide();
      this.suggestions.fade({ duration: 0.1 });
      this.input.value = '';
      if(this.hideInput)
        this.input.hide();
    }.bind(this));
    this.ul.select('li.existing').each(function(li)
    {
      li.down().observe('click', this.element_removed.bind(this));
      li.observe('click', function(e) { e.stop; });
    }.bind(this)); 
  },
  
  // do something or start timer
  process_key_press: function(e)
  {
    if(this.ul.select('li.suggestion').size() >= this.max)
    {
      this.maxError.show();
      this.help.hide();
      this.input.value = '';
      return;
    }
    if(this.suggestions.visible())
    {
      switch(e.keyCode)
      {
        case Event.KEY_ESC:
          this.help.hide();
          this.error.hide();
          this.maxError.hide();
          this.suggestions.hide();
          this.input.value = '';
          if(this.hideInput)
            this.input.hide();
          e.stop();
          return;
        case Event.KEY_DOWN:
          this.index++;
          if(this.index >= this.suggestions.select('li').size())
            this.index = 0;
          this.show_selected_index();
          return;
        case Event.KEY_UP:
          this.index--;
          if(this.index < 0)
            this.index = this.suggestions.select('li').size() - 1;
          this.show_selected_index();
          return;
        case Event.KEY_LEFT:
        case Event.KEY_RIGHT:
          return;
      }
    }
    
    this.start_timer();
  },
  
  // show the selected index
  show_selected_index: function()
  {
    this.suggestions.select('li').each(function(li)
    {
      li.removeClassName('selected');
    });
    this.suggestions.select('li')[this.index].addClassName('selected');
  },
  
  // start the timer to ajax query if no more text typed
  start_timer: function()
  {
    if($F(this.input) == '')
    {
      this.suggestions.hide();
      this.error.hide();
      this.maxError.hide();
      this.help.show();
      return;
    }
    if($F(this.input) == this.previousValue)
      return;
    this.previousValue = $F(this.input);
    var previous = $F(this.input);
    this.end_timer.bind(this).delay(0.4, previous);
  },
  
  // query if no more text added
  end_timer: function(previous)
  {
    if($F(this.input) == previous)
    {
      this.help.hide();
      this.error.hide();
      this.maxError.hide();
      this.show_suggestions();
    }
  },
  
  // get some suggestions for autocompleting and show them
  show_suggestions: function()
  {
    this.previousValue = '';
    this.index = -1;
    var recipientIds = new Array();
    this.hiddenFields.select('input[type=hidden]').each(function(field)
    {
      recipientIds.push(field.value);
    });
    
    new Ajax.Request('', {
      parameters: {
        sessid : sessid,
        module : this.module,
        func   : this.func,
        filter : $F(this.input),
        selected : recipientIds.toJSON(true),
        TO_WEB : TO_WEB
      },
      onComplete: function(response)
      {
        if(response.status.toString()[0] != '2')
          return;
        
        this.suggestions.update(response.responseText);
        if(this.suggestions.select('li').size() > 0)
        {
          this.suggestions.select('li').each(function(li)
          {
            li.observe('click', function(e)
            {
              e.stop();
              this.suggestions.hide();
              this.element_added(li);
              li.stopObserving();
            }.bind(this));
          }.bind(this));
          this.suggestions.show();
        }
        else
        {
          this.error.show();
        }
      }.bind(this)
    });
  },
  
  // focus the text box, show help if necessary
  focus_input: function()
  {
    this.input.show();
    this.input.focus();
    this.position_popups();
    if(!this.suggestions.visible())
      this.help.show();
  },
  
  // an element added
  element_added: function(li)
  {
    li.select('*').invoke('stopObserving');
    
    // insert hidden form element
    this.add_hidden_field(li);
    
    // add to visual list
    this.add_to_visual_list(li);
    
    // focus input, show help
    this.input.value = '';
    this.input.show();
    this.help.show();
    
    this.position_popups();
    
    this.input.focus();
  },
  
  // add hidden form element
  add_hidden_field: function(li)
  {
    var hiddenField = new Element('input', {
      type  : 'hidden',
      value : li.innerHTML,
      name  : this.name + '[]',
      id    : this.name + '-field-' + li.id.substr(li.id.indexOf('-') + 1)
      });
    this.hiddenFields.insert({ bottom: hiddenField });
  },
  
  // attempt to add a new entity not from suggestions - may want to do something
  // like creating an li and then calling the element_added function with it
  original_entry: function(value) {},
  
  // override this to do something with the li to display to user that it is
  // selected
  add_to_visual_list: function(li) {},
  
  // an element removed
  element_removed: function(e)
  {
    var li = e.element().up('li');
    $(this.name + '-field-' + li.id.substr(li.id.indexOf('-') + 1)).remove();
    this.remove_from_visual_list(li);
    
    // focus input, show help
    this.input.value = '';
    this.input.focus();
    this.help.show();
    
    this.position_popups();
  },
  
  // override this to undo whatever happened in add_to_visual_list
  remove_from_visual_list: function(li) {}
});

// extension on above: adds the already selected users to the "beginning" of the
// text field
Zehn.RecipientsSuggester = Class.create(Zehn.Suggester, {
  // add hidden form element
  add_hidden_field: function(li)
  {
    var hiddenField = new Element('input', {
      type  : 'hidden',
      value : li.id.substr(li.id.indexOf('-') + 1),
      name  : this.name + '[]',
      id    : this.name + '-field-' + li.id.substr(li.id.indexOf('-') + 1)
      });
    this.hiddenFields.insert({ bottom: hiddenField });
  },
  
  // an element added
  add_to_visual_list: function(li)
  {
    li.addClassName('recipient');
    li.select('img').each(function(img) { img.remove(); });
    li.update(li.innerHTML.replace(/<b>/g, ''));
    li.update(li.innerHTML.replace(/<\/b>/g, ''));
    li.insert({ top: this.ul.next('span.sample-del-img').innerHTML });
    li.down('img.del-img').observe('click', this.element_removed.bind(this));
    li.observe('click', function(e) { e.stop(); });
    this.input.up('li').insert({ before: li });
  },
  
  // remove from the visual list
  remove_from_visual_list: function(li)
  {
    li.remove();
  }
});


// auto expanding textarea
Zehn.AutoExpandTextarea = Class.create({
  initialize: function(textarea, alwaysSpace, increaseSpace, contract)
  {
    this.textarea = textarea;
    if(typeof(alwaysSpace) == 'undefined')
      this.alwaysSpace = 18;
    else
      this.alwaysSpace = alwaysSpace;
    if(typeof(increaseSpace) == 'undefined')
      this.increaseSpace = 36;
    else
      this.increaseSpace = increaseSpace;
    if(typeof(contract) == 'undefined')
      this.contract = false;
    else
      this.contract = contract;
    
    // hide the overflow
    this.textarea.setStyle({ overflow: 'hidden', overflowX: 'auto' });
    this.dummyDiv = new Element('div').update($F(this.textarea));
    this.dummyDiv.clonePosition(this.textarea, { setHeight: false });
    this.dummyDiv.setStyle({ position: 'absolute', display: 'none' });
    $w(this.textarea.className).each(function(className)
    {
      this.dummyDiv.addClassName(className + '-dummy');
    }.bind(this));
    this.textarea.insert({ after: this.dummyDiv });
    this.previousHeight = this.textarea.getHeight();
    
    // need to expand already based on existing content?
    this.check_expand(true);
    
    // on focus and blurs start/stop observing
    this.textarea.observe('focus', this.start_observing.bind(this));
    this.textarea.observe('blur', this.stop_observing.bind(this));
    this.expanding = false;
  },
  
  // stop observing for potential expansion
  stop_observing: function()
  {
    clearInterval(this.interval);
  },
  
  // start observing for potential expansion
  start_observing: function()
  {
    this.interval = window.setInterval(this.check_expand.bind(this), 500);
  },
  
  // see if we need to expand
  check_expand: function(first)
  {
    if(this.expanding) return;
    this.dummyDiv.update($F(this.textarea).replace(/\n/g, "<br />") + '&nbsp;');
    if((this.previousHeight < (this.dummyDiv.getHeight() + this.alwaysSpace) &&
       this.dummyDiv.getHeight() < 800) || this.contract)
    {
      this.previousHeight = this.dummyDiv.getHeight() + this.increaseSpace;
      this.expanding = true;
      if(typeof(first) == 'boolean' && first)
      {
        this.textarea.setStyle({ height: this.previousHeight + 'px' });
      }
      else
      {
        new Effect.Morph(this.textarea, {
          style: 'height: ' + this.previousHeight + 'px;',
          afterFinish: function() { this.expanding = false }.bind(this)
        });
      }
    }
  }
});

// password strength meter
Zehn.PasswordStrength = Class.create({
  initialize: function(input, meter)
  {
    this.input = $(input);
    this.meter = $(meter);
    this.input.observe('keyup', this.update_strength.bind(this));
  },
  
  // update the password strength
  update_strength: function()
  {
    intScore = 0;
    var passwd = $F(this.input);
    if(passwd.match(/[a-z]/))
      intScore = (intScore + 2);
    if(passwd.match(/[A-Z]/))
      intScore = (intScore+5);
    if(passwd.match(/\d+/))
       intScore = (intScore+5);
    if(passwd.match(/(\d.*\d.*\d)/))
       intScore = (intScore+5);
    if(passwd.match(/[!,@#$%^&*?_~]/))
       intScore = (intScore+5);
    if(passwd.match(/([!,@#$%^&*?_~].*[!,@#$%^&*?_~])/))
       intScore = (intScore+5);
    if(passwd.match(/[a-z]/) && passwd.match(/[A-Z]/))
       intScore = (intScore+2);
    if(passwd.match(/\d/) && passwd.match(/\D/))
       intScore = (intScore+2);
    if(passwd.match(/[a-z]/) &&
       passwd.match(/[A-Z]/) &&
       passwd.match(/\d/) &&
       passwd.match(/[!,@#$%^&*?_~]/))
       intScore = (intScore+2);
    intScore = intScore*6;
    this.meter.morph('width: ' + intScore + 'px;');
  }
});

// more content link
Zehn.MoreLink = Class.create({
  initialize: function(link, module, func, perPage, contentCSSRule,
  contentContainer, paramsModifier, onMoreShown)
  {
    this.link = $(link);
    if(typeof(contentContainer) != 'undefined')
      this.container = $(contentContainer);
    else
      this.container = false;
    if(typeof(paramsModifier) == 'function')
      this.paramsModifier = paramsModifier;
    else
      this.paramsModifier = false;
    if(typeof(onMoreShown) == 'function')
      this.onMoreShown = onMoreShown;
    else
      this.onMoreShown = false;
    this.perPage = perPage;
    this.contentCSSRule = contentCSSRule;
    this.entriesShown = this.count_entries();
    this.module = module;
    this.func = func;
    link.observe('click', this.show_more.bind(this));
  },
  
  // count the number of shown content entries
  count_entries: function()
  {
    if(!this.container)
    {
      return $$(this.contentCSSRule).size();
    }
    else
      return this.container.select(this.contentCSSRule).size();
  },
  
  // show some more entries
  show_more: function(e)
  {
    e.stop();
    var parameters = {
      sessid  : sessid,
      module  : this.module,
      func    : this.func,
      TO_WEB  : TO_WEB
    };
    if(typeof(this.paramsModifier) == 'function')
      parameters = this.paramsModifier(parameters);
    
    new Ajax.Request('', {
      parameters: parameters,
      onComplete: function(response)
      {
        if(response.status.toString()[0] != '2')
          return;
        
        if(!this.container)
          this.link.up('div').insert({ before: response.responseText });
        else
          this.container.insert({ bottom: response.responseText });
        
        var newShown = this.count_entries() - this.entriesShown;
        this.entriesShown = this.count_entries();
        if((this.entriesShown % this.perPage) != 0 || newShown == 0)
        {
          this.link.remove();
        }
        
        document.fire("moreContent:added");
        if(typeof(this.onMoreShown) == 'function')
          this.onMoreShown();
      }.bind(this)
    });
  }
});

// colour palette picker
Zehn.ColourPicker = Class.create(Configurable, {
  initialize: function(slider, sample, input, options)
  {
    this.slider = $(slider);
    this.sample = $(sample);
    this.input = $(input);
   
    // work out what the three colours are now
    this.color = input.readAttribute('value');
    this.r = 255 - parseInt(this.color.substr(0, 2), 16);
    this.g = 255 - parseInt(this.color.substr(2, 2), 16);
    this.b = 255 - parseInt(this.color.substr(4, 2), 16);
    
    // set options with Configurable
    this.set_options(options);
    
    // add observers
    this.add_observers();
    
  },

  // add the observers
  add_observers: function()
  {
    new Control.Slider(this.slider.select('.handle'), this.slider, {
      range: this.options.range,
      sliderValue: [this.r, this.g, this.b],
      onSlide: this.color_changed.bind(this),
      onChange: this.color_changed.bind(this)
    });
  },
  
  // color changed
  color_changed: function(values)
  {
    this.r = (255 - parseInt(values[0])).toString(16);
    if(this.r.length == 1) this.r = '0' + this.r;
    this.g = (255 - parseInt(values[1])).toString(16);
    if(this.g.length == 1) this.g = '0' + this.g;
    this.b = (255 - parseInt(values[2])).toString(16);
    if(this.b.length == 1) this.b = '0' + this.b;
    this.color = this.r + this.g + this.b;
    this.sample.setStyle({backgroundColor: "#" + this.color });
    this.input.writeAttribute('value', this.color);
    this.options.onUpdate(this.color);
  }
});
//set default options
Zehn.ColourPicker.DEFAULT_OPTIONS = {
  onUpdate: function() {},
  range: $R(0, 255)
};
