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