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     
26     Roo.bootstrap.Form.superclass.constructor.call(this, config);
27     
28     Roo.bootstrap.Form.popover.apply();
29     
30     this.addEvents({
31         /**
32          * @event clientvalidation
33          * If the monitorValid config option is true, this event fires repetitively to notify of valid state
34          * @param {Form} this
35          * @param {Boolean} valid true if the form has passed client-side validation
36          */
37         clientvalidation: true,
38         /**
39          * @event beforeaction
40          * Fires before any action is performed. Return false to cancel the action.
41          * @param {Form} this
42          * @param {Action} action The action to be performed
43          */
44         beforeaction: true,
45         /**
46          * @event actionfailed
47          * Fires when an action fails.
48          * @param {Form} this
49          * @param {Action} action The action that failed
50          */
51         actionfailed : true,
52         /**
53          * @event actioncomplete
54          * Fires when an action is completed.
55          * @param {Form} this
56          * @param {Action} action The action that completed
57          */
58         actioncomplete : true
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         return this;
532     },
533
534     /**
535      * Resets this form.
536      * @return {BasicForm} this
537      */
538     reset : function(){
539         var items = this.getItems();
540         items.each(function(f){
541             f.reset();
542         });
543
544         Roo.each(this.childForms || [], function (f) {
545             f.reset();
546         });
547
548
549         return this;
550     },
551     
552     getItems : function()
553     {
554         var r=new Roo.util.MixedCollection(false, function(o){
555             return o.id || (o.id = Roo.id());
556         });
557         var iter = function(el) {
558             if (el.inputEl) {
559                 r.add(el);
560             }
561             if (!el.items) {
562                 return;
563             }
564             Roo.each(el.items,function(e) {
565                 iter(e);
566             });
567         };
568
569         iter(this);
570         return r;
571     }
572
573 });
574
575 Roo.apply(Roo.bootstrap.Form, {
576     
577     popover : {
578         
579         padding : 5,
580         
581         isApplied : false,
582         
583         isMasked : false,
584         
585         form : false,
586         
587         target : false,
588         
589         toolTip : false,
590         
591         intervalID : false,
592         
593         maskEl : false,
594         
595         apply : function()
596         {
597             if(this.isApplied){
598                 return;
599             }
600             
601             this.maskEl = {
602                 top : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-top-mask" }, true),
603                 left : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-left-mask" }, true),
604                 bottom : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-bottom-mask" }, true),
605                 right : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-right-mask" }, true)
606             };
607             
608             this.maskEl.top.enableDisplayMode("block");
609             this.maskEl.left.enableDisplayMode("block");
610             this.maskEl.bottom.enableDisplayMode("block");
611             this.maskEl.right.enableDisplayMode("block");
612             
613             this.toolTip = new Roo.bootstrap.Tooltip({
614                 cls : 'roo-form-error-popover',
615                 alignment : {
616                     'left' : ['r-l', [-2,0], 'right'],
617                     'right' : ['l-r', [2,0], 'left'],
618                     'bottom' : ['tl-bl', [0,2], 'top'],
619                     'top' : [ 'bl-tl', [0,-2], 'bottom']
620                 }
621             });
622             
623             this.toolTip.render(Roo.get(document.body));
624
625             this.toolTip.el.enableDisplayMode("block");
626             
627             Roo.get(document.body).on('click', function(){
628                 this.unmask();
629             }, this);
630             
631             Roo.get(document.body).on('touchstart', function(){
632                 this.unmask();
633             }, this);
634             
635             this.isApplied = true
636         },
637         
638         mask : function(form, target)
639         {
640             this.form = form;
641             
642             this.target = target;
643             
644             if(!this.form.errorMask || !target.el){
645                 return;
646             }
647             
648             var scrollable = this.target.el.findScrollableParent() || this.target.el.findParent('div.modal', 100, true) || Roo.get(document.body);
649             
650             Roo.log(scrollable);
651             
652             var ot = this.target.el.calcOffsetsTo(scrollable);
653             
654             var scrollTo = ot[1] - this.form.maskOffset;
655             
656             scrollTo = Math.min(scrollTo, scrollable.dom.scrollHeight);
657             
658             scrollable.scrollTo('top', scrollTo);
659             
660             var box = this.target.el.getBox();
661             Roo.log(box);
662             var zIndex = Roo.bootstrap.Modal.zIndex++;
663
664             
665             this.maskEl.top.setStyle('position', 'absolute');
666             this.maskEl.top.setStyle('z-index', zIndex);
667             this.maskEl.top.setSize(Roo.lib.Dom.getDocumentWidth(), box.y - this.padding);
668             this.maskEl.top.setLeft(0);
669             this.maskEl.top.setTop(0);
670             this.maskEl.top.show();
671             
672             this.maskEl.left.setStyle('position', 'absolute');
673             this.maskEl.left.setStyle('z-index', zIndex);
674             this.maskEl.left.setSize(box.x - this.padding, box.height + this.padding * 2);
675             this.maskEl.left.setLeft(0);
676             this.maskEl.left.setTop(box.y - this.padding);
677             this.maskEl.left.show();
678
679             this.maskEl.bottom.setStyle('position', 'absolute');
680             this.maskEl.bottom.setStyle('z-index', zIndex);
681             this.maskEl.bottom.setSize(Roo.lib.Dom.getDocumentWidth(), Roo.lib.Dom.getDocumentHeight() - box.bottom - this.padding);
682             this.maskEl.bottom.setLeft(0);
683             this.maskEl.bottom.setTop(box.bottom + this.padding);
684             this.maskEl.bottom.show();
685
686             this.maskEl.right.setStyle('position', 'absolute');
687             this.maskEl.right.setStyle('z-index', zIndex);
688             this.maskEl.right.setSize(Roo.lib.Dom.getDocumentWidth() - box.right - this.padding, box.height + this.padding * 2);
689             this.maskEl.right.setLeft(box.right + this.padding);
690             this.maskEl.right.setTop(box.y - this.padding);
691             this.maskEl.right.show();
692
693             this.toolTip.bindEl = this.target.el;
694
695             this.toolTip.el.setStyle('z-index', Roo.bootstrap.Modal.zIndex++);
696
697             var tip = this.target.blankText;
698
699             if(this.target.getValue() !== '' ) {
700                 
701                 if (this.target.invalidText.length) {
702                     tip = this.target.invalidText;
703                 } else if (this.target.regexText.length){
704                     tip = this.target.regexText;
705                 }
706             }
707
708             this.toolTip.show(tip);
709
710             this.intervalID = window.setInterval(function() {
711                 Roo.bootstrap.Form.popover.unmask();
712             }, 10000);
713
714             window.onwheel = function(){ return false;};
715             
716             (function(){ this.isMasked = true; }).defer(500, this);
717             
718         },
719         
720         unmask : function()
721         {
722             if(!this.isApplied || !this.isMasked || !this.form || !this.target || !this.form.errorMask){
723                 return;
724             }
725             
726             this.maskEl.top.setStyle('position', 'absolute');
727             this.maskEl.top.setSize(0, 0).setXY([0, 0]);
728             this.maskEl.top.hide();
729
730             this.maskEl.left.setStyle('position', 'absolute');
731             this.maskEl.left.setSize(0, 0).setXY([0, 0]);
732             this.maskEl.left.hide();
733
734             this.maskEl.bottom.setStyle('position', 'absolute');
735             this.maskEl.bottom.setSize(0, 0).setXY([0, 0]);
736             this.maskEl.bottom.hide();
737
738             this.maskEl.right.setStyle('position', 'absolute');
739             this.maskEl.right.setSize(0, 0).setXY([0, 0]);
740             this.maskEl.right.hide();
741             
742             this.toolTip.hide();
743             
744             this.toolTip.el.hide();
745             
746             window.onwheel = function(){ return true;};
747             
748             if(this.intervalID){
749                 window.clearInterval(this.intervalID);
750                 this.intervalID = false;
751             }
752             
753             this.isMasked = false;
754             
755         }
756         
757     }
758     
759 });
760