/* forms-lite.js

   Copyright (c) 2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
   W3C liability, trademark, document use and software licensing
   rules apply, see:

   http://www.w3.org/Consortium/Legal/copyright-documents
   http://www.w3.org/Consortium/Legal/copyright-software
*/
// http://en.wikipedia.org/wiki/DOM_Events
// this test could be improved up!
//isIE = (typeof window.pageYOffset =='undefined');

// make sure to call xformstiny() from onload

// absolute reference to the graphic for date picker close button
var closeButtonURI = "http://people.w3.org/~dsr/forms-lite/close.png";

// English names for use in pretty printing dates
var days = ["Su","Mo","Tu","We","Th","Fr","Sa"];
var weekday = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
var month = ["Jan","Feb","Mar","Apr","May","Jun",
             "Jul","Aug","Sep","Oct","Nov","Dec"];


// need to identify which browsers have native wf2 support, taken from
// http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2006-October/007339.html
var nativewf2 = (window.HTMLDataListElement || window.HTMLDatalistElement);
//var netfront = ((navigator.userAgent).indexOf("NetFront") >= 0 ? true : false);
var escapeBug = false;

// preload close button for date picker
//setTimeout("var c = new Image(); c.src = closeButtonURI;", 100);

//----------------------------------------------------------
// initialize forms-lite interpreter

function xformstiny()
{
  var i, j,  form, field, fields, type, disabled, readonly, type;
  var required, pattern, calc, constraint, relevant, tip, label;
  //alert("ua: " + navigator.userAgent);

  // determine if UA doesn't unescape attribute values
  field  = document.createElement("p");
  field.innerHTML = '<span title="&amp;&lt;&gt;">test</span>';
  document.body.appendChild(field);
  escapeBug = field.firstChild.getAttribute("title") == "&amp;&lt;&gt;";
  document.body.removeChild(field);

  // partial work around Opera's WF2 implementation for type
  // same technique doesn't work for required attribute :-(
  if (nativewf2)
  {
    fields = document.getElementsByTagName("input");

    for (i = 0; i < fields.length; ++i)
    {
      field = fields[i];
      type = field.getAttribute("type");

      if (type == "number" || type == "date")
      {
        field.setAttribute("datatype", type);
        field.setAttribute("type", "text");
        field.type = "text";
      }
    }
  }

  // reformat fieldsets as tables and add rows as needed
  initializeFieldsets();

  // determine which labels apply to which fields
  var labels = document.getElementsByTagName("label");

  for (i = 0; i < labels.length; ++i)
  {
    label = labels[i];
    form = label.form;
    id = label.getAttribute("for");
    
    // Internet Explorer returns null for "for" attribute
    // and you instead need to use the .htmlFor property
    if (!id)
      id = label.htmlFor;

    if (!id)
      continue;

    field = document.getElementById(id);
    
    // double check to work around IE and Opera
    // which find first field with matching name
    if (!field || field.getAttribute("id") != id)
    {
      field = form[id];

      if (!field)
        continue;
    }

    label.myfield = field;  // might be an array
    tip = label.getAttribute("title");

    if (field.length)
    {
      for (j = 0; j < field.length; ++j)
        setLabel(field[j], label, tip);
    }
    else
      setLabel(field, label, tip);
  }

  // initialize select elements
  fields = document.getElementsByTagName("select")

  for (i = 0; i < fields.length; ++i)
  {
    field = fields[i];
 
    field.dependents = [];
    field.dependees = [];
    field.datatype = "select";
    field.onchange = updateFn(field);
    field.onfocus = focusInFn(field);
    field.onblur = focusOutFn(field);

    if (field.getAttribute("editable"))
      initCombo(field);


    form = field.form;

    if (!form.fields)
      form.fields = [field];
    else
      form.fields[form.fields.length] = field;

    initializeConstraints(form, field);
  }

  // initialize textarea elements
  fields = document.getElementsByTagName("textarea");
  for (i = 0; i < fields.length; ++i)
  {
    field = fields[i];
    field.onfocus = focusInFn(field);
    field.onblur = focusOutFn(field);


    form = field.form;

    if (!form.fields)
      form.fields = [field];
    else
      form.fields[form.fields.length] = field;

    initializeConstraints(form, field);
  }

  // initialize input elements
  fields = document.getElementsByTagName("input")

  for (i = 0; i < fields.length; ++i)
  {
    field = fields[i];
 
    field.dependents = [];
    field.dependees = [];

    form = field.form;

    if (!form.fields)
      form.fields = [field];
    else
      form.fields[form.fields.length] = field;
  }

  for (i = 0; i < fields.length; ++i)
  {
    field = fields[i];
    field.dependents = [];
    form = field.form;

    // another Opera work around
    type = field.getAttribute("datatype");

    if (!type)
      type = field.getAttribute("type");

    if (type)
    {
      if (type == "range")
      {
        if (!nativewf2)
          initRangeControl(field);
      }
      else if (type == "date")
        initDateControl(field);

      field.datatype = type;
    }
    else
      field.datatype = field.type;

    initializeConstraints(form, field);

    field.onchange = updateFn(field);
    field.onfocus = focusInFn(field);
    field.onblur = focusOutFn(field);

    // prevent propagation of keystrokes to avoid
    // activating short cuts when typing into field
    field.onkeydown = stopPropagation;

    // IE doesn't raise onchanged on radio buttons or checkboxes
    // until you move the focus away from the radio group
    if (!field.addEvent)
      field.onclick = updateFn(field);
  }

  // initialize calculated fields
  if (fields.length > 0)
    updateFields(fields);

  // check if any fields are invalid
  for (i = 0; i < fields.length; ++i)
  {
    field = fields[i];
    validateField(field);
  }

  for (i = 0; i < document.forms.length; ++i)
  {
    form = document.forms[i];
    checkRelevancy(form);
  }

  // initialize submit and reset processing
  for (i = 0; i < document.forms.length; ++i)
  {
    form = document.forms[i];
    form.onsubmit = submitfn(form);
    form.onreset = resetfn(form);
  }
}

