/******************************************************************************
 *
 *                   INDIGEN SOLUTIONS CODE PROPERTY
 *       The present javascript code is property of Indigen Solutions. This 
 *     code can only be used inside Internet/Intranet web sites located on 
 *  *web servers*, as the outcome of a licensed Indigen Solutions application 
 *  only. Any unauthorized use, reverse-engineering, alteration, transmission, 
 * transformation, facsimile, or copying of any means (electronic or not) is 
 *     strictly prohibited and will be prosecuted. Removal of the present 
 *              copyright notice is strictly prohibited
 *         Copyright (c) 2004 Indigen Solutions. All Rights Reserved.
 *
 * RCS Id                       $Id: istruct.js,v 1.13 2006/07/24 09:21:26 indigen Exp $
 * 
 ******************************************************************************/

/**
 * @fileoverview This file contains the implementation of the IStruct class
 * @author mig
 * @version 1.0
 */

/**
 * Construct a default IType instance
 * @param {object} formal the description of how actual data can be edited. The
 * definition and use of formal is left to classes extending <code>IType</code>
 * @class The <code>IType</code> defines a data type and related methods for
 * get, setting, editing and validating the data
 */
function IType(formal) {
  this.formal = formal;
  if(formal) {
    if(formal.defaultValue)
      this.defaultData = formal.defaultValue;
    if(formal.name)
      IStruct.addNamedIType(formal.name, this);
  }
  this.setData = IType.prototype.setData;
  this.setDataField = IType.prototype.setDataField;
  this.edit = IType.prototype.edit;
  this.getValue = IType.prototype.getValue;
  this.updateData = IType.prototype.updateData;
  this.canDirectData = IType.prototype.canDirectData;
  this.validate = IType.prototype.validate;
  this.getDefaultData = IType.prototype.getDefaultData;
  this.draw = IType.prototype.draw;
  this.displayLabel = IType.prototype.displayLabel;
  this.start = IType.prototype.start;
  this.startBlinking = IType.prototype.startBlinking;
  this.endBlinking = IType.prototype.endBlinking;
  this.getParentNode = IType.prototype.getParentNode;
  this.notify = IType.prototype.notify;
  this.setClass = IType.prototype.setClass;
}

/**
 * Set the data to be managed directly by the object
 * @param {any} data any data representing the actual to be managed by the IType object
 */
IType.prototype.setData = function(data) {
  this.directData = data;
  this.isDirectData = true;
}

/**
 * Set the data to be managed indirectly by the object.
 * @param {Object_or_Array} base the data containing the actual data to be managed by the object
 * @param {String_or_int} field the name of the object member or index in the array to access the actual data
 */
IType.prototype.setDataField = function(base, field) {
  this.baseData = base;
  this.dataField = field;
  this.isDirectData = false;
}

/**
 * Modify the DOM to generate an editor for the data. This method is to be overwritten in sub-classes.
 * @param {Node} parentNode the HTML element where to attach the editor
 */
IType.prototype.edit = function(parentNode) {
  alert("Method IType.edit must be overwritten");
}

/**
 * Get the value for the instance.
 **/
IType.prototype.getValue = function() {
  alert("Method IType.getValue must be overwritten");
}

/**
 * Update actual data with the edited values. This method is to be overwritten in sub-classes.
 */
IType.prototype.updateData = function() {
  alert("Method IType.updateData must be overwritten");
}

/**
 * Determine whether the managed data can be modified directly by the <code>IType</code> instance or
 * if the actual data is accessed from its container.
 * @return boolean <code>true</code> if actual data is modified directly, <code>false</code> otherwise
 */
IType.prototype.canDirectData = function() {
  return true;
}

/**
 * Check whether the current edited data is valid, displaying warnings if necessary
 * @return {boolean} <code>true</code> if data is valid, <code>false</code> otherwise
 */
IType.prototype.validate = function() {
  return true;
}

/**
 * Return the default data for this object
 * @return {any} the default empty data for the object
 */
IType.prototype.getDefaultData = function() {
  var value = null;
  if ( this.formal && this.formal.defaultData ) {
    eval("value = "+this.formal.defaultData);		  
  } else if ( this.defaultData ) {
    eval("value = "+this.defaultData);
  }
  return value;
}

/**
 * Returns the IType parent node
 * @return {Node} the object parent node or null if not found
 */
IType.prototype.getParentNode = function() {
  return this.doc.getElementById(this.parentNodeId);
}

/**
 * Highlight the object and display the given message in an alert dialog.
 * Once user click the dialog button, the initial state is restored
 * @param {string} message the message to be displayed in an alert dialog
 */
IType.prototype.notify = function(message) {
  var pn = this.getParentNode();
  var bc = pn.style.backgroundColor;
  pn.style.backgroundColor = "Red";
  alert(message);
  pn.style.backgroundColor = bc;
}

/**
 * Utility to set the class attribute of a DOM node.
 * The method takes a variable number of arguments, additional arguments
 * go by pair
 * @param {Node} the node to set the class attribute
 * @param {string} formalField the name of the class fied in the formal
 * @param {string} defValue the class value if the formal field is not set
 */
IType.prototype.setClass = function(node) {
  var args = this.setClass.arguments;
  var clazz=""
    for(var i=1;i<args.length;i+=2) {
      if(i>1)
      clazz+=" ";
      if(this.formal && this.formal[args[i]]) 
      clazz+=this.formal[args[i]];
      else
      clazz+=args[i+1];
    }
  node.className = clazz;
}

/**
 * Stores parent node and document and call {@link #edit} method
 * @param {Node} parentNode the DOM object to attach the IType object to.
 */
IType.prototype.draw = function(parentNode) {
  this.doc = parentNode.ownerDocument;
  this.parentNode = parentNode;
  this.parentNodeId = parentNode.getAttribute("id");
  if(this.parentNodeId==null || this.parentNodeId=="") {
    this.parentNodeId = "itype-"+IStruct.getId();
    parentNode.setAttribute("id",this.parentNodeId);
  }
  this.edit(parentNode);
}

IType.prototype.displayLabel = function() {
  return true;
};

/**
 * Call {@link #draw} method and refresh the display. This is the method to be 
 * called by applications to display the editor.
 * @param {Node} parentNode the DOM object to attach the IType object to.
 */
IType.prototype.start = function(parentNode) {
  this.draw(parentNode);
}

/**
 * Global to remember which object is blinking
 */
IType.blinkingIType = null;

/**
 * Request the object to startblinking
 */
IType.prototype.startBlinking = function() {
  this.originalBkgColor = this.parentNode.style.backgroundColor;
  this.originalPadding = this.parentNode.style.padding;
  this.originalBorder = this.parentNode.style.border;
  IType.blinkingIType = this;
  IType.blink(0);
}

/**
 * This method is called when blinking is over. If the <code>message</code>
 * object member is set, it is displayed in an alert dialog.
 */
IType.prototype.endBlinking = function() {
  this.parentNode.style.backgroundColor = this.originalBkgColor;
  this.parentNode.style.padding = this.originalPadding;
  this.parentNode.style.border = this.originalBorder;
  if(this.message!=null)
    alert(this.message);
}

/**
 * Performs the actual blinking
 * @param {int} step blink count to avoid blinking for ever
 */
IType.blink = function(step) {
  step = parseInt(step);
  if(step>7) {
    IType.blinkingIType.endBlinking();
    return;
  }
  if((step%2)==0) {
    IType.blinkingIType.parentNode.style.backgroundColor = "Red"; 
    IType.blinkingIType.parentNode.style.padding = "5px";
  } else {
    IType.blinkingIType.parentNode.style.backgroundColor = "White"; 
    IType.blinkingIType.parentNode.style.border = "5px solid White"; 
  }
  setTimeout("IType.blink("+(step+1)+")",250);
}

/**
 * Construct a new IStruct instance
 * @param {Object} formals the formal definition of the data. <code>formal</code> is an object with the following 
 * members:<ul>
 * <li><code>fields</code> an array of object with the following members:<ul>
 * <li><code>field</code> the name of the member in the actual</li>
 * <li><code>label</code> the text to be displayed as title for this member</li>
 * <li><code>description</code> help text to explain this member</li>
 * <li><code>type</code> the name of the <code>IType</code> sub-class to manage the member actual data</li>
 * <li><code>formal</code> optionally, the formal of the <code>IType</code> sub-class to manage the member actual data</li>
 * </ul></li>
 * <li><code>staticData</code> an array of members and associated values that are automatically added
 * to the actual data<ul>
 * <li><code>field</code> the name of the member</li>
 * <li><code>value</code> the value for the member</li>
 * </ul></li>
 * <li><code>tableClass</code> the CSS class for IStruct container, default <code>istruct-table</code></li>
 * <li><code>labelClass</code> the CSS class for IStruct label container, default <code>ifield-td-label</code></li>
 * <li><code>valueClass</code> the CSS class for IStruct value container, default <code>ifield-td-value</code></li>
 * </ul>
 * @class The <code>IStruct</code> object provides facilities for
 * editing data of <code>Object</code> type using given constraints
 */
