1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a form.
 26  * @class
 27  * This class represents a form.
 28  * 
 29  * @param	{hash}	params		a hash of parameters
 30  * 
 31  * @extends		DwtComposite
 32  * 
 33  * @private
 34  */
 35 DwtForm = function(params) {
 36 	if (arguments.length == 0) return;
 37 	params = Dwt.getParams(arguments, DwtForm.PARAMS);
 38 	params.className = params.className || "DwtForm";
 39 	DwtComposite.apply(this, arguments);
 40 	this.setScrollStyle(DwtControl.SCROLL);
 41 
 42 	// data
 43 	this._tabGroup = new DwtTabGroup(this._htmlElId);
 44 
 45 	// context
 46 	this._context = {
 47 		set: AjxCallback.simpleClosure(this.set, this),
 48 		get: AjxCallback.simpleClosure(this.get, this)
 49 	};
 50 
 51 	// construct form
 52 	this._dirty = {};
 53 	this._ignore = {};
 54 	this._invalid = {};
 55 	this._errorMessages = {};
 56 	this.setModel(params.model);
 57 	this.setForm(params.form);
 58 	this.reset();
 59 };
 60 DwtForm.prototype = new DwtComposite;
 61 DwtForm.prototype.constructor = DwtForm;
 62 
 63 DwtForm.prototype.toString = function() {
 64 	return "DwtForm";
 65 };
 66 
 67 //
 68 // Constants
 69 //
 70 
 71 DwtForm.PARAMS = DwtControl.PARAMS.concat("form", "model");
 72 
 73 //
 74 // Public methods
 75 //
 76 
 77 /**
 78  * Sets the value.
 79  * 
 80  * @param	{string}	id	the id
 81  * @param	{string}	value		the value
 82  * @param	{boolean}	force		if <code>true</code>, to force update
 83  */
 84 DwtForm.prototype.setValue = function(id, value, force) {
 85 	if (typeof id != "string") id = String(id);
 86 	if (id.match(/\./) || id.match(/\[/)) {
 87 		var parts = id.replace(/\[(\d+)\](\.)?/,".$1$2").split(".");
 88 		var control = this.getControl(parts[0]);
 89 		if (Dwt.instanceOf(control, "DwtForm")) {
 90 			control.setValue(parts.slice(1).join("."), value, force);
 91 		}
 92 		return;
 93 	}
 94 	var item = this._items[id];
 95 	if (!item) return;
 96 	if (!force && value == item.value) return;
 97 	this._setModelValue(id, value);
 98 	this._setControlValue(id, value);
 99 };
100 
101 /**
102  * Gets the value.
103  * 
104  * @param	{string}	id	the id
105  * @param	{string}	defaultValue		the default value
106  * @return	{string}	the value
107  */
108 DwtForm.prototype.getValue = function(id, defaultValue) {
109 	if (typeof id !== "string") {
110 		id = String(id);
111 	}
112 	if (id.match(/\./) || id.match(/\[/)) {
113 		var parts = id.replace(/\[(\d+)\](\.)?/,".$1$2").split(".");
114 		var control = this.getControl(parts[0]);
115 		if (Dwt.instanceOf(control, "DwtForm")) {
116 			return control.getValue(parts.slice(1).join("."));
117 		}
118 		return null;
119 	}
120 	var item = this._items[id];
121 	if (!item) {
122 		return;
123 	}
124 	if (item.getter) {
125 		return this._call(item.getter) || defaultValue;
126 	}
127 	var value = this._getControlValue(id);
128     if (value == null) {
129 		value = item.value;
130 	}
131 
132     //added <|| ""> because ... if value="" than it always returns defaultValue which could be undefined.
133 	return value || defaultValue || "";
134 };
135 
136 /**
137  * Gets the control for the item.
138  * 
139  * @param	{string}	id		the id
140  * @return	{DwtControl}	the control
141  */
142 DwtForm.prototype.getControl = function(id) {
143 	if (typeof id != "string") id = String(id);
144 	var item = this._items[id];
145 	return item && item.control;
146 };
147 
148 /**
149  * Checks if the id is relevant (meaning: is visible and is enabled).
150  * 
151  * @param	{string}	id 		the id
152  * @return	 {boolean}	<code>true</code> if the item is relevant
153  */
154 DwtForm.prototype.isRelevant = function(id) {
155 	return this.isVisible(id) && this.isEnabled(id);
156 };
157 
158 DwtForm.prototype.getTabGroupMember = function() {
159 	return this._tabGroup;
160 };
161 
162 // control methods
163 
164 /**
165  * Sets the label.
166  * 
167  * @param	{string}	id 		the id
168  * @param	{string}	label 		the label
169  */
170 DwtForm.prototype.setLabel = function(id, label) {
171 	var item = this._items[id];
172 	if (!item) return;
173 	if (label == this.getLabel(id)) return;
174 	var control = item.control;
175 	if (!control) return;
176 	if (control.setLabel) { control.setLabel(label); return; }
177 	if (control.setText) { control.setText(label); return; }
178 };
179 
180 /**
181  * Sets the image.
182  *
183  * @param	{string}	id 		the id
184  * @param	{string}	image 	the image
185  * @param	{string}	altText	alternate text for non-visual users
186  */
187 DwtForm.prototype.setImage = function(id, image, altText) {
188 	var item = this._items[id];
189 	if (!item) {
190 		return;
191 	}
192 	var control = item.control;
193 	if (!control) {
194 		return;
195 	}
196 	control.setImage(image, null, altText);
197 };
198 
199 
200 /**
201  * Gets the label.
202  * 
203  * @param	{string}	id 		the id
204  * @return	{string}	the label
205  */
206 DwtForm.prototype.getLabel = function(id) {
207 	var item = this._items[id];
208 	var control = item && item.control;
209 	if (control) {
210 		if (control.getLabel) return control.getLabel();
211 		if (control.getText) return control.getText();
212 	}
213 	return "";
214 };
215 
216 DwtForm.prototype.setVisible = function(id, visible) {
217 	// set the form's visibility
218 	if (arguments.length == 1) {
219 		DwtComposite.prototype.setVisible.call(this, arguments[0]);
220 		return;
221 	}
222 	// set control's visibility
223 	var item = this._items[id];
224 	var control = item && item.control;
225 	if (!control) return;
226 	if (control.setVisible) {
227 		control.setVisible(visible);
228 	}
229 	else {
230 		Dwt.setVisible(control, visible);
231 	}
232 	// if there's a corresponding "*_row" element
233 	var el = document.getElementById([this._htmlElId, id, "row"].join("_"));
234 	if (el) {
235 		Dwt.setVisible(el, visible);
236 	}
237 };
238 
239 DwtForm.prototype.isVisible = function(id) {
240 	// this form's visibility
241 	if (arguments.length == 0) {
242 		return DwtComposite.prototype.isVisible.call(this);
243 	}
244 	// control's visibility
245 	var item = this._items[id];
246 	var control = item && item.control;
247 	if (!control) return false;
248 	if (control.getVisible) return control.getVisible();
249 	if (control.isVisible) return control.isVisible();
250 	return  Dwt.getVisible(control);
251 };
252 
253 /**
254  * Sets the enabled flag.
255  * 
256  * @param	{string}	id 		the id
257  * @param	{boolean}	enabled		if <code>true</code>, the item is enabled
258  */
259 DwtForm.prototype.setEnabled = function(id, enabled) {
260 	// set the form enabled
261 	if (arguments.length == 1) {
262 		DwtComposite.prototype.setEnabled.call(this, arguments[0]);
263 		return;
264 	}
265 	// set the control enabled
266 	var item = this._items[id];
267 	var control = item && item.control;
268 	if (!control) return;
269 	if (control.setEnabled) {
270 		control.setEnabled(enabled);
271 	}
272 	else {
273 		control.disabled = !enabled;
274 	}
275 };
276 
277 /**
278  * Checks if the item is enabled.
279  * 
280  * @param	{string}	id 		the id
281  * @return	{boolean}	<code>true</code> if the item is enabled
282  */
283 DwtForm.prototype.isEnabled = function(id) {
284 	// this form enabled?
285 	if (arguments.length == 0) {
286 		return DwtComposite.prototype.isEnabled.call(this);
287 	}
288 	// the control enabled?
289 	var item = this._items[id];
290 	var control = item && item.control;
291 	if (!control) return false;
292 	if (control.isEnabled) return control.isEnabled();
293 	if (control.getEnabled) return control.getEnabled();
294 	return  !control.disabled;
295 };
296 
297 /**
298  * Sets the valid flag.
299  * 
300  * @param	{string}	id 		the id
301  * @param	{boolean}	valid		if <code>true</code>, the item is valid
302  */
303 DwtForm.prototype.setValid = function(id, valid) {
304 	if (typeof(id) == "boolean") {
305 		valid = arguments[0];
306 		for (id in this._items) {
307 			this.setValid(id, valid);
308 		}
309 		return;
310 	}
311 	if (valid) {
312 		delete this._invalid[id]; 
313 	}
314 	else {
315 		this._invalid[id] = true;
316 	}
317 };
318 
319 /**
320  * Checks if the item is valid.
321  * 
322  * @param	{string}	id 		the id
323  * @return	{boolean}	<code>true</code> if the item is valid
324  */
325 DwtForm.prototype.isValid = function(id) {
326 	if (arguments.length == 0 || AjxUtil.isUndefined(id)) {
327 		for (var id in this._invalid) {
328 			return false;
329 		}
330 		return true;
331 	}
332 	return !(id in this._invalid);
333 };
334 
335 /**
336  * Sets the error message.
337  * 
338  * @param	{string}	id 		the id
339  * @param	{string}	message	the message
340  */
341 DwtForm.prototype.setErrorMessage = function(id, message) {
342 	if (!id || id == "") {
343 		this._errorMessages = {};
344 		return;
345 	}
346 	if (!message) {
347 		delete this._errorMessages[id]; 
348 	} else {
349 		this._errorMessages[id] = message;
350 	}
351 };
352 
353 /**
354  * Gets the error message.
355  * 
356  * @param	{string}	id 		the id
357  * @return	{string|array}	the message(s)
358  */
359 DwtForm.prototype.getErrorMessage = function(id) {
360 	if (arguments.length == 0) {
361 		var messages = {};
362 		for (var id in this._invalid) {
363 			messages[id] = this._errorMessages[id];
364 		}
365 		return messages;
366 	}
367 	return this._errorMessages[id];
368 };
369 
370 DwtForm.prototype.getInvalidItems = function() {
371 	return AjxUtil.keys(this._invalid);
372 };
373 
374 DwtForm.prototype.setDirty = function(id, dirty, skipNotify) {
375 	if (typeof id == "boolean") {
376 		dirty = arguments[0];
377 		for (id in this._items) {
378 			this.setDirty(id, dirty, true);
379 		}
380 		if (!skipNotify && this._ondirty) {
381 			this._call(this._ondirty, ["*"]);
382 		}
383 		return;
384 	}
385 	if (dirty) {
386 		this._dirty[id] = true;
387 	}
388 	else {
389 		delete this._dirty[id]; 
390 	}
391 	if (!skipNotify && this._ondirty) {
392 		var item = this._items[id];
393 		if (!item.ignore || !this._call(item.ignore)) {
394 			this._call(this._ondirty, [id]);
395 		}
396 	}
397 };
398 DwtForm.prototype.isDirty = function(id) {
399 	if (arguments.length == 0) {
400 		for (var id in this._dirty) {
401 			var item = this._items[id];
402 			if (item.ignore && this._call(item.ignore)) {
403 				continue;
404 			}
405 			return true;
406 		}
407 		return false;
408 	}
409 	var item = this._items[id];
410 	return item.ignore && this._call(item.ignore) ? false : id in this._dirty;
411 };
412 DwtForm.prototype.getDirtyItems = function() {
413 	// NOTE: This avoids needing a closure
414 	DwtForm.__acceptDirtyItem.form = this;
415 	return AjxUtil.keys(this._dirty, DwtForm.__acceptDirtyItem);
416 };
417 DwtForm.__acceptDirtyItem = function(id) {
418 	var form = arguments.callee.form;
419 	var item = form._items[id];
420 	return !item.ignore || !form._call(item.ignore);
421 };
422 
423 DwtForm.prototype.setIgnore = function(id, ignore) {
424 	if (typeof id == "boolean") {
425 		this._ignore = {};
426 		return;
427 	}
428 	if (ignore) {
429 		this._ignore[id] = true;
430 		return;
431 	}
432 	delete this._ignore[id];
433 };
434 DwtForm.prototype.isIgnore = function(id) {
435 	return id in this._ignore;
436 };
437 
438 // convenience control methods
439 
440 DwtForm.prototype.set = function(id, value) {
441 	this.setValue(id, value, true);
442 	this.update();
443 };
444 DwtForm.prototype.get = DwtForm.prototype.getValue;
445 
446 // properties
447 
448 DwtForm.prototype.setModel = function(model, reset) {
449 	this._context.model = this.model = model;
450 };
451 
452 DwtForm.prototype.setForm = function(form) {
453 	this._context.form = this.form = form;
454 	this._createHtml(form.template);
455 };
456 
457 // form maintenance
458 
459 DwtForm.prototype.validate = function(id) {
460 	if (arguments.length == 0) {
461 		this.setValid(true);
462 		for (var id in this._items) {
463 			this._validateItem(id);
464 		}
465 		return this.isValid();
466 	}
467 	return this._validateItem(id);
468 };
469 
470 DwtForm.prototype._validateItem = function(id) {
471 	if (!id) return true;
472 	var item = this._items[id];
473 	if (!item) return true;
474 	try {
475 		var value = this.getValue(id);
476 		var outcome = item.validator ? item.validator(value) : ((item.control && item.control.validator) ? item.control.validator(value) : true);
477 		// the validator may return false to signify that the validation failed (but preferably throw an error with a message)
478 		// it may return true to signify that the field validates
479 		// It also may return a string or hash (truthy) that we may put into the value field (for normalization of data; e.g. if 13/10/2009 is transformed to 1/10/2010 by the validator)
480 		this.setValid(id, Boolean(outcome) || outcome === "");
481 		if (AjxUtil.isString(outcome) || AjxUtil.isObject(outcome)) {
482 			this._setControlValue(id, outcome); // Set display value
483 			item.value = item.setter ? this._call(item.setter, [outcome]) : outcome; // Set model value
484 			var dirty = !Boolean(this._call(item.equals, [item.value,item.ovalue]));
485 			this.setDirty(id, dirty);
486 		}
487 	}
488 	catch (e) {
489 		this.setErrorMessage(id, AjxUtil.isString(e) ? e : e.message);
490 		this.setValid(id, false);
491 	}
492 	return !(id in this._invalid);
493 };
494 
495 DwtForm.prototype.reset = function(useCurrentValues) {
496 	// init state
497 	this._dirty = {};
498 	this._ignore = {};
499 	this._invalid = {};
500 	for (var id in this._items) {
501 		var item = this._items[id];
502 		if (item.control instanceof DwtForm) {
503 			item.control.reset(useCurrentValues);
504 		}
505 		var itemDef = this._items[id].def;
506 		if (!itemDef) continue;
507 		this._initControl(itemDef, useCurrentValues);
508 	}
509 	// update values
510 	this.update();
511 	for (var id in this._items) {
512 		var item = this._items[id];
513 		item.ovalue = item.value;
514 	}
515 	// clear state
516 	this.validate();
517     this.setDirty(false);
518 	// call handler
519 	if (this._onreset) {
520 		this._call(this._onreset);
521 	}
522 };
523 
524 DwtForm.prototype.update = function() {
525 	// update all the values first
526 	for (var id in this._items) {
527 		var item = this._items[id];
528 		if (item.control instanceof DwtForm) {
529 			item.control.update();
530 		}
531 		if (item.getter) {
532 			this.setValue(id, this._call(item.getter));
533 		}
534 	}
535 	// now set visible/enabled/ignore based on values
536 	for (var id in this._items) {
537 		var item = this._items[id];
538 		if (item.visible) {
539 			this.setVisible(id, Boolean(this._call(item.visible)));
540 		}
541 		if (item.enabled) {
542 			this.setEnabled(id, Boolean(this._call(item.enabled)));
543 		}
544 		if (item.ignore) {
545 			this.setIgnore(id, Boolean(this._call(item.ignore)));
546 		}
547 	}
548 	// call handler
549 	if (this._onupdate) {
550 		this._call(this._onupdate);
551 	}
552 };
553 
554 //
555 // Protected methods
556 //
557 
558 DwtForm.prototype._setModelValue = function(id, value) {
559 	var item = this._items[id];
560 	item.value = item.setter ? this._call(item.setter, [value]) : value;
561 	var dirty = !Boolean(this._call(item.equals, [item.value,item.ovalue]));
562 	this.setDirty(id, dirty);
563 	this.validate(id);
564 	return dirty;
565 };
566 
567 DwtForm.prototype._setControlValue = function(id, value) {
568 	var control = this._items[id].control;
569 	if (control) {
570 		// TODO: display value
571 		if (control instanceof DwtCheckbox || control instanceof DwtRadioButton) {
572 			control.setSelected(value);
573 			return;
574 		}
575 		if (control instanceof DwtMenuItem && control.isStyle(DwtMenuItem.CHECK_STYLE)) {
576 			control.setChecked(value, true);
577 			return;
578 		}
579 		if (control.setSelectedValue) { control.setSelectedValue(value); return; }
580 		if (control.setValue) { control.setValue(value); return; }
581 		if (control.setText && !(control instanceof DwtButton)) { control.setText(value); return; }
582 		if (!(control instanceof DwtControl)) {
583 			// TODO: support other native form elements like select
584 			if (control.type == "checkbox" || control == "radio") {
585 				control.checked = value;
586 			}
587 			else {
588 				// TODO: handle error setting form input value
589 				control.value = value;
590 			}
591 			return;
592 		}
593 	}
594 };
595 DwtForm.prototype._getControlValue = function(id) {
596 	var control = this._items[id].control;
597 	if (control) {
598 		if (control instanceof DwtCheckbox || control instanceof DwtRadioButton) {
599 			return control.isSelected();
600 		}
601 		if (control.getSelectedValue) {
602 			return control.getSelectedValue();
603 		}
604 		if (control.getValue) {
605 			return control.getValue();
606 		}
607 		if (control.getText && !(control instanceof DwtButton)) {
608 			return control.getText();
609 		}
610 		if (!(control instanceof DwtControl)) {
611 			if (control.type == "checkbox" || control == "radio") return control.checked;
612 			return control.value;
613 		}
614 	}
615 };
616 
617 DwtForm.prototype._deleteItem = function(id) {
618 	delete this._items[id];
619 	delete this._dirty[id];
620 	delete this._invalid[id];
621 	delete this._ignore[id];
622 };
623 
624 // utility
625 
626 DwtForm.prototype._call = function(func, args) {
627 	if (func) {
628 		if (args) return func.apply(this, args);
629 		// NOTE: Hack for IE which barfs with null args on apply
630 		return func.call(this);
631 	}
632 };
633 
634 // html creation
635 
636 DwtForm.prototype._createHtml = function(templateId) {
637 	this._createHtmlFromTemplate(templateId || this.TEMPLATE, { id: this._htmlElId });
638 };
639 
640 DwtForm.prototype._createHtmlFromTemplate = function(templateId, data) {
641 	DwtComposite.prototype._createHtmlFromTemplate.apply(this, arguments);
642 
643 	// initialize state
644 	var tabIndexes = [];
645 	this._items = {};
646 	this._tabGroup.removeAllMembers();
647 	this._onupdate = null;
648 	this._onreset = null;
649 	this._ondirty = null;
650 
651 	// create form
652 	var form = this.form;
653 	if (form && form.items) {
654 		// create controls
655 		this._registerControls(form.items, null, tabIndexes);
656 		// create handlers
657 		this._onupdate = DwtForm.__makeFunc(form.onupdate);
658 		this._onreset = DwtForm.__makeFunc(form.onreset);
659 		this._ondirty = DwtForm.__makeFunc(form.ondirty);
660 	}
661 
662 	// add links to list of tabIndexes
663 	var links = this.getHtmlElement().getElementsByTagName("A");
664 	for (var i = 0; i < links.length; i++) {
665 		var link = links[i];
666 		if (!link.href || link.getAttribute("notab") == "true") continue;
667         var controlId = link.id && link.id.substr(this.getHTMLElId().length+1);
668 		if (this._items[controlId]) continue;
669 		tabIndexes.push({
670 			tabindex:	link.getAttribute("tabindex") || Number.MAX_VALUE,
671 			control:	link
672 		});
673 	}
674 
675 	// add controls to tab group
676 	tabIndexes.sort(DwtForm.__byTabIndex);
677 	for (var i = 0; i < tabIndexes.length; i++) {
678 		var control = tabIndexes[i].control;
679 		var member = (control.getTabGroupMember && control.getTabGroupMember()) || control;
680 		this._tabGroup.addMember(member);
681 	}
682 };
683 
684 DwtForm.prototype._registerControls = function(itemDefs, parentDef,
685                                                tabIndexes, params,
686                                                parent, defaultType) {
687 	for (var i = 0; i < itemDefs.length; i++) {
688 		this._registerControl(itemDefs[i], parentDef, tabIndexes, params, parent, defaultType);
689 	}
690 };
691 
692 DwtForm.prototype._registerControl = function(itemDef, parentDef,
693                                               tabIndexes, params,
694                                               parent, defaultType) {
695 	// create item entry
696 	var id = itemDef.id || [this._htmlElId, Dwt.getNextId()].join("_");
697 	var item = this._items[id] = {
698 		id:			id, // for convenience
699 		def:		itemDef,
700 		parentDef:	parentDef,
701 		equals:		DwtForm.__makeFunc(itemDef.equals) || DwtForm.__equals,
702 		getter:		DwtForm.__makeGetter(itemDef),
703 		setter:		DwtForm.__makeSetter(itemDef),
704 		value:		itemDef.value,
705 		visible:	DwtForm.__makeFunc(itemDef.visible),
706 		enabled:	DwtForm.__makeFunc(itemDef.enabled),
707 		validator:	DwtForm.__makeFunc(itemDef.validator),
708 		ignore:		DwtForm.__makeFunc(itemDef.ignore),
709 		control:	itemDef.control
710 	};
711 	// NOTE: This is used internally for indexing of rows
712 	if (itemDef.aka) {
713 		this._items[id].aka = itemDef.aka;
714 		this._items[itemDef.aka] = item;
715 	}
716 
717 	// is control already created?
718 	var control = item.control;
719 	if (control) {
720 		return control;
721 	}
722 
723 	// create control
724 	parent = parent || this;
725 	var type = itemDef.type = itemDef.type || defaultType;
726 	var isMenu = (parentDef && parentDef.menu == itemDef);
727 	var element = document.getElementById([parent._htmlElId,id].join("_"));
728 	if (Dwt.instanceOf(type, "DwtRadioButtonGroup")) {
729 		// create control
730 		control = new window[type]({});
731 		item.control = control;
732 
733 		// add children
734 		var nparams = {
735 			name:  [parent._htmlElId, id].join("_"),
736 			value: itemDef.value
737 		};
738 		if (itemDef.items) {
739 			for (var i = 0; i < itemDef.items.length; i++) {
740 				var radioItemDef = itemDef.items[i];
741 				var checked = radioItemDef.checked || radioItemDef.value == itemDef.value;
742 				var radio = this._registerControl(radioItemDef, itemDef, tabIndexes, nparams, parent, "DwtRadioButton");
743 				this._items[radioItemDef.id].value = checked;
744 				if (radio) {
745 					control.addRadio(radio.getInputElement().id, radio, checked);
746 					// handlers
747 					var handler = DwtForm.__makeFunc(radioItemDef.onclick || itemDef.onclick);
748 					radio.addSelectionListener(new AjxListener(this, this._radio2group2model, [radioItemDef.id, id, handler]));
749 					// HACK: Work around fact that the DwtRadioButtonGroup overwrites
750 					//       the radio button input element's onclick handler.
751 					DwtForm.__hack_fixRadioButtonHandler(radio);
752 				}
753 			}
754 		}
755 	}
756 	else if (type) {
757 		if (Dwt.instanceOf(type, "DwtInputField")) {
758 			item.value = item.value || "";
759 		}
760 		if (Dwt.instanceOf(type, "DwtFormRows")) {
761 		    item.equals = DwtFormRows.__equals;
762 		}
763 		if (element || isMenu) {
764 			control = item.control = this._createControl(itemDef, parentDef, tabIndexes, params, parent, defaultType);
765 		}
766 	}
767 	else if (element) {
768 		this._attachElementHandlers(itemDef, parentDef, tabIndexes, parent, element);
769 		control = item.control = element;
770 		if (itemDef.items) {
771 			this._registerControls(itemDef.items, itemDef, tabIndexes, null, parent, null);
772 		}
773 	}
774 	if (element && control instanceof DwtControl) {
775 		control.replaceElement(element);
776 	}
777 	if (element && control instanceof DwtInputField) {
778 		control.getInputElement().id += "_input";
779 		control.setHandler(DwtEvent.ONPASTE, this._onPaste.bind(this, id));
780 	}
781 
782 	// add to list of tab indexes
783 	if (itemDef.notab == null) {
784 		itemDef.notab = element && element.getAttribute("notab") == "true";
785 	}
786 	if (tabIndexes && control && !itemDef.notab && !(control instanceof DwtRadioButtonGroup)) {
787 		tabIndexes.push({
788 			tabindex:	(element && element.getAttribute("tabindex")) || Number.MAX_VALUE,
789 			control:	control
790 		});
791 	}
792 
793 	// clean up
794 	if (control instanceof DwtListView) {
795 		item.getter = item.getter || AjxCallback.simpleClosure(this.__list_getValue, this, id);
796 		item.setter = item.setter || AjxCallback.simpleClosure(this.__list_setValue, this, id);
797 	}
798 
799 	// return control
800 	return control;
801 };
802 
803 
804 DwtForm.prototype._onPaste = function(id, evt) {
805 	// Delay the value processing - the paste text will not be applied to the control till after this event
806 	AjxTimedAction.scheduleAction(new AjxTimedAction(this, this._applyPasteInput, [id]), 100);
807 };
808 
809 DwtForm.prototype._applyPasteInput = function(id, value) {
810 	this._setModelValue(id, this._getControlValue(id));
811 }
812 
813 
814 DwtForm.prototype._attachElementHandlers = function(itemDef, parentDef, tabIndexes, parent, element) {
815 	var id = itemDef.id;
816 	var name = element.nodeName.toLowerCase();
817 	var type = element.type;
818 	if (type == "checkbox" || type == "radio") {
819 		var parentId;
820 		if (type == "radio") {
821 			parentId = element.name;
822 			if (!this._items[parentId]) this._items[parentId] = { id: parentId };
823 			if (element.checked) {
824 				this._items[parentId].value = element.value;
825 			}
826 		}
827 		// checked
828 		var onclick = element.onclick ;
829 		var handler = DwtForm.__makeFunc(itemDef.onclick);
830 		element.onclick = AjxCallback.simpleClosure(this._htmlInput_checked, this, id, parentId, handler, onclick);
831 	}
832 	else if (name == "select") {
833 		// map selectedIndex to value of option
834 		var onchange = element.onchange;
835 		var handler = DwtForm.__makeFunc(itemDef.onchange);
836 		element.onchange = AjxCallback.simpleClosure(this._htmlSelect_selectedIndex, this, id, handler, onchange);
837 	}
838 	else if (name == "button" || name == "a" || 
839 	         type == "button" || type == "reset" || type == "submit") {
840 		// checked
841 		var onclick = element.onclick ;
842 		var handler = DwtForm.__makeFunc(itemDef.onclick);
843 		element.onclick = AjxCallback.simpleClosure(this._htmlElement, this, id, handler, onclick);
844 	}
845 	else if (name == "textarea" || name == "input") { // type == "text" ||  || type == "file" || type == "password") {
846 		// value
847 		var onchange = element.onchange;
848 		var handler = DwtForm.__makeFunc(itemDef.onchange);
849 		element.onchange = AjxCallback.simpleClosure(this._htmlInput_value, this, id, handler, onchange);
850 	}
851 	// TODO: attach other handlers
852 	return element;
853 };
854 
855 DwtForm.prototype._createControl = function(itemDef, parentDef,
856                                             tabIndexes, params,
857                                             parent, defaultType) {
858 	var id = itemDef.id || [this._htmlElId, Dwt.getNextId()].join("_");
859 	var type = itemDef.type = itemDef.type || defaultType;
860 	params = params ? AjxUtil.createProxy(params) : {};
861 	params.id = params.id || [this._htmlElId, id].join("_");
862 	params.parent = parent || this;
863 	params.template = itemDef.template || params.template;
864 	params.className = itemDef.className || params.className;
865 
866 	// constructor params for radio buttons
867 	var isRadioButton = Dwt.instanceOf(type, "DwtRadioButton");
868 	var isCheckBox = Dwt.instanceOf(type, "DwtCheckbox");
869 	if (isRadioButton || isCheckBox) {
870 		params.name = itemDef.name || params.name;
871         params.value = itemDef.value || params.value;
872 		params.checked = itemDef.checked != null ? itemDef.checked : params.checked;
873 	}
874 
875 	// constructor params for input fields
876 	var isTextField = Dwt.instanceOf(type, "DwtInputField");
877 	if (isTextField) {
878 		params.type = itemDef.password ? DwtInputField.PASSWORD : null;
879 		params.size = itemDef.cols;
880 		params.rows = itemDef.rows;
881 	}
882 
883 	var isTabPage = Dwt.instanceOf(type, "DwtTabViewPage");
884 	if (isTabPage) {
885 		params.contentTemplate = itemDef.template;
886 		delete itemDef.template;
887 	}
888 
889     var isTree = Dwt.instanceOf(type, "DwtTree");
890     if (isTree) {
891         params.style = itemDef.style;
892     }
893 
894 	// add extra params
895 	params.formItemDef = itemDef;
896 	if (itemDef.params) {
897 		for (var p in itemDef.params) {
898 			params[p] = itemDef.params[p];
899 		}
900 	}
901 
902 	// create control
903 	var control = new window[type](params);
904 
905 	// init select
906 	if (control instanceof DwtSelect) {
907 		var options = itemDef.items;
908 		if (options) {
909 			for (var i = 0; i < options.length; i++) {
910 				var option = options[i];
911 				// convert to format that DwtSelect#addOption recognizes
912 				option.displayValue = option.label || option.value;
913 				control.addOption(option);
914 			}
915 		}
916 		var handler = DwtForm.__makeFunc(itemDef.onchange);
917 		control.addChangeListener(new AjxListener(this, this._control2model, [id, handler]));
918 	}
919 
920 	// init button, menu item
921 	else if (control instanceof DwtButton || control instanceof DwtMenuItem) {
922 		if (itemDef.label) { control.setText(itemDef.label); }
923 		if (itemDef.image) { control.setImage(itemDef.image, null, itemDef.imageAltText); }
924 		if (itemDef.menu) {
925 			var isMenu = Dwt.instanceOf(itemDef.menu.type || "DwtMenu", "DwtMenu");
926 			var menu;
927 			if (isMenu) {
928 				menu = this._registerControl(itemDef.menu, itemDef, null, null, control, "DwtMenu");
929 			}
930 			else {
931 				menu = new DwtMenu({parent:control});
932 				var style = Dwt.instanceOf(itemDef.menu.type, "DwtCalendar") ?
933 							DwtMenu.CALENDAR_PICKER_STYLE : DwtMenu.GENERIC_WIDGET_STYLE;
934 				this._registerControl(itemDef.menu, itemDef, null, { style: style }, menu);
935 			}
936 			control.setMenu(menu);
937 		}
938 		var parentId;
939 		if (parent instanceof DwtToolBar || parent instanceof DwtMenu) {
940 			parentId = parentDef.id;
941 		}
942 		// handlers
943 		var handler = DwtForm.__makeFunc(itemDef.onclick || (parentDef && parentDef.onclick));
944 		control.addSelectionListener(new AjxListener(this, this._item2parent, [id, parentId, handler]));
945 	}
946 
947 	// init checkbox, radio button
948 	else if (control instanceof DwtCheckbox && !(control instanceof DwtRadioButton)) {
949 		var handler = DwtForm.__makeFunc(itemDef.onclick);
950 		control.addSelectionListener(new AjxListener(this, this._control2model, [id, handler]));
951 	}
952 
953 	// init input field
954 	else if (control instanceof DwtInputField) {
955 		var changehandler = DwtForm.__makeFunc(itemDef.onchange);
956 		var onkeyup = AjxCallback.simpleClosure(this._input2model2handler, this, id, changehandler);
957 		control.addListener(DwtEvent.ONKEYUP, onkeyup);
958         if (AjxEnv.isFirefox){
959             var onkeydown = this._onkeydownhandler.bind(this, id, changehandler);
960             control.addListener(DwtEvent.ONKEYDOWN, onkeydown);
961         }
962 		var blurhandler = DwtForm.__makeFunc(itemDef.onblur);
963         if (blurhandler) {
964 		    var onblur = AjxCallback.simpleClosure(this._input2model2handler, this, id, blurhandler);
965 		    control.addListener(DwtEvent.ONBLUR, onblur);
966         }
967 
968 		itemDef.tooltip = itemDef.tooltip || itemDef.hint;
969 		control.setHint(itemDef.hint);
970 		control.setLabel(itemDef.label || itemDef.tooltip);
971 	}
972 
973 	// init list
974 	else if (control instanceof DwtListView) {
975 		control.addSelectionListener(new AjxListener(this, this._handleListSelection, [id]));
976 	}
977 
978 	// init menu
979 	else if (control instanceof DwtMenu) {
980 		if (itemDef.items) {
981 			var menuItemDefs = itemDef.items;
982 			for (var i = 0; i < menuItemDefs.length; i++) {
983 				var menuItemDef = menuItemDefs[i];
984 				if (menuItemDef.type == DwtMenuItem.SEPARATOR_STYLE) {
985 					new DwtMenuItem({parent:control, style:DwtMenuItem.SEPARATOR_STYLE});
986 					continue;
987 				}
988 				this._registerControl(menuItemDef, itemDef, null, null, control, "DwtMenuItem");
989 			}
990 		}
991 	}
992 
993 	// init tabs
994 	else if (control instanceof DwtTabView) {
995 		var pageDefs = itemDef.items;
996 		if (pageDefs) {
997 			this._registerControls(pageDefs, itemDef, null, null, control, "DwtTabViewPage");
998 		}
999 	}
1000 
1001 	// init tab page
1002 	else if (control instanceof DwtTabViewPage && parent instanceof DwtTabView) {
1003 		var key = parent.addTab(itemDef.label, control);
1004 		if (itemDef.image) {
1005 			parent.getTabButton(key).setImage(itemDef.image, null, itemDef.imageAltText);
1006 		}
1007 		if (itemDef.items) {
1008 			this._registerControls(itemDef.items, itemDef, tabIndexes, null, control);
1009 		}
1010 	}
1011 
1012 	// init toolbar
1013 	else if (control instanceof DwtToolBar) {
1014 		var toolbarItemDefs = itemDef.items;
1015 		if (toolbarItemDefs) {
1016 			for (var i = 0; i < toolbarItemDefs.length; i++) {
1017 				var toolbarItemDef = toolbarItemDefs[i];
1018 				if (toolbarItemDef.type == DwtToolBar.SPACER) {
1019 					control.addSpacer(toolbarItemDef.size);
1020 					continue;
1021 				}
1022 				if (toolbarItemDef.type == DwtToolBar.SEPARATOR) {
1023 					control.addSeparator(toolbarItemDef.className);
1024 					continue;
1025 				}
1026 				if (toolbarItemDef.type == DwtToolBar.FILLER) {
1027 					control.addFiller(toolbarItemDef.className);
1028 					continue;
1029 				}
1030 				this._registerControl(toolbarItemDef, itemDef, null, null, control, "DwtToolBarButton");
1031 			}
1032 		}
1033 	}
1034 	else if (control instanceof DwtCalendar) {
1035 		if (itemDef.onselect instanceof AjxListener) {
1036 			control.addSelectionListener(itemDef.onselect);
1037 		}
1038 	}
1039 
1040 	// TODO: other controls (e.g. combobox, listview, slider, spinner, tree)
1041 
1042 	// init anonymous composites
1043 	else if (control instanceof DwtComposite) {
1044 		if (itemDef.items) {
1045 			this._registerControls(itemDef.items, itemDef, tabIndexes, null, control);
1046 		}
1047 	}
1048 
1049     // size control
1050     if (itemDef.width || itemDef.height) {
1051         if (control instanceof DwtInputField) {
1052             Dwt.setSize(control.getInputElement(), itemDef.width, itemDef.height);
1053         }
1054         else {
1055             control.setSize(itemDef.width, itemDef.height);
1056         }
1057     }
1058 
1059 	if (itemDef.tooltip) {
1060 		control.setToolTipContent(itemDef.tooltip);
1061 	}
1062 
1063 	// return control
1064 	return control;
1065 };
1066 
1067 DwtForm.prototype._onkeydownhandler  = function(id, changehandler){
1068     setTimeout(this._input2model2handler.bind(this, id, changehandler), 500);
1069 };
1070 
1071 DwtForm.prototype._initControl = function(itemDef, useCurrentValues) {
1072 	var id = itemDef.id;
1073 	if (itemDef.label) this.setLabel(id, itemDef.label);
1074 	var item = this._items[id];
1075 	if (useCurrentValues) {
1076 		item.ovalue = item.value;
1077 	}
1078 	else if (itemDef.value) {
1079 		if (Dwt.instanceOf(itemDef.type, "DwtRadioButton")) {
1080 			item.ovalue = item.value = item.control && item.control.isSelected();
1081 		}
1082 		else {
1083 			this.setValue(id, itemDef.value, true);
1084 			item.ovalue = item.value;
1085 		}
1086 	}
1087 	else {
1088 		item.ovalue = null;
1089 	}
1090 	if (typeof itemDef.enabled == "boolean") this.setEnabled(id, itemDef.enabled);
1091 	if (typeof itemDef.visible == "boolean") this.setVisible(id, itemDef.visible);
1092 };
1093 
1094 // html handlers
1095 
1096 DwtForm.prototype._htmlElement = function(id, formHandler, elementHandler, evt) {
1097 	if (formHandler) {
1098 		this._call(formHandler, [id]);
1099 	}
1100 	if (elementHandler) {
1101 		elementHandler(evt);
1102 	}
1103 };
1104 
1105 DwtForm.prototype._htmlInput_checked = function(id, parentId, handler, onclick, evt) {
1106 	var control = this.getControl(id);
1107 	var checked = control.checked;
1108 	this._setModelValue(id, checked);
1109 	if (parentId && checked) {
1110 		this._setModelValue(parentId, control.value);
1111 	}
1112 	this.update();
1113 	this._htmlElement(id, handler, onclick, evt);
1114 };
1115 
1116 DwtForm.prototype._htmlInput_value = function(id, handler, onchange, evt) {
1117 	this._setModelValue(id, this.getControl(id).value);
1118 	this.update();
1119 	this._htmlElement(id, handler, onchange, evt);
1120 };
1121 
1122 DwtForm.prototype._htmlSelect_selectedIndex = function(id, handler, onchange, evt) {
1123 	var select = this.getControl(id);
1124 	this._setModelValue(id, select.options[select.selectedIndex].value);
1125 	this.update();
1126 	this._htmlElement(id, handler, onchange, evt);
1127 };
1128 
1129 // dwt handlers
1130 
1131 DwtForm.prototype._control2model = function(id, handler) {
1132 	this._setModelValue(id, this._getControlValue(id));
1133 	this.update();
1134 	if (handler) {
1135 		this._call(handler, [id]);
1136 	}
1137 };
1138 
1139 DwtForm.prototype._radio2group2model = function(radioId, groupId, handler) {
1140 	this._setModelValue(groupId, this.getControl(radioId).getValue());
1141 	this._setModelValue(radioId, this._getControlValue(radioId));
1142 	this.update();
1143 	if (handler) {
1144 		this._call(handler, [radioId]);
1145 	}
1146 };
1147 
1148 DwtForm.prototype._input2model2handler = function(id, handler) {
1149 	this._setModelValue(id, this._getControlValue(id));
1150 	this.update();
1151 	if (handler) {
1152 		this._call(handler, [id]);
1153 	}
1154 };
1155 
1156 DwtForm.prototype._item2parent = function(itemId, parentId, handler) {
1157 	var control = this.getControl(itemId);
1158 	var itemDef = this._items[itemId].def;
1159 	if (control instanceof DwtButtonColorPicker || (itemDef.menu && !itemDef.onclick)) {
1160 		control._toggleMenu(); // HACK: button should have public API
1161 	}
1162 	else if (parentId) {
1163 		this._setModelValue(parentId, this._getControlValue(itemId) || itemId);
1164 		this.update();
1165 	}
1166 	if (handler) {
1167 		this._call(handler, [itemId]);
1168 	}
1169 };
1170 
1171 DwtForm.prototype._handleListSelection = function(id, evt) {
1172 	this.update();
1173 };
1174 
1175 // setters and getters
1176 
1177 DwtForm.prototype.__list_getValue = function(id) {
1178 	return this.getControl(id).getSelection();
1179 };
1180 DwtForm.prototype.__list_setValue = function(id, value) {
1181 	this.getControl(id).setSelection(value);
1182 };
1183 
1184 //
1185 // Private functions
1186 //
1187 
1188 // code generation
1189 
1190 DwtForm.__makeGetter = function(item) {
1191 	var getter = item.getter;
1192 	if (getter) return DwtForm.__makeFunc(getter);
1193 
1194 	var ref = item.ref;
1195 	if (!ref) return null;
1196 
1197 	var parts = ref.split(".");
1198 	var body = [
1199 		"var context = this.model;"
1200 	];
1201 	for (var i = 0; i < parts.length; i++) {
1202 		var name = parts[i];
1203 		var fname = DwtForm.__makeFuncName(name);
1204 		if (i == parts.length - 1) break;
1205 		body.push(
1206 			"context = context && (context.",fname," ? context.",fname,"() : context.",name,");"
1207 		);
1208 	}
1209 	body.push(
1210 		"var value = context ? (context.",fname," ? context.",fname,"() : context.",name,") : this._items.",name,".value;",
1211 		"return value !== undefined ? value : defaultValue;"
1212 	);
1213 	return new Function("defaultValue", body.join(""));
1214 };
1215 
1216 DwtForm.__makeSetter = function(item) {
1217 	var setter = item.setter;
1218 	if (setter) return DwtForm.__makeFunc(setter);
1219 
1220 	var ref = item.ref;
1221 	if (!ref) return null;
1222 
1223 	var parts = ref.split(".");
1224 	var body = [
1225 		"var context = this.model;"
1226 	];
1227 	for (var i = 0; i < parts.length; i++) {
1228 		var isLast = i == parts.length - 1;
1229 		var name = parts[i];
1230 		var fname = DwtForm.__makeFuncName(name, isLast ? "set" : "get");
1231 		if (isLast) break;
1232 		body.push(
1233 			"context = context && (context.",fname," ? context.",fname,"() : context.",name,");"
1234 		);
1235 	}
1236 	body.push(
1237 		"if (context) {",
1238 			"if (context.",fname,") {",
1239 				"context.",fname,"(value);",
1240 			"}",
1241 			"else {",
1242 				"context.",name," = value;",
1243 			"}",
1244 		"}"
1245 	);
1246 	return new Function("value", body.join("\n"));
1247 };
1248 
1249 DwtForm.__makeFuncName = function(name, prefix) {
1250 	return [prefix||"get",name.substr(0,1).toUpperCase(),name.substr(1)].join("");
1251 };
1252 
1253 DwtForm.__makeFunc = function(value) {
1254 	if (value == null) return null;
1255 	if (typeof value == "function" && !(value instanceof RegExp)) return value;
1256 	var body = [
1257 		"with (this._context) {",
1258 			"return (",value,");",
1259 		"}"
1260 	].join("");
1261 	return new Function(body);
1262 };
1263 
1264 DwtForm.__equals = function(a, b) {
1265 	return a == b;
1266 };
1267 
1268 // Array.sort
1269 
1270 DwtForm.__byTabIndex = function(a, b) {
1271 	return a.tabindex - b.tabindex;
1272 };
1273 
1274 // hacks
1275 
1276 DwtForm.__hack_fixRadioButtonHandler = function(radio) {
1277 	var handlers = [radio.getInputElement().onclick, DwtCheckbox.__handleClick];
1278 	var handler = function(evt) {
1279 		for (var i = 0; i < handlers.length; i++) {
1280 			var func = handlers[i];
1281 			if (func) {
1282 				func(evt);
1283 			}
1284 		}
1285 	};
1286 	Dwt.setHandler(radio.getInputElement(), DwtEvent.ONCLICK, handler);
1287 };
1288 
1289 //
1290 // Class: DwtFormRows
1291 //
1292 
1293 // TODO: tab-group
1294 
1295 /**
1296  * 
1297  * @extends		DwtForm
1298  * 
1299  * @private
1300  */
1301 DwtFormRows = function(params) {
1302 	if (arguments.length == 0) return;
1303 	this._itemDef = params.formItemDef || {};
1304 	params.className = params.className || "DwtFormRows";
1305 	DwtForm.call(this, {
1306 		id:params.id, parent:params.parent,
1307 		form:{}, template:this._itemDef.template
1308 	});
1309 
1310 	// init state
1311 	this._rowsTabGroup = new DwtTabGroup(this._htmlElId);
1312 
1313 	// save state
1314 	this._rowDef = this._itemDef.rowitem || {};
1315 	this._equals = DwtForm.__makeFunc(this._rowDef.equals) || DwtForm.__equals;
1316 	this._rowCount = 0;
1317 	this._minRows = this._itemDef.minrows || 1;
1318 	this._maxRows = this._itemDef.maxrows || Number.MAX_VALUE;
1319 	if (this._itemDef.rowtemplate) {
1320 		this.ROW_TEMPLATE = this._itemDef.rowtemplate;
1321 	}
1322 
1323 	// add default rows
1324 	var itemDefs = this._itemDef.items || [];
1325 	for (var i = 0; i < itemDefs .length; i++) {
1326 		this.addRow(itemDefs[i]);
1327 	}
1328 
1329 	// add empty rows to satisfy minimum row count
1330 	for ( ; i < this._minRows; i++) {
1331 		this.addRow();
1332 	}
1333 
1334 	// remember listeners
1335 	this._onaddrow = DwtForm.__makeFunc(this._itemDef.onaddrow);
1336 	this._onremoverow = DwtForm.__makeFunc(this._itemDef.onremoverow);
1337 };
1338 DwtFormRows.prototype = new DwtForm;
1339 DwtFormRows.prototype.constructor = DwtFormRows;
1340 
1341 DwtFormRows.prototype.toString = function() {
1342 	return "DwtFormRows";
1343 };
1344 
1345 // Data
1346 
1347 DwtFormRows.prototype.TEMPLATE = "dwt.Widgets#DwtFormRows";
1348 DwtFormRows.prototype.ROW_TEMPLATE = "dwt.Widgets#DwtFormRow";
1349 
1350 // Public methods
1351 
1352 DwtFormRows.prototype.getTabGroupMember = function() {
1353 	return this._rowsTabGroup;
1354 };
1355 
1356 DwtFormRows.prototype.setValue = function(array) {
1357 	if (arguments.length > 1) {
1358 		DwtForm.prototype.setValue.apply(this, arguments);
1359 		return;
1360 	}
1361 	// adjust row count
1362 	var min = Math.max(array.length, this._minRows);
1363 	for (var i = this._rowCount; i > min; i--) {
1364 		this.removeRow(i-1);
1365 	}
1366 	var max = Math.min(array.length, this._maxRows);
1367 	for (var i = this._rowCount; i < max; i++) {
1368 		this.addRow();
1369 	}
1370 	// initialize values
1371 	for (var i = 0; i < max; i++) {
1372 		this.setValue(String(i), array[i], true);
1373 	}
1374 	for (var i = array.length; i < this._rowCount; i++) {
1375 		this.setValue(String(i), null, true);
1376 	}
1377 };
1378 
1379 DwtFormRows.prototype.getValue = function() {
1380 	if (arguments.length > 0) {
1381 		return DwtForm.prototype.getValue.apply(this, arguments);
1382 	}
1383 	var array = new Array(this._rowCount);
1384 	for (var i = 0; i < this._rowCount; i++) {
1385 		array[i] = this.getValue(String(i));
1386 	}
1387 	return array;
1388 };
1389 
1390 DwtFormRows.prototype.getRowCount = function() {
1391 	return this._rowCount;
1392 };
1393 
1394 DwtFormRows.prototype.addRow = function(itemDef, index) {
1395 	if (this._rowCount >= this._maxRows) {
1396 		return;
1397 	}
1398 	itemDef = itemDef || (this._rowDef && AjxUtil.createProxy(this._rowDef));
1399 	if (!itemDef) {
1400 		return;
1401 	}
1402 
1403 	if (index == null) index = this._rowCount;
1404 
1405 	// move other rows "up"
1406 	for (var i = this._rowCount - 1; i >= index; i--) {
1407 		var oindex = i, nindex = i+1;
1408 		var item = this._items[oindex];
1409 		item.aka = String(nindex);
1410 		delete this._items[oindex];
1411 		this._items[item.aka] = item;
1412 		this._setControlIds(item.id, item.aka);
1413 	}
1414 
1415 	// initialize definition
1416 	itemDef.id = itemDef.id || Dwt.getNextId();
1417 	itemDef.aka = String(index);
1418 	this._rowCount++;
1419 
1420 	// create row html
1421 	var data = { id: [this.getHTMLElId(), itemDef.id].join("_") };
1422 	var rowHtml = AjxTemplate.expand(this.ROW_TEMPLATE, data);
1423 
1424 	var rowsEl = this._rowsEl;
1425 	rowsEl.appendChild(Dwt.toDocumentFragment(rowHtml, data.id+"_row"));
1426 	var rowEl = rowsEl.lastChild;
1427 	if (index != this._rowCount - 1) {
1428 		rowsEl.insertBefore(rowEl, rowsEl.childNodes[index]);
1429 	}
1430 
1431 	// create controls
1432 	var tabIndexes = [];
1433 	var rowControl = this._registerControl(itemDef, null, tabIndexes);
1434 
1435 	var addDef = this._itemDef.additem ? AjxUtil.createProxy(this._itemDef.additem) : { image: "Add", tooltip: ZmMsg.addRow };
1436 	addDef.id = addDef.id || itemDef.id+"_add";
1437 	addDef.visible = "this.getRowCount() < this.getMaxRows()";
1438 	addDef.ignore = true;
1439 	var addButton = this._registerControl(addDef,null,tabIndexes,null,null,"DwtButton");
1440 	if (!addDef.onclick) {
1441 		addButton.addSelectionListener(new AjxListener(this, this._handleAddRow, [itemDef.id]));
1442 	}
1443 
1444 	var removeDef = this._itemDef.removeitem ? AjxUtil.createProxy(this._itemDef.removeitem) : { image: "Remove", tooltip: ZmMsg.removeRow };
1445 	removeDef.id = removeDef.id || itemDef.id+"_remove";
1446 	removeDef.visible = "this.getRowCount() > this.getMinRows()";
1447 	removeDef.ignore = true;
1448 	var removeButton = this._registerControl(removeDef,null,tabIndexes,null,null,"DwtButton");
1449 	if (!removeDef.onclick) {
1450 		removeButton.addSelectionListener(new AjxListener(this, this._handleRemoveRow, [itemDef.id]));
1451 	}
1452 
1453 	// remember where we put it
1454 	var item = this._items[itemDef.id];
1455 	item._rowEl = rowEl;
1456 	item._addId= addDef.id;
1457 	item._removeId = removeDef.id;
1458 
1459 	// set control identifiers
1460 	this._setControlIds(item.id, index);
1461 
1462 	// create tab group for row
1463 	var tabGroup = new DwtTabGroup(itemDef.id);
1464 	tabIndexes.sort(DwtForm.__byTabIndex);
1465 	for (var i = 0; i < tabIndexes.length; i++) {
1466 		var control = tabIndexes[i].control;
1467 		tabGroup.addMember(control.getTabGroupMember() || control);
1468 	}
1469 
1470 	// add to tab group
1471 	if (index == this._rowCount - 1) {
1472 		this._rowsTabGroup.addMember(tabGroup);
1473 	}
1474 	else {
1475 		var indexItemDef = this._items[String(index+1)];
1476 		this._rowsTabGroup.addMemberBefore(tabGroup, DwtTabGroup.getByName(indexItemDef.id));
1477 	}
1478 
1479 	// update display and notify handler
1480 	this.update();
1481 	if (this._onaddrow) {
1482 		this._call(this._onaddrow, [index]);
1483 	}
1484 
1485 	return rowControl;
1486 };
1487 
1488 DwtFormRows.prototype.removeRow = function(indexOrId) {
1489 	if (this._rowCount <= this._minRows) {
1490 		return;
1491 	}
1492 
1493 	var item = this._items[indexOrId];
1494 
1495 	// this only recognizes if a properly accessible widgets (i.e. those that
1496 	// receive browser focus) had focus
1497 	var hadFocus = Dwt.isAncestor(item._rowEl, document.activeElement);
1498 
1499 	// delete item at specified index
1500 	if (item.control instanceof DwtControl) {
1501 		this.removeChild(item.control);
1502 	}
1503 	delete this._items[item.aka];
1504 	this._deleteItem(item.id);
1505 
1506 	// delete add item
1507 	var addItem = this._items[item._addId];
1508 	if (addItem) {
1509 		this.removeChild(addItem.control);
1510 		this._deleteItem(addItem.id);
1511 	}
1512 
1513 	// delete remove item
1514 	var removeItem = this._items[item._removeId];
1515 	if (removeItem) {
1516 		this.removeChild(removeItem.control);
1517 		this._deleteItem(removeItem.id);
1518 	}
1519 
1520 	// shift everything down one, removing old last row
1521 	var fromIndex = Number(item.aka);
1522 	for (var i = fromIndex + 1; i < this._rowCount; i++) {
1523 		var oindex = i, nindex = i-1;
1524 		this._items[nindex] = this._items[oindex];
1525 		this._items[nindex].aka = String(nindex);
1526 		this._setControlIds(this._items[nindex].id, this._items[nindex].aka);
1527 	}
1528 	this._deleteItem(String(--this._rowCount));
1529 
1530 	// remove row element
1531 	var rowEl = item._rowEl;
1532 	rowEl.parentNode.removeChild(rowEl);
1533 	delete item._rowEl;
1534 
1535 	// remove from tab group
1536 	var tabGroup = DwtTabGroup.getByName(item.id);
1537 	this._rowsTabGroup.removeMember(tabGroup);
1538 
1539 	// update display and notify handler
1540 	this.update();
1541 
1542 	if (hadFocus) {
1543 		var otherItem = this._items[item.aka] || this._items[this._rowCount - 1];
1544 		otherItem.control.getTabGroupMember().focus();
1545 	}
1546 
1547 	if (this._onremoverow) {
1548 		this._call(this._onremoverow, [Number(item.aka)]);
1549 	}
1550 };
1551 
1552 DwtFormRows.prototype.getMinRows = function() {
1553 	return this._minRows;
1554 };
1555 DwtFormRows.prototype.getMaxRows = function() {
1556 	return this._maxRows;
1557 };
1558 DwtFormRows.prototype.getRowCount = function() {
1559 	return this._rowCount;
1560 };
1561 
1562 DwtFormRows.prototype.getIndexForRowId = function(rowId) {
1563 	var children = this._rowsEl.childNodes;
1564 	for (var i = 0; i < children.length; i++) {
1565 		if (children[i].id == [this._htmlElId,rowId,"row"].join("_")) {
1566 			return i;
1567 		}
1568 	}
1569 	return -1;
1570 };
1571 
1572 DwtFormRows.__equals = function(a,b) {
1573 	if (a === b) return true;
1574 	if (!a || !b || a.length != b.length) return false;
1575 	for (var i = 0; i < a.length; i++) {
1576 		if (!this._call(this._equals, [a[i],b[i]])) {
1577 			return false;
1578 		}
1579 	}
1580 	return true;
1581 };
1582 
1583 // Protected methods
1584 
1585 /** Override to set child controls' identifiers. */
1586 DwtFormRows.prototype._setControlIds = function(rowId, index) {
1587 	var id = [this.getHTMLElId(), index].join("_");
1588 	var item = this._items[rowId];
1589 	this._setControlId(item && item.control, id);
1590 	var addButton = this._items[item._addId];
1591 	this._setControlId(addButton && addButton.control, id+"_add");
1592 	var removeButton = this._items[item._removeId];
1593 	this._setControlId(removeButton && removeButton.control, id+"_remove");
1594 	// TODO: update parentid attribute of children
1595 };
1596 
1597 DwtFormRows.prototype._setControlId = function(control, id) {
1598 	if (!control) return;
1599 	if (control instanceof DwtControl) {
1600 		control.setHtmlElementId(id);
1601 	}
1602 	else {
1603 		control.id = id;
1604 	}
1605 };
1606 
1607 DwtFormRows.prototype._handleAddRow = function(rowId) {
1608 	if (this.getRowCount() < this.getMaxRows()) {
1609 		var index = this.getIndexForRowId(rowId) + 1;
1610 		this.addRow(null, index);
1611 	}
1612 };
1613 
1614 DwtFormRows.prototype._handleRemoveRow = function(rowId) {
1615 	this.removeRow(rowId);
1616 };
1617 
1618 // DwtForm methods
1619 
1620 DwtFormRows.prototype._setModelValue = function(id, value) {
1621 	if (DwtForm.prototype._setModelValue.apply(this, arguments)) {
1622 		this.parent.setDirty(this._itemDef.id, true);
1623 	}
1624 };
1625 
1626 // DwtControl methods
1627 
1628 DwtFormRows.prototype._createHtmlFromTemplate = function(templateId, data) {
1629 	DwtForm.prototype._createHtmlFromTemplate.apply(this, arguments);
1630 	this._rowsEl = document.getElementById(this._htmlElId+"_rows");
1631 };
1632 
1633 
1634