function getFieldType(field) // another Opera work around
{
  return field.getAttribute("datatype") || field.getAttribute("type");
}

function setLabel(field, label, tip2)
{
  // use mylabels as labels property is taken by opera
  if (field.mylabels)
    field.mylabels[field.mylabels.length] = label;
  else
    field.mylabels = [label];

  var tip1 = field.getAttribute("title");

  if (tip1 && !tip2)
    label.setAttribute("title", tip1);
  else if (!tip1 && tip2)
    field.setAttribute("title", tip2);
  else // no title attribute on field or label
  {
    tip1 = null;
    var type = getFieldType(field);

    if (type == "number")
      tip1 = "a number";
    else if (type == "date")
      tip1 = "a date, e.g. 26 Sep 2006";

    if (tip1)
    {
      field.setAttribute("title", tip1);
      label.setAttribute("title", tip1);
    }
  }
}

function initializeConstraints(form, field)
{
  var disabled = field.getAttribute("disabled");

  if (disabled)
    addClass(field, "disabled");

  var readonly = field.getAttribute("readonly");

  if (readonly)
    addClass(field, "readonly");

  pattern = field.getAttribute("pattern");

  // pattern property already taken by opera
  if (pattern)
    field.regexp = new RegExp(pattern);

  var calc = field.getAttribute("calculate");

  if (calc)
    field.calc = prepareExpression(form, field, calc, true);

  var constraint = field.getAttribute("constraint");

  if (constraint)
    field.constraint = prepareExpression(form, field, constraint, false);

  var required = field.getAttribute("needed");

  if (!required) // another Opera work around
    required = field.getAttribute("required");

  if (required)
  {
    field.reqexpr = prepareExpression(form, field, required, false);
  }

  var relevant = field.getAttribute("relevant");

  if (relevant)
  {
    field.relevant = prepareExpression(form, field, relevant, false);

    if (form.relevant)
      form.relevant[form.relevant.length] = field;
    else
      form.relevant = [field];
  }
}

function updateFn(field)
{
  var fn = function(event) {

    updateFields([field]);

    if (event && event.type == "click" && event.stopPropagation)
      event.stopPropagation();
  };

  return fn;
}

function focusInFn(field)
{
  var fn = function() {
    if (field.type != "radio" && field.type != "checkbox")
      addClass(field, "focus");
  };

  return fn;
}

function focusOutFn(field)
{
  var fn = function() {
    removeClass(field, "focus");
  };

  return fn;
}


// used to prevent event propagation from field controls
function stopPropagation(event)
{
  event = event ? event : window.event;
  event.cancelBubble = true;  // for IE

  if (event.stopPropagation)
    event.stopPropagation();

  return true;
}