function IStruct(formals) {
  this.defaultData = "{}";
  this.base = IType;
  this.base(formals);
  this.edit = IStruct.prototype.edit;
  this.getValue = IStruct.prototype.getValue;
  this.updateData = IStruct.prototype.updateData;
  this.validate = IStruct.prototype.validate;
}
	
IStruct.prototype = new IType;

/**
 * Get the value for the instance.
 **/
IStruct.prototype.getValue = function() {
  var value = {};
  for(var i=0;i<this.entries.length;i++) {
    var entry = this.entries[i];
    value[entry.itype.dataField] = entry.itype.getValue();
  }
  return value;
}

/**
 * Update the actual data with content of the editor. This is performed by calling 
 * the <code>updateData</code> method of each member.
 */
IStruct.prototype.updateData = function() {
  for(var i=0;i<this.entries.length;i++) {
    var entry = this.entries[i];
    entry.itype.updateData();
  }
  if(this.formal && this.formal.staticData && this.formal.staticData[0]) {
    for(var i=0;i<this.formal.staticData.length;i++) {
      var data = this.formal.staticData[i];
      this.directData[data.field] = data.value;
    }
  }
}

/**
* Display the form for editing the data
* @param {Node} parentNode the DOM node the form is attached to
*/
IStruct.prototype.edit = function(parentNode) {
  this.doc = parentNode.ownerDocument;
  var doc = this.doc;
  var table = doc.createElement("table");
  parentNode.appendChild(table);
  table.setAttribute("cellSpacing","0");
  table.setAttribute("cellPadding","0");
  this.setClass(table,"tableClass","istruct-table");
  var tbody = doc.createElement("tbody");
  table.appendChild(tbody);
  this.entries = [];
  var globalTr = null;
  if ( this.formal.byCols ) {
    globalTr = doc.createElement("tr");
    tbody.appendChild(globalTr);
  }
  for(var i=0;i<this.formal.fields.length;i++) {
    var entry = {};
    this.entries.push(entry);
    if(this.formal==null) {
      alert("Missing formal");
      break;			
    }
    if(this.formal.fields==null) {
      alert("Missing formal fields");
      break;
    }
    var formal = this.formal.fields[i];
    if(formal.field==null) {
      alert("Missing formal field");
      break;
    }
    if(formal.label==null) {
      alert("Missing formal label");
      break;
    }
    if(formal.type==null) {
      alert("Missing formal type");
      break;
    }
    var fieldName = formal.field;
    var tdLabel = doc.createElement("td");
    this.setClass(tdLabel,"labelClass","istruct-td-label");
    var tdValue = doc.createElement("td");
    if ( globalTr )
      this.setClass(tdValue,"valueClass","istruct-td-globaltr-value");
    else
      this.setClass(tdValue,"valueClass","istruct-td-value");
    if ( formal.required )
      tdLabel.innerHTML = formal.label + "<font size=\"1\" color=\"red\">*</font>";
    else if ( formal.label_html )
      tdLabel.innerHTML = formal.label;
    else
      tdLabel.appendChild(doc.createTextNode(formal.label));
    var itype;
    try {
      itype = eval("new "+formal.type+"(formal.formal)");
    } catch(e) {
      alert("Error instantiating "+formal.type+": "+e);
      break;
    }
    if(itype.canDirectData()) {
      if(this.directData[fieldName] == null) {
	this.directData[fieldName] = itype.getDefaultData();
      }
      itype.setData(this.directData[fieldName]);
    } else {
      itype.setDataField(this.directData, fieldName);
    }
    if ( formal.type == 'IHiddenField' ) {
      var div = doc.createElement("div");
      parentNode.appendChild(div);
      itype.draw(div);
    } else {
      var tr = null;
      if ( globalTr )
	tr = globalTr;
      else {
	tr = doc.createElement("tr");
	tbody.appendChild(tr);
      }
      if(formal.description != null)
	tr.setAttribute("title",formal.description);
      if ( (formal.formal && formal.formal.noLabel) || !itype.displayLabel() ) {
	tdValue.setAttribute("colSpan", "2");
	tr.appendChild(tdValue);
      } else {
	if ( formal.formal && formal.formal.labelAfter ) {
	  tr.appendChild(tdValue);
	  this.setClass(tdValue, 'valueClass', 'istruct-td-value1');
	  tr.appendChild(tdLabel);
	  this.setClass(tdLabel, 'labelClass', 'istruct-td-label1');
	} else {
	tr.appendChild(tdLabel);
	tr.appendChild(tdValue);		
      }
      }
      itype.draw(tdValue);
    }
    entry.itype = itype;
    if ( formal.readonly )
      mapDOMElementsRecursively(tdValue, function(elem) {elem.disabled = true;});
  }
  if ( globalTr ) {
    var tdWidth = doc.createElement("td");
    tdWidth.style.width = "100%";
    tr.appendChild(tdWidth);
  }
}

/**
* Check the availability of the edited data by checking each of its fields
*/
IStruct.prototype.validate = function() {
  for(var i=0;i<this.entries.length;i++) {
    var entry = this.entries[i];
    var v = entry.itype.validate();
    if(v == false)
      return false;
  }
  return true;
}

/**
 * Construct a new static field.
 * @param {object} formals an object describing the behaviour of the editor.
 **/
function IStaticField(formals) {
  this.defaultData = "''";
  this.base = IType;
  this.base(formals);
  this.edit = IStaticField.prototype.edit;
  this.getValue = IStaticField.prototype.getValue;
  this.updateData = IStaticField.prototype.updateData;
  this.validate = IStaticField.prototype.validate;
  this.canDirectData = IStaticField.prototype.canDirectData;
  this.displayLabel = IStaticField.prototype.displayLabel;
}

IStaticField.prototype = new IType;

/**
* Returns <code>false</code> to indicate that this object does not handle
* direct data
*/
IStaticField.prototype.canDirectData = function() {
  return false;
}

/**
 * Display the string editor
 * @param {node} parentNode the HTML node to attach the editor to
 */
IStaticField.prototype.edit = function(parentNode) {
  this.doc = parentNode.ownerDocument; 
  var doc = this.doc;
  this.text = "";
  if( this.baseData[this.dataField] != null )
    this.text = this.baseData[this.dataField];
  else
    this.text = this.getDefaultData();
  if ( this.text )
    parentNode.innerHTML = this.text;
}

/**
 * Get the value for the instance.
 **/
IStaticField.prototype.getValue = function() {
  return this.text;
}

/**
 * Update the actual data with the content of the editor
 */
IStaticField.prototype.updateData = function() {
  this.baseData[this.dataField] = this.getValue();
}

/**
 * Check the validity of the data
 */
IStaticField.prototype.validate = function() {
  return true;
}

/**
 * No label for this component.
 **/
IStaticField.prototype.displayLabel = function() {
  return false;
};

/**
 * Construct a new hidden field.
 * @param {object} formals an object describing the behaviour of the editor.
 **/
function IHiddenField(formals) {
  this.defaultData = "''";
  this.base = IType;
  this.base(formals);
  this.edit = IHiddenField.prototype.edit;
  this.getValue = IHiddenField.prototype.getValue;
  this.updateData = IHiddenField.prototype.updateData;
  this.validate = IHiddenField.prototype.validate;
  this.canDirectData = IHiddenField.prototype.canDirectData;
  this.displayLabel = IHiddenField.prototype.displayLabel;
}

IHiddenField.prototype = new IType;

/**
* Returns <code>false</code> to indicate that this object does not handle
* direct data
*/
IHiddenField.prototype.canDirectData = function() {
  return false;
}

/**
* Display the string editor
* @param {node} parentNode the HTML node to attach the editor to
*/
IHiddenField.prototype.edit = function(parentNode) {
  this.doc = parentNode.ownerDocument; 
  var doc = this.doc;
  var input = doc.createElement("input");
  input.setAttribute("type", "hidden");
  this.inputId = "ihiddenfield-" + IStruct.getId();
  input.setAttribute("id", this.inputId);
  parentNode.appendChild(input);
  if ( this.baseData[this.dataField] != null )
    input.setAttribute("value", this.baseData[this.dataField]);
  else
    input.setAttribute("value", this.getDefaultData());
}

/**
 * Get the value for the instance.
 **/
IHiddenField.prototype.getValue = function() {
  var input = this.doc.getElementById(this.inputId);
  return input.value;
}

/**
 * Update the actual data with the content of the editor
 */
IHiddenField.prototype.updateData = function() {
  this.baseData[this.dataField] = this.getValue();
}

/**
 * Check the validity of the data
 */
IHiddenField.prototype.validate = function() {
  // Nothing to do.
  return true;
}

/**
 * No label for this component.
 **/
IHiddenField.prototype.displayLabel = function() {
  return false;
};

