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