//----------------------------------------------------------
// initialize relevancy processing and repeating fields
function initializeFieldsets()
{
  var i, j, form, fieldsets, fieldset, node, relevant, repeat;
  var name, field, table, tbody, tr, td, template, type;

  var fieldsets = document.getElementsByTagName("fieldset");

  for (i = 0; i < fieldsets.length; ++i)
  {
    fieldset = fieldsets[i];
    relevant = fieldset.getAttribute("relevant");

    fieldset.datatype = "fieldset";
    form = findForm(fieldset.parentNode);
    
    name = fieldset.getAttribute("name");
    
    if (name)
      form[name] = fieldset;

    if (relevant)
    {
      //fieldset.form = form;

      if (form.relevant)
        form.relevant[form.relevant.length] = fieldset;
      else
        form.relevant = [fieldset];

      fieldset.relevant = prepareExpression(form, fieldset, relevant, false);
    }

    // create fieldset object model via iteration thru content

    var legend, tag;
    var labels = [];
    fieldset.fields = [];  // list of field names

    for (var node = fieldset.firstChild; node; node = node.nextSibling)
    {
      if (node.nodeType != 1)
        continue;

      tag = node.nodeName.toLowerCase();

      if (tag == "input" || tag == "select" || tag == "fieldset")
        insertFieldInFieldset(fieldset, node);
      else if (tag == "label")
        labels[labels.length] = node;
      else if (tag == "form")
        alert("form isn't allowed within fieldset");
    }

    // now deal with repeat-number attribute
    repeat = fieldset.getAttribute("repeat-number");

    if (!repeat)
      repeat = fieldset.getAttribute("repeat");

    if (!repeat)
      continue;

    fieldset.value = repeat;

    if (0 <= repeat && repeat < 999)
    {
      table = document.createElement("table");
      addClass(table, "fieldset");
      
      // without tbody the new table won't show in IE
      tbody = document.createElement("tbody");
      table.appendChild(tbody);

      // legend may not appear in node iteration above
      legend = fieldset.getElementsByTagName("legend");

      if (legend)
        legend = legend[0];

      node = legend ? legend.nextSibling : fieldset.firstChild;

      while (node)
      {
        var next = node.nextSibling;
        fieldset.removeChild(node);
        node = next;
      }

      fieldset.appendChild(table);

      tr = document.createElement("tr");
      tbody.appendChild(tr);
      var fields = fieldset.fields;

      // only include labels if matching number of fields
      if (labels.length == fields.length)
      {
        for (j = 0; j < labels.length; ++j)
        {
          var label = labels[j];
          td = document.createElement("td");
          tr.appendChild(td);
          td.appendChild(label);
        }

        tr = document.createElement("tr");
        tbody.appendChild(tr);
      }

      for (var row = 0; row < repeat; ++row)
      {
        tr = document.createElement("tr");
        tbody.appendChild(tr);

        for (var cell = 0; cell < fields.length; ++cell)
        {
          name = fields[cell];
          td = document.createElement("td");
          field = cloneField(fieldset, name, row);
          td.appendChild(field);
          tr.appendChild(td);
        }
      }
    }
  }
}

function cloneField(fieldset, name, row)
{
  if (name == "item")
    var x = 0;

  var field = fieldset[name];

  if (typeof (field.length) != "undefined")
  {
    if (field.nodeName && field.nodeName.toLowerCase() == "select")
    {
      if (row > 0)
      {
        field = field.cloneNode(true);
        field.selectIndex = 0;
        insertFieldInFieldset(fieldset, field);
      }

      return field;
    }

    if (row >= field.length)
    {
      field = field[0].cloneNode(true);
      field.value = "";
      insertFieldInFieldset(fieldset, field);
    }
    else
      field = field[row]

    return field;
  }

  if (row > 0)
  {
    field = field.cloneNode(true);
    field.value = "";
    insertFieldInFieldset(fieldset, field);
  }

  return field;
}

// return false if field name clashes with existing property
function insertFieldInFieldset(fieldset, field)
{
  var name = field.name;
  var entry = fieldset[name];

  // work around for konqueror/safari
  if (typeof (entry) == "function")
  {
    entry = fieldset[name] = null;
  }

  if (entry)
  {
    if (entry.length)
    {
      var tag = entry[0].nodeName;

      if (tag)
      {
        tag = tag.toLowerCase();

        if (tag == "input" || tag == "select")
        {
          fieldset[name][entry.length] = field;
          return true;
        }
      }
    }
    else
    {
      tag = entry.nodeName;

      if (tag)
      {
        tag = tag.toLowerCase();

        if (tag == "input" || tag == "select")
        {
          fieldset[name] = [entry,field];
          return true;
        }
      }
    }

    return false;
  }
  else
  {
    fieldset[name] = field;
    fieldset.fields[fieldset.fields.length] = name;
  }

  return true;
}

function findForm(node)
{
  while (node)
  {
    if (node.nodeType == 1 && node.nodeName.toLowerCase() == "form" )
      return node;

    node = node.parentNode;
  }

  return node;  
}

function checkRelevancy(form)
{
  var i, fieldsets, fieldset;

  if (form.relevant)
  {
    fieldsets = form.relevant;

    for (i = 0; i < fieldsets.length; ++i)
    {
      fieldset = fieldsets[i];

      try {
        if (eval(fieldset.relevant))
          setRelevant(fieldset);
        else
          setIrrelevant(fieldset);
      }
      catch (e)
      {
        alert("exception in relevancy test: " + e);
      }
    }
  }
}

function setIrrelevant(el)
{
  if (el.nodeName.toLowerCase() == "fieldset")
  {
    var fields = el.getElementsByTagName("input");

    if (fields)
    {
       for (var i = 0; i < fields.length; ++i)
         fields[i].disabled = true;
    }
  }
  else
    el.disabled = true;

  addClassValue(el, "irrelevant");
}

function setRelevant(el)
{
  if (el.nodeName.toLowerCase() == "fieldset")
  {
    var fields = el.getElementsByTagName("input");

    if (fields)
    {
       for (var i = 0; i < fields.length; ++i)
         fields[i].disabled = false;
    }
  }
  else
    el.disabled = false;

  removeClassValue(el, "irrelevant");
}

//----------------------------------------------------------
// form submit processing

function submitfn(form)
{
  var fn = function() {
    var okay = true;

    for (var i = 0; i < form.fields.length; ++i)
    {
      var field = form.fields[i];

      validateField(field);

      if (typeof (field.reqexpr) != "undefined" && field.value == "")
      {
        var $ = findFieldIndex(form, field);

        if (eval(field.reqexpr))
        {
          addClassValue(field, "missing");
          okay = false;
        }
      }
      else
        removeClassValue(field, "missing");

      if (hasClass(field, "invalid"))
        okay = false;
    }

    return okay;
  };

  return fn;
}

