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