461 lines
15 KiB
JavaScript
461 lines
15 KiB
JavaScript
/*
|
|
Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved.
|
|
Available via Academic Free License >= 2.1 OR the modified BSD license.
|
|
see: http://dojotoolkit.org/license for details
|
|
*/
|
|
|
|
|
|
if(!dojo._hasResource["dijit.form._FormMixin"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
|
|
dojo._hasResource["dijit.form._FormMixin"] = true;
|
|
dojo.provide("dijit.form._FormMixin");
|
|
dojo.require("dojo.window");
|
|
|
|
|
|
dojo.declare("dijit.form._FormMixin", null, {
|
|
// summary:
|
|
// Mixin for containers of form widgets (i.e. widgets that represent a single value
|
|
// and can be children of a <form> node or dijit.form.Form widget)
|
|
// description:
|
|
// Can extract all the form widgets
|
|
// values and combine them into a single javascript object, or alternately
|
|
// take such an object and set the values for all the contained
|
|
// form widgets
|
|
|
|
/*=====
|
|
// value: Object
|
|
// Name/value hash for each child widget with a name and value.
|
|
// Child widgets without names are not part of the hash.
|
|
//
|
|
// If there are multiple child widgets w/the same name, value is an array,
|
|
// unless they are radio buttons in which case value is a scalar (since only
|
|
// one radio button can be checked at a time).
|
|
//
|
|
// If a child widget's name is a dot separated list (like a.b.c.d), it's a nested structure.
|
|
//
|
|
// Example:
|
|
// | { name: "John Smith", interests: ["sports", "movies"] }
|
|
=====*/
|
|
|
|
// state: [readonly] String
|
|
// Will be "Error" if one or more of the child widgets has an invalid value,
|
|
// "Incomplete" if not all of the required child widgets are filled in. Otherwise, "",
|
|
// which indicates that the form is ready to be submitted.
|
|
state: "",
|
|
|
|
// TODO:
|
|
// * Repeater
|
|
// * better handling for arrays. Often form elements have names with [] like
|
|
// * people[3].sex (for a list of people [{name: Bill, sex: M}, ...])
|
|
//
|
|
//
|
|
|
|
reset: function(){
|
|
dojo.forEach(this.getDescendants(), function(widget){
|
|
if(widget.reset){
|
|
widget.reset();
|
|
}
|
|
});
|
|
},
|
|
|
|
validate: function(){
|
|
// summary:
|
|
// returns if the form is valid - same as isValid - but
|
|
// provides a few additional (ui-specific) features.
|
|
// 1 - it will highlight any sub-widgets that are not
|
|
// valid
|
|
// 2 - it will call focus() on the first invalid
|
|
// sub-widget
|
|
var didFocus = false;
|
|
return dojo.every(dojo.map(this.getDescendants(), function(widget){
|
|
// Need to set this so that "required" widgets get their
|
|
// state set.
|
|
widget._hasBeenBlurred = true;
|
|
var valid = widget.disabled || !widget.validate || widget.validate();
|
|
if(!valid && !didFocus){
|
|
// Set focus of the first non-valid widget
|
|
dojo.window.scrollIntoView(widget.containerNode || widget.domNode);
|
|
widget.focus();
|
|
didFocus = true;
|
|
}
|
|
return valid;
|
|
}), function(item){ return item; });
|
|
},
|
|
|
|
setValues: function(val){
|
|
dojo.deprecated(this.declaredClass+"::setValues() is deprecated. Use set('value', val) instead.", "", "2.0");
|
|
return this.set('value', val);
|
|
},
|
|
_setValueAttr: function(/*Object*/ obj){
|
|
// summary:
|
|
// Fill in form values from according to an Object (in the format returned by get('value'))
|
|
|
|
// generate map from name --> [list of widgets with that name]
|
|
var map = { };
|
|
dojo.forEach(this.getDescendants(), function(widget){
|
|
if(!widget.name){ return; }
|
|
var entry = map[widget.name] || (map[widget.name] = [] );
|
|
entry.push(widget);
|
|
});
|
|
|
|
for(var name in map){
|
|
if(!map.hasOwnProperty(name)){
|
|
continue;
|
|
}
|
|
var widgets = map[name], // array of widgets w/this name
|
|
values = dojo.getObject(name, false, obj); // list of values for those widgets
|
|
|
|
if(values === undefined){
|
|
continue;
|
|
}
|
|
if(!dojo.isArray(values)){
|
|
values = [ values ];
|
|
}
|
|
if(typeof widgets[0].checked == 'boolean'){
|
|
// for checkbox/radio, values is a list of which widgets should be checked
|
|
dojo.forEach(widgets, function(w, i){
|
|
w.set('value', dojo.indexOf(values, w.value) != -1);
|
|
});
|
|
}else if(widgets[0].multiple){
|
|
// it takes an array (e.g. multi-select)
|
|
widgets[0].set('value', values);
|
|
}else{
|
|
// otherwise, values is a list of values to be assigned sequentially to each widget
|
|
dojo.forEach(widgets, function(w, i){
|
|
w.set('value', values[i]);
|
|
});
|
|
}
|
|
}
|
|
|
|
/***
|
|
* TODO: code for plain input boxes (this shouldn't run for inputs that are part of widgets)
|
|
|
|
dojo.forEach(this.containerNode.elements, function(element){
|
|
if(element.name == ''){return}; // like "continue"
|
|
var namePath = element.name.split(".");
|
|
var myObj=obj;
|
|
var name=namePath[namePath.length-1];
|
|
for(var j=1,len2=namePath.length;j<len2;++j){
|
|
var p=namePath[j - 1];
|
|
// repeater support block
|
|
var nameA=p.split("[");
|
|
if(nameA.length > 1){
|
|
if(typeof(myObj[nameA[0]]) == "undefined"){
|
|
myObj[nameA[0]]=[ ];
|
|
} // if
|
|
|
|
nameIndex=parseInt(nameA[1]);
|
|
if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){
|
|
myObj[nameA[0]][nameIndex] = { };
|
|
}
|
|
myObj=myObj[nameA[0]][nameIndex];
|
|
continue;
|
|
} // repeater support ends
|
|
|
|
if(typeof(myObj[p]) == "undefined"){
|
|
myObj=undefined;
|
|
break;
|
|
};
|
|
myObj=myObj[p];
|
|
}
|
|
|
|
if(typeof(myObj) == "undefined"){
|
|
return; // like "continue"
|
|
}
|
|
if(typeof(myObj[name]) == "undefined" && this.ignoreNullValues){
|
|
return; // like "continue"
|
|
}
|
|
|
|
// TODO: widget values (just call set('value', ...) on the widget)
|
|
|
|
// TODO: maybe should call dojo.getNodeProp() instead
|
|
switch(element.type){
|
|
case "checkbox":
|
|
element.checked = (name in myObj) &&
|
|
dojo.some(myObj[name], function(val){ return val == element.value; });
|
|
break;
|
|
case "radio":
|
|
element.checked = (name in myObj) && myObj[name] == element.value;
|
|
break;
|
|
case "select-multiple":
|
|
element.selectedIndex=-1;
|
|
dojo.forEach(element.options, function(option){
|
|
option.selected = dojo.some(myObj[name], function(val){ return option.value == val; });
|
|
});
|
|
break;
|
|
case "select-one":
|
|
element.selectedIndex="0";
|
|
dojo.forEach(element.options, function(option){
|
|
option.selected = option.value == myObj[name];
|
|
});
|
|
break;
|
|
case "hidden":
|
|
case "text":
|
|
case "textarea":
|
|
case "password":
|
|
element.value = myObj[name] || "";
|
|
break;
|
|
}
|
|
});
|
|
*/
|
|
|
|
// Note: no need to call this._set("value", ...) as the child updates will trigger onChange events
|
|
// which I am monitoring.
|
|
},
|
|
|
|
getValues: function(){
|
|
dojo.deprecated(this.declaredClass+"::getValues() is deprecated. Use get('value') instead.", "", "2.0");
|
|
return this.get('value');
|
|
},
|
|
_getValueAttr: function(){
|
|
// summary:
|
|
// Returns Object representing form values. See description of `value` for details.
|
|
// description:
|
|
|
|
// The value is updated into this.value every time a child has an onChange event,
|
|
// so in the common case this function could just return this.value. However,
|
|
// that wouldn't work when:
|
|
//
|
|
// 1. User presses return key to submit a form. That doesn't fire an onchange event,
|
|
// and even if it did it would come too late due to the setTimout(..., 0) in _handleOnChange()
|
|
//
|
|
// 2. app for some reason calls this.get("value") while the user is typing into a
|
|
// form field. Not sure if that case needs to be supported or not.
|
|
|
|
// get widget values
|
|
var obj = { };
|
|
dojo.forEach(this.getDescendants(), function(widget){
|
|
var name = widget.name;
|
|
if(!name || widget.disabled){ return; }
|
|
|
|
// Single value widget (checkbox, radio, or plain <input> type widget)
|
|
var value = widget.get('value');
|
|
|
|
// Store widget's value(s) as a scalar, except for checkboxes which are automatically arrays
|
|
if(typeof widget.checked == 'boolean'){
|
|
if(/Radio/.test(widget.declaredClass)){
|
|
// radio button
|
|
if(value !== false){
|
|
dojo.setObject(name, value, obj);
|
|
}else{
|
|
// give radio widgets a default of null
|
|
value = dojo.getObject(name, false, obj);
|
|
if(value === undefined){
|
|
dojo.setObject(name, null, obj);
|
|
}
|
|
}
|
|
}else{
|
|
// checkbox/toggle button
|
|
var ary=dojo.getObject(name, false, obj);
|
|
if(!ary){
|
|
ary=[];
|
|
dojo.setObject(name, ary, obj);
|
|
}
|
|
if(value !== false){
|
|
ary.push(value);
|
|
}
|
|
}
|
|
}else{
|
|
var prev=dojo.getObject(name, false, obj);
|
|
if(typeof prev != "undefined"){
|
|
if(dojo.isArray(prev)){
|
|
prev.push(value);
|
|
}else{
|
|
dojo.setObject(name, [prev, value], obj);
|
|
}
|
|
}else{
|
|
// unique name
|
|
dojo.setObject(name, value, obj);
|
|
}
|
|
}
|
|
});
|
|
|
|
/***
|
|
* code for plain input boxes (see also dojo.formToObject, can we use that instead of this code?
|
|
* but it doesn't understand [] notation, presumably)
|
|
var obj = { };
|
|
dojo.forEach(this.containerNode.elements, function(elm){
|
|
if(!elm.name) {
|
|
return; // like "continue"
|
|
}
|
|
var namePath = elm.name.split(".");
|
|
var myObj=obj;
|
|
var name=namePath[namePath.length-1];
|
|
for(var j=1,len2=namePath.length;j<len2;++j){
|
|
var nameIndex = null;
|
|
var p=namePath[j - 1];
|
|
var nameA=p.split("[");
|
|
if(nameA.length > 1){
|
|
if(typeof(myObj[nameA[0]]) == "undefined"){
|
|
myObj[nameA[0]]=[ ];
|
|
} // if
|
|
nameIndex=parseInt(nameA[1]);
|
|
if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){
|
|
myObj[nameA[0]][nameIndex] = { };
|
|
}
|
|
} else if(typeof(myObj[nameA[0]]) == "undefined"){
|
|
myObj[nameA[0]] = { }
|
|
} // if
|
|
|
|
if(nameA.length == 1){
|
|
myObj=myObj[nameA[0]];
|
|
} else{
|
|
myObj=myObj[nameA[0]][nameIndex];
|
|
} // if
|
|
} // for
|
|
|
|
if((elm.type != "select-multiple" && elm.type != "checkbox" && elm.type != "radio") || (elm.type == "radio" && elm.checked)){
|
|
if(name == name.split("[")[0]){
|
|
myObj[name]=elm.value;
|
|
} else{
|
|
// can not set value when there is no name
|
|
}
|
|
} else if(elm.type == "checkbox" && elm.checked){
|
|
if(typeof(myObj[name]) == 'undefined'){
|
|
myObj[name]=[ ];
|
|
}
|
|
myObj[name].push(elm.value);
|
|
} else if(elm.type == "select-multiple"){
|
|
if(typeof(myObj[name]) == 'undefined'){
|
|
myObj[name]=[ ];
|
|
}
|
|
for(var jdx=0,len3=elm.options.length; jdx<len3; ++jdx){
|
|
if(elm.options[jdx].selected){
|
|
myObj[name].push(elm.options[jdx].value);
|
|
}
|
|
}
|
|
} // if
|
|
name=undefined;
|
|
}); // forEach
|
|
***/
|
|
return obj;
|
|
},
|
|
|
|
isValid: function(){
|
|
// summary:
|
|
// Returns true if all of the widgets are valid.
|
|
// Deprecated, will be removed in 2.0. Use get("state") instead.
|
|
|
|
return this.state == "";
|
|
},
|
|
|
|
onValidStateChange: function(isValid){
|
|
// summary:
|
|
// Stub function to connect to if you want to do something
|
|
// (like disable/enable a submit button) when the valid
|
|
// state changes on the form as a whole.
|
|
//
|
|
// Deprecated. Will be removed in 2.0. Use watch("state", ...) instead.
|
|
},
|
|
|
|
_getState: function(){
|
|
// summary:
|
|
// Compute what this.state should be based on state of children
|
|
var states = dojo.map(this._descendants, function(w){
|
|
return w.get("state") || "";
|
|
});
|
|
|
|
return dojo.indexOf(states, "Error") >= 0 ? "Error" :
|
|
dojo.indexOf(states, "Incomplete") >= 0 ? "Incomplete" : "";
|
|
},
|
|
|
|
disconnectChildren: function(){
|
|
// summary:
|
|
// Remove connections to monitor changes to children's value, error state, and disabled state,
|
|
// in order to update Form.value and Form.state.
|
|
dojo.forEach(this._childConnections || [], dojo.hitch(this, "disconnect"));
|
|
dojo.forEach(this._childWatches || [], function(w){ w.unwatch(); });
|
|
},
|
|
|
|
connectChildren: function(/*Boolean*/ inStartup){
|
|
// summary:
|
|
// Setup connections to monitor changes to children's value, error state, and disabled state,
|
|
// in order to update Form.value and Form.state.
|
|
//
|
|
// You can call this function directly, ex. in the event that you
|
|
// programmatically add a widget to the form *after* the form has been
|
|
// initialized.
|
|
|
|
var _this = this;
|
|
|
|
// Remove old connections, if any
|
|
this.disconnectChildren();
|
|
|
|
this._descendants = this.getDescendants();
|
|
|
|
// (Re)set this.value and this.state. Send watch() notifications but not on startup.
|
|
var set = inStartup ? function(name, val){ _this[name] = val; } : dojo.hitch(this, "_set");
|
|
set("value", this.get("value"));
|
|
set("state", this._getState());
|
|
|
|
// Monitor changes to error state and disabled state in order to update
|
|
// Form.state
|
|
var conns = (this._childConnections = []),
|
|
watches = (this._childWatches = []);
|
|
dojo.forEach(dojo.filter(this._descendants,
|
|
function(item){ return item.validate; }
|
|
),
|
|
function(widget){
|
|
// We are interested in whenever the widget changes validity state - or
|
|
// whenever the disabled attribute on that widget is changed.
|
|
dojo.forEach(["state", "disabled"], function(attr){
|
|
watches.push(widget.watch(attr, function(attr, oldVal, newVal){
|
|
_this.set("state", _this._getState());
|
|
}));
|
|
});
|
|
});
|
|
|
|
// And monitor calls to child.onChange so we can update this.value
|
|
var onChange = function(){
|
|
// summary:
|
|
// Called when child's value or disabled state changes
|
|
|
|
// Use setTimeout() to collapse value changes in multiple children into a single
|
|
// update to my value. Multiple updates will occur on:
|
|
// 1. Form.set()
|
|
// 2. Form.reset()
|
|
// 3. user selecting a radio button (which will de-select another radio button,
|
|
// causing two onChange events)
|
|
if(_this._onChangeDelayTimer){
|
|
clearTimeout(_this._onChangeDelayTimer);
|
|
}
|
|
_this._onChangeDelayTimer = setTimeout(function(){
|
|
delete _this._onChangeDelayTimer;
|
|
_this._set("value", _this.get("value"));
|
|
}, 10);
|
|
};
|
|
dojo.forEach(
|
|
dojo.filter(this._descendants, function(item){ return item.onChange; } ),
|
|
function(widget){
|
|
// When a child widget's value changes,
|
|
// the efficient thing to do is to just update that one attribute in this.value,
|
|
// but that gets a little complicated when a checkbox is checked/unchecked
|
|
// since this.value["checkboxName"] contains an array of all the checkboxes w/the same name.
|
|
// Doing simple thing for now.
|
|
conns.push(_this.connect(widget, "onChange", onChange));
|
|
|
|
// Disabling/enabling a child widget should remove it's value from this.value.
|
|
// Again, this code could be more efficient, doing simple thing for now.
|
|
watches.push(widget.watch("disabled", onChange));
|
|
}
|
|
);
|
|
},
|
|
|
|
startup: function(){
|
|
this.inherited(arguments);
|
|
|
|
// Initialize value and valid/invalid state tracking. Needs to be done in startup()
|
|
// so that children are initialized.
|
|
this.connectChildren(true);
|
|
|
|
// Make state change call onValidStateChange(), will be removed in 2.0
|
|
this.watch("state", function(attr, oldVal, newVal){ this.onValidStateChange(newVal == ""); });
|
|
},
|
|
|
|
destroy: function(){
|
|
this.disconnectChildren();
|
|
this.inherited(arguments);
|
|
}
|
|
|
|
});
|
|
|
|
}
|