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