/**
 * FormsForWeb
 * (c) 2000-2004 Lucom GmbH, Erkrath
 *
 * @author	Henning Meinhardt (henning.meinhardt@lucom.com)
 * @version	2.2
 * @since	2.0
 *
 * $Id: ffw.js,v 1.12.2.1.2.3 2005/10/06 11:08:19 henning Exp $
 *
 * Global code and Util library
 */

// ----------------------------------------------------- String.trim() extension

String.prototype.trim = function() {
	// skip leading and trailing whitespace
	// and return everything in between
	var x=this;
	x=x.replace(/^\s*(.*)/, "$1");
	x=x.replace(/(.*?)\s*$/, "$1");
	return x;
}

// ----------------------------------------------------- Number.fill() extension

Number.prototype.fill = function (size) {
	var s = this.toString();
	while (s.length < size)
		s = String ("0" + s);
	return s;
}

// ----------------------------------------------------------- FormsForWebGlobal

function FormsForWebGlobal (objName) {

	// attribute names
	this.ATTR_TYPE =				"type";
	this.ATTR_TOOLTIP =				"tooltip";
	this.ATTR_CLIENT_ERROR =		"clientError";
	this.ATTR_SERVER_ERROR =		"serverError";
	this.ATTR_NOTIFY_ON_CHANGE =	"notifyOnChange";
	this.ATTR_CUSTOM_FORMATTER =	"customFormatter";
	this.ATTR_MANDATORY =			"mandatory";
	this.ATTR_LOOKUP = 				"lookup";
	this.ATTR_LOOKUP_INSTANCE =		"lookup.instance";
	this.ATTR_SELECT =				"select";
	this.ATTR_SELECT_INSTANCE =		"select.instance";

	// notification identifiers
	this.NOTIFY_CHANGE =			"change";

	this.objName = objName;
	this.contextPath = null;
	this.locale = null;
	this.monthNames = null;
	this.tipTimeout = 750;
	this.tipControl = null;
	this.tipShow = false;
	this.tipFixed = false;
	this.tipFocusHook = false;
	this.tipX = 0;
	this.tipY = 0;

	this.toolbarFixed = false;

	this.useStandardSelectBox = false;

	this.animateImages = true;

	this.handleChangeHook = false;

	this.lastFocussedId = null;

	this.CAUSE_NONE =				0;
	this.CAUSE_INVALID_DATA =		1;
	this.CAUSE_NOT_NULLABLE =		2;
	this.CAUSE_MIN_LENGTH_EXCEED =	3;
	this.CAUSE_MAX_LENGTH_EXCEED =	4;
	this.CAUSE_MIN_VALUE_EXCEED =	5;
	this.CAUSE_MAX_VALUE_EXCEED =	6;
	this.CAUSE_SCALE_EXCEED =		7;
	this.CAUSE_PRECISION_EXCEED =	8;

	this.causes = new Array();
	if (typeof (messages) != "undefined") {
		this.causes[this.CAUSE_INVALID_DATA] = messages["causes.INVALID_DATA"];
		this.causes[this.CAUSE_NOT_NULLABLE] = messages["causes.NOT_NULLABLE"];
		this.causes[this.CAUSE_MIN_LENGTH_EXCEED] = messages["causes.MIN_LENGTH_EXCEED"];
		this.causes[this.CAUSE_MAX_LENGTH_EXCEED] = messages["causes.MAX_LENGTH_EXCEED"];
		this.causes[this.CAUSE_MIN_VALUE_EXCEED] = messages["causes.MIN_VALUE_EXCEED"];
		this.causes[this.CAUSE_MAX_VALUE_EXCEED] = messages["causes.MAX_VALUE_EXCEED"];
		this.causes[this.CAUSE_SCALE_EXCEED] = messages["causes.SCALE_EXCEED"];
		this.causes[this.CAUSE_PRECISION_EXCEED] = messages["causes.PRECISION_EXCEED"];
	}

	this.util = new Util();

	this.init = function (locale, monthNames, contextPath) {
		this.locale = locale;
		this.monthNames = monthNames;
		this._stringHandler = new StringHandler();
		this._numberHandler = new NumberHandler();
		this._dateHandler = new DateHandler();
		this._booleanHandler = new BooleanHandler();
		this.contextPath = contextPath;
	}

	this.getContextPath = function() {
		return this.contextPath;
	}

	this.getControl = function (encodedId) {
		if (browser.getElementById)
			return document.getElementById (encodedId);
		else
			return document.all[encodedId];
	}

	this.getIndexedControl = function (id, index) {
		var pos = id.lastIndexOf (".");
		if (pos < 0)
			return null;	// illegal argument
		var prefix = id.substring (0, pos);
		var suffix = id.substring (pos + 1);
		return this.getControl (prefix + ":" + index + "." + suffix);
	}

	this.getControlId = function (control) {
		return (control) ? control.id : null;
	}

	// returns control's plain ID (wihtout rowIndex)
	this.getPlainControlId = function (control) {
		var str = this.getControlId (control);
		var pos = str.lastIndexOf (".");
		if (pos < 0)
			return str;
		var prefix = str.substring (0, pos);
		var suffix = str.substring (pos + 1);
		pos = prefix.lastIndexOf (":");
		if (pos < 0)
			return null;	// illegal argument
		prefix = prefix.substring (0, pos);
		return prefix + "." + suffix;
	}

	// returns -1 if the control is not indexed
	this.getControlIndex = function (control) {
		var str = this.getControlId (control);
		var pos = str.lastIndexOf (".");
		if (pos < 0)
			return -1;
		var prefix = str.substring (0, pos);
		pos = prefix.lastIndexOf (":");
		if (pos < 0)
			return null;	// illegal argument
		return prefix.substring (pos + 1);
	}

	this.getControlValue = function (control) {

		if (typeof (control.value) != "undefined")
			return control.value;
		else
			return control.innerHTML;
	}

	// handleChange will be fired -> i.e. "public" change of data
	this.setControlValue = function (control, value) {

		if (typeof (control.value) != "undefined")
			control.value = value;
		else
			control.innerHTML = value;

		this.handleChange (control, null);	// xxx some browsers (ie, opera) will not fire onChange after internal modification!
		// xxxx todo: custom handler additions will not be executed this way!
	}

	// handleChange will not be fired -> i.e. "hidden" change of data
	this.setControlValueNoInvoke = function (control, value) {

		this.handleChangeHook = true;	// in case the browser will fire the event!
		if (typeof (control.value) != "undefined")
			control.value = value;
		else
			control.innerHTML = value;
		this.handleChangeHook = false;
	}

	this.animate = function (image, status) {

		if (!this.animateImages || browser.nav4)
			return;

		var i = image.src.lastIndexOf (".");
		var basename = image.src.substring (0, i);
		var ext = image.src.substring (i + 1);
		if (basename.substring (basename.length - 5) == "_over")
			basename = basename.substring (0, basename.length - 5);
		if (status == 1)
			basename = basename + "_over";
		image.src = basename + "." + ext;
	}

	// ---------------------------------------------------- control's attributes

	this.Attributes = function() {
		this.arr = new Array();
		this.put = function (name, value) {
			this.arr[name] = value;
		}
		this.get = function (name) {
			return this.arr[name];
		}
	}

	this.attributes = new Array();

	this.getAttribs = function (control) {
		var controlId = this.getControlId (control);
		var attribs = this.attributes[controlId];
		if (!attribs) {
			attribs = new this.Attributes();
			this.attributes[controlId]=attribs;
		}
		return attribs;
	}

	// Stores a temporary value under a given name into a control.
	// This method might be used in conjunction with restoreTempValue() when
	// a temporary value has to be set on the control but the original value
	// has to be restored later.
	// Example:
	//   ffw.storeTempValue (control, "mytemp", control.style.backgroundColor);
	//   control.style.backgroundColor = ..set new value;
	//   ... do something ...
	//   var restoredValue = ffw.restoreTempValue (control, "mytemp);
	//   control.style.backgroundColor = restoredValue;
	this.storeTempValue = function (control, name, value) {

		var attribs = this.getAttribs (control);
		attribs.put ("." + name, value);
	}

	// Restores that value that was previously stored in a control using
	// storeTempValue(). The restored values is removed from the object
	// and returned to the caller. Thus, any subsequent call to
	// restoreTempValue() using the same parameters will return null.
	// See storeTempValue() for an usage example
	this.restoreTempValue = function (control, name) {

		var attribs = this.getAttribs (control);
		var value = attribs.get ("." + name);
		attribs.put ("." + name, null);
		return value;
	}

	// -------------------------------------------------------------------- I18N

	// note: additional parameters possible for placeholders
	this.getMessage = function (key) {

		var msg = messages[key];
		if (!msg)
			return null;

		for (var i = 1; i < this.getMessage.arguments.length; i++) {
			var arg = this.getMessage.arguments[i];
			msg = this.replace (msg, "{" + i + "}", arg, true);
		}
		return msg;
	}

	// replace "find" by "replace" in "source"
	this.replace = function (source, find, replace, all) {

		if (!source)
			return null;

		var buf = "";
		var pos = 0, posLast = 0;
		var lenFind = find.length;

		while ((pos = source.indexOf (find, posLast)) >= 0) {
			buf += source.substring (posLast, pos);
			if (replace != null)
				buf += replace;
			posLast = pos + lenFind;
			if (!all)
				break;
		}
		buf += source.substring (posLast);

		return buf;
	}

	// -------------------------------------------------------------------------

	this.getHandler = function (control) {

		var attribs = this.getAttribs (control);
		var type = attribs.get (this.ATTR_TYPE);
		var customFormatter = attribs.get (this.ATTR_CUSTOM_FORMATTER);

		if (customFormatter)
			return eval (customFormatter);
		if (type == null)
			return null;
		if (type == "string")
			return this._stringHandler;
		if (type == "number")
			return this._numberHandler;
		if (type == "date")
			return this._dateHandler;
		if (type == "boolean")
			return this._booleanHandler;

		alert ("unknown type: " + type + " for control " +
			this.getControlId (control));
		return null;
	}
	this.getSelect = function (control) {

		if (this.useStandardSelectBox) {
			return null;
		}

		var attribs = this.getAttribs (control);

		// check if an instance has already been created
		var instance = attribs.get (this.ATTR_SELECT_INSTANCE);
		if (instance)
			return instance;

		// check if a select box is defined for this control
		var attribs = this.getAttribs (control);
		var selectAttribs = attribs.get (this.ATTR_SELECT);
		if (!selectAttribs)
			return null;

		// create a new select box instance
		var implClass = this.util.getAttribute (selectAttribs, 0);
		if (implClass == null)
			implClass = "Select";
		instance = eval ("new " + implClass + " (control)");
		attribs.put (this.ATTR_SELECT_INSTANCE, instance);
		return instance;
	}

	this.getLookup = function (control) {

		var attribs = this.getAttribs (control);

		// check if an instance has already been created
		var instance = attribs.get (this.ATTR_LOOKUP_INSTANCE);
		if (instance)
			return instance;

		// check if a lookup box is defined for this control
		var attribs = this.getAttribs (control);
		var lookupAttribs = attribs.get (this.ATTR_LOOKUP);
		if (!lookupAttribs)
			return null;

		// create a new lookup box instance
		instance = new Lookup (control);
		attribs.put (this.ATTR_LOOKUP_INSTANCE, instance);
		return instance;
	}

	this.handleChange = function (control, event) {

		// update the select box (if any). This will handle auto-completion of
		// manually inserted values as well
		var select = this.getSelect (control);
		if (select) {
			var hasChanged = select.changed();
			// If selection has changed, a new "changed" event will be fired.
			// So stop processing here. See docs on select.changed() for more
			// details.
			if (hasChanged)
				return;
		}

		var attribs = this.getAttribs (control);

		// eventually notify model about the change, but don't do validation
		// here. This is not necessary as the form is going to be submitted
		// anyway right now...
		if (attribs.get (ffw.ATTR_NOTIFY_ON_CHANGE)) {
			this.format (control, false);
			notify (control, ffw.NOTIFY_CHANGE);
			return;
		}

		// reset the errors
		attribs.put (ffw.ATTR_SERVER_ERROR, null);
		attribs.put (ffw.ATTR_CLIENT_ERROR, null);

		if (this.handleChangeHook)
			return;

		// format and validate the control
		this.format (control, false);
		this.validate (control);
	}

	this.handleFocus = function (control, event) {

		this.lastFocussedId = ffw.getControlId (control);

		if (!this.tipFocusHook)
			this.format (control, true);

		var attribs = this.getAttribs (control);
		var htmlEditor = attribs.get("HTMLEditor");

		// activate / deactivate HTML Editor
		if (HTMLEditorIsIncluded) {
			if (htmlEditor) {
				var editorObject = this.getHTMLEditorObject(control);
				activateEditor(editorObject, control);
			} else
				deactivateEditor();
		}

		if (attribs.get ("highlightBorder")) {
			var body = (htmlEditor) ?
				this.getHTMLEditorObject (control) : control;
			this.storeTempValue (control, "savedBorder", "." + body.style.border);
			body.style.border = attribs.get ("highlightBorder");
		}

		if (control != this.selectControl)
			this.hideBox();
		var select = this.getSelect (control);
		if (select) {
			if (!select.isAutoPopupHooked() ||
				control != this.selectControl) {
				if (select.isAutoPopup())
					this.showBox (control);
			}
			select.unhookAutoPopup();
		}
	}

	this.getLastFocussedId = function() {
		return this.lastFocussedId;
	}

	this.getViewSettings = function() {

		var left = -1;
		if (browser.ie && ffw.toolbarFixed)
			left = document.getElementById("ScrollArea").scrollLeft; // special handling for IE with fixed toolbar
		else if (document.documentElement.scrollLeft)
			left = document.documentElement.scrollLeft;
		else if (document.body.scrollLeft)
			left = document.body.scrollLeft;
		else if (window.scrollLeft)
			left = window.scrollLeft;

		var top = -1;
		if (browser.ie && ffw.toolbarFixed)
			top = document.getElementById("ScrollArea").scrollTop; // special handling for IE with fixed toolbar
		else if (document.documentElement.scrollTop)
			top = document.documentElement.scrollTop;
		else if (document.body.scrollTop)
			top = document.body.scrollTop;
		else if (window.scrollTop)
			top = window.scrollTop;

		var s = left + ";" + top;
		var id = ffw.lastFocussedId;
		if (id != null)
			s += ";" + id;
		return s;
	}

	this.restoreViewSettings = function (viewSettings) {
		var pos1 = viewSettings.indexOf (";");
		var pos2 = viewSettings.indexOf (";", pos1 + 1);
		var left = viewSettings.substring (0, pos1);
		var top = (pos2 < 0) ?
			viewSettings.substring (pos1 + 1) :
			viewSettings.substring (pos1 + 1, pos2);
		var id = (pos2 < 0) ?
			null :
			viewSettings.substring (pos2 + 1);
		if (left == -1) left = 0;
		if (top == -1) top = 0;

		if (browser.ie && ffw.toolbarFixed) {
			document.getElementById("ScrollArea").scrollLeft = left;
			document.getElementById("ScrollArea").scrollTop = top;
		} else
			window.scrollTo(left, top);

		if (id != null) {
			var control = ffw.getControl (id);
			if (control != null && typeof (control) != "undefined" && control.style.visibility != "hidden"
			    && control.type != "hidden")
				control.focus();
		}
	}

	this.getHTMLEditorObject = function(control) {
		obj = document.getElementById("editor_" + control.name);
		if (obj == null)
			obj = document.getElementById("editorContent");

		return obj;
	}


	this.handleBlur = function (control, event) {

		this.format (control, false);

		var attribs = this.getAttribs (control);
		var htmlEditor = attribs.get("HTMLEditor");
		var savedBorder = this.restoreTempValue (control, "savedBorder");
		if (attribs.get ("highlightBorder") && savedBorder) {
			var saved = savedBorder.substring (1);
			if (!saved || saved.length == 0)
				saved = "none";
			var body = (htmlEditor) ?
			this.getHTMLEditorObject (control) : control;
			body.style.border = saved;
		}
		// pass data of HTMLEditor to TextArea
		if (HTMLEditorIsIncluded && htmlEditor) {
			passData();
		}

	}

	this.handleOver = function (control, event) {
		this.initTip (control, event);
	}

	this.handleMove = function (control, event) {
		if (this.tipShow && !this.tipFixed) {
			var pos = this.getCursorPosition (event);
			this.setTipPosition (pos[0], pos[1]);
		}
	}

	this.handleOut = function (control, event) {
		this.hideTip();
	}

	this.handleClick = function (control, event) {
	}

	this.validate = function (control) {

		var attribs = this.getAttribs (control);
		var error = false;

		var select = this.getSelect (control);
		if (select && select.isMandatory()) {
			if (select.getSelectedIndex() < 0 &&
				this.getControlValue (control) > "") {	// 02.05.03: only show error if control is filled
				this.setError (control, messages["error.SELECTION_REQUIRED"]);
				error = true;
			}
		}

		var handler = this.getHandler (control);
		if (!error && handler)
			error |= handler.validate (control);

		this.refreshError (control);

		return !error;
	}

	this.refreshError = function (control) {

		var attribs = this.getAttribs (control);
		var serverError = attribs.get (this.ATTR_SERVER_ERROR);
		var clientError = attribs.get (this.ATTR_CLIENT_ERROR);
		if (serverError) {
			control.style.color = "white";
			control.style.backgroundColor = "#EE1111";
		} else if (clientError) {
			control.style.color = "white";
			control.style.backgroundColor = "#EE1111";
		} else {
			control.style.color = "";
			control.style.backgroundColor = "";
		}
		if (this.tipControl == control &&
			(this.tipShow)) {
			this.showTip();	// refresh
		}
	}

	this.validateAll = function (form) {

		var error = false;
		with (form) {
			for (var i = 0; i < elements.length; i++)
				if (this.getControlId (elements[i]))	// skip internal elements
					error |= this.validate (elements[i]);
		}
		return !error;
	}

	this.setError = function (control, error) {

		var attribs = this.getAttribs (control);
		attribs.put (this.ATTR_CLIENT_ERROR, error);
		attribs.put (this.ATTR_SERVER_ERROR, null);
		this.refreshError (control);
	}

	this.clearError = function (control) {

		var attribs = this.getAttribs (control);
		attribs.put (this.ATTR_SERVER_ERROR, null);
		attribs.put (this.ATTR_CLIENT_ERROR, null);
		this.refreshError (control);
	}

	this.format = function (control, forInput) {
		// skip this for standard select-boxes
		if (control.type == "select-one") return;
		var handler = this.getHandler (control);
		if (handler) {
			var str = handler.format (control, forInput);
			if (str != null) {
				this.setControlValueNoInvoke (control, str);
				if (forInput)
					control.select();
			}
		}
	}

	// only used by IE4FormHandler (in onKeyUp handler)
	this.checkTextArea = function (control, maxLength, maxLines) {
		if (maxLines < 1)
			return;
		var cut = this.cutString (this.getControlValue (control), maxLength, maxLines);
		if (cut.length < control.value.length)
			this.setControlValueNoInvoke (control, cut);
	}

	this.cutString = function (value, maxLength, maxLines) {
		if (value.length > maxLength)
			value = value.substring (0, maxLength);

		if (value.length == 0)
			return value;

		var count = 1;
		var pos = value.indexOf ("\r\n");
		while (pos > -1) {
			count++;
			if (count > maxLines)
				return value.substring (0, pos);
			pos = value.indexOf ("\r\n", pos + 1);
		}
		return value;
	}

	// Get cursor position with respect to the page.
	this.getCursorPosition = function (event) {

		var x = 0;
		var y = 0;

		if (window.event) {	// IE
//			x = window.event.x + document.body.scrollLeft;
			x = window.event.clientX + document.body.scrollLeft;
//			x = window.event.clientX + document.documentElement.scrollLeft +
//				document.body.scrollLeft;
//			y = window.event.y + document.body.scrollTop;
			y = window.event.clientY + document.body.scrollTop;
//			y = window.event.clientY + document.documentElement.scrollTop +
//				document.body.scrollTop;
		} else if (event.clientX) {	// NAV and MOZ
			x = event.clientX + window.scrollX;
			y = event.clientY + window.scrollY;
		}

		var pos = new Array(2);
		pos[0] = x;
		pos[1] = y;

		return pos;
	}

	this.initTip = function (control, event) {

		this.hideTip();	// avoid screen flickering in Netscape 6
		this.tipControl = control;
		this.tipShow = true;

		var pos = this.getCursorPosition (event);
		this.setTipPosition (pos[0], pos[1]);

		window.setTimeout (this.objName + ".showTip();", this.tipTimeout);
	}

	this.setTipPosition = function (x, y) {

		this.tipX = x;
		this.tipY = y;
		if (this.tipShow)
			this.moveTip();
	}

	this.moveTip = function() {

		var theTip = this.getTooltip();
		theTip.style.left = (this.tipX + 10) + "px";
		theTip.style.top  = (this.tipY + 5) + "px";
	}

	// note: the tooltip will only be shown if this.showTip is set to true.
	this.showTip = function() {

		// shall we display the tooltip?
		if (!this.tipShow)
			return;

		var attribs = this.getAttribs (this.tipControl);
		var tooltip = attribs.get (this.ATTR_TOOLTIP);
		var serverError = attribs.get (this.ATTR_SERVER_ERROR);
		var clientError = attribs.get (this.ATTR_CLIENT_ERROR);

		var header = null;
		var body = null;
		var headerClass = null;
		var bodyClass = null;

		// handle serverError (this has precedence over clientError)
		if (serverError) {
			header = messages["error.SERVER_ERROR"];
			body = this.formatServerError (serverError);
			bodyClass = 'bodyError';
			headerClass = 'headerError';

		// handle clientError
		} else if (clientError) {
			header = messages["error.CLIENT_ERROR"];
			body = attribs.get (this.ATTR_CLIENT_ERROR);
			bodyClass = 'bodyError';
			headerClass = 'headerError';

		// handle tooltip
		} else {
			if (attribs.get (this.ATTR_MANDATORY))
				header = messages["error.MANDATORY"];
			body = tooltip;
			if (header == null && body == null)
				this.tipShow = false; // there is nothing to display
			bodyClass = 'body';
			headerClass = 'header';
		}

		// shall we still display the tooltip?
		if (!this.tipShow) {
			this.hideTip();  // remove tooltip if an error has been corrected
			return;
		}

		// get the tooltip
		var theTip = this.getTooltip();

		// compose the content
		var content = "";
		if (typeof (header) != "undefined" && header != null)
			content += '<div class="' + headerClass + '">' + header + '</div>';
		if (typeof (body) != "undefined")
			content += '<div class="' + bodyClass + '">' + body + '</div>';

		// show the tooltip
		this.tipFocusHook = true;
		theTip.innerHTML = content;
		this.moveTip();
		theTip.style.visibility = "visible";
		this.tipFocusHook = false;
	}

	this.hideTip = function() {

		// get the tooltip
		var theTip = this.getTooltip();

		// hide the tooltip
		this.tipFocusHook = true;
		theTip.style.visibility="hidden";
		this.tipFocusHook = false;
		this.setTipPosition (0,0);	// to release scrollbars if tip is outside viewpoint
		this.tipShow = false;
	}

	this.isTipVisible = function() {

		// get the tooltip
		var theTip = this.getTooltip();

		return (theTip.style.visibility == "visible");
	}

	this.getTooltip = function() {

		if (browser.getElementById)
			return document.getElementById ('ffw._tooltip');
		else
			return document.all['ffw._tooltip'];
	}

	this.formatServerError = function (serverError) {
		var text = "";
		var pos = 0;
		while (pos < serverError.length) {
			if (text > "")
				text += "<br>";
			var posEnd = serverError.indexOf ("//", pos + 2);
			if (posEnd < 0)
				posEnd = serverError.length;
			var pos2 = serverError.indexOf (":", pos + 2);
			var cause = (pos2 < 0) ?
				serverError.substring (pos + 2, posEnd) :
				serverError.substring (pos + 2, pos2);
			var msg = (pos2 < 0) ?
				null :
				serverError.substring (pos2 + 1, posEnd);
			text += this.causes[cause];
			if (msg != null)
				text += "<br>" + msg;
			pos = posEnd;
		}
		return text;
	}

	this.selectBox = null;

	this.setSelectBox = function (selectBox) {
		this.selectBox = selectBox;
	}
	this.getSelectBox = function() {
		return this.selectBox;
	}

	this.selectControl = null;

	this.toggleSelectBox = function (control) {

		if (!control)   // make sure we got a control. otherwise just exit
			return;

		var select = this.getSelect (control);

		if (control == this.selectControl)
			select.toggleBox();
		else {
			this.hideBox();
			this.showBox (control);
		}

		select.hookAutoPopup();
		control.focus();
	}

	this.openLookup = function (url) {

		var child = window.open (url,
			"lookup", "width=500,height=400" +
			",resizable=yes,dependent=yes,hotkeys=no,locationbar=no,toolbar=no" +
			",status=no,scrollbars=yes,menubar=no,directories=no");
		child.opener = self;
	}

	this.showBox = function (control) {

		var select = this.getSelect (control);

		if (control != this.selectControl) {
			this.selectControl = control;
			select.initBox();
		}
		select.showBox();
	}

	this.hideBox = function() {
		if (!this.selectControl)
			return;
		var select = this.getSelect (this.selectControl);
		select.hideBox();
		this.selectControl = null;
	}
}