// reset field state to match reset operation
function resetfn(form)
{
  var fn = function() {
    var i, fields, field;

    // and now re-initialize input elements
    fields = document.getElementsByTagName("input");

    updateFields(fields);
    checkRelevancy(form);

    for (i = 0; i < fields.length; ++i)
      removeClassValue(fields[i], "missing");
  };

  return fn;
}

//----------------------------------------------------------

function initRangeControl(field)
{
  var down = document.createElement("button");
  down.className = "picker";
  down.setAttribute("type", "button");
  //down.style.padding="0";
  //down.style.verticalAlign="bottom";
  down.onclick = rangeDownFn(field);
  down.title = "decrement value";
  down.innerHTML = "&lt;";

  var up = document.createElement("button");
  up.className = "picker";
  up.setAttribute("type", "button");
  //up.style.padding="0";
  //up.style.verticalAlign="bottom";
  up.onclick = rangeUpFn(field);
  up.title = "increment value";
  up.innerHTML = "&gt;";

  var parent = field.parentNode;
  var spacer = document.createTextNode(" ");

   if (parent.lastChild == field)
   {
     parent.appendChild(spacer);
     parent.appendChild(down);
     parent.appendChild(up);
   }
   else
   {
     var next = field.nextSibling;
     parent.insertBefore(up, next);
     parent.insertBefore(down, up);
     parent.insertBefore(spacer, down);
   }
}

function rangeDownFn(field)
{
  var fn = function(event) {
    var min = eval(field.getAttribute("min"))
    var max = eval(field.getAttribute("max"))
    var step = eval(field.getAttribute("step"));
    var value = eval(field.value);

    if (!step)
      step = 1;

    if (field.value == "" && min != "")
    {
      field.value = min.toString();
    }
    else
    {
      value -= step;

      if (value < min)
        value = min;

      field.value = value;
    }

    updateFields([field]);
    stopPropagation(event);
  };

  return fn;
}

function rangeUpFn(field)
{
  var fn = function(event) {
    var min = eval(field.getAttribute("min"))
    var max = eval(field.getAttribute("max"))
    var step = eval(field.getAttribute("step"));
    var value = eval(field.value);

    if (!step)
      step = 1;

    if (field.value == "" && min != "")
    {
      field.value = min.toString();
    }
    else
    {
      value += step;

      if (value > max)
        value -= step;

      field.value = value;
    }

    updateFields([field]);
    stopPropagation(event);
  };

  return fn;
}

//----------------------------------------------------------

function initDateControl(field)
{
  var button = document.createElement("button");
  button.setAttribute("type", "button");
  button.onclick = pickerFn(field);
  button.innerHTML = "#";
  button.className = "picker";
  button.title = "Date picker";

  var parent = field.parentNode;

   if (parent.lastChild == field)
   {
     parent.appendChild(button);
   }
   else
   {
     var next = field.nextSibling;
     parent.insertBefore(button, next);
   }
}

// see http://www.w3schools.com/jsref/jsref_obj_date.asp

function createDatePicker(date)
{
  var td, i, j;

  var table = document.createElement("table");
  table.className = "picker";
  var caption = document.createElement("caption");
  table.appendChild(caption);
  table.legend = caption;  //table.caption = caption fails in Safari
  var tbody = document.createElement("tbody");
  table.appendChild(tbody);
  var tr = document.createElement("tr");
  tbody.appendChild(tr);

  for (i = 0; i < 7; ++i)
  {
    td = document.createElement("td");
    tr.appendChild(td);
    td.innerHTML = days[i];
  }

  table.c = [];

  for (i = 0; i < 6; ++i)
  {
    tr = document.createElement("tr");
    tbody.appendChild(tr);

    for (j = 0; j < 7; ++j)
    {
      td = document.createElement("td");
      tr.appendChild(td);
      td.innerHTML = "&nbsp;";
      table.c[table.c.length] = td;
    }
  }

  table.returnDateFn = function (table, field, cell) {
    return function (event) {
      field.value = cell.date;
      updateFields([field]);
      table.field.table = null;
      table.parentNode.removeChild(table);
      stopPropagation(event);
    }
  };

  table.closeFn = function (table) {
    return function (event) {
      table.field.table = null;
      table.parentNode.removeChild(table);
      stopPropagation(event);
    }
  };

  table.prevFn = function(table) {
    return function (event) {
      var d = table.date;
      d.setMonth(d.getMonth() - 1);
      table.initDate(d);
      stopPropagation(event);
    }
  };

  table.nextFn = function (table) {
    return function (event) {
      var d = table.date;
      d.setMonth(d.getMonth() + 1);
      table.initDate(d);
      stopPropagation(event);
    }
  };

  table.initDate = function () {
    var d = new Date(table.date);
    d.setDate(1);  // set to 1st of month
    table.legend.innerHTML = "";

    var f = document.createElement("img");
    f.onclick = table.closeFn(table);
    f.align = "right";
    f.title = "close";
    f.setAttribute("src", closeButtonURI);
    f.setAttribute("alt", "X");
    table.legend.appendChild(f);

    f = document.createElement("span");
    f.onclick = table.prevFn(table);
    f.className = "link";
    f.title = "previous month";
    f.innerHTML = "&laquo;";
    table.legend.appendChild(f);

    f = document.createElement("span");
    f.className = "static";
    f.innerHTML = "&nbsp;"+month[d.getMonth()] + " " + d.getFullYear()+"&nbsp;";
    table.legend.appendChild(f);

    f = document.createElement("span");
    f.onclick = table.nextFn(table);
    f.className = "link";
    f.title = "next month";
    f.innerHTML = "&raquo;";
    table.legend.appendChild(f);

    var first = d.getDay();
    var day = d.getDate();
    var m = d.getMonth();
    var dl = 86400000;  // milliseconds in a day
    d.setTime(d.getTime() - first * dl);

    for (var i = 0; i < table.c.length; ++i)
    {
      var cell = table.c[i];
      cell.innerHTML = "";
      var span = document.createElement("span");
      cell.appendChild(span);
      span.onclick = table.returnDateFn(table, date, cell);
      span.title = "select date";
      span.innerHTML = d.getDate();

      if (d.getMonth() == m)
        cell.className = "current";
      else
        cell.className = "";

      cell.date = weekday[d.getDay()] + ", " +
          d.getDate() + " " +
          month[d.getMonth()] + " " +
          d.getFullYear();

      d.setTime(d.getTime() + dl);
    }
  };

  table.field = date;
  table.date = date.value ? new Date(table.field.value) : new Date();
  table.initDate();
  return table;
}