/**
 * Construct a new editor to manage data as a single line string
 * @param {object} formals an object describing the behaviour of the editor,
 * or <code>null</code> for defaults. If non-null, the formal may the following
 * optional members<ul>
 * <li><code>size</code> the <code>size</code> attribute of the input</li>
 * <li><code>maxlength</code> the <code>maxlength</code> attribute of the input</li>
 * <li><code>password</code> if set to <code>true</code> password input is used</li>
 * <li><code>nullIfEmpty</code> if set to <code>true</code> the actual field is set to null instead of empty string</li>
 * <li><code>removeIfEmpty</code> if set to <code>true</code> the actual field is deleted instead of set to an empty string</li>
 * <li><code>reject</code> a list of regular expressions to be rejected,
 * with the corresponding error message:<ul>
 * <li><code>expr</code> the regular expression to be matched</li>
 * <li><code>message</code> the message to be displayed if expression matches</li>
 * </ul></li>
 * <li><code>expr</code> the regular expression to be matched</li>
 * <li><code>message</code> the message to be displayed if value does not match</li>
 * <li><code>inputClass</code> the CSS class for the input element, default <code>itextfield-input</code></li>
 * </ul>
 * @class The <code>ITextField</code> class manages data as a single line string
 */
function ITextField(formals) {
  this.defaultData = "''";
  this.base = IType;
  this.base(formals);
  this.edit = ITextField.prototype.edit;
  this.getValue = ITextField.prototype.getValue;
  this.updateData = ITextField.prototype.updateData;
  this.validate = ITextField.prototype.validate;
  this.canDirectData = ITextField.prototype.canDirectData;
}

ITextField.prototype = new IType;

/**
 * Returns <code>false</code> to indicate that this object does not handle
 * direct data
 */
ITextField.prototype.canDirectData = function() {
  return false;
}

/**
 * Display the string editor
 * @param {node} parentNode the HTML node to attach the editor to
 */
ITextField.prototype.edit = function(parentNode) {
  this.doc = parentNode.ownerDocument; 
  var doc = this.doc;
  var input = doc.createElement("input");
  if(this.formal && this.formal.password==true)
    input.setAttribute("type","password");
  else
    input.setAttribute("type","text");
  this.setClass(input,"inputClass","itextfield-input");
  if(this.formal) {
    if(this.formal.maxlength)
      input.setAttribute("maxlength",this.formal.maxlength);
    if(this.formal.size)
      input.setAttribute("size",this.formal.size);
  }
  this.inputId = "itextfield-"+IStruct.getId();
  input.setAttribute("id",this.inputId);
  parentNode.appendChild(input);
  if(this.baseData[this.dataField]!=null)
    input.setAttribute("value",this.baseData[this.dataField]);
  else
    input.setAttribute("value",this.getDefaultData());
}

/**
 * Get the value for the instance.
 **/
ITextField.prototype.getValue = function() {
  var input = this.doc.getElementById(this.inputId);
  return input.value;
}

/**
 * Update the actual data with the content of the editor
 */
ITextField.prototype.updateData = function() {
  var value = this.getValue();
  if(value=="") {
    if(this.formal) {
      if(this.formal.nullIfEmpty==true)
	value=null;
      if(this.formal.removeIfEmpty==true) {
	try {
	  delete this.baseData[this.dataField];
	  return;
	} catch(e) {
	}
      }
    }
  }
  this.baseData[this.dataField] = value; 
}

/**
* Check the validity of the data
*/
ITextField.prototype.validate = function() {
  if(this.formal) {
    var message = "Bad format";
    var value = this.getValue();
    if(this.formal.reject) {
      for(var i=0;i<this.formal.reject.length;i++) {
	var rej = this.formal.reject[i];
	if(rej.expr.test(value)) {
	  if(rej.message)
	    message = rej.message;
	  this.notify(message);
	  this.doc.getElementById(this.inputId).focus();
	  return false;
	}
      }
    }
    if(this.formal.expr) {
      if(this.formal.message)
	message = this.formal.message;
      if(!this.formal.expr.test(value)) {
	this.notify(message);
	this.doc.getElementById(this.inputId).focus();
	return false;
      }
    }
  }
  return true;
}

/**
 * Construct a new editor to manage data as a boolean value
 * @param {object} formals an object describing the behaviour of the editor,
 * or <code>null</code> for defaults. If non-null, the formal may the following
 * optional members<ul>
 * <li><code>inputClass</code> the CSS class for the input element, default <code>icheckfield-input</code></li>
 * </ul>
 * @class The <code>ICheckField</code> class manages data as a boolean value
 */
function ICheckField(formals) {
  this.defaultData = "false";
  this.base = IType;
  this.base(formals);
  this.edit = ICheckField.prototype.edit;
  this.getValue = ICheckField.prototype.getValue;
  this.updateData = ICheckField.prototype.updateData;
  this.canDirectData = ICheckField.prototype.canDirectData;
  this.checkChanged = ICheckField.prototype.checkChanged;
}

ICheckField.prototype = new IType;

/**
* Returns <code>false</code> to indicate that this object does not handle
* direct data
*/
ICheckField.prototype.canDirectData = function() {
  return false;
}

/**
* Display the boolean editor
* @param {node} parentNode the HTML node to attach the editor to
*/
ICheckField.prototype.edit = function(parentNode) {
  this.doc = parentNode.ownerDocument;
  var doc = this.doc;
  this.input = doc.createElement("input");
  if ( this.formal && this.formal.group ) {
    if ( !ICheckField.groups[this.formal.group] )
      ICheckField.groups[this.formal.group] = [];
    ICheckField.groups[this.formal.group].push(this.input);
    this.id=IStruct.registerElement(this);
    var fnt = new Function("IStruct.getRegisteredElement(\""+this.id+"\").checkChanged()");
    if(this.doc.addEventListener)
      this.input.addEventListener("click", fnt, false);
    else
      if(this.doc.attachEvent)
	this.input.attachEvent("onclick", fnt);
  }
  this.input.setAttribute("type","checkbox");
  this.input.style.border = '0px solid';
  this.setClass(this.input,"inputClass","icheckfield-input");
  this.inputId = "icheckfield-"+IStruct.getId();
  this.input.setAttribute("id",this.inputId);
  parentNode.appendChild(this.input);
  var checked = this.getDefaultData() == true;
  if(this.baseData[this.dataField]!=null)
    checked = this.baseData[this.dataField] == true;
  if(checked)
    this.input.setAttribute("checked","on");
  else
    this.input.removeAttribute("checked");
}

ICheckField.groups = {};

ICheckField.prototype.checkChanged = function() {
  if ( this.getValue() && this.formal && this.formal.group ) {
    for ( var i in ICheckField.groups[this.formal.group] ) {
      var input = ICheckField.groups[this.formal.group][i];
      if ( input != this.input )
	input.checked = false;
    }
  }
}

/**
 * Get the value for the instance.
 **/
ICheckField.prototype.getValue = function() {
  var input = this.doc.getElementById(this.inputId);
  return ( input.checked == true );
}

/**
 * Update the actual data with the content of the editor
 */
ICheckField.prototype.updateData = function() {
  this.baseData[this.dataField] = this.getValue();
}

/**
 * Construct a new editor to manage data as a multiline string
 * @param {object} formals an object describing the behaviour of the editor,
 * or <code>null</code> for defaults. If non-null, in addition to {@link #ITextField}
 * formal parameters, the formal may have the following optional members<ul>
 * <li><code>rows</code> the <code>rows</code> attribute of the textarea</li>
 * <li><code>cols</code> the <code>cols</code> attribute of the textarea</li>
 * </ul>
 * Note that the following {@link ITextField} formals are invalid for an ITextArea<ul>
 * <li><code>size</code></li>
 * <li><code>maxlength</code></li>
 * <li><code>password</code></li>
 * <li><code>inputClass</code> the CSS class for the textarea element, default <code>itextarea-textarea</code></li>
 * </ul>
 * @class The <code>ITextArea</code> class manages data as a multiline string
 */
function ITextArea(formals) {
  this.defaultData = "''";
  this.base = ITextField;
  this.base(formals);
  this.edit = ITextArea.prototype.edit;
}

ITextArea.prototype = new ITextField;

/**
 * Display the multiline string editor
 * @param {node} parentNode the HTML node to attach the editor to
 */
ITextArea.prototype.edit = function(parentNode) {
  this.doc = parentNode.ownerDocument; 
  var doc = this.doc;
  var textarea = doc.createElement("textarea");
  this.setClass(textarea,"inputClass","itextarea-textarea");
  if(this.formal!=null) {
    if(this.formal.rows!=null) {
      textarea.setAttribute("rows",this.formal.rows);
    }
    if(this.formal.cols!=null) {
      textarea.setAttribute("cols",this.formal.cols);
    }
  }
  this.inputId = "itextarea-"+IStruct.getId();
  textarea.setAttribute("id",this.inputId);
  parentNode.appendChild(textarea);
  var value="";
  if(this.baseData[this.dataField]!=null)
    value = this.baseData[this.dataField];
  else
    value = this.getDefaultData();
  textarea.value = value;
}

