Roo/bootstrap/Form.js
[roojs1] / Roo / bootstrap / Form.js
1 /*
2  * - LGPL
3  *
4  * form
5  *
6  */
7
8 /**
9  * @class Roo.bootstrap.Form
10  * @extends Roo.bootstrap.Component
11  * Bootstrap Form class
12  * @cfg {String} method  GET | POST (default POST)
13  * @cfg {String} labelAlign top | left (default top)
14  * @cfg {String} align left  | right - for navbars
15  * @cfg {Boolean} loadMask load mask when submit (default true)
16
17  *
18  * @constructor
19  * Create a new Form
20  * @param {Object} config The config object
21  */
22
23
24 Roo.bootstrap.Form = function(config){
25     Roo.bootstrap.Form.superclass.constructor.call(this, config);
26     
27     Roo.bootstrap.Form.popover.apply();
28     
29     this.addEvents({
30         /**
31          * @event clientvalidation
32          * If the monitorValid config option is true, this event fires repetitively to notify of valid state
33          * @param {Form} this
34          * @param {Boolean} valid true if the form has passed client-side validation
35          */
36         clientvalidation: true,
37         /**
38          * @event beforeaction
39          * Fires before any action is performed. Return false to cancel the action.
40          * @param {Form} this
41          * @param {Action} action The action to be performed
42          */
43         beforeaction: true,
44         /**
45          * @event actionfailed
46          * Fires when an action fails.
47          * @param {Form} this
48          * @param {Action} action The action that failed
49          */
50         actionfailed : true,
51         /**
52          * @event actioncomplete
53          * Fires when an action is completed.
54          * @param {Form} this
55          * @param {Action} action The action that completed
56          */
57         actioncomplete : true
58     });
59
60 };
61
62 Roo.extend(Roo.bootstrap.Form, Roo.bootstrap.Component,  {
63
64      /**
65      * @cfg {String} method
66      * The request method to use (GET or POST) for form actions if one isn't supplied in the action options.
67      */
68     method : 'POST',
69     /**
70      * @cfg {String} url
71      * The URL to use for form actions if one isn't supplied in the action options.
72      */
73     /**
74      * @cfg {Boolean} fileUpload
75      * Set to true if this form is a file upload.
76      */
77
78     /**
79      * @cfg {Object} baseParams
80      * Parameters to pass with all requests. e.g. baseParams: {id: '123', foo: 'bar'}.
81      */
82
83     /**
84      * @cfg {Number} timeout Timeout for form actions in seconds (default is 30 seconds).
85      */
86     timeout: 30,
87     /**
88      * @cfg {Sting} align (left|right) for navbar forms
89      */
90     align : 'left',
91
92     // private
93     activeAction : null,
94
95     /**
96      * By default wait messages are displayed with Roo.MessageBox.wait. You can target a specific
97      * element by passing it or its id or mask the form itself by passing in true.
98      * @type Mixed
99      */
100     waitMsgTarget : false,
101
102     loadMask : true,
103     
104     /**
105      * @cfg {Boolean} errorMask (true|false) default false
106      */
107     errorMask : false,
108
109     getAutoCreate : function(){
110
111         var cfg = {
112             tag: 'form',
113             method : this.method || 'POST',
114             id : this.id || Roo.id(),
115             cls : ''
116         };
117         if (this.parent().xtype.match(/^Nav/)) {
118             cfg.cls = 'navbar-form navbar-' + this.align;
119
120         }
121
122         if (this.labelAlign == 'left' ) {
123             cfg.cls += ' form-horizontal';
124         }
125
126
127         return cfg;
128     },
129     initEvents : function()
130     {
131         this.el.on('submit', this.onSubmit, this);
132         // this was added as random key presses on the form where triggering form submit.
133         this.el.on('keypress', function(e) {
134             if (e.getCharCode() != 13) {
135                 return true;
136             }
137             // we might need to allow it for textareas.. and some other items.
138             // check e.getTarget().
139
140             if(e.getTarget().nodeName.toLowerCase() === 'textarea'){
141                 return true;
142             }
143
144             Roo.log("keypress blocked");
145
146             e.preventDefault();
147             return false;
148         });
149         
150     },
151     // private
152     onSubmit : function(e){
153         e.stopEvent();
154     },
155
156      /**
157      * Returns true if client-side validation on the form is successful.
158      * @return Boolean
159      */
160     isValid : function(){
161         var items = this.getItems();
162         var valid = true;
163         var target = false;
164         
165         items.each(function(f){
166             
167             if(f.validate()){
168                 return;
169             }
170             valid = false;
171
172             if(!target && f.el.isVisible(true)){
173                 target = f;
174             }
175            
176         });
177         
178         if(this.errorMask && !valid){
179             Roo.bootstrap.Form.popover.mask(this, target);
180         }
181         
182         return valid;
183     },
184     
185     /**
186      * Returns true if any fields in this form have changed since their original load.
187      * @return Boolean
188      */
189     isDirty : function(){
190         var dirty = false;
191         var items = this.getItems();
192         items.each(function(f){
193            if(f.isDirty()){
194                dirty = true;
195                return false;
196            }
197            return true;
198         });
199         return dirty;
200     },
201      /**
202      * Performs a predefined action (submit or load) or custom actions you define on this form.
203      * @param {String} actionName The name of the action type
204      * @param {Object} options (optional) The options to pass to the action.  All of the config options listed
205      * below are supported by both the submit and load actions unless otherwise noted (custom actions could also
206      * accept other config options):
207      * <pre>
208 Property          Type             Description
209 ----------------  ---------------  ----------------------------------------------------------------------------------
210 url               String           The url for the action (defaults to the form's url)
211 method            String           The form method to use (defaults to the form's method, or POST if not defined)
212 params            String/Object    The params to pass (defaults to the form's baseParams, or none if not defined)
213 clientValidation  Boolean          Applies to submit only.  Pass true to call form.isValid() prior to posting to
214                                    validate the form on the client (defaults to false)
215      * </pre>
216      * @return {BasicForm} this
217      */
218     doAction : function(action, options){
219         if(typeof action == 'string'){
220             action = new Roo.form.Action.ACTION_TYPES[action](this, options);
221         }
222         if(this.fireEvent('beforeaction', this, action) !== false){
223             this.beforeAction(action);
224             action.run.defer(100, action);
225         }
226         return this;
227     },
228
229     // private
230     beforeAction : function(action){
231         var o = action.options;
232
233         if(this.loadMask){
234             this.el.mask(o.waitMsg || "Sending", 'x-mask-loading');
235         }
236         // not really supported yet.. ??
237
238         //if(this.waitMsgTarget === true){
239         //  this.el.mask(o.waitMsg || "Sending", 'x-mask-loading');
240         //}else if(this.waitMsgTarget){
241         //    this.waitMsgTarget = Roo.get(this.waitMsgTarget);
242         //    this.waitMsgTarget.mask(o.waitMsg || "Sending", 'x-mask-loading');
243         //}else {
244         //    Roo.MessageBox.wait(o.waitMsg || "Sending", o.waitTitle || this.waitTitle || 'Please Wait...');
245        // }
246
247     },
248
249     // private
250     afterAction : function(action, success){
251         this.activeAction = null;
252         var o = action.options;
253
254         //if(this.waitMsgTarget === true){
255             this.el.unmask();
256         //}else if(this.waitMsgTarget){
257         //    this.waitMsgTarget.unmask();
258         //}else{
259         //    Roo.MessageBox.updateProgress(1);
260         //    Roo.MessageBox.hide();
261        // }
262         //
263         if(success){
264             if(o.reset){
265                 this.reset();
266             }
267             Roo.callback(o.success, o.scope, [this, action]);
268             this.fireEvent('actioncomplete', this, action);
269
270         }else{
271
272             // failure condition..
273             // we have a scenario where updates need confirming.
274             // eg. if a locking scenario exists..
275             // we look for { errors : { needs_confirm : true }} in the response.
276             if (
277                 (typeof(action.result) != 'undefined')  &&
278                 (typeof(action.result.errors) != 'undefined')  &&
279                 (typeof(action.result.errors.needs_confirm) != 'undefined')
280            ){
281                 var _t = this;
282                 Roo.log("not supported yet");
283                  /*
284
285                 Roo.MessageBox.confirm(
286                     "Change requires confirmation",
287                     action.result.errorMsg,
288                     function(r) {
289                         if (r != 'yes') {
290                             return;
291                         }
292                         _t.doAction('submit', { params :  { _submit_confirmed : 1 } }  );
293                     }
294
295                 );
296                 */
297
298
299                 return;
300             }
301
302             Roo.callback(o.failure, o.scope, [this, action]);
303             // show an error message if no failed handler is set..
304             if (!this.hasListener('actionfailed')) {
305                 Roo.log("need to add dialog support");
306                 /*
307                 Roo.MessageBox.alert("Error",
308                     (typeof(action.result) != 'undefined' && typeof(action.result.errorMsg) != 'undefined') ?
309                         action.result.errorMsg :
310                         "Saving Failed, please check your entries or try again"
311                 );
312                 */
313             }
314
315             this.fireEvent('actionfailed', this, action);
316         }
317
318     },
319     /**
320      * Find a Roo.form.Field in this form by id, dataIndex, name or hiddenName
321      * @param {String} id The value to search for
322      * @return Field
323      */
324     findField : function(id){
325         var items = this.getItems();
326         var field = items.get(id);
327         if(!field){
328              items.each(function(f){
329                 if(f.isFormField && (f.dataIndex == id || f.id == id || f.getName() == id)){
330                     field = f;
331                     return false;
332                 }
333                 return true;
334             });
335         }
336         return field || null;
337     },
338      /**
339      * Mark fields in this form invalid in bulk.
340      * @param {Array/Object} errors Either an array in the form [{id:'fieldId', msg:'The message'},...] or an object hash of {id: msg, id2: msg2}
341      * @return {BasicForm} this
342      */
343     markInvalid : function(errors){
344         if(errors instanceof Array){
345             for(var i = 0, len = errors.length; i < len; i++){
346                 var fieldError = errors[i];
347                 var f = this.findField(fieldError.id);
348                 if(f){
349                     f.markInvalid(fieldError.msg);
350                 }
351             }
352         }else{
353             var field, id;
354             for(id in errors){
355                 if(typeof errors[id] != 'function' && (field = this.findField(id))){
356                     field.markInvalid(errors[id]);
357                 }
358             }
359         }
360         //Roo.each(this.childForms || [], function (f) {
361         //    f.markInvalid(errors);
362         //});
363
364         return this;
365     },
366
367     /**
368      * Set values for fields in this form in bulk.
369      * @param {Array/Object} values Either an array in the form [{id:'fieldId', value:'foo'},...] or an object hash of {id: value, id2: value2}
370      * @return {BasicForm} this
371      */
372     setValues : function(values){
373         if(values instanceof Array){ // array of objects
374             for(var i = 0, len = values.length; i < len; i++){
375                 var v = values[i];
376                 var f = this.findField(v.id);
377                 if(f){
378                     f.setValue(v.value);
379                     if(this.trackResetOnLoad){
380                         f.originalValue = f.getValue();
381                     }
382                 }
383             }
384         }else{ // object hash
385             var field, id;
386             for(id in values){
387                 if(typeof values[id] != 'function' && (field = this.findField(id))){
388
389                     if (field.setFromData &&
390                         field.valueField &&
391                         field.displayField &&
392                         // combos' with local stores can
393                         // be queried via setValue()
394                         // to set their value..
395                         (field.store && !field.store.isLocal)
396                         ) {
397                         // it's a combo
398                         var sd = { };
399                         sd[field.valueField] = typeof(values[field.hiddenName]) == 'undefined' ? '' : values[field.hiddenName];
400                         sd[field.displayField] = typeof(values[field.name]) == 'undefined' ? '' : values[field.name];
401                         field.setFromData(sd);
402
403                     } else {
404                         field.setValue(values[id]);
405                     }
406
407
408                     if(this.trackResetOnLoad){
409                         field.originalValue = field.getValue();
410                     }
411                 }
412             }
413         }
414
415         //Roo.each(this.childForms || [], function (f) {
416         //    f.setValues(values);
417         //});
418
419         return this;
420     },
421
422     /**
423      * Returns the fields in this form as an object with key/value pairs. If multiple fields exist with the same name
424      * they are returned as an array.
425      * @param {Boolean} asString
426      * @return {Object}
427      */
428     getValues : function(asString){
429         //if (this.childForms) {
430             // copy values from the child forms
431         //    Roo.each(this.childForms, function (f) {
432         //        this.setValues(f.getValues());
433         //    }, this);
434         //}
435
436
437
438         var fs = Roo.lib.Ajax.serializeForm(this.el.dom);
439         if(asString === true){
440             return fs;
441         }
442         return Roo.urlDecode(fs);
443     },
444
445     /**
446      * Returns the fields in this form as an object with key/value pairs.
447      * This differs from getValues as it calls getValue on each child item, rather than using dom data.
448      * @return {Object}
449      */
450     getFieldValues : function(with_hidden)
451     {
452         var items = this.getItems();
453         var ret = {};
454         items.each(function(f){
455             if (!f.getName()) {
456                 return;
457             }
458             var v = f.getValue();
459             if (f.inputType =='radio') {
460                 if (typeof(ret[f.getName()]) == 'undefined') {
461                     ret[f.getName()] = ''; // empty..
462                 }
463
464                 if (!f.el.dom.checked) {
465                     return;
466
467                 }
468                 v = f.el.dom.value;
469
470             }
471
472             // not sure if this supported any more..
473             if ((typeof(v) == 'object') && f.getRawValue) {
474                 v = f.getRawValue() ; // dates..
475             }
476             // combo boxes where name != hiddenName...
477             if (f.name !== false && f.name != '' && f.name != f.getName()) {
478                 ret[f.name] = f.getRawValue();
479             }
480             ret[f.getName()] = v;
481         });
482
483         return ret;
484     },
485
486     /**
487      * Clears all invalid messages in this form.
488      * @return {BasicForm} this
489      */
490     clearInvalid : function(){
491         var items = this.getItems();
492
493         items.each(function(f){
494            f.clearInvalid();
495         });
496
497
498
499         return this;
500     },
501
502     /**
503      * Resets this form.
504      * @return {BasicForm} this
505      */
506     reset : function(){
507         var items = this.getItems();
508         items.each(function(f){
509             f.reset();
510         });
511
512         Roo.each(this.childForms || [], function (f) {
513             f.reset();
514         });
515
516
517         return this;
518     },
519     getItems : function()
520     {
521         var r=new Roo.util.MixedCollection(false, function(o){
522             return o.id || (o.id = Roo.id());
523         });
524         var iter = function(el) {
525             if (el.inputEl) {
526                 r.add(el);
527             }
528             if (!el.items) {
529                 return;
530             }
531             Roo.each(el.items,function(e) {
532                 iter(e);
533             });
534
535
536         };
537
538         iter(this);
539         return r;
540
541
542
543
544     }
545
546 });
547
548 Roo.apply(Roo.bootstrap.Form, {
549     
550     popover : {
551         
552         padding : 5,
553         
554         isApplied : false,
555         
556         isMasked : false,
557         
558         form : false,
559         
560         target : false,
561         
562         toolTip : false,
563         
564         intervalID : false,
565         
566         maskEl : false,
567         
568         apply : function()
569         {
570             if(this.isApplied){
571                 return;
572             }
573             
574             this.maskEl = {
575                 top : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-top-mask" }, true),
576                 left : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-left-mask" }, true),
577                 bottom : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-bottom-mask" }, true),
578                 right : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-right-mask" }, true)
579             };
580             
581             this.maskEl.top.enableDisplayMode("block");
582             this.maskEl.left.enableDisplayMode("block");
583             this.maskEl.bottom.enableDisplayMode("block");
584             this.maskEl.right.enableDisplayMode("block");
585             
586             this.toolTip = new Roo.bootstrap.Tooltip({
587                 cls : 'roo-form-error-popover',
588                 alignment : {
589                     'left' : ['r-l', [-2,0], 'right'],
590                     'right' : ['l-r', [2,0], 'left'],
591                     'bottom' : ['tl-bl', [0,2], 'top'],
592                     'top' : [ 'bl-tl', [0,-2], 'bottom']
593                 }
594             });
595             
596             this.toolTip.render(Roo.get(document.body));
597
598             this.toolTip.el.enableDisplayMode("block");
599             
600             Roo.get(document.body).on('click', function(){
601                 this.unmask();
602             }, this);
603             
604             this.isApplied = true
605         },
606         
607         mask : function(form, target)
608         {
609             this.form = form;
610             
611             this.target = target;
612             
613             if(!this.form.errorMask || !target.el){
614                 return;
615             }
616             
617             var scrollable = this.target.el.findScrollableParent() || this.target.el.findParent('div.modal', 100, true) || Roo.get(document.body);
618             
619             var scrolled = scrollable.getScroll();
620             
621             var ot = this.target.el.calcOffsetsTo(scrollable);
622             
623             var scrollTo = 0;
624             
625             if(ot[1] <= scrolled.top){
626                 scrollTo = ot[1] - 100;
627             } else {
628                 scrollTo = ot[1] + Roo.lib.Dom.getViewHeight() - 100;
629             }
630             
631             scrollable.scrollTo('top', scrollTo);
632             
633             var box = this.target.el.getBox();
634             
635             var zIndex = Roo.bootstrap.Modal.zIndex++;
636             
637             this.maskEl.top.setSize(Roo.lib.Dom.getDocumentWidth(), box.y - this.padding);
638             this.maskEl.top.setXY([0, 0]);
639             this.maskEl.top.setStyle('z-index', zIndex);
640             this.maskEl.top.show();
641             
642             this.maskEl.left.setSize(Roo.lib.Dom.getDocumentWidth() - box.right - this.padding, box.height + this.padding * 2);
643             this.maskEl.left.setXY([box.right + this.padding, box.y - this.padding]);
644             this.maskEl.left.setStyle('z-index', zIndex);
645             this.maskEl.left.show();
646             
647             this.maskEl.bottom.setSize(Roo.lib.Dom.getDocumentWidth(), Roo.lib.Dom.getDocumentHeight() - box.bottom - this.padding);
648             this.maskEl.bottom.setXY([0, box.bottom + this.padding]);
649             this.maskEl.bottom.setStyle('z-index', zIndex);
650             this.maskEl.bottom.show();
651             
652             this.maskEl.right.setSize(box.x - this.padding, box.height + this.padding * 2);
653             this.maskEl.right.setXY([0, box.y - this.padding]);
654             this.maskEl.right.setStyle('z-index', zIndex);
655             this.maskEl.right.show();
656             
657             
658             this.toolTip.bindEl = this.target.el;
659         
660             this.toolTip.el.setStyle('z-index', Roo.bootstrap.Modal.zIndex++);
661
662             var tip = this.target.blankText;
663
664             if(this.target.getValue() !== '' && this.target.regexText.length){
665                 tip = this.target.regexText;
666             }
667
668             this.toolTip.show(tip);
669             
670             this.intervalID = window.setInterval(function() {
671                 Roo.bootstrap.Form.popover.unmask();
672             }, 10000);
673
674             window.onwheel = function(){ return false;};
675             
676             (function(){ this.isMasked = true; }).defer(500, this);
677             
678         },
679         
680         unmask : function()
681         {
682             if(!this.isApplied || !this.isMasked || !this.form || !this.target || !this.form.errorMask){
683                 return;
684             }
685             
686             this.maskEl.top.setSize(0, 0).setXY([0, 0]).hide();
687             this.maskEl.left.setSize(0, 0).setXY([0, 0]).hide();
688             this.maskEl.bottom.setSize(0, 0).setXY([0, 0]).hide();
689             this.maskEl.right.setSize(0, 0).setXY([0, 0]).hide();
690             
691             this.toolTip.hide();
692             
693             this.toolTip.el.hide();
694             
695             window.onwheel = function(){ return true;};
696             
697             if(this.intervalID){
698                 window.clearInterval(this.intervalID);
699                 this.intervalID = false;
700             }
701             
702             this.isMasked = false;
703             
704         }
705         
706     }
707     
708 });
709