function pickerFn(date)
{
  return function (event) {
    if (date.table)
    {
      date.table.parentNode.removeChild(date.table);
      date.table = null;
    }
    else
    {
      var picker = createDatePicker(date);
      date.parentNode.insertBefore(picker, date.nextSibling.nextSibling);
      var pos = findPos(date);
      picker.style.left = pos[0];
      picker.style.top = pos[1] + date.offsetHeight;
      date.table = picker;
    }
    stopPropagation(event);
  };
}

function findPos(obj)
{
  var left = 0, top = 0;
  if (obj.offsetParent)
  {
    left = obj.offsetLeft
    top = obj.offsetTop
    while (obj = obj.offsetParent)
    {
      left += obj.offsetLeft
      top += obj.offsetTop
    }
  }
  return [left,top];
}

//----------------------------------------------------------

function initCombo(combo)
{
  combo.button = document.createElement("button");
  combo.button.setAttribute("type", "button");
  combo.button.innerHTML = "E";
  combo.button.title = "toggle edit/select";
  combo.button.style.padding = "0";
  combo.button.style.verticalAlign = "bottom";
  combo.button.onclick = comboFn(combo);

  combo.parentNode.insertBefore(combo.button, combo);

  var width = combo.getAttribute("editable");
  combo.textfield = document.createElement("input");
  combo.textfield.className = "combotext";
  combo.textfield.style.display = "none";
  combo.textfield.style.visibility = "hidden";
  combo.textfield.size = width ? width : 12;

  var option = document.createElement("option");
  combo.insertBefore(option, combo.firstChild);
  combo.selectedIndex = 0;

  if (combo.nextSibling)
    combo.parentNode.insertBefore(combo.textfield, combo.nextSibling);
  else
    combo.parentNode.appendChild(combo.textfield);
}

function comboFn(combo)
{
  return function () {
    if (combo.textfield.style.display == "none")
      showComboText(combo);
    else
      showComboSelect(combo);
  };
}

function showComboSelect(combo)
{
  combo.options[0].value = combo.textfield.value;
  combo.options[0].innerHTML = combo.textfield.value;
  combo.selectedIndex = 0;
  combo.textfield.style.display = "none";
  combo.textfield.style.visibility = "hidden";
  combo.style.display = "inline";
  combo.style.visibility = "visible";
}

function showComboText(combo)
{
  combo.textfield.value = combo.value;
  combo.textfield.style.display = "inline";
  combo.textfield.style.visibility = "visible";
  combo.textfield.focus();
  combo.style.display = "none";
  combo.style.visibility = "hidden";
}

//----------------------------------------------------------

// apply pattern and other validation tests
// and adjust class for field and its labels
// the 'invalid' class can be used for styling