/**
 * Construct a new editor to manage data as finite set choice
 * @param {object} formals an object describing the behaviour of the editor.
 * The formal must have the following members<ul>
 * <li><code>entries</code> an array of objects, describing the choices:<ul>
 * <li><code>label</code> the text to be displayed for this choice
 * <li><code>value</code> the value to assign the data with</li>
 * </ul>
 * </li>
 * <li><code>inputClass</code> the CSS class for the select element, default <code>iselectfield-select</code></li>
 * </ul>
 * @class The <code>ISelectField</code> class manages data as a choice in a list
*/
function ISelectField(formals) {
  this.defaultData = "'none'";
  this.base = IType;
  this.base(formals);
  this.edit = ISelectField.prototype.edit;
  this.getValue = ISelectField.prototype.getValue;
  this.updateData = ISelectField.prototype.updateData;
  this.getValue = ISelectField.prototype.getValue;
  this.canDirectData = ISelectField.prototype.canDirectData;
}

ISelectField.prototype = new IType;

/**
* Returns <code>false</code> to indicate that this object does not handle
* direct data
*/
ISelectField.prototype.canDirectData = function() {
  return false;
}

/**
* Display the choice editor
* @param {node} parentNode the HTML node to attach the editor to
*/
ISelectField.prototype.edit = function(parentNode) {
  this.doc = parentNode.ownerDocument;
  var doc = this.doc;
  var select = doc.createElement("select");
  this.setClass(select,"inputClass","iselectfield-select");
  this.selectId = "iselectfield-"+IStruct.getId();
  select.setAttribute("id",this.selectId);
  parentNode.appendChild(select);
  if(this.formal==null) {
    alert("ISelectField missing formal");
    return;
  }
  if(this.formal.entries==null) {
    alert("ISelectField missing formal entries");
    return;
  }
  var value = this.getDefaultData();
  if(this.baseData[this.dataField]!=null)
    value = this.baseData[this.dataField];
  for(var i=0;i<this.formal.entries.length;i++) {
    var option = this.formal.entries[i];
    if(option.value==null) {
      alert("ISelectField entry missing value");
      return;
    }
    if(option.label==null) {
      alert("ISelectField entry missing label");
      return;
    }
    var opt = doc.createElement("option");
    opt.setAttribute("value",option.value);
    if(option.value == value) {
      opt.setAttribute("selected","selected");
    }
    select.appendChild(opt);
    opt.appendChild(doc.createTextNode(option.label));
  }
}

/**
 * Get the value for instance.
 **/
ISelectField.prototype.getValue = function() {
  var select = this.doc.getElementById(this.selectId);
  return select.value;
}

/**
 * Update the actual data with the content of the editor
 */
ISelectField.prototype.updateData = function() {
  this.baseData[this.dataField] = this.getValue();
}

/**
 * Construct a new editor to manage data as an array of data
 * @param {object} formals an object describing the behaviour of the editor.
 * The formal must have the following members<ul>
 * <li><code>entries</code> an array of objects, describing the possible array item types:<ul>
 * <li><code>label</code> an optional label to be displayed in the add
 * type selector. If not set, <code>type</code> member is used</li>
 * <li><code>type</code> the class name for editing this type of item</li>
 * <li><code>typeTest</code> a piece of javascript code to be evaluated as 
 * a boolean to check if a particular <code>data</code> is to be handled
 * by this editor</li>
 * <li><code>formal</code> an optional formal object to instantiate the corresponding editor</li>
 * <li><code>tableClass</code> the CSS class for the array table, default <code>iarray-table</code></li>
 * <li><code>valueClass</code> the CSS class for the value table cell, default <code>iarray-td-value</code></li>
 * <li><code>addCellClass</code> the CSS class for the table cell containing the add selector, default <code>iarray-td-sel</code></li>
 * <li><code>addSelClass</code> the CSS class for the add selector, default <code>iarray-sel</code></li>
 * <li><code>nocmdClass</code> the CSS class for the table cell representing a disabled command, default <code>iarray-nocmd</code></li>
 * <li><code>cmdClass</code> the CSS class for the table cell representing an enabled command, default <code>iarray-cmd</code></li>
 * <li><code>upClass</code> additional CSS class for the table cell representing an UP command, default <code>iarray-up</code></li>
 * <li><code>downClass</code> additional CSS class for the table cell representing a DOWN command, default <code>iarray-down</code></li>
 * <li><code>removeClass</code> additional CSS class for the table cell representing a REMOVE command, default <code>iarray-remove</code></li>
 * <li><code>addClass</code> additional CSS class for the table cell representing an ADD command, default <code>iarray-add</code></li>
 * <li><code>buttonClass</code> the CSS class for the button containers, default <code>iarray-div-cmd</code></li>
 * </ul>
 * </li>
 * </ul>
 * @class The <code>IArray</code> class manages data as an array
 */
function IArray(formals) {
  this.defaultData = "[]";
  this.base = IType;
  this.base(formals);
  this.edit = IArray.prototype.edit;
  this.getValue = IArray.prototype.getValue;
  this.updateData = IArray.prototype.updateData;
  this.upItem = IArray.prototype.upItem; 
  this.downItem = IArray.prototype.downItem;
  this.removeItem = IArray.prototype.removeItem;
  this.expcolItem = IArray.prototype.expcolItem;
  this.doExpcolItem = IArray.prototype.doExpcolItem;
  this.appendItem = IArray.prototype.appendItem;
  this.addLine = IArray.prototype.addLine;
  this.buildCommands = IArray.prototype.buildCommands;
  this.validate = IArray.prototype.validate;
}

IArray.prototype = new IType;

/**
* Handle a UP command on an array item
* @param {int} index the item index to be moved upward
*/
IArray.prototype.upItem = function(index) {
  try {
    var entry = this.entries[index];
    if(entry.itype.dataField==index) {
      entry.itype.dataField=index-1;
    }
    if(this.entries[index-1].itype.dataField==index-1) {
      this.entries[index-1].itype.dataField=index;
    }
    this.entries.splice(index,1);
    this.entries.splice(index-1,0,entry);
    var data = this.directData[index];
    this.directData.splice(index,1);
    this.directData.splice(index-1,0,data);
    var tr = this.doc.getElementById(entry.trId);
    var trPrev = tr.previousSibling;
    tr.parentNode.removeChild(tr);
    trPrev.parentNode.insertBefore(tr,trPrev);
    this.buildCommands();
  } catch(e) {
    alert("Error removing entry: "+e);
  }
}

/**
 * Handle a DOWN command on an array item
 * @param {int} index the item index to be moved downward
 */
IArray.prototype.downItem = function(index) {
  try {
    var entry = this.entries[index];
    if(entry.itype.dataField==index) {
      entry.itype.dataField=index+1;
    }
    if(this.entries[index+1].itype.dataField=index+1) {
      this.entries[index+1].itype.dataField=index;
    }
    this.entries.splice(index,1);
    this.entries.splice(index+1,0,entry);
    var data = this.directData[index];
    this.directData.splice(index,1);
    this.directData.splice(index+1,0,data);
    var tr = this.doc.getElementById(entry.trId);
    var trNext = tr.nextSibling.nextSibling;
    var parent = tr.parentNode;
    parent.removeChild(tr);
    parent.insertBefore(tr,trNext);
    this.buildCommands();
  } catch(e) {
    alert("Error removing entry: "+e);
  }
}

/**
 * Handle a REMOVE command on an array item
 * @param {int} index the item index to be removed
 */
IArray.prototype.removeItem = function(index) {
  try {
    var entry = this.entries[index];
    this.entries.splice(index,1);
    var tr = this.doc.getElementById(entry.trId);
    this.directData.splice(index,1);
    for(var i=index;i<this.entries.length;i++) {
      var itype = this.entries[i].itype;
      if(itype.dataField == i+1)
	itype.dataField = i;
    }
    tr.parentNode.removeChild(tr);
    this.buildCommands();
  } catch(e) {
    alert("Error removing entry: "+e);
  }
}

/**
 * Handle a EXPAND COLLAPSE command on an array item
 * @param {int} index the item index to be removed
 */
IArray.prototype.expcolItem = function(index) {
  try {
    var entry = this.entries[index];
    entry.expanded = !entry.expanded;
    this.doExpcolItem(entry);
    this.buildCommands();
  } catch(e) {
    alert("Error expand or collapse entry: "+e);
  }
}

/**
 * Do a EXPAND COLLAPSE command on an array entry
 * @param {int} index the item index to be removed
 */
