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