function validateField(field)
{
  removeClassValue(field, "invalid")


  if (field.value == "")
    return;

  removeClassValue(field, "missing");

  if (typeof(field.regexp) != "undefined")
  {
    if (field.value.match(field.regexp) == null)
      addClassValue(field, "invalid");
  }

  var type = getFieldType(field);

  if (type == "number" || type == "range")
  {
    try {
      type = typeof eval(field.value);
    }
    catch (e) {
      type = "string";
    }

    if (type == "number")
    {
      var min = field.getAttribute("min");

      if (min && field.value < eval(min))
        addClassValue(field, "invalid");

      var max = field.getAttribute("max");

      if (max && field.value > eval(max))
        addClassValue(field, "invalid");
    }
    else
      addClassValue(field, "invalid");
  }
  else if (type == "date")
  {
    var date = new Date(field.value);

    if (isNaN(Number(date)))
      addClassValue(field, "invalid");
    else
    {
      // pretty print date (in English)
      // toLocaleDateString() returns nn/nn/nnnn
      // which is ambiguous for day and month
      field.value = weekday[date.getDay()] + ", " +
          date.getDate() + " " +
          month[date.getMonth()] + " " +
          date.getFullYear();

      var min = field.getAttribute("min");

      if (min)
      {
        if (Number(date) < Number(new Date(min)))
          addClassValue(field, "invalid");
      }

      var max = field.getAttribute("max");

      if (max)
      {
        if (Number(date) > Number(new Date(max)))
          addClassValue(field, "invalid");
      }
    }
  }

  if (field.constraint && field.value != "")
  {
    var form = field.form; // needed for scoping eval
    var $;  // initialised to row index in fieldset

    var valid = eval(field.constraint);

    if (!valid)
      addClassValue(field, "invalid");
  }
}

//----------------------------------------------------------

// form method that updates any dependent
// fields that are calculated from this one
// using a toplogical sort to ensure that such
// fields are calculated in the right order
function updateFields(fields)
{
  var graph = sortGraph(fields);
  //alert("graph is " + showGraph(graph));

  for (var i = 0; i < graph.length; ++i)
  {
    var field = graph[i];

    if (!field.name)
      continue;

    var form = field.form;  // needed for the eval
    var $ = findFieldIndex(form, field);

    if (typeof(field.calc) == "string" && field.calc != "")
    {
      try { var value = eval(field.calc); }
      catch (e) { value = ""; }
      field.value = value.toString() == "NaN" ? "" : value;
    }
  }

  for (var i = 0; i < graph.length; ++i)
    validateField(graph[i]);

  // refresh relevancy tests
  checkRelevancy(fields[0].form);
}

function findFieldIndex(form, field)
{
  var group = form[field.name];
  var unknown;

  for (var i = 0; i < group.length; ++i)
  {
    if (group[i] == field)
      return i;
  }

  return unknown;
}

function showGraph(list)
{
  var s = "";

  for (var i = 0; i < list.length; ++i)
  {
    var f = list[i];
    var j = findFieldIndex(f.form, f);
    s += list[i].name + "["+ j + "] ";
  }

  return s;
}

//----------------------------------------------------------
// need a better naming scheme that evaluation over all
// checkboxes versus over all rows in a repeated fieldset

// sum eval(expr) over all rows in given fieldset
function sumover(fieldset, expr)
{
  var form = fieldset.form;
  var repeat = fieldset.value;
  var sum = 0;
  var value;

  for (var $ = 0; $ < repeat; ++$)
  {
    value = eval(expr);
    if (isNaN(value)) value = 0;
    if (typeof(value)=="undefined") value = "";
    sum += value;
  }

  return sum;
}

// count all ticked checkboxes in a named fieldset
function count(fieldset)
{
  var sum = 0;

  if (!fieldset)
    return 0;

  var fields = fieldset.getElementsByTagName("input");

  for (var i = 0; i < fields.length; ++i)
  {
    var field = fields[i];

    if (field.type == "checkbox" && field.checked)
      sum += 1;
  }

  return sum;
}

// sum eval(expr) over all ticked checkboxes in fieldset
function countover(fieldset, expr)
{
  var form = fieldset.form;
  var sum = 0;

 if (!fieldset)
    return 0;

  var fields = fieldset.getElementsByTagName("input");

  for (var i = 0; i < fields.length; ++i)
  {
    var field = fields[i];

    if (field.type == "checkbox" && field.checked)
      sum += eval(expr);
  }

  return sum;
}

// find value for a group of radio fields
function radioValue(field)
{
  for (i = 0; i < field.length; ++i)
  {
    if (field[i].checked)
      return field[i].value;
  }

  return null;
}

// coerce value to a boolean, note that
// "boolean" is a reserved word in ECMAScript
function bool(x)
{
  return x ? true : false;
}

// coerce value to a number
function number(x)
{
  if (typeof(x) == "number")
    return x;

  if (typeof(x) == "string")
    return parseFloat(x);

  return x ? 1 : 0;
}

function defined(x)
{
  return !undefined(x);
}

// for testing a value is undefined
function undefined(x)
{
  return x == "" || typeof(x) == "undefined";
}

//----------------------------------------------------------
// regular expression for parsing calculations
// used to determine list of field names present
// could be improved to allow for accented chars
var FieldNameStrt = "[A-Za-z]";
var FieldNameChar = "[A-Za-z0-9_\.]";
var FieldName = "(" + FieldNameStrt + ")(" +
                 FieldNameChar + ")*" +
                 "(?!([\(\'\"]|"+FieldNameChar+"+))";

var fieldRegExpGlobal = new RegExp(FieldName, "g");

var fieldRegExpOnce = new RegExp(FieldName);

function escapeText(text)
{
  text = text.replace(/&/g, "&amp;");
  text = text.replace(/</g, "&lt;");
  text = text.replace(/>/g, "&gt;");
  return text;
}