IArray.prototype.doExpcolItem = function(entry) {
  var tdValue = this.doc.getElementById(entry.tdValueId);
  if ( !tdValue )
    return;
  var table = null;
  for ( table = tdValue.firstChild; table && (table.nodeType != 3) && (table.nodeName.toLowerCase() != "table"); table = table.nextSibling );
  if ( !table )
    return;

  var tbody = null;
  for ( tbody = table.firstChild; tbody && (tbody.nodeType != 3) && (tbody.nodeName.toLowerCase() != "tbody"); tbody = tbody.nextSibling );
  if ( !tbody )
    return;
  var first = true;
  
  for ( tr = tbody.firstChild; tr && (tr.nodeType != 3); tr = tr.nextSibling ) {
    if ( tr.nodeName.toLowerCase() != "tr" )
      continue;
    if ( first ) {
      first = false;
      continue;
    } else {
      if ( entry.expanded )
	tr.style.display = "";
      else
	tr.style.display = "none";
      var selects = tr.getElementsByTagName('select');
      for ( var i = 0; i < selects.length; i++ ) {
	var select = selects[i];
	var parent = select.parentNode;
	parent.removeChild(select);
	parent.appendChild(select);
	/*
	value problem
	var html = select.parentNode.innerHTML;
	select.parentNode.innerHTML = html;
	*/
      }	
    }    
  }
}

/**
 * Add a new item at the end of the array
 */
IArray.prototype.appendItem = function() {
  var typeIndex = 0;
  var typeSelector = this.doc.getElementById(this.typeSelectId);
  if(typeSelector!=null) {
    typeIndex = typeSelector.value;
  }
  this.addLine(this.directData.length,typeIndex, true);
  this.buildCommands();
}

/**
 * Add a new editor line. This method is called for each item when creating the 
 * initial editor, and then once every created item.
 * @param {int} i the index of the line
 * @param {int} typeIndex the index of the line editor type, or if <code>null</code>
 * the editor type is guessed from the actual data
 */
IArray.prototype.addLine = function(i, typeIndex, expanded) {
  var doc = this.doc;
  var ent0 = {};
  this.entries.push(ent0);
  var data = this.directData[i];
  var tr = doc.createElement("tr");
  var selector = doc.getElementById(this.selectorTrId);
  var tbody = selector.parentNode;
  tbody.insertBefore(tr, tbody.lastChild);
  ent0.trId = "iarray-"+IStruct.getId();
  tr.setAttribute("id",ent0.trId);
  var td1 = doc.createElement("td");
  td1.style.verticalAlign = "top";
  tr.appendChild(td1);
  ent0.tdUpId = "iarray-"+IStruct.getId();
  var div1 = doc.createElement("div");
  div1.setAttribute("id",ent0.tdUpId);
  div1.innerHTML = "&nbsp;";
  this.setClass(div1, "buttonClass", "iarray-div-cmd");
  td1.appendChild(div1);
  var td2 = doc.createElement("td");
  td2.style.verticalAlign = "top";
  tr.appendChild(td2);
  ent0.tdDownId = "iarray-"+IStruct.getId();
  var div2 = doc.createElement("div");
  div2.setAttribute("id",ent0.tdDownId);
  div2.innerHTML = "&nbsp;";
  this.setClass(div2, "buttonClass", "iarray-div-cmd");
  td2.appendChild(div2);
  var td3 = doc.createElement("td");
  td3.style.verticalAlign = "top";
  ent0.tdRemoveId = "iarray-"+IStruct.getId();
  tr.appendChild(td3);
  var div3 = doc.createElement("div");
  div3.setAttribute("id",ent0.tdRemoveId);
  div3.innerHTML = "&nbsp;";
  this.setClass(div3, "buttonClass", "iarray-div-cmd");
  td3.appendChild(div3);
  var td4 = doc.createElement("td");
  td4.style.verticalAlign = "top";
  ent0.tdExpcolId = "iarray-"+IStruct.getId();
  tr.appendChild(td4);
  var div4 = doc.createElement("div");
  div4.setAttribute("id",ent0.tdExpcolId);
  div4.innerHTML = "&nbsp;";
  this.setClass(div4, "buttonClass", "iarray-div-cmd");
  td4.appendChild(div4);
  var tdValue = doc.createElement("td");
  ent0.tdValueId = "iarray-"+IStruct.getId();
  tdValue.setAttribute("id",ent0.tdValueId);
  this.setClass(tdValue,"valueClass","iarray-td-value");
  tr.appendChild(tdValue);
  if(this.formal==null) {
    alert("IArray missing formal");
    return;
  }
  if(this.formal.entries==null) {
    alert("IArray missing formal entries");
    return;
  }
  if(!this.formal.entries instanceof Array) {
    alert("IArray formal entries is not a array");
    return;
  }
  var entry=null;
  if(typeIndex!=null) {
    entry = this.formal.entries[typeIndex];
  } else {
    for(var j=0;j<this.formal.entries.length;j++) {
      var ent = this.formal.entries[j];
      if(ent.type==null) {
	alert("IArray formal entry has no type");
	return;
      }				
      if(ent.typeTest==null) {
	entry=ent;
	break;
      }
      var check = eval(ent.typeTest);
      if(check==true) {
	entry = ent;
	break;
      }
    }
  }
  if(entry==null) {
    alert("Could not map IArray data to formal entry");
    return;
  }
  var itype;
  try {
    // FIXME : deep copy for formal (to correct bug to many hselect field in an array).
    var tmp = util_serialize(entry.formal);
    itype = eval("new "+entry.type+"(" + tmp + ")");
  } catch(e) {
    alert("Error instantiating "+entry.type+": "+e);
    return;
  }
  if(i==this.directData.length)
    this.directData.push(itype.getDefaultData());
  if(itype.canDirectData()) {
    itype.setData(this.directData[i]);
  } else {
    itype.setDataField(this.directData, i);
  }
  itype.draw(tdValue);
  ent0.itype = itype;
  if ( itype.formal && itype.formal.fields ) {
    ent0.expanded = expanded;
    var theThis = this;
    var func = function() {
      theThis.doExpcolItem(ent0);
    };
    setTimeout(func, 100);
  } else {
    ent0.expanded = true;
    ent0.tdExpcolId = '';
  }
}

/**
 * Update the comand cells depending of the item index. For instance, the first
 * item must not have a triggerable UP command
 */
IArray.prototype.buildCommands = function() {
  for(var i=0;i<this.entries.length;i++) {
    var ent = this.entries[i];
    var td1 = this.doc.getElementById(ent.tdUpId);
    var td2 = this.doc.getElementById(ent.tdDownId);
    var td3 = this.doc.getElementById(ent.tdRemoveId);
    var td4 = this.doc.getElementById(ent.tdExpcolId);
    if ( td1 ) {
    if(i>0) {
      td1.onclick = new Function("IStruct.getRegisteredElement('"+this.id+"').upItem("+i+");");
      this.setClass(td1,"cmdClass","iarray-cmd","upClass","iarray-up");
    }	else {
      td1.onclick = new Function("");
      this.setClass(td1,"nocmdClass","iarray-nocmd");
    }
    }
    if ( td2 ) {
    if(i<this.entries.length-1) {
      td2.onclick = new Function("IStruct.getRegisteredElement('"+this.id+"').downItem("+i+");");
      this.setClass(td2,"cmdClass","iarray-cmd","downClass","iarray-down");
    } else {
      td2.onclick = new Function("");
      this.setClass(td2,"nocmdClass","iarray-nocmd");
    }
    }
    if ( td3 ) {
    this.setClass(td3,"cmdClass","iarray-cmd","removeClass","iarray-remove");
    td3.onclick = new Function("IStruct.getRegisteredElement('"+this.id+"').removeItem("+i+");");
    }
    if ( td4 ) {
    if ( ent.expanded )
      this.setClass(td4,"cmdClass","iarray-cmd","expcolClass","iarray-expcol-expanded");
    else
      this.setClass(td4,"cmdClass","iarray-cmd","expcolClass","iarray-expcol-collapse");      
    td4.onclick = new Function("IStruct.getRegisteredElement('"+this.id+"').expcolItem("+i+");");
  }
}
}

/**
 * Display the array editor
 * @param {node} parentNode the HTML node to attach the editor to
 */
IArray.prototype.edit = function(parentNode) {
  this.doc = parentNode.ownerDocument;
  var doc = this.doc;
  if(!this.directData instanceof Array) {
    alert("IArray data is not an array");
    return;
  }
  table = doc.createElement("table");  
  table.setAttribute("border","0");
  table.setAttribute("cellSpacing","0");
  table.setAttribute("cellPadding","0");
  parentNode.appendChild(table);
  this.setClass(table,"tableClass","iarray-table");
  this.entries = [];
  this.id=IStruct.registerElement(this);
  var tr = doc.createElement("tr");
  var tbody = doc.createElement("tbody");
  table.appendChild(tbody);
  tbody.appendChild(tr);
  this.selectorTrId = "iarray-"+IStruct.getId();
  tr.setAttribute("id",this.selectorTrId);
  tr.appendChild(doc.createElement("td"));	
  tr.appendChild(doc.createElement("td"));
  tr.appendChild(doc.createElement("td"));
  tr.appendChild(doc.createElement("td"));
  var td = doc.createElement("td");
  tr.appendChild(td);
  this.setClass(td,"cmdClass","iarray-cmd","addClass","iarray-add");
  td.onclick = new Function("IStruct.getRegisteredElement('"+this.id+"').appendItem();");
  var div = doc.createElement("div");
  this.setClass(div, "buttonClass", "iarray-div-add-cmd");
  td.appendChild(div);
  td = doc.createElement("td");
  tr.appendChild(td);
  this.setClass(td,"addCellClass","iarray-td-sel");
  if(this.formal.entries.length>1) {    
    var select = this.doc.createElement("select");
    this.setClass(select,"addSelClass","iarray-sel");
    this.typeSelectId = "iarray-"+IStruct.getId();
    select.setAttribute("id",this.typeSelectId);
    td.appendChild(select);
    for(var i=0;i<this.formal.entries.length;i++) {
      var entry = this.formal.entries[i];
      var option = doc.createElement("option");
      select.appendChild(option);
      option.setAttribute("value",i);
      var label=entry.label;
      if(label==null)
	label = entry.type;
      option.appendChild(doc.createTextNode(label));
    }
  } else {
    td.innerHTML = "&nbsp;";
  }
  for(var i=0;i<this.directData.length;i++) {
    this.addLine(i,null,false);
  }  
  for ( var i in this.entries ) {
    var ent = this.entries[i];
    this.doExpcolItem(ent);
  }
  var td = doc.createElement("td");
  td.style.width = "100%";
  tr.appendChild(td);
  this.buildCommands();
}

