text: define([
  	"dojo/_base/lang",
  	"dojo/_base/array",
  	"dojo/on",
  	"dojo/dom",
  	"dojo/dom-attr",
  	"dojo/query",
  	"./_Mixin",
  	"dijit/form/_FormWidget",
  	"dijit/_base/manager",
  	"dojo/_base/declare"
  ], function(lang, array, on, dom, domAttr, query, _Mixin, _FormWidget, manager, declare){
  	var fm = lang.getObject("dojox.form.manager", true),
  		aa = fm.actionAdapter,
  		keys = fm._keys,
  
  		ce = fm.changeEvent = function(node){
  			// summary:
  			//		Function that returns a valid "onchange" event for a given form node.
  			// node: Node
  			//		Form node.
  
  			var eventName = "click";
  			switch(node.tagName.toLowerCase()){
  				case "textarea":
  					eventName = "keyup";
  					break;
  				case "select":
  					eventName = "change";
  					break;
  				case "input":
  					switch(node.type.toLowerCase()){
  						case "text":
  						case "password":
  							eventName = "keyup";
  							break;
  					}
  					break;
  				// button, input/button, input/checkbox, input/radio,
  				// input/file, input/image, input/submit, input/reset
  				// use "onclick" (the default)
  			}
  			return eventName;	// String
  		},
  
  		registerNode = function(node, groupNode){
  			var name = domAttr.get(node, "name");
  			groupNode = groupNode || this.domNode;
  			if(name && !(name in this.formWidgets)){
  				// verify that it is not part of any widget
  				for(var n = node; n && n !== groupNode; n = n.parentNode){
  					if(domAttr.get(n, "widgetId") && manager.byNode(n).isInstanceOf(_FormWidget)){
  						// this is a child of some widget --- bail out
  						return null;
  					}
  				}
  				// register the node
  				if(node.tagName.toLowerCase() == "input" && node.type.toLowerCase() == "radio"){
  					var a = this.formNodes[name];
  					a = a && a.node;
  					if(a && lang.isArray(a)){
  						a.push(node);
  					}else{
  						this.formNodes[name] = {node: [node], connections: []};
  					}
  				}else{
  					this.formNodes[name] = {node: node, connections: []};
  				}
  			}else{
  				name = null;
  			}
  			return name;
  		},
  
  		getObserversFromNode = function(name){
  			var observers = {};
  			aa(function(_, n){
  				var o = domAttr.get(n, "data-dojo-observer" || "observer");
  				if(o && typeof o == "string"){
  					array.forEach(o.split(","), function(o){
  						o = lang.trim(o);
  						if(o && lang.isFunction(this[o])){
  							observers[o] = 1;
  						}
  					}, this);
  				}
  			}).call(this, null, this.formNodes[name].node);
  			return keys(observers);
  		},
  
  		connectNode = function(name, observers){
  			var t = this.formNodes[name], c = t.connections;
  			if(c.length){
  				array.forEach(c, function(item){ item.remove(); });
  				c = t.connections = [];
  			}
  			aa(function(_, n){
  				// the next line is a crude workaround for Button that fires onClick instead of onChange
  				var eventName = ce(n);
  				array.forEach(observers, function(o){
  					c.push(on(n, eventName, lang.hitch(this, function(evt){
  						if(this.watching){
  							this[o](this.formNodeValue(name), name, n, evt);
  						}
  					})));
  				}, this);
  			}).call(this, null, t.node);
  		};
  
  	return declare("dojox.form.manager._NodeMixin", null, {
  		// summary:
  		//		Mixin to orchestrate dynamic forms (works with DOM nodes).
  		// description:
  		//		This mixin provides a foundation for an enhanced form
  		//		functionality: unified access to individual form elements,
  		//		unified "change" event processing, and general event
  		//		processing. It complements dojox/form/manager/_Mixin
  		//		extending the functionality to DOM nodes.
  
  		destroy: function(){
  			// summary:
  			//		Called when the widget is being destroyed
  
  			for(var name in this.formNodes){
  				array.forEach(this.formNodes[name].connections, function(item){
  					item.remove();
  				});
  			}
  			this.formNodes = {};
  
  			this.inherited(arguments);
  		},
  
  		// register/unregister widgets and nodes
  
  		registerNode: function(node){
  			// summary:
  			//		Register a node with the form manager
  			// node: String|Node
  			//		A node, or its id
  			// returns: Object
  			//		Returns self
  			if(typeof node == "string"){
  				node = dom.byId(node);
  			}
  			var name = registerNode.call(this, node);
  			if(name){
  				connectNode.call(this, name, getObserversFromNode.call(this, name));
  			}
  			return this;
  		},
  
  		unregisterNode: function(name){
  			// summary:
  			//		Removes the node by name from internal tables unregistering
  			//		connected observers
  			// name: String
  			//		Name of the to unregister
  			// returns: Object
  			//		Returns self
  			if(name in this.formNodes){
  				array.forEach(this.formNodes[name].connections, function(item){
  					item.remove();
  				});
  				delete this.formNodes[name];
  			}
  			return this;
  		},
  
  		registerNodeDescendants: function(node){
  			// summary:
  			//		Register node's descendants (form nodes) with the form manager
  			// node: String|Node
  			//		A widget, or its widgetId, or its DOM node
  			// returns: Object
  			//		Returns self
  
  			if(typeof node == "string"){
  				node = dom.byId(node);
  			}
  
  			query("input, select, textarea, button", node).
  				map(function(n){
  					return registerNode.call(this, n, node);
  				}, this).
  				forEach(function(name){
  					if(name){
  						connectNode.call(this, name, getObserversFromNode.call(this, name));
  					}
  				}, this);
  
  			return this;
  		},
  
  		unregisterNodeDescendants: function(node){
  			// summary:
  			//		Unregister node's descendants (form nodes) with the form manager
  			// node: String|Node
  			//		A widget, or its widgetId, or its DOM node
  			// returns: Object
  			//		Returns self
  
  			if(typeof node == "string"){
  				node = dom.byId(node);
  			}
  
  			query("input, select, textarea, button", node).
  				map(function(n){ return domAttr.get(node, "name") || null; }).
  				forEach(function(name){
  					if(name){
  						this.unregisterNode(name);
  					}
  				}, this);
  
  			return this;
  		},
  
  		// value accessors
  
  		formNodeValue: function(elem, value){
  			// summary:
  			//		Set or get a form element by name.
  			// elem: String|Node|Array
  			//		Form element's name, DOM node, or array or radio nodes.
  			// value: Object?
  			//		Optional. The value to set.
  			// returns: Object
  			//		For a getter it returns the value, for a setter it returns
  			//		self. If the elem is not valid, null will be returned.
  
  			var isSetter = arguments.length == 2 && value !== undefined, result;
  
  			if(typeof elem == "string"){
  				elem = this.formNodes[elem];
  				if(elem){
  					elem = elem.node;
  				}
  			}
  
  			if(!elem){
  				return null;	// Object
  			}
  
  			if(lang.isArray(elem)){
  				// input/radio array
  				if(isSetter){
  					array.forEach(elem, function(node){
  						node.checked = "";
  					});
  					array.forEach(elem, function(node){
  						node.checked = node.value === value ? "checked" : "";
  					});
  					return this;	// self
  				}
  				// getter
  				array.some(elem, function(node){
  					if(node.checked){
  						result = node;
  						return true;
  					}
  					return false;
  				});
  				return result ? result.value : "";	// String
  			}
  			// all other elements
  			switch(elem.tagName.toLowerCase()){
  				case "select":
  					if(elem.multiple){
  						// multiple is allowed
  						if(isSetter){
  							if(lang.isArray(value)){
  								var dict = {};
  								array.forEach(value, function(v){
  									dict[v] = 1;
  								});
  								query("> option", elem).forEach(function(opt){
  									opt.selected = opt.value in dict;
  								});
  								return this;	// self
  							}
  							// singular property
  							query("> option", elem).forEach(function(opt){
  								opt.selected = opt.value === value;
  							});
  							return this;	// self
  						}
  						// getter
  						result = query("> option", elem).filter(function(opt){
  							return opt.selected;
  						}).map(function(opt){
  							return opt.value;
  						});
  						return result.length == 1 ? result[0] : result;	// Object
  					}
  					// singular
  					if(isSetter){
  						query("> option", elem).forEach(function(opt){
  							opt.selected = opt.value === value;
  						});
  						return this;	// self
  					}
  					// getter
  					return elem.value || ""; // String
  				case "button":
  					if(isSetter){
  						elem.innerHTML = "" + value;
  						return this;
  					}
  					// getter
  					return elem.innerHTML;
  				case "input":
  					if(elem.type.toLowerCase() == "checkbox"){
  						// input/checkbox element
  						if(isSetter){
  							elem.checked = value ? "checked" : "";
  							return this;
  						}
  						// getter
  						return Boolean(elem.checked);
  					}
  			}
  			// the rest of inputs
  			if(isSetter){
  				elem.value = "" + value;
  				return this;
  			}
  			// getter
  			return elem.value;
  		},
  
  		// inspectors
  
  		inspectFormNodes: function(inspector, state, defaultValue){
  			// summary:
  			//		Run an inspector function on controlled form elements returning a result object.
  			// inspector: Function
  			//		A function to be called on a form element. Takes three arguments: a name, a node or
  			//		an array of nodes, and a supplied value. Runs in the context of the form manager.
  			//		Returns a value that will be collected and returned as a state.
  			// state: Object?
  			//		Optional. If a name-value dictionary --- only listed names will be processed.
  			//		If an array, all names in the array will be processed with defaultValue.
  			//		If omitted or null, all form elements will be processed with defaultValue.
  			// defaultValue: Object?
  			//		Optional. The default state (true, if omitted).
  
  			var name, result = {};
  
  			if(state){
  				if(lang.isArray(state)){
  					array.forEach(state, function(name){
  						if(name in this.formNodes){
  							result[name] = inspector.call(this, name, this.formNodes[name].node, defaultValue);
  						}
  					}, this);
  				}else{
  					for(name in state){
  						if(name in this.formNodes){
  							result[name] = inspector.call(this, name, this.formNodes[name].node, state[name]);
  						}
  					}
  				}
  			}else{
  				for(name in this.formNodes){
  					result[name] = inspector.call(this, name, this.formNodes[name].node, defaultValue);
  				}
  			}
  
  			return result;	// Object
  		}
  	});
  });
  