function unescapeText(text)
{
  text = text.replace(/\&amp;/g, "&");
  text = text.replace(/\&lt;/g, "<");
  text = text.replace(/\&gt;/g, ">");
  return text;
}

// rewrite expression for use with eval
function prepareExpression(form, field, expr, dependents)
{
  var matches, match, split, i, j, res= "";

  if (escapeBug)
    expr = unescapeText(expr);

  // sumover(x, y) function needs special treatment
  // 2nd argument must be a string that is eval'd
  // for each row in the fieldset given by 1st argument
  // e.g. "sumover(lineItem, quantity*unitprice)"
  if (/^sumover\(/.test(expr))
  {
    // strip "sumover(" and trailing ")"
    expr = expr.substr(8, expr.length-9);
    matches = expr.match(fieldRegExpOnce);

    if (matches.length > 0)
    {
      var fieldset = matches[0];
      expr = expr.substr(fieldset.length+1);  // strip off comma
      res = rewriteExpression(form, field, expr, dependents);

      if (res)
        res = "sumover(form."+fieldset+", \'" + res + "\')";
    }

    return res;
  }

  // like sumover but iterates over ticked checkboxes
  if (/^countover\(/.test(expr))
  {
    expr = expr.substr(10, expr.length-11);
    matches = expr.match(termre);
    matches = expr.match(fieldRegExpOnce);

    if (matches.length > 0)
    {
      var fieldset = matches[0];
      expr = expr.substr(fieldset.length+1);  // strip off comma
      res = rewriteExpression(form, field, expr, dependents);

      if (res)
        res = "countover(form."+fieldset+", \'" + res + "\')";
    }

    return res;
  }

  return rewriteExpression(form, field, expr, dependents);
}

function rewriteExpression(form, field, expr, dependents)
{
  var res = "";

  for (;;)
  {
    var matches = fieldRegExpOnce.exec(expr);

    if (!matches)
      return res + expr;

    res = res + expr.substr(0, matches.index)
              + rewriteFieldAccess(form, matches[0]);

    expr = expr.substr(matches.index+matches[0].length);

    if (dependents)
      addDependent(form, field, matches[0]);
 
  }

  return res;
}

function rewriteFieldAccess(form, name)
{
  var names = name.split("\.");
  var field = form[names[0]];

  for (var i = 1; i < names.length; ++i)
    field = field[names[i]];

  if (!field)
    return name;

  if (field.nodeName && field.nodeName.toLowerCase() == "fieldset")
    return "form."+name;

  if (typeof (field.value) == "undefined")
  {
    if (field[0].type == "radio")
      return "radioValue(form."+name+")";

    name = name + "[$]";
    field = field[0];
  }

  var type = getFieldType(field);

  switch (type)
  {
    case "number":
      return "eval(form."+name+".value)";

    case "date":
      return "(new Date(form."+name+".value))";

    case "checkbox":
      return "(form."+name+".checked == true)";

    case "fieldset":
      return "form."+name;
  }

  return "(form."+name+".value)";
}

// set up bidirectional links between
// dependent and dependee fields

function addDependent(form, field, name)
{
  var index = findFieldIndex(form, field);
  var d, dependee;
  var names = name.split("\.");

  dependee = form[names[0]];

  for (var i = 1; i < names.length; ++i)
    dependee = dependee[names[i]];
  
  if (!dependee)
    alert(name + " can't be found with field.form[name]");

  // add all checkboxes within fieldset as dependees
  if (dependee.datatype == "fieldset")
  {
    var fields = dependee.getElementsByTagName("input");

    for (i = 0; i < fields.length; ++i)
    {
      var f = fields[i];

      if (f.type == "checkbox")
      {
        d = f.dependents;
        d[d.length] = field;
        d = field.dependees;
        d[d.length] = dependee;
      }
    }

    return;
  }

  if (dependee.length)
  {
    if (dependee[0].type == "radio" || typeof (index) == "undefined")
    {
      for (var i = 0; i < dependee.length; ++i)
      {
        d = dependee[i].dependents;
        d[d.length] = field;
        d = field.dependees;
        d[d.length] = dependee;
      }
    }
    else // just initialize the same index
    {
      d = dependee[index].dependents;
      d[d.length] = field;
      d = field.dependees;
      d[d.length] = dependee;
    }
  }
  else if (d = dependee.dependents) // dependee is a non-repeated field
  {
    d[d.length] = field;
    d = field.dependees;
    d[d.length] = dependee;
  }
}

//----------------------------------------------------------

// Return a toplogical sort of the given
// graph after inclusion of dependents
// each node is assumed to have a list of
// dependents and dependees that are set up
// in advance, e.g. when the form is loaded.
function sortGraph(graph)
{
  // expand graph to include all dependents
  // and initialize associated dependee counts
  graph = initializeGraph(graph);

  var sorted = [];

  do
  {
    var found = false;

    // find unvisited node that is ready to output
    for (var i = 0; i < graph.length; ++i)
    {
      var node = graph[i];

      if (!node.visited && node.dependeeCount == 0)
      {
        sorted[sorted.length] = node;
        node.visited = true;
        found = true;
        var dependents = node.dependents;

        for (var j = 0; j < dependents.length; ++j)
          dependents[j].dependeeCount -= 1;
      }
    }
  }
  while (found);

  // check for cyclic dependencies via
  // residual positive dependee counts

  for (i = 0; i < graph.length; ++i)
  {
    if (graph[i].dependeeCount > 0)
    {
      alert("Error: cyclic dependencies between fields" +
       " and involving field named " + graph[i].name);
      deInitializeGraph(graph);
      throw "cyclic dependency";
    }
  }

  // reset flags associated with sorting
  deInitializeGraph(graph);
  return graph;
}

// recursively append all dependents to build a
// list of all nodes in the graph we need to sort
function initializeGraph(list)
{
  var i, j, length, dependent, dependees, dependee;

  if (!list)
    list = [];

  length = list.length;

  // iterate through initial set of list items
  // recursively adding their dependents

  for (i = 0; i < length; ++i)
  {
    dependent = list[i];

    if (!dependent.queued)
      addDependentNodes(list, dependent, false);
  }

  // the list will now include all dependents
  // so now compute dependee counts and exclude
  // dependees that aren't in current subgraph
  for (i = 0; i < list.length; ++i)
  {
    dependent = list[i];
    dependees = dependent.dependees;

    if (dependees)
    {
      dependent.dependeeCount = dependees.length;

      // treat as visited any dependee nodes that
      // are not present in the current graph
      for (j = 0; j < dependees.length; ++j)
      {
        dependee = dependees[j];

        if (!dependee.queued)
          dependent.dependeeCount -= 1;
      }
    }
    else
      dependent.dependeeCount = 0;
  }

  return list;
}

function deInitializeGraph(graph)
{
  for (var i = 0; i < graph.length; ++i)
  {
    var node = graph[i];
    node.visited = false;
    node.queued = false;
  }
}

function addDependentNodes(list, node, insert)
{
  var dependents = node.dependents;
  node.queued = true;

  if (insert)
    list[list.length] = node;

  if (dependents)
  {
    for (var i = 0; i < dependents.length; ++i)
    {
      var dependent = dependents[i];

      if (!dependent.queued)
        list = addDependentNodes(list, dependent, true);
    }
  }

  return list;
}
/*
// for testing purposes only
function obj(name)
{
  this.name = name;
  this.toString = function () { return this.name};
}

// for testing purposes only
function testGraph()
{
  var a = new obj("a");
  var b = new obj("b");
  var c = new obj("c");
  var d = new obj("d");
  var e = new obj("e");
  var f = new obj("f");

  // note b depends on f but f is not
  // included in the returned graph
  a.dependents = [d,b];
  a.dependees = [];
  b.dependents = [c,d];
  b.dependees = [a,f];
  c.dependents = [];
  c.dependees = [b];
  d.dependents = [e];
  d.dependees = [a,b];
  e.dependents = [];
  e.dependees = [d];
  f.dependents = [b];
  f.dependees = [];

  return [a];
}

// for testing purposes only
function cyclicGraph()
{
  var a = new obj("a");
  var b = new obj("b");
  var c = new obj("c");
  var d = new obj("d");
  var e = new obj("e");
  var f = new obj("f");

  // note b depends on f but f is not
  // included in the returned graph
  a.dependents = [d,b];
  a.dependees = [];
  b.dependents = [c,d];
  b.dependees = [a,f];
  c.dependents = [a];
  c.dependees = [b,a];
  d.dependents = [e];
  d.dependees = [a,b];
  e.dependents = [];
  e.dependees = [d];
  f.dependents = [b];
  f.dependees = [];

  return [a];
}
*/
//----------------------------------------------------------
// add/remove classes to fields and associated labels

function addClassValue(field, value)
{
  var labels = field.mylabels;
  addClass(field, value);

  if (labels)
  {
    for (var i = 0; i < labels.length; ++i)
      addClass(labels[i], value);
  }
}

function removeClassValue(field, value)
{
  var labels = field.mylabels;
  removeClass(field, value);

  // only strip class from label if it no
  // longer applies to any of the fields
  if (labels)
  {
    var fields = labels[0].myfield;

    if (fields.length)
    {
      for (var i = 0; i < fields.length; ++i)
        if (hasClass(fields[i], value))
          break;

      if (i < fields.length)
        return;
    }

    for (var i = 0; i < labels.length; ++i)
      removeClass(labels[i], value);
  }
}

// IE get/set Attribute requires "class" to be "className"
// but element.className seems to work on most browsers

function getClassList(element)
{
  return element.className;
}

function hasClass(element, name)
{
  var regexp = new RegExp("(^| )" + name + "\W*");

  if (regexp.test(element.className))
    return true;

  return false;

}

function removeClass(element, name)
{
  var clsval = element.className;
  var regexp = new RegExp("(^| )" + name + "\W*");

  if (clsval)
  {
    clsval = clsval.replace(regexp, "");
    element.className = clsval;
  }
}

function addClass(element, name)
{
  if (!hasClass(element, name))
  {
    var clsval = element.className;
    element.className =  (clsval ? clsval + " " + name : name);
  }
}