var ffw = new FormsForWebGlobal ("ffw");

// ------------------------------------------------------------------------ Util

function Util() {

	this.getAttribute = function (attributes, index) {
		return eval ("this._internalGetAttribute (" +
			index + "," + attributes + ")");
	}

	this._internalGetAttribute = function (index) {
		return this._internalGetAttribute.arguments[index + 1];
	}

	this.UNSET_DATEPART = "#";

	this.adjustYear = function (year) {
		var y = year;
		if (y < 100)
			y += 1900;
		else if (y < 200)
			y += 1900;	// Netscape bug
		return y;
	}

	this.countLines = function (text) {
		if (!text || text.length == 0)
			return 0;
		var count = 1;
		var pos = text.indexOf ("\r\n");
		while (pos > -1) {
			count++;
			pos = text.indexOf ("\r\n", pos + 1);
		}
		return count;
	}

	// returns a float representing the parsed number.
	// If parsing fails, null is returned.
	// @todo: error handling might be replaced by throwing exceptions.
	// (exceptions are not supported in older JavaScript version!)
	this.parseNumber = function (str, prefix, suffix,
		decimalPoint, groupingSeparator) {

		str = str.trim();

		// empty string?
		if (str.length == 0)
			return null;

		// remove prefix and suffix
		if (prefix) {
			if (prefix.toUpperCase() ==
				str.substring (0, prefix.length).toUpperCase())
				str = str.substring (prefix.length).trim();
		}
		if (suffix) {
			if (suffix.toUpperCase() ==
				str.substring (str.length - suffix.length).toUpperCase())
				str = str.substring (0, str.length - suffix.length).trim();
		}
		// extract negative sign
		var negSign = false;
		if (str.charAt (0) == "-") {
			str = str.substring (1).trim();
			negSign = true;
		}

		// no digits remaining?
		if (str.length == 0)
			return null;

		// make the number uniform (remove grouping separators, unify decimal point)
		var s = "";
		var fraction = false;
		for (var i = 0; i < str.length; i++) {
			ch = str.charAt (i);
			if (ch >= '0' && ch <= '9')
				s += ch;
			else if (ch == decimalPoint && !fraction) {
				s += ".";
				fraction = true;
			} else if (ch != groupingSeparator || fraction)
				return null;	// groupingSeparator not allowed in fraction part
		}
		if (s.length == 0)
			s = "0";
		else if (s.charAt (0) == ".")
			s = "0" + s;

		// parse the number
		var number = parseFloat (s);

		// apply the negative sign again (if there was one before)
		if (negSign)
			number = -number;

		return number;
	}

	// Returns a string representing the formatted number.
	// May return null to indicate that formatting failed
	this.formatNumber = function (number, prefix, suffix, precision, scale,
		decimalPoint, groupingSeparator, groupingSize) {

		// round to scale
		if (!scale)
			scale = 0;

		number *= Math.pow (10, scale);
		number = Math.round (number);
		number /= Math.pow (10, scale);

		// separate into integer part and fraction
		var intpart, fraction;
		var temp = Math.abs (number).toString();
		var pos = temp.indexOf (".");
		if (pos < 0) {
			intpart = temp;
			fraction = "";
		} else {
			intpart = temp.substring (0, pos);
			fraction = temp.substring (pos + 1);
		}

		// handle integer part
		var digits = intpart.length;
		if (groupingSeparator && digits > groupingSize) {
			pos = 0;
			temp = "";
			var firstchunk = digits % groupingSize;
			if (firstchunk > 0) {
				temp += intpart.substring (0, firstchunk) + groupingSeparator;
				pos += firstchunk;
				digits -= firstchunk;
			}
			while (digits > 0) {
				temp += intpart.substring (pos, pos + groupingSize);
				pos += groupingSize;
				digits -= groupingSize;
				if (digits > 0)
					temp += groupingSeparator;
			}
			intpart = temp;
		}

		// handle fraction
		if (scale > 0) {
			while (fraction.length < scale)
				fraction += "0";
			fraction = fraction.substring (0, scale);
		} else
			fraction = "";

		// put them all together
		var str = intpart;
		if (scale > 0)
			str += decimalPoint + fraction;
		if (number < 0)
			str = "-" + str;
		if (prefix)
			str = prefix + " " + str;
		if (suffix)
			str += " " + suffix;

		return str;
	}

	function ParsePosition (str) {
		this.str = str;
		this.pos = 0;
		this.markPos = 0;

		this.advance = function (amount) {
			this.pos +=amount;
			if (this.pos > this.str.length)
				this.pos = this.str.length;
		}

		this.mark = function() {
			this.markPos = this.pos;
		}

		this.rollback = function() {
			this.pos = this.markPos;
		}

		this.get = function() {
			return this.str.substring (this.pos);
		}

		this.skipSeparator = function() {
			while (this.pos < this.str.length &&
				" .,/-:".indexOf (this.str.charAt (this.pos)) >= 0)
				this.pos++;
		}

		this.skip = function (str, ignoreCase) {
			if ((ignoreCase && (this.get().toUpperCase().indexOf (
				str.toUpperCase())) == 0) ||
				(!ignoreCase && (this.get().indexOf (str) == 0)))
				this.pos += str.length;
			this.skipSeparator();
		}

		this.scanDigits = function (digits, min, max) {
			var startPos = this.pos;
			while (this.pos < this.str.length &&
				(this.pos - startPos) < digits &&
				"0123456789".indexOf (this.str.charAt (this.pos)) >= 0)
				this.pos++;
			var endPos = this.pos;
			this.skipSeparator();
			if (startPos == endPos)
				return null;
			var value = Number (this.str.substring (startPos, endPos));
			if ((min != null && value < min) || (max != null && value > max))
				return null;

			return value;
		}
	}

	function FieldSizePair (field, size) {
		this.field = field;
		this.size = size;
	}

	// Note: this is adapted from java.text.SimpleDataFormat source code.
	// Returns an array of tokens and string literals.
	this.tokenizeMask = function (mask) {
		var tokens = new Array();
		var current = null;
		var patternChars = "yMdHms";
		var token = 0;
		for (var i = 0; i < mask.length; i++) {
			var thisChar = mask.charAt(i);
			var index = patternChars.indexOf(thisChar);
			if (index == -1) {
				current = null;
				if (thisChar == "'") {
					// Quoted text section; skip to next single quote
					var pos = mask.indexOf("'",i+1);
					if (pos == -1) {
						tokens[token++] = new FieldSizePair (null, 0);
						alert ("can't parse mask!");
					}
					if ((pos+1 < mask.length) && (mask.charAt(pos+1) == "'")) {
						tokens[token++] = String (mask.substring(i+1,pos+1));
					} else {
						tokens[token++] = String (mask.substring(i+1,pos));
					}
					i = pos;
				} else {
					// A special character
					tokens[token++] = String (thisChar);
				}
			} else {
				// A valid field
				if (current != null &&
					patternChars.charAt (index) == current.field) {
					current.size++;
				} else {
					current = new FieldSizePair (patternChars.charAt (index), 1);
					tokens[token++] = current;
				}
			}
		}
		return tokens;
	}

	// returns a simple String containing the tokens in the order of
	// appearance. Each token is contained only once, regardless of it's size.
	this.getTokenOrder = function (tokens) {

		var order = "";
		for (var i = 0; i < tokens.length; i++) {
			var token = tokens[i];
			if (token.size) {
				// a FieldSizePair
				order += token.field;
			}
		}
		return order;
	}

	// Returns a string representing the parsed date. The returned string has
	// the fixed format "yyyyMMddHHmmss". If parsing fails, null is returned.
	// Note: parsing doesn't include validation, i.e. a date like 99.99.9999
	// would be accepted here, too. The caller is responsible for validating
	// the data!
	// @todo: error handling might be replaced by throwing exceptions.
	// (exceptions are not supported in older JavaScript version!)
	// Note: twoDigCenturyBorder: if null, all 2-dig years are interpreted as
	// being in the 21th century (20xx), otherwise all years >= the border
	// are treated as being in the 20th century (19xx).
	this.parseDate = function (str, mask, twoDigCenturyBorder,
		acceptRelative, autoComplete, monthNames) {

		str = str.trim();

		// empty string?
		if (str.length == 0)
			return null;

		var tokens = this.tokenizeMask (mask);
		var order = this.getTokenOrder (tokens);
		var iYear = order.indexOf ("y");
		var iMonth = order.indexOf ("M");
		var iDay = order.indexOf ("d");
		var iHour = order.indexOf ("H");
		var iMin = order.indexOf ("m");
		var iSec = order.indexOf ("s");

		var year = null, month = null, day = null,
			hour = null, min = null, sec = null;
		var now = new Date();	// note: now.getYear() might return incorrect values (browser bug)!

		var ppos = new ParsePosition (str);
		for (var i = 0; i < tokens.length; i++) {
			var token = tokens[i];
			if (token.size) {
				// a FieldSizePair
				switch (token.field) {
				case "y":
					ppos.mark();
					year = this.scanYear (ppos, twoDigCenturyBorder);
					if (year == null && autoComplete)
						ppos.rollback();
					break;
				case "M":
					ppos.mark();
					month = this.scanMonth (ppos, true, monthNames);
// HM 30.04.03 disabled as it produced funny results when entered "12.13" in
// a control formatted as "dd.MM.yyyy" -> result was "12.01.13"
// (according to Lutz' mail of 30.04.03)
//					if (month == null && autoComplete)
//						ppos.rollback();
					break;
				case "d":
					ppos.mark();
					day = this.scanDay (ppos, month, year);
					if (day == null && autoComplete)
						ppos.rollback();
					break;
				case "H":
					hour = ppos.scanDigits (2, 0, 23);
					break;
				case "m":
					min = ppos.scanDigits (2, 0, 59);
					break;
				case "s":
					sec = ppos.scanDigits (2, 0, 59);
					break;
				default:
					alert ('unknown token.field:' + token.field);
					ppos.skip (token.field, true);
					break;
				}
			} else {
				// a single char or string
				ppos.skip (token, true);
			}
		}

		if (autoComplete) {

			// --- handle date part

			// handle "(..)dMy(..)" and "(..)yMd(..)" - only day missing -> 1st of month
			if (((iYear > iMonth && iMonth > iDay && iDay >= 0) ||
				(iDay > iMonth && iMonth > iYear && iYear >= 0)) &&
				day == null && month != null && year != null)
				day = 1;

			// handle "(..)dMy(..)" and "(..)yMd(..)" - only year missing -> current year
			else if (((iYear > iMonth && iMonth > iDay && iDay >= 0) ||
				(iDay > iMonth && iMonth > iYear && iYear >= 0)) &&
				day != null && month != null && year == null)
				year = this.adjustYear (now.getYear());

			// handle "(..)Mdy(..)" - only year missing -> current year
			else if (iYear > iDay && iDay > iMonth && iMonth >= 0 &&
				day != null && month != null && year == null)
				year = this.adjustYear (now.getYear());

			// handle "(..)My(..)" and "(..)yM(..)" - only month missing -> January
			else if (iYear >= 0 && iMonth >= 0 &&
				month == null && year != null)
				month = 1;

			// handle "(..)My(..)" and "(..)yM(..)" - only year missing -> current year
			else if (iYear >= 0 && iMonth >= 0 &&
				month != null && year == null)
				year = this.adjustYear (now.getYear());

			// handle "(..)dM(..)" and "(..)Md(..)" - only month missing -> current month
			else if (iMonth >= 0 && iDay >= 0 &&
				day != null && month == null)
				month = now.getMonth() + 1;

			// handle "(..)dM(..)" and "(..)Md(..)" - only day missing -> 1st of month
			else if (iMonth >= 0 && iDay >= 0 &&
				day == null && month != null)
				day = 1;

			// --- handle time part

			// handle "(..)Hms(..)" - only seconds missing -> set to 00
			if (iSec > iMin && iMin > iHour && iHour >= 0 &&
				hour != null && min != null && sec == null)
				sec = 0;

			// handle "(..)Hm(..)" - only minute -> set to 00
			else if (iMin > iHour && iHour >= 0 &&
				hour != null && min == null)
				min = 0;
		}

		// check if we got values for all required date parts
		if ((iYear >= 0 && year == null) ||
			(iMonth >= 0 && month == null) ||
			(iDay >= 0 && day == null) ||
			(iHour >= 0 && hour == null) ||
			(iMin >= 0 && min == null) ||
			(iSec >= 0 && sec == null))
			return null;

		return this.makeDateStr (year, month, day, hour, min, sec);
	}

	// returns a string representing the formatted date.
	// Note: dateStr must be in format "yyyyMMddHHmmss"!
	this.formatDate = function (dateStr, mask, monthNames) {

		if (!this.isValidDateStr (dateStr))
			return null;

		// make all unset parts 0, to be able to convert them to Numbers
		dateStr = dateStr.replace (/\#/g, "0");	// can't use UNSET_DATEPART constant here ;-(

		var tokens = this.tokenizeMask (mask);
		var str = "";

		for (var i = 0; i < tokens.length; i++) {
			var token = tokens[i];
			if (token.size) {
				// a FieldSizePair
				switch (token.field) {
				case "y":
					str += this.getYearPart (dateStr).fill (4).substring (
						(token.size == 4) ? 0 : 2, 4);
					break;
				case "M":
					var month = this.getMonthPart (dateStr);
					if (token.size == 4)
						str += monthNames[2*(month - 1)];
					else if (token.size == 3)
						str += monthNames[2*(month - 1) + 1];
					else
						str += month.fill (token.size);
					break;
				case "d":
					var day = this.getDayPart (dateStr);
					str += day.fill (token.size);
					break;
				case "H":
					var hour = this.getHourPart (dateStr);
					str += hour.fill (token.size);
					break;
				case "m":
					var min = this.getMinPart (dateStr);
					str += min.fill (token.size);
					break;
				case "s":
					var sec = this.getSecPart (dateStr);
					str += sec.fill (token.size);
					break;
				default:
					alert ('unknown token.field:' + token.field);
					str += token.field;
					break;
				}
			} else {
				// a single char or string
				str += token;
			}
		}

		return str;
	}

	this.scanDay = function (ppos, month, year) {
		var max = (month != null && year != null) ?
			this.getDaysInMonth (month, year) : 31;
		return ppos.scanDigits (2, 1, max);
	}

	this.getDaysInMonth = function (month, year) {
		var days = 31;
		if (month == 4 || month == 6 || month == 9 || month == 11)
			days = 30;
		else if (month == 2) {
			if (year % 4 > 0 || (year % 100 == 0 && year % 400 > 0))
				days = 28;
			else
				days = 29;
		}
		return days;
	}

	this.scanMonth = function (ppos, scanName, monthNames) {
		if (scanName) {
			var index = this.scanMonthName (ppos, monthNames);
			if (index)
				return index;
		}
		return ppos.scanDigits (2, 1, monthNames.length / 2);
	}

	this.scanMonthName = function (ppos, monthNames) {
		str = ppos.get().toUpperCase();
		for (var i = 0; i < monthNames.length / 2; i++) {
			if (str.indexOf (monthNames[2*i].toUpperCase()) == 0) {
				ppos.advance (monthNames[2*i].length);
				ppos.skipSeparator();
				return i + 1;
			}
			if (str.indexOf (monthNames[2*i + 1].toUpperCase()) == 0) {
				ppos.advance (monthNames[2*i + 1].length);
				ppos.skipSeparator();
				return i + 1;
			}
		}
		return null;
	}

	this.scanYear = function (ppos, twoDigCenturyBorder) {
		var startpos = ppos.pos;
		var year = ppos.scanDigits (4, 0, null);
		var endpos = ppos.pos;
		ppos.skipSeparator();
		if (year == null)
			return null;
		if (endpos - startpos > 2)
			return year.fill (4);
		else
			return ((twoDigCenturyBorder && year >= twoDigCenturyBorder) ?
				1900 : 2000) + year;
	}

	// takes individual values for year, month, date, hour, minute and second
	// and returns a corresponding string in format "yyyyMMddHHmmss"
	this.makeDateStr = function (year, month, day, hour, min, sec) {
		var dateStr = "";
		var unset = this.UNSET_DATEPART + this.UNSET_DATEPART;
		dateStr += (year != null) ? Number (year).fill (4) : (unset + unset);
		dateStr += (month != null) ? Number (month).fill (2) : unset;
		dateStr += (day != null) ? Number (day).fill (2) : unset;
		dateStr += (hour != null) ? Number (hour).fill (2) : unset;
		dateStr += (min != null) ? Number (min).fill (2) : unset;
		dateStr += (sec != null) ? Number (sec).fill (2) : unset;
		return dateStr;
	}

	// takes a date and returns a corresponding string
	// in format "yyyyMMddHHmmss"
	// might take a second boolean parameter "adjust" that controls year adjustment (optional, default to false)
	this.toDateStr = function (date) {
		var adjust = this.toDateStr.arguments.length > 1 ?
			this.toDateStr.arguments[1] : false;
		var year = (adjust) ?
			this.adjustYear (date.getYear()) : date.getYear();
		return this.makeDateStr (year, date.getMonth() + 1, date.getDate(),
			date.getHours(), date.getMinutes(), date.getSeconds());
	}

	this.isValidDateStr = function (dateStr) {
		return dateStr != null && dateStr.length == 14;
	}

	this.isCompleteDateStr = function (dateStr) {
		return this.isValidDateStr (dateStr) &&
			dateStr.indexOf (this.UNSET_DATEPART) < 0;
	}

	this.isCompleteDatePart = function (dateStr) {
		return dateStr.substring (0, 8).indexOf (this.UNSET_DATEPART) < 0;
	}

	this.isCompleteTimePart = function (dateStr) {
		return dateStr.substring (8, 14).indexOf (this.UNSET_DATEPART) < 0;
	}

	// Returns 1 if dateStr > other, -1 if dateStr < other,
	// 0 if they are the same, null if not compareable. Two date strings
	// are only compareable if wither the date part or the time part is
	// complete in both date strings (or both parts, of course).
	this.compareDate = function (dateStr, other) {

		var compareDate =
			this.isCompleteDatePart (dateStr) &&
			this.isCompleteDatePart (other);

		var compareTime =
			this.isCompleteTimePart (dateStr) &&
			this.isCompleteTimePart (other);

		if (!compareDate && !compareTime)
			return null;

		var s = "";
		var sOther = "";
		if (compareDate) {
			s += dateStr.substring (0, 8);
			sOther += other.substring (0, 8);
		}
		if (compareTime) {
			s += dateStr.substring (8, 14);
			sOther += other.substring (8, 14);
		}

		if (s == sOther)
			return 0;
		else if (s > sOther)
			return 1;
		else if (s < sOther)
			return -1;
		else
			return null;
	}

	// Returns the year part of a date string as Number,
	// or null in case of an unset value or an invalid date string.
	this.getYearPart = function (dateStr) {
		return this.internal_getDatePart (dateStr, 0, 4);
	}

	// Returns the month part of a date string as Number,
	// or null in case of an unset value or an invalid date string.
	this.getMonthPart = function (dateStr) {
		return this.internal_getDatePart (dateStr, 4, 6);
	}

	// Returns the day part of a date string as Number,
	// or null in case of an unset value or an invalid date string.
	this.getDayPart = function (dateStr) {
		return this.internal_getDatePart (dateStr, 6, 8);
	}

	// Returns the hour part of a date string as Number,
	// or null in case of an unset value or an invalid date string.
	this.getHourPart = function (dateStr) {
		return this.internal_getDatePart (dateStr, 8, 10);
	}

	// Returns the minute part of a date string as Number,
	// or null in case of an unset value or an invalid date string.
	this.getMinPart = function (dateStr) {
		return this.internal_getDatePart (dateStr, 10, 12);
	}

	// Returns the second part of a date string as Number,
	// or null in case of an unset value or an invalid date string.
	this.getSecPart = function (dateStr) {
		return this.internal_getDatePart (dateStr, 12, 14);
	}

	// Internal helper method that returns a date part as Number.
	// Returns null in case of an error or unset part
	this.internal_getDatePart = function (dateStr, from, to) {
		if (!this.isValidDateStr (dateStr))
			return null;
		var s = dateStr.substring (from, to);
		return (s.indexOf (this.UNSET_DATEPART) < 0) ?
			Number (s) : null;
	}
}

