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     hideFields : function(items)
574     {
575         Roo.each(items, function(i){
576             
577             var f = this.findField(i);
578             
579             if(!f){
580                 return;
581             }
582             
583             if(f.xtype == 'DateField'){
584                 f.hideElement();
585                 return;
586             }
587             
588             f.hide();
589             
590         }, this);
591     },
592     
593     showFields : function(items)
594     {
595         Roo.each(items, function(i){
596             
597             var f = this.findField(i);
598             
599             if(!f){
600                 return;
601             }
602             
603             if(f.xtype == 'DateField'){
604                 f.showElement();
605                 return;
606             }
607             
608             f.show();
609             
610         }, this);
611     }
612
613 });
614
615 Roo.apply(Roo.bootstrap.Form, {
616     
617     popover : {
618         
619         padding : 5,
620         
621         isApplied : false,
622         
623         isMasked : false,
624         
625         form : false,
626         
627         target : false,
628         
629         toolTip : false,
630         
631         intervalID : false,
632         
633         maskEl : false,
634         
635         apply : function()
636         {
637             if(this.isApplied){
638                 return;
639             }
640             
641             this.maskEl = {
642                 top : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-top-mask" }, true),
643                 left : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-left-mask" }, true),
644                 bottom : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-bottom-mask" }, true),
645                 right : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-right-mask" }, true)
646             };
647             
648             this.maskEl.top.enableDisplayMode("block");
649             this.maskEl.left.enableDisplayMode("block");
650             this.maskEl.bottom.enableDisplayMode("block");
651             this.maskEl.right.enableDisplayMode("block");
652             
653             this.toolTip = new Roo.bootstrap.Tooltip({
654                 cls : 'roo-form-error-popover',
655                 alignment : {
656                     'left' : ['r-l', [-2,0], 'right'],
657                     'right' : ['l-r', [2,0], 'left'],
658                     'bottom' : ['tl-bl', [0,2], 'top'],
659                     'top' : [ 'bl-tl', [0,-2], 'bottom']
660                 }
661             });
662             
663             this.toolTip.render(Roo.get(document.body));
664
665             this.toolTip.el.enableDisplayMode("block");
666             
667             Roo.get(document.body).on('click', function(){
668                 this.unmask();
669             }, this);
670             
671             Roo.get(document.body).on('touchstart', function(){
672                 this.unmask();
673             }, this);
674             
675             this.isApplied = true
676         },
677         
678         mask : function(form, target)
679         {
680             this.form = form;
681             
682             this.target = target;
683             
684             if(!this.form.errorMask || !target.el){
685                 return;
686             }
687             
688             var scrollable = this.target.el.findScrollableParent() || this.target.el.findParent('div.modal', 100, true) || Roo.get(document.body);
689             
690             Roo.log(scrollable);
691             
692             var ot = this.target.el.calcOffsetsTo(scrollable);
693             
694             var scrollTo = ot[1] - this.form.maskOffset;
695             
696             scrollTo = Math.min(scrollTo, scrollable.dom.scrollHeight);
697             
698             scrollable.scrollTo('top', scrollTo);
699             
700             var box = this.target.el.getBox();
701             Roo.log(box);
702             var zIndex = Roo.bootstrap.Modal.zIndex++;
703
704             
705             this.maskEl.top.setStyle('position', 'absolute');
706             this.maskEl.top.setStyle('z-index', zIndex);
707             this.maskEl.top.setSize(Roo.lib.Dom.getDocumentWidth(), box.y - this.padding);
708             this.maskEl.top.setLeft(0);
709             this.maskEl.top.setTop(0);
710             this.maskEl.top.show();
711             
712             this.maskEl.left.setStyle('position', 'absolute');
713             this.maskEl.left.setStyle('z-index', zIndex);
714             this.maskEl.left.setSize(box.x - this.padding, box.height + this.padding * 2);
715             this.maskEl.left.setLeft(0);
716             this.maskEl.left.setTop(box.y - this.padding);
717             this.maskEl.left.show();
718
719             this.maskEl.bottom.setStyle('position', 'absolute');
720             this.maskEl.bottom.setStyle('z-index', zIndex);
721             this.maskEl.bottom.setSize(Roo.lib.Dom.getDocumentWidth(), Roo.lib.Dom.getDocumentHeight() - box.bottom - this.padding);
722             this.maskEl.bottom.setLeft(0);
723             this.maskEl.bottom.setTop(box.bottom + this.padding);
724             this.maskEl.bottom.show();
725
726             this.maskEl.right.setStyle('position', 'absolute');
727             this.maskEl.right.setStyle('z-index', zIndex);
728             this.maskEl.right.setSize(Roo.lib.Dom.getDocumentWidth() - box.right - this.padding, box.height + this.padding * 2);
729             this.maskEl.right.setLeft(box.right + this.padding);
730             this.maskEl.right.setTop(box.y - this.padding);
731             this.maskEl.right.show();
732
733             this.toolTip.bindEl = this.target.el;
734
735             this.toolTip.el.setStyle('z-index', Roo.bootstrap.Modal.zIndex++);
736
737             var tip = this.target.blankText;
738
739             if(this.target.getValue() !== '' ) {
740                 
741                 if (this.target.invalidText.length) {
742                     tip = this.target.invalidText;
743                 } else if (this.target.regexText.length){
744                     tip = this.target.regexText;
745                 }
746             }
747
748             this.toolTip.show(tip);
749
750             this.intervalID = window.setInterval(function() {
751                 Roo.bootstrap.Form.popover.unmask();
752             }, 10000);
753
754             window.onwheel = function(){ return false;};
755             
756             (function(){ this.isMasked = true; }).defer(500, this);
757             
758         },
759         
760         unmask : function()
761         {
762             if(!this.isApplied || !this.isMasked || !this.form || !this.target || !this.form.errorMask){
763                 return;
764             }
765             
766             this.maskEl.top.setStyle('position', 'absolute');
767             this.maskEl.top.setSize(0, 0).setXY([0, 0]);
768             this.maskEl.top.hide();
769
770             this.maskEl.left.setStyle('position', 'absolute');
771             this.maskEl.left.setSize(0, 0).setXY([0, 0]);
772             this.maskEl.left.hide();
773
774             this.maskEl.bottom.setStyle('position', 'absolute');
775             this.maskEl.bottom.setSize(0, 0).setXY([0, 0]);
776             this.maskEl.bottom.hide();
777
778             this.maskEl.right.setStyle('position', 'absolute');
779             this.maskEl.right.setSize(0, 0).setXY([0, 0]);
780             this.maskEl.right.hide();
781             
782             this.toolTip.hide();
783             
784             this.toolTip.el.hide();
785             
786             window.onwheel = function(){ return true;};
787             
788             if(this.intervalID){
789                 window.clearInterval(this.intervalID);
790                 this.intervalID = false;
791             }
792             
793             this.isMasked = false;
794             
795         }
796         
797     }
798     
799 });
800