/**
 * Get the value for instance.
 **/
IArray.prototype.getValue = function() {
  var value = [];
  for (var i=0;i<this.entries.length;i++) {
    var entry = this.entries[i];
    value.push(entry.itype.getValue());
  }
  return value;
}

/**
 * Update the actual data with the content of the editor. This is performed
 * by calling the sub-editors <code>updateData</code> method for each item
 */
IArray.prototype.updateData = function() {
  for(var i=0;i<this.entries.length;i++) {
    var entry = this.entries[i];
    entry.itype.updateData();
  }
}

/**
 * Check the availability of the edited data by checking each of its fields
 */
IArray.prototype.validate = function() {
  for(var i=0;i<this.entries.length;i++) {
    var entry = this.entries[i];
    var v = entry.itype.validate();
    if(v == false)
      return false;
  }
  return true;
}

/**
 * Construct a new editor to manage data as set of strings within a list of
 * possible values
 * @param {object} formals an object describing the behaviour of the editor.
 * The formal have the following members<ul>
 * <li><code>entries</code> an array of entries representing the choices:<ul>
 * <li><code>value</code> the value to be stored</li>
 * <li><code>label</code> the text to be displayed for this value</li>
 * </ul></li>
 * <li><code>minItems</code> the minimum number of selected entries (optional)</li>
 * <li><code>minItemsMsg</code> the message to be displayed in case there are too few entries (optional)</li>
 * <li><code>maxItems</code> the maximum number of selected entries (optional) </li>
 * <li><code>maxItemsMsg</code> the message to be displayed in case there are too many entries (optional)</li>
 * <li><code>size</code> the number of visible items (optional)</li>
 * <li><code>inputClass</code> the CSS class for the select element, default <code>iselectfield-select</code></li>
 * </ul>
 * @class The <code>IMultiSelect</code> class manages data as a choice of multiple items in a list
 */
function IMultiSelect(formals) {
  this.defaultData = "[]";
  this.base = IType;
  this.base(formals);
  this.edit = IMultiSelect.prototype.edit;
  this.getValue = IMultiSelect.prototype.getValue;
  this.updateData = IMultiSelect.prototype.updateData;
  this.canDirectData = IMultiSelect.prototype.canDirectData;
  this.validate = IMultiSelect.prototype.validate;
}

IMultiSelect.prototype = new IType;

/**
 * Returns <code>false</code> to indicate that this object does not handle
 * direct data
 */
IMultiSelect.prototype.canDirectData = function() {
  return false;
}

/**
 * Display the choice editor
 * @param {node} parentNode the HTML node to attach the editor to
 */
IMultiSelect.prototype.edit = function(parentNode) {
  this.doc = parentNode.ownerDocument;
  var doc = this.doc;
  var select = doc.createElement("select");
  this.setClass(select,"inputClass","iselectfield-select");
  this.selectId = "iselectfield-"+IStruct.getId();
  select.setAttribute("id",this.selectId);
  select.setAttribute("multiple","multiple");
  if(this.formal && this.formal.size)
    select.setAttribute("size",this.formal.size);
  parentNode.appendChild(select);
  if(this.formal==null) {
    alert("IMultiSelect missing formal");
    return;
  }
  if(this.formal.entries==null || this.formal.entries[0]==null) {
    alert("IMultiSelect missing or bad formal entries");
    return;
  }
  var value = this.getDefaultData();
  if(this.baseData[this.dataField]!=null)
    value = this.baseData[this.dataField];
  for(var i=0;i<this.formal.entries.length;i++) {
    var option = this.formal.entries[i];
    var opt = doc.createElement("option");
    opt.setAttribute("value",option.value);
    for(var j=0;j<value.length;j++) {
      if(option.value==value[j]) {
	opt.selected = true;
	break;
      }
    }
    select.appendChild(opt);
    opt.appendChild(doc.createTextNode(option.label));
  }
}

/**
 * Get the value for instance.
 **/
IMultiSelect.prototype.getValue = function() {
  var select = this.doc.getElementById(this.selectId);
  var value=[];
  var index=0;
  for(var opt=select.firstChild;opt;opt=opt.nextSibling) {
    if(opt.selected)
      value.push(this.formal.entries[index].value);
    index++;
  }
  return value;
}

/**
 * Update the actual data with the content of the editor
 */
IMultiSelect.prototype.updateData = function() {
  this.baseData[this.dataField] = this.getValue();
}

/**
 * Check validity of the current choice and eventually display warning
 */
IMultiSelect.prototype.validate = function() {
  var select = this.doc.getElementById(this.selectId);
  var count=0;
  for(var opt=select.firstChild;opt;opt=opt.nextSibling) {
    if(opt.selected)
      count++;
  }
  var ok=true;
  var message=null;
  if(this.formal) {
    if(this.formal.minItems) {
      if(count<this.formal.minItems) {
	message = "Minimum "+this.formal.minItems+" selection(s)";
	ok = false;
      }
      if(count>this.formal.maxItems) {
	message = "Maximum "+this.formal.maxItems+" selection(s)";
	ok = false;
      }
    }
  }
  if(ok==false) {
    this.notify(message);
    select.focus();
  }	
  return ok;
}

/**
 * Construct a new I18N instance for managing internationalization
 * @param {Object} formals the formal definition of the data. <code>formal</code> is an object with the following 
 * members:<ul>
 * <li><code>i18nSelId</code> the id of a select element describing languages</li>
 * <li><code>i18nType</code> the <code>IType</code> sub-class to manage language dependent data</li>
 * <li><code>...</code> any formal attributes for instantiating the <code>i18nType</code> object</li>
 * <li><code>i18nStaticData</code> an array of members and associated values that are automatically added
 * to the actual data<ul>
 * <li><code>field</code> the name of the member</li>
 * <li><code>value</code> the value for the member</li>
 * </ul></li>
 * <li><code>i18nTableClass</code> the CSS class for the container table, default <code>i18n-table</code></li>
 * <li><code>i18nLabelClass</code> the CSS class for the cell containing the language label, default <code>i18n-td-label</code></li>
 * <li><code>i18nValueClass</code> the CSS class for the cell containing value, default <code>i18n-td-value</code></li>
 * </ul>
 * @class The <code>I18N</code> object provides facilities for editing language dependent data
 */
function I18N(formals) {
  this.defaultData = "{}";
  this.base = IType;
  this.base(formals);
  this.displayLabel = I18N.prototype.displayLabel;
  this.edit = I18N.prototype.edit;
  this.getValue = I18N.prototype.getValue;
  this.updateData = I18N.prototype.updateData;
  this.validate = I18N.prototype.validate;
  this.selectChanged = I18N.prototype.selectChanged;
  this.selectLang = I18N.prototype.selectLang;
  this.canDirectData = I18N.prototype.canDirectData;
  if(formals==null) {
    alert("I18N missing formal");
    return;
  }
  if(formals.i18nSelId==null) {
    alert("I18N missing i18nSelId formal");
    return;
  }
  if(formals.i18nType==null) {
    alert("I18N missing i18nType formal");
    return;
  }
  this.i18nSelId = formals.i18nSelId;
}

I18N.prototype = new IType;

I18N.prototype.displayLabel = function() {
  try {
    var itype = eval("new "+this.formal.i18nType+"(this.formal)");
    return itype.displayLabel();
  } catch ( e ) { }
};

