]>
Commit | Line | Data |
---|---|---|
2f01fe57 | 1 | /* |
81bea17a | 2 | Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. |
2f01fe57 AD |
3 | Available via Academic Free License >= 2.1 OR the modified BSD license. |
4 | see: http://dojotoolkit.org/license for details | |
5 | */ | |
6 | ||
7 | ||
81bea17a AD |
8 | if(!dojo._hasResource["dijit.form._FormMixin"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. |
9 | dojo._hasResource["dijit.form._FormMixin"] = true; | |
2f01fe57 AD |
10 | dojo.provide("dijit.form._FormMixin"); |
11 | dojo.require("dojo.window"); | |
81bea17a AD |
12 | |
13 | ||
14 | dojo.declare("dijit.form._FormMixin", null, { | |
15 | // summary: | |
16 | // Mixin for containers of form widgets (i.e. widgets that represent a single value | |
17 | // and can be children of a <form> node or dijit.form.Form widget) | |
18 | // description: | |
19 | // Can extract all the form widgets | |
20 | // values and combine them into a single javascript object, or alternately | |
21 | // take such an object and set the values for all the contained | |
22 | // form widgets | |
23 | ||
24 | /*===== | |
25 | // value: Object | |
26 | // Name/value hash for each child widget with a name and value. | |
27 | // Child widgets without names are not part of the hash. | |
28 | // | |
29 | // If there are multiple child widgets w/the same name, value is an array, | |
30 | // unless they are radio buttons in which case value is a scalar (since only | |
31 | // one radio button can be checked at a time). | |
32 | // | |
33 | // If a child widget's name is a dot separated list (like a.b.c.d), it's a nested structure. | |
34 | // | |
35 | // Example: | |
36 | // | { name: "John Smith", interests: ["sports", "movies"] } | |
37 | =====*/ | |
38 | ||
39 | // state: [readonly] String | |
40 | // Will be "Error" if one or more of the child widgets has an invalid value, | |
41 | // "Incomplete" if not all of the required child widgets are filled in. Otherwise, "", | |
42 | // which indicates that the form is ready to be submitted. | |
43 | state: "", | |
44 | ||
45 | // TODO: | |
46 | // * Repeater | |
47 | // * better handling for arrays. Often form elements have names with [] like | |
48 | // * people[3].sex (for a list of people [{name: Bill, sex: M}, ...]) | |
49 | // | |
50 | // | |
51 | ||
52 | reset: function(){ | |
53 | dojo.forEach(this.getDescendants(), function(widget){ | |
54 | if(widget.reset){ | |
55 | widget.reset(); | |
56 | } | |
57 | }); | |
58 | }, | |
59 | ||
60 | validate: function(){ | |
61 | // summary: | |
62 | // returns if the form is valid - same as isValid - but | |
63 | // provides a few additional (ui-specific) features. | |
64 | // 1 - it will highlight any sub-widgets that are not | |
65 | // valid | |
66 | // 2 - it will call focus() on the first invalid | |
67 | // sub-widget | |
68 | var didFocus = false; | |
69 | return dojo.every(dojo.map(this.getDescendants(), function(widget){ | |
70 | // Need to set this so that "required" widgets get their | |
71 | // state set. | |
72 | widget._hasBeenBlurred = true; | |
73 | var valid = widget.disabled || !widget.validate || widget.validate(); | |
74 | if(!valid && !didFocus){ | |
75 | // Set focus of the first non-valid widget | |
76 | dojo.window.scrollIntoView(widget.containerNode || widget.domNode); | |
77 | widget.focus(); | |
78 | didFocus = true; | |
79 | } | |
80 | return valid; | |
81 | }), function(item){ return item; }); | |
82 | }, | |
83 | ||
84 | setValues: function(val){ | |
85 | dojo.deprecated(this.declaredClass+"::setValues() is deprecated. Use set('value', val) instead.", "", "2.0"); | |
86 | return this.set('value', val); | |
87 | }, | |
88 | _setValueAttr: function(/*Object*/ obj){ | |
89 | // summary: | |
90 | // Fill in form values from according to an Object (in the format returned by get('value')) | |
91 | ||
92 | // generate map from name --> [list of widgets with that name] | |
93 | var map = { }; | |
94 | dojo.forEach(this.getDescendants(), function(widget){ | |
95 | if(!widget.name){ return; } | |
96 | var entry = map[widget.name] || (map[widget.name] = [] ); | |
97 | entry.push(widget); | |
98 | }); | |
99 | ||
100 | for(var name in map){ | |
101 | if(!map.hasOwnProperty(name)){ | |
102 | continue; | |
103 | } | |
104 | var widgets = map[name], // array of widgets w/this name | |
105 | values = dojo.getObject(name, false, obj); // list of values for those widgets | |
106 | ||
107 | if(values === undefined){ | |
108 | continue; | |
109 | } | |
110 | if(!dojo.isArray(values)){ | |
111 | values = [ values ]; | |
112 | } | |
113 | if(typeof widgets[0].checked == 'boolean'){ | |
114 | // for checkbox/radio, values is a list of which widgets should be checked | |
115 | dojo.forEach(widgets, function(w, i){ | |
116 | w.set('value', dojo.indexOf(values, w.value) != -1); | |
117 | }); | |
118 | }else if(widgets[0].multiple){ | |
119 | // it takes an array (e.g. multi-select) | |
120 | widgets[0].set('value', values); | |
121 | }else{ | |
122 | // otherwise, values is a list of values to be assigned sequentially to each widget | |
123 | dojo.forEach(widgets, function(w, i){ | |
124 | w.set('value', values[i]); | |
125 | }); | |
126 | } | |
127 | } | |
128 | ||
129 | /*** | |
130 | * TODO: code for plain input boxes (this shouldn't run for inputs that are part of widgets) | |
131 | ||
132 | dojo.forEach(this.containerNode.elements, function(element){ | |
133 | if(element.name == ''){return}; // like "continue" | |
134 | var namePath = element.name.split("."); | |
135 | var myObj=obj; | |
136 | var name=namePath[namePath.length-1]; | |
137 | for(var j=1,len2=namePath.length;j<len2;++j){ | |
138 | var p=namePath[j - 1]; | |
139 | // repeater support block | |
140 | var nameA=p.split("["); | |
141 | if(nameA.length > 1){ | |
142 | if(typeof(myObj[nameA[0]]) == "undefined"){ | |
143 | myObj[nameA[0]]=[ ]; | |
144 | } // if | |
145 | ||
146 | nameIndex=parseInt(nameA[1]); | |
147 | if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){ | |
148 | myObj[nameA[0]][nameIndex] = { }; | |
149 | } | |
150 | myObj=myObj[nameA[0]][nameIndex]; | |
151 | continue; | |
152 | } // repeater support ends | |
153 | ||
154 | if(typeof(myObj[p]) == "undefined"){ | |
155 | myObj=undefined; | |
156 | break; | |
157 | }; | |
158 | myObj=myObj[p]; | |
159 | } | |
160 | ||
161 | if(typeof(myObj) == "undefined"){ | |
162 | return; // like "continue" | |
163 | } | |
164 | if(typeof(myObj[name]) == "undefined" && this.ignoreNullValues){ | |
165 | return; // like "continue" | |
166 | } | |
167 | ||
168 | // TODO: widget values (just call set('value', ...) on the widget) | |
169 | ||
170 | // TODO: maybe should call dojo.getNodeProp() instead | |
171 | switch(element.type){ | |
172 | case "checkbox": | |
173 | element.checked = (name in myObj) && | |
174 | dojo.some(myObj[name], function(val){ return val == element.value; }); | |
175 | break; | |
176 | case "radio": | |
177 | element.checked = (name in myObj) && myObj[name] == element.value; | |
178 | break; | |
179 | case "select-multiple": | |
180 | element.selectedIndex=-1; | |
181 | dojo.forEach(element.options, function(option){ | |
182 | option.selected = dojo.some(myObj[name], function(val){ return option.value == val; }); | |
183 | }); | |
184 | break; | |
185 | case "select-one": | |
186 | element.selectedIndex="0"; | |
187 | dojo.forEach(element.options, function(option){ | |
188 | option.selected = option.value == myObj[name]; | |
189 | }); | |
190 | break; | |
191 | case "hidden": | |
192 | case "text": | |
193 | case "textarea": | |
194 | case "password": | |
195 | element.value = myObj[name] || ""; | |
196 | break; | |
197 | } | |
198 | }); | |
199 | */ | |
200 | ||
201 | // Note: no need to call this._set("value", ...) as the child updates will trigger onChange events | |
202 | // which I am monitoring. | |
203 | }, | |
204 | ||
205 | getValues: function(){ | |
206 | dojo.deprecated(this.declaredClass+"::getValues() is deprecated. Use get('value') instead.", "", "2.0"); | |
207 | return this.get('value'); | |
208 | }, | |
209 | _getValueAttr: function(){ | |
210 | // summary: | |
211 | // Returns Object representing form values. See description of `value` for details. | |
212 | // description: | |
213 | ||
214 | // The value is updated into this.value every time a child has an onChange event, | |
215 | // so in the common case this function could just return this.value. However, | |
216 | // that wouldn't work when: | |
217 | // | |
218 | // 1. User presses return key to submit a form. That doesn't fire an onchange event, | |
219 | // and even if it did it would come too late due to the setTimout(..., 0) in _handleOnChange() | |
220 | // | |
221 | // 2. app for some reason calls this.get("value") while the user is typing into a | |
222 | // form field. Not sure if that case needs to be supported or not. | |
223 | ||
224 | // get widget values | |
225 | var obj = { }; | |
226 | dojo.forEach(this.getDescendants(), function(widget){ | |
227 | var name = widget.name; | |
228 | if(!name || widget.disabled){ return; } | |
229 | ||
230 | // Single value widget (checkbox, radio, or plain <input> type widget) | |
231 | var value = widget.get('value'); | |
232 | ||
233 | // Store widget's value(s) as a scalar, except for checkboxes which are automatically arrays | |
234 | if(typeof widget.checked == 'boolean'){ | |
235 | if(/Radio/.test(widget.declaredClass)){ | |
236 | // radio button | |
237 | if(value !== false){ | |
238 | dojo.setObject(name, value, obj); | |
239 | }else{ | |
240 | // give radio widgets a default of null | |
241 | value = dojo.getObject(name, false, obj); | |
242 | if(value === undefined){ | |
243 | dojo.setObject(name, null, obj); | |
244 | } | |
245 | } | |
246 | }else{ | |
247 | // checkbox/toggle button | |
248 | var ary=dojo.getObject(name, false, obj); | |
249 | if(!ary){ | |
250 | ary=[]; | |
251 | dojo.setObject(name, ary, obj); | |
252 | } | |
253 | if(value !== false){ | |
254 | ary.push(value); | |
255 | } | |
256 | } | |
257 | }else{ | |
258 | var prev=dojo.getObject(name, false, obj); | |
259 | if(typeof prev != "undefined"){ | |
260 | if(dojo.isArray(prev)){ | |
261 | prev.push(value); | |
262 | }else{ | |
263 | dojo.setObject(name, [prev, value], obj); | |
264 | } | |
265 | }else{ | |
266 | // unique name | |
267 | dojo.setObject(name, value, obj); | |
268 | } | |
269 | } | |
270 | }); | |
271 | ||
272 | /*** | |
273 | * code for plain input boxes (see also dojo.formToObject, can we use that instead of this code? | |
274 | * but it doesn't understand [] notation, presumably) | |
275 | var obj = { }; | |
276 | dojo.forEach(this.containerNode.elements, function(elm){ | |
277 | if(!elm.name) { | |
278 | return; // like "continue" | |
279 | } | |
280 | var namePath = elm.name.split("."); | |
281 | var myObj=obj; | |
282 | var name=namePath[namePath.length-1]; | |
283 | for(var j=1,len2=namePath.length;j<len2;++j){ | |
284 | var nameIndex = null; | |
285 | var p=namePath[j - 1]; | |
286 | var nameA=p.split("["); | |
287 | if(nameA.length > 1){ | |
288 | if(typeof(myObj[nameA[0]]) == "undefined"){ | |
289 | myObj[nameA[0]]=[ ]; | |
290 | } // if | |
291 | nameIndex=parseInt(nameA[1]); | |
292 | if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){ | |
293 | myObj[nameA[0]][nameIndex] = { }; | |
294 | } | |
295 | } else if(typeof(myObj[nameA[0]]) == "undefined"){ | |
296 | myObj[nameA[0]] = { } | |
297 | } // if | |
298 | ||
299 | if(nameA.length == 1){ | |
300 | myObj=myObj[nameA[0]]; | |
301 | } else{ | |
302 | myObj=myObj[nameA[0]][nameIndex]; | |
303 | } // if | |
304 | } // for | |
305 | ||
306 | if((elm.type != "select-multiple" && elm.type != "checkbox" && elm.type != "radio") || (elm.type == "radio" && elm.checked)){ | |
307 | if(name == name.split("[")[0]){ | |
308 | myObj[name]=elm.value; | |
309 | } else{ | |
310 | // can not set value when there is no name | |
311 | } | |
312 | } else if(elm.type == "checkbox" && elm.checked){ | |
313 | if(typeof(myObj[name]) == 'undefined'){ | |
314 | myObj[name]=[ ]; | |
315 | } | |
316 | myObj[name].push(elm.value); | |
317 | } else if(elm.type == "select-multiple"){ | |
318 | if(typeof(myObj[name]) == 'undefined'){ | |
319 | myObj[name]=[ ]; | |
320 | } | |
321 | for(var jdx=0,len3=elm.options.length; jdx<len3; ++jdx){ | |
322 | if(elm.options[jdx].selected){ | |
323 | myObj[name].push(elm.options[jdx].value); | |
324 | } | |
325 | } | |
326 | } // if | |
327 | name=undefined; | |
328 | }); // forEach | |
329 | ***/ | |
330 | return obj; | |
331 | }, | |
332 | ||
333 | isValid: function(){ | |
334 | // summary: | |
335 | // Returns true if all of the widgets are valid. | |
336 | // Deprecated, will be removed in 2.0. Use get("state") instead. | |
337 | ||
338 | return this.state == ""; | |
339 | }, | |
340 | ||
341 | onValidStateChange: function(isValid){ | |
342 | // summary: | |
343 | // Stub function to connect to if you want to do something | |
344 | // (like disable/enable a submit button) when the valid | |
345 | // state changes on the form as a whole. | |
346 | // | |
347 | // Deprecated. Will be removed in 2.0. Use watch("state", ...) instead. | |
348 | }, | |
349 | ||
350 | _getState: function(){ | |
351 | // summary: | |
352 | // Compute what this.state should be based on state of children | |
353 | var states = dojo.map(this._descendants, function(w){ | |
354 | return w.get("state") || ""; | |
355 | }); | |
356 | ||
357 | return dojo.indexOf(states, "Error") >= 0 ? "Error" : | |
358 | dojo.indexOf(states, "Incomplete") >= 0 ? "Incomplete" : ""; | |
359 | }, | |
360 | ||
361 | disconnectChildren: function(){ | |
362 | // summary: | |
363 | // Remove connections to monitor changes to children's value, error state, and disabled state, | |
364 | // in order to update Form.value and Form.state. | |
365 | dojo.forEach(this._childConnections || [], dojo.hitch(this, "disconnect")); | |
366 | dojo.forEach(this._childWatches || [], function(w){ w.unwatch(); }); | |
367 | }, | |
368 | ||
369 | connectChildren: function(/*Boolean*/ inStartup){ | |
370 | // summary: | |
371 | // Setup connections to monitor changes to children's value, error state, and disabled state, | |
372 | // in order to update Form.value and Form.state. | |
373 | // | |
374 | // You can call this function directly, ex. in the event that you | |
375 | // programmatically add a widget to the form *after* the form has been | |
376 | // initialized. | |
377 | ||
378 | var _this = this; | |
379 | ||
380 | // Remove old connections, if any | |
381 | this.disconnectChildren(); | |
382 | ||
383 | this._descendants = this.getDescendants(); | |
384 | ||
385 | // (Re)set this.value and this.state. Send watch() notifications but not on startup. | |
386 | var set = inStartup ? function(name, val){ _this[name] = val; } : dojo.hitch(this, "_set"); | |
387 | set("value", this.get("value")); | |
388 | set("state", this._getState()); | |
389 | ||
390 | // Monitor changes to error state and disabled state in order to update | |
391 | // Form.state | |
392 | var conns = (this._childConnections = []), | |
393 | watches = (this._childWatches = []); | |
394 | dojo.forEach(dojo.filter(this._descendants, | |
395 | function(item){ return item.validate; } | |
396 | ), | |
397 | function(widget){ | |
398 | // We are interested in whenever the widget changes validity state - or | |
399 | // whenever the disabled attribute on that widget is changed. | |
400 | dojo.forEach(["state", "disabled"], function(attr){ | |
401 | watches.push(widget.watch(attr, function(attr, oldVal, newVal){ | |
402 | _this.set("state", _this._getState()); | |
403 | })); | |
404 | }); | |
405 | }); | |
406 | ||
407 | // And monitor calls to child.onChange so we can update this.value | |
408 | var onChange = function(){ | |
409 | // summary: | |
410 | // Called when child's value or disabled state changes | |
411 | ||
412 | // Use setTimeout() to collapse value changes in multiple children into a single | |
413 | // update to my value. Multiple updates will occur on: | |
414 | // 1. Form.set() | |
415 | // 2. Form.reset() | |
416 | // 3. user selecting a radio button (which will de-select another radio button, | |
417 | // causing two onChange events) | |
418 | if(_this._onChangeDelayTimer){ | |
419 | clearTimeout(_this._onChangeDelayTimer); | |
420 | } | |
421 | _this._onChangeDelayTimer = setTimeout(function(){ | |
422 | delete _this._onChangeDelayTimer; | |
423 | _this._set("value", _this.get("value")); | |
424 | }, 10); | |
425 | }; | |
426 | dojo.forEach( | |
427 | dojo.filter(this._descendants, function(item){ return item.onChange; } ), | |
428 | function(widget){ | |
429 | // When a child widget's value changes, | |
430 | // the efficient thing to do is to just update that one attribute in this.value, | |
431 | // but that gets a little complicated when a checkbox is checked/unchecked | |
432 | // since this.value["checkboxName"] contains an array of all the checkboxes w/the same name. | |
433 | // Doing simple thing for now. | |
434 | conns.push(_this.connect(widget, "onChange", onChange)); | |
435 | ||
436 | // Disabling/enabling a child widget should remove it's value from this.value. | |
437 | // Again, this code could be more efficient, doing simple thing for now. | |
438 | watches.push(widget.watch("disabled", onChange)); | |
439 | } | |
440 | ); | |
441 | }, | |
442 | ||
443 | startup: function(){ | |
444 | this.inherited(arguments); | |
445 | ||
446 | // Initialize value and valid/invalid state tracking. Needs to be done in startup() | |
447 | // so that children are initialized. | |
448 | this.connectChildren(true); | |
449 | ||
450 | // Make state change call onValidStateChange(), will be removed in 2.0 | |
451 | this.watch("state", function(attr, oldVal, newVal){ this.onValidStateChange(newVal == ""); }); | |
452 | }, | |
453 | ||
454 | destroy: function(){ | |
455 | this.disconnectChildren(); | |
456 | this.inherited(arguments); | |
457 | } | |
458 | ||
459 | }); | |
460 | ||
2f01fe57 | 461 | } |