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