/**
* Display the object editor
* @param {node} parentNode the HTML node to attach the editor to
*/
I18N.prototype.edit = function(parentNode) {
  if(this.baseData[this.dataField]==null)
    this.baseData[this.dataField] = this.getDefaultData();
  var select = this.doc.getElementById(this.i18nSelId);
  if(select==null) {
    alert("I18N did not find a select element with id "+this.i18nSelId);
    return;
  }
  // build map of available languages and their name
  this.langMap = {};
  var node=select.firstChild;
  while(node) {
    if(node.nodeName.toLowerCase()=="option") {
      var value = node.getAttribute("value");
      if(value && value!="" && node.firstChild) {
	var label = /[ \n\r]*(.*)[ \n\r]*/.exec(node.firstChild.nodeValue)[1];
	if(label && label!="") {
	  this.langMap[value] = label;
	}
      }
      
    }
    node = node.nextSibling;
  }
  this.id = IStruct.registerElement(this);
  var fnt = new Function("IStruct.getRegisteredElement(\""+this.id+"\").selectChanged()");
  if(this.doc.addEventListener)
    select.addEventListener("change", fnt, false);
  else
    if(this.doc.attachEvent)
      select.attachEvent("onchange", fnt);
  this.entries = {};
  var table = this.doc.createElement("table");
  parentNode.appendChild(table);
  this.setClass(table,"i18nTableClass","i18n-table");
  table.setAttribute("cellpadding","0");
  table.setAttribute("cellspacing","0");
  var tbody = this.doc.createElement("tbody");
  table.appendChild(tbody);
  for(var l in this.langMap) {
    var entry = { lang: l };
    this.entries[l] = entry;
    var tr = this.doc.createElement("tr");
    tbody.appendChild(tr);
    entry.defDisplay = tr.style.display;
    tr.style.display="none";
    entry.trId = "i18n-"+IStruct.getId();
    tr.setAttribute("id",entry.trId);
    var itype;
    try {
      itype = eval("new "+this.formal.i18nType+"(this.formal)");
    } catch(e) {
      alert("I18N error instantiating "+formal.type+": "+e);
      break;
    }
    if(itype.canDirectData()) {
      if(this.baseData[this.dataField][entry.lang] == null) {
	this.baseData[this.dataField][entry.lang] = itype.getDefaultData();
      }
      itype.setData(this.baseData[this.dataField][entry.lang]);
    } else {
      itype.setDataField(this.baseData[this.dataField], entry.lang);
    }
    entry.itype = itype; 
    if ( itype.displayLabel() ) {
      var td = this.doc.createElement("td");
      tr.appendChild(td);
      this.setClass(td,"i18nLabelClass","i18n-td-label");
      td.appendChild(this.doc.createTextNode(this.langMap[l]));
    }
    var tdValue = this.doc.createElement("td");
    entry.tdValueId = "i18n-"+IStruct.getId();
    tdValue.setAttribute("id",entry.tdValueId);
    tr.appendChild(tdValue);
    this.setClass(tdValue,"i18nValueClass","i18n-td-value");
    itype.draw(tdValue);
  }
  this.selectChanged();
}

/**
 * Indicate that the language selection has changed. The method
 * access the language selector and update current language accordingly
 */
I18N.prototype.selectChanged = function() {
  var select = this.doc.getElementById(this.i18nSelId);
  var lang = select.value;
  this.selectLang(lang);
}

/**
 * Set the current language and display editor accordingly
 * @param {string} lang the new current language
 */
I18N.prototype.selectLang = function(lang) {
  for(var l in this.entries) {
    var entry = this.entries[l];
    var tr = this.doc.getElementById(entry.trId);
    if(l==lang) {
      tr.style.backgroundColor = "White";
      tr.style.display = entry.defDisplay;
    } else {
      tr.style.backgroundColor = "Yellow";
      tr.style.display = "none";
    }
  }
  this.selectedLang = lang;
}

/**
 * Get the value for instance.
 **/
I18N.prototype.getValue = function() {
  var value = {};
  for(var l in this.entries) {
    var entry = this.entries[l];
    value[l] = entry.itype.getValue();
  }
  return value;
}

/**
 * Update the actual data with the content of the editor
 */
I18N.prototype.updateData = function() {
  for(var l in this.entries) {
    var entry = this.entries[l];
    entry.itype.updateData();
  }
  if(this.formal && this.formal.i18nStaticData && this.formal.i18nStaticData[0]) {
    for(var i=0;i<this.formal.i18nStaticData.length;i++) {
      var data = this.formal.i18nStaticData[i];
      this.baseData[this.dataField][data.field] = data.value;
    }
  }
}

/**
 * Check validity of the current choice and eventually display warning
 */
I18N.prototype.validate = function() {
  var selected = this.selectedLang;
  for(var l in this.entries) {
    var entry = this.entries[l];
    this.selectLang(entry.lang);
    if(entry.itype.validate()==false) {
      this.selectLang(selected);
      return false;
    }
  }
  this.selectLang(selected);
  return true;
}

/**
 * Indicate that the object cannot directly manage the data but instead
 * access the data from its container and a field name 
 */
I18N.prototype.canDirectData = function() {
  return false;
}

/**
 * Construct a new editor to manage data as hierarchical select values. The actual data uses a dotted notation.
 * @param {object} formals an object describing the behaviour of the editor, with the following fields:<ul>
 * <li><code>entries</code> an array of objects with the fields:<ul>
 * <li><code>value</code> the value for this node/leaf</li>
 * <li><code>label</code> the label that is displayed for this option</li>
 * <li><code>entries</code> an optional array with the same syntax describing the sub-choices</li>
 * <li><code>nodata</code> optional, if set to true, the entry is not reflected in the actual</li>
 * </ul></li>
 * <li><code>tableClass</code> the class for the table containing the whole field, default is <code>ihselectfield-table</code></li>
 * <li><code>inputClass</code> the CSS primary class for the select element, default <code>ihselectfield-select</code>.
 * In addition to the primary class, a secondary class is used for each select which is the base class prefixed by the
 * level (0 based). For instance, the select HTML element at the second position is written this way:
 * <code>&lt;select class="ihselectfield-select ihselectfield-select1"&gt;</li>
 * <li><code>tdClass</code> the class for the each column in the table containing the whole field, default is <code>ihselectfield-td</code>.
 * The same secondary class mechanism as <code>inputClass</code> is used</li>
 * </ul>
 * @class The <code>IHSelectField</code> class manages data as a hierarchical tree of choices
 */
function IHSelectField(formals) {
  this.defaultData = null;
  this.base = IType;
  this.base(formals);
  this.edit = IHSelectField.prototype.edit;
  this.getValue = IHSelectField.prototype.getValue;
  this.updateData = IHSelectField.prototype.updateData;
  this.canDirectData = IHSelectField.prototype.canDirectData;
  this.buildSelect = IHSelectField.prototype.buildSelect;
  this.displayValue = IHSelectField.prototype.displayValue;
  this.newValue = IHSelectField.prototype.newValue;
}

IHSelectField.prototype = new IType;

/**
 * Display the hierarchical select editor
 * @param {node} parentNode the HTML node to attach the editor to
 */
IHSelectField.prototype.edit = function(parentNode) {
  var table = this.doc.createElement("table");
  table.setAttribute("cellspacing","0");
  table.setAttribute("cellpadding","0");
  this.setClass(table,"tableClass","ihselectfield-table");
  parentNode.appendChild(table);
  var tbody = this.doc.createElement("tbody");
  table.appendChild(tbody);
  var tr = this.doc.createElement("tr");
  tbody.appendChild(tr);
  this.column = [];
  if(this.formal == null) {
    alert("IHSelectField: missing formal");
    return;
  }
  if(this.formal.entries[0] == null) {
    alert("IHSelectField: missing formal entries");
    return;
  }
  this.id = IStruct.registerElement(this);
  this.buildSelect(tr,this.formal,0,"");
  var data = this.baseData[this.dataField]; 
  if(data!=null)
    this.displayValue(data,this.formal,0);
  else
    this.displayValue(this.formal.entries[0].value,this.formal,0);
}

/**
 * Update the state of the select elements to reflect the value
 * @param {string} value the dotted data string, or null if selects must be hidden
 * @param {object} formal the part of the formal handling the data
 */
IHSelectField.prototype.displayValue = function(value,formal) {
  var separator = ( ( this.formal.separator ) ? this.formal.separator : "." );
  var select = this.doc.getElementById(formal.selectId);
  var found = false;
  if(value==null) {
    for(var i=0;i<formal.entries.length;i++) {
      var entry = formal.entries[i];
      if(entry.entries!=null)
	this.displayValue(null,entry);
      select.style.display = "none";
    }
  } else {
    var parts = new RegExp("^\\" + separator + "?(.*?)(\\" + separator + ".*)?$").exec(value);
    for(var i=0;i<formal.entries.length;i++) {
      var entry = formal.entries[i];
      if(parts[1]==entry.value) {
	select.value = parts[1];
	found = true;
	if(entry.entries!=null) {
	  if(parts[2]!=null && parts[2]!="") {
	    this.displayValue(parts[2],entry);
	  } else {
	    this.displayValue(entry.entries[0].value,entry);
	  }
	}
      } else if(entry.entries!=null) {
	this.displayValue(null,entry);
      }
    }
    if(found) {
      select.style.display = "inline";
      // force redraw of select option as a workaround for ie
      /*
      var parent = select.parentNode;
      parent.removeChild(select);
      parent.appendChild(select);
      */
    } else {
      select.style.display = "none";
      alert("IHSelectField: not found formal for '"+parts[0]+"' in value '"+this.baseData[this.dataField]+"'");
    }
  }
}

