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