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