/**
 * Called when a selection has changed in a select element
 * @param {string} selectId the id attribute of the select element responsible for the event
 * @param {string} the begining of the data leading to this node
 */
IHSelectField.prototype.newValue = function(selectId, baseValue) {
  var separator = ( ( this.formal.separator ) ? this.formal.separator : "." );
  var select = this.doc.getElementById(selectId);
  var value = select.value;
  if(baseValue!="")
    value = baseValue + separator + value;
  this.displayValue(value,this.formal,0);
}

/**
 * Construct the select elements recursively
 * @param {Node} tr the table row element to attach columns to
 * @param {object} formal the formal information for this part of the tree structure
 * @param {level} level the 0-based depth of the node
 * @param {string} fValue the dotted value leading to this node 
 */
IHSelectField.prototype.buildSelect = function(tr,formal,level,fValue) {
  var separator = ( ( this.formal.separator ) ? this.formal.separator : "." );
  var td=null;
  if(this.column[level]==null) {
    this.column[level] = { selectIds: [] };
    this.column[level].tdId = "ihselectfield-td-"+IStruct.getId();
    td = this.doc.createElement("td");
    td.setAttribute("id",this.column[level].tdId);
    this.setClass(td,"tdClass","ihselectfield-td",
		  "tdClass"+level,"ihselectfield-td"+level);
    tr.appendChild(td);
  } else {
    td = this.doc.getElementById(this.column[level].tdId);
  }
  var select = this.doc.createElement("select");
  selId = "ihselectfield-sel-"+IStruct.getId();
  select.setAttribute("id",selId);
  select.onchange = new Function("IStruct.getRegisteredElement('"+this.id+"').newValue('"+selId+"','"+fValue+"')");
  this.setClass(select,"inputClass","ihselectfield-select",
		"inputClass"+level,"ihselectfield-select"+level);
  select.style.display = "none";
  td.appendChild(select);
  this.column[level].selectIds.push(selId);
  formal.selectId = selId;
  for(var i=0;i<formal.entries.length;i++) {
    var entry = formal.entries[i];
    entry.index = i;
    if(entry.value == null) {
      alert("IHSelectField: missing value");
      return;
    }
    if(entry.label == null) {
      alert("IHSelectField: missing label");
      return;
    }
    var option = this.doc.createElement("option");
    option.setAttribute("value",entry.value);
    select.appendChild(option);
    option.appendChild(this.doc.createTextNode(entry.label));
    if(entry.entries!=null) {
      var fValue0 = null;
      if(fValue=="")
	fValue0 = entry.value;
      else
	fValue0 = fValue + separator + entry.value;
      this.buildSelect(tr,entry,level+1,fValue0);
    }
  }
}

/**
 * Get the value for instance.
 **/
IHSelectField.prototype.getValue = function() {
  var separator = ( ( this.formal.separator ) ? this.formal.separator : "." );
  var value="";
  var formal = this.formal;
  var loop = true;
  while(loop) {
    if(formal.entries==null)
      break;
    var select = this.doc.getElementById(formal.selectId);
    var found = false;
    for(var i=0;i<formal.entries.length;i++) {
      var entry = formal.entries[i];
      if(entry.value == select.value) {
	formal = entry;
	found = true;
	if(entry.nodata == true)
	  loop = false;
	break;
      }
    }
    if(found == false) {
      alert("IHSelectField: updateData not found '"+select.value+"', stay at value '"+value+"'");
      break;
    }
    if(loop==true) {
      if(value=="")
	value = select.value;
      else
	value += separator + select.value;
    }
  }
  return value;
}

/**
 * Update the actual data with the content of the editor
 */
IHSelectField.prototype.updateData = function() {
  this.baseData[this.dataField] = this.getValue();
}

/**
 * Returns <code>false</code> to indicate that this object does not handle
 * direct data
 */
IHSelectField.prototype.canDirectData = function() {
  return false;
}

/**
 * Construct a new editor to manage data as hierarchical select values. The actual data uses a dotted notation.
 * @param {object} formals an object describing the behaviour of the editor. All attributes are identical to <code>IHSelectField</code>,
 * except for <code>entries</code> which is now an of dotted strings representing the possible values of the whole tree. An additional 
 * attribute, <code>stopperLabel</code> can be added to the formal to indicate the option to be displayed in the next select when
 * a non-leaf node can be accessible, default is <code>--</code>
 * @class The <code>ISimpleHSelectField</code> class manages data as a hierarchical tree of choices, using simpler
 * formal entries definition
 */
function ISimpleHSelectField(formals) {
  this.base = IHSelectField;
  this.base(formals);
  this.buildFormals = ISimpleHSelectField.prototype.buildFormals;
  this.addFormal = ISimpleHSelectField.prototype.addFormal;
  this.fixFormal = ISimpleHSelectField.prototype.fixFormal;
  this.formal = {};
  for(var k in formals) {
    if(k != "entries") {
      this.formal[k] = formals[k];
    }
  }
  this.buildFormals(formals.entries,this.formal);
  this.fixFormal(this.formal);
}

ISimpleHSelectField.prototype = new IHSelectField;

/**
 * Create a regular formal structure based on a simplified one
 * @param {array} vFormals the simplified formal as an array of dotted string values
 * @param {object} oFormal the object to be filled with new formal structure
 */
ISimpleHSelectField.prototype.buildFormals = function(vFormals,oFormal) {
  for(var i=0;i<vFormals.length;i++) {
    var vFormal = vFormals[i];
    this.addFormal(vFormal,oFormal);
  }
}

/**
 * Modify the formal structure to accept a new option
 * @param {string} vFormal the dotted string representing the new option
 * @param {object} oFormal the object to be filled with new formal structure
 */
ISimpleHSelectField.prototype.addFormal = function(vFormal,oFormal) {
  var parts = /^\.?(.*?)(\..*)?$/.exec(vFormal);
  if(oFormal.entries==null) {
    oFormal.entries = [];
  }
  var entry = null;
  for(var i=0;i<oFormal.entries.length;i++) {
    if(oFormal.entries[i].value == parts[1]) {
      entry = oFormal.entries[i];
      break;
    }
  }
  if(entry==null) {
    entry = {
      value: parts[1],
      label: parts[1]
    };
    oFormal.entries.push(entry);
  }
  if(parts[2]!=null && parts[2]!="") {
    this.addFormal(parts[2],entry);
  } else {
    entry.accessible = true;
  }
}

/**
 * Make sure accessible non-leaf nodes can be selected by adding a special option
 * if the next select
 * @param {object} oFormal the object formal
 */
ISimpleHSelectField.prototype.fixFormal = function(oFormal) {
  if(oFormal.accessible == true) {
    if(oFormal.entries!=null &&	oFormal.entries[0].nodata!=true) {
      var entry = {
	value: "none",
	label: "--",
	nodata: true
      };
      if(this.formal.stopperLabel!=null)
	entry.label = this.formal.stopperLabel;
      oFormal.entries.splice(0,0,entry);
    }
  }
  if(oFormal.entries!=null) {
    for(var i=0;i<oFormal.entries.length;i++) {
      this.fixFormal(oFormal.entries[i]);
    }
  }
}

/**
 * Counter to ensure unicity of IDs
 */
IStruct.lastId = 0;

/**
 * A map of registered objects
 */
IStruct.registeredElements = {}

/**
 * A map of named itypes.
 **/
IStruct.namedITypes = {}

/**
 * Returns a new unique id
 */
IStruct.getId = function() {
  var id = ""+IStruct.lastId++;
  return id;
}

/**
 * Store an object globally and returns the key to retrieve it
 * @param {any} obj the object to be stored
 * @return {string} the key to retrieve the object
 */
IStruct.registerElement = function(obj) {
  var id = "istruct-obj-" + IStruct.getId();
  IStruct.registeredElements[id]=obj;	
  return id;
}

/**
 * Store an itype globally.
 * @param {IType} itype the IType to be stored
 * @param {string} the key to retrieve the object
 */
IStruct.addNamedIType = function(name, itype) {
  IStruct.namedITypes[name] = itype;
}

/**
 * Returns a previously registered object, given its key
 * @param {string} id the object key
 * @return {any} the object or null if not found
 */
IStruct.getRegisteredElement = function(id) {
  return IStruct.registeredElements[id];
}

/**
 * Returns the previously registered itype, given its name
 * @param {string} name the itype name
 * @return {IType} the IType or null if not found
 **/
IStruct.getNamedIType = function(name) {
  return IStruct.namedITypes[name];
}

/**
 * Add missing push method in IE
 */
if(Array!=null) {
  if(Array.push == null) {
    /**
     * Implement method push  of Array as it is missing on IE
     */
    Array.prototype.push = function (element) {
      this[this.length] = element;
      return this.length;
    };
  }
}
