Roo/form/Form.js
[roojs1] / Roo / form / Form.js
1 /*
2  * Based on:
3  * Ext JS Library 1.1.1
4  * Copyright(c) 2006-2007, Ext JS, LLC.
5  *
6  * Originally Released Under LGPL - original licence link has changed is not relivant.
7  *
8  * Fork - LGPL
9  * <script type="text/javascript">
10  */
11
12 /**
13  * @class Roo.form.Form
14  * @extends Roo.form.BasicForm
15  * Adds the ability to dynamically render forms with JavaScript to {@link Roo.form.BasicForm}.
16  * @constructor
17  * @param {Object} config Configuration options
18  */
19 Roo.form.Form = function(config){
20     var xitems =  [];
21     if (config.items) {
22         xitems = config.items;
23         delete config.items;
24     }
25    
26     
27     Roo.form.Form.superclass.constructor.call(this, null, config);
28     this.url = this.url || this.action;
29     if(!this.root){
30         this.root = new Roo.form.Layout(Roo.applyIf({
31             id: Roo.id()
32         }, config));
33     }
34     this.active = this.root;
35     /**
36      * Array of all the buttons that have been added to this form via {@link addButton}
37      * @type Array
38      */
39     this.buttons = [];
40     this.allItems = [];
41     this.addEvents({
42         /**
43          * @event clientvalidation
44          * If the monitorValid config option is true, this event fires repetitively to notify of valid state
45          * @param {Form} this
46          * @param {Boolean} valid true if the form has passed client-side validation
47          */
48         clientvalidation: true,
49         /**
50          * @event rendered
51          * Fires when the form is rendered
52          * @param {Roo.form.Form} form
53          */
54         rendered : true
55     });
56     
57     if (this.progressUrl) {
58             // push a hidden field onto the list of fields..
59             this.addxtype( {
60                     xns: Roo.form, 
61                     xtype : 'Hidden', 
62                     name : 'UPLOAD_IDENTIFIER' 
63             });
64         }
65         
66     
67     Roo.each(xitems, this.addxtype, this);
68     
69     Roo.form.Form.popover.apply();
70     
71 };
72
73 Roo.extend(Roo.form.Form, Roo.form.BasicForm, {
74     /**
75      * @cfg {Number} labelWidth The width of labels. This property cascades to child containers.
76      */
77     /**
78      * @cfg {String} itemCls A css class to apply to the x-form-item of fields. This property cascades to child containers.
79      */
80     /**
81      * @cfg {String} buttonAlign Valid values are "left," "center" and "right" (defaults to "center")
82      */
83     buttonAlign:'center',
84
85     /**
86      * @cfg {Number} minButtonWidth Minimum width of all buttons in pixels (defaults to 75)
87      */
88     minButtonWidth:75,
89
90     /**
91      * @cfg {String} labelAlign Valid values are "left," "top" and "right" (defaults to "left").
92      * This property cascades to child containers if not set.
93      */
94     labelAlign:'left',
95
96     /**
97      * @cfg {Boolean} monitorValid If true the form monitors its valid state <b>client-side</b> and
98      * fires a looping event with that state. This is required to bind buttons to the valid
99      * state using the config value formBind:true on the button.
100      */
101     monitorValid : false,
102
103     /**
104      * @cfg {Number} monitorPoll The milliseconds to poll valid state, ignored if monitorValid is not true (defaults to 200)
105      */
106     monitorPoll : 200,
107     
108     /**
109      * @cfg {String} progressUrl - Url to return progress data 
110      */
111     
112     progressUrl : false,
113     /**
114      * @cfg {boolean|FormData} formData - true to use new 'FormData' post, or set to a new FormData({dom form}) Object, if
115      * sending a formdata with extra parameters - eg uploaded elements.
116      */
117     
118     formData : false,
119     
120     /**
121      * Opens a new {@link Roo.form.Column} container in the layout stack. If fields are passed after the config, the
122      * fields are added and the column is closed. If no fields are passed the column remains open
123      * until end() is called.
124      * @param {Object} config The config to pass to the column
125      * @param {Field} field1 (optional)
126      * @param {Field} field2 (optional)
127      * @param {Field} etc (optional)
128      * @return Column The column container object
129      */
130     column : function(c){
131         var col = new Roo.form.Column(c);
132         this.start(col);
133         if(arguments.length > 1){ // duplicate code required because of Opera
134             this.add.apply(this, Array.prototype.slice.call(arguments, 1));
135             this.end();
136         }
137         return col;
138     },
139
140     /**
141      * Opens a new {@link Roo.form.FieldSet} container in the layout stack. If fields are passed after the config, the
142      * fields are added and the fieldset is closed. If no fields are passed the fieldset remains open
143      * until end() is called.
144      * @param {Object} config The config to pass to the fieldset
145      * @param {Field} field1 (optional)
146      * @param {Field} field2 (optional)
147      * @param {Field} etc (optional)
148      * @return FieldSet The fieldset container object
149      */
150     fieldset : function(c){
151         var fs = new Roo.form.FieldSet(c);
152         this.start(fs);
153         if(arguments.length > 1){ // duplicate code required because of Opera
154             this.add.apply(this, Array.prototype.slice.call(arguments, 1));
155             this.end();
156         }
157         return fs;
158     },
159
160     /**
161      * Opens a new {@link Roo.form.Layout} container in the layout stack. If fields are passed after the config, the
162      * fields are added and the container is closed. If no fields are passed the container remains open
163      * until end() is called.
164      * @param {Object} config The config to pass to the Layout
165      * @param {Field} field1 (optional)
166      * @param {Field} field2 (optional)
167      * @param {Field} etc (optional)
168      * @return Layout The container object
169      */
170     container : function(c){
171         var l = new Roo.form.Layout(c);
172         this.start(l);
173         if(arguments.length > 1){ // duplicate code required because of Opera
174             this.add.apply(this, Array.prototype.slice.call(arguments, 1));
175             this.end();
176         }
177         return l;
178     },
179
180     /**
181      * Opens the passed container in the layout stack. The container can be any {@link Roo.form.Layout} or subclass.
182      * @param {Object} container A Roo.form.Layout or subclass of Layout
183      * @return {Form} this
184      */
185     start : function(c){
186         // cascade label info
187         Roo.applyIf(c, {'labelAlign': this.active.labelAlign, 'labelWidth': this.active.labelWidth, 'itemCls': this.active.itemCls});
188         this.active.stack.push(c);
189         c.ownerCt = this.active;
190         this.active = c;
191         return this;
192     },
193
194     /**
195      * Closes the current open container
196      * @return {Form} this
197      */
198     end : function(){
199         if(this.active == this.root){
200             return this;
201         }
202         this.active = this.active.ownerCt;
203         return this;
204     },
205
206     /**
207      * Add Roo.form components to the current open container (e.g. column, fieldset, etc.).  Fields added via this method
208      * can also be passed with an additional property of fieldLabel, which if supplied, will provide the text to display
209      * as the label of the field.
210      * @param {Field} field1
211      * @param {Field} field2 (optional)
212      * @param {Field} etc. (optional)
213      * @return {Form} this
214      */
215     add : function(){
216         this.active.stack.push.apply(this.active.stack, arguments);
217         this.allItems.push.apply(this.allItems,arguments);
218         var r = [];
219         for(var i = 0, a = arguments, len = a.length; i < len; i++) {
220             if(a[i].isFormField){
221                 r.push(a[i]);
222             }
223         }
224         if(r.length > 0){
225             Roo.form.Form.superclass.add.apply(this, r);
226         }
227         return this;
228     },
229     
230
231     
232     
233     
234      /**
235      * Find any element that has been added to a form, using it's ID or name
236      * This can include framesets, columns etc. along with regular fields..
237      * @param {String} id - id or name to find.
238      
239      * @return {Element} e - or false if nothing found.
240      */
241     findbyId : function(id)
242     {
243         var ret = false;
244         if (!id) {
245             return ret;
246         }
247         Roo.each(this.allItems, function(f){
248             if (f.id == id || f.name == id ){
249                 ret = f;
250                 return false;
251             }
252         });
253         return ret;
254     },
255
256     
257     
258     /**
259      * Render this form into the passed container. This should only be called once!
260      * @param {String/HTMLElement/Element} container The element this component should be rendered into
261      * @return {Form} this
262      */
263     render : function(ct)
264     {
265         
266         
267         
268         ct = Roo.get(ct);
269         var o = this.autoCreate || {
270             tag: 'form',
271             method : this.method || 'POST',
272             id : this.id || Roo.id()
273         };
274         this.initEl(ct.createChild(o));
275
276         this.root.render(this.el);
277         
278        
279              
280         this.items.each(function(f){
281             f.render('x-form-el-'+f.id);
282         });
283
284         if(this.buttons.length > 0){
285             // tables are required to maintain order and for correct IE layout
286             var tb = this.el.createChild({cls:'x-form-btns-ct', cn: {
287                 cls:"x-form-btns x-form-btns-"+this.buttonAlign,
288                 html:'<table cellspacing="0"><tbody><tr></tr></tbody></table><div class="x-clear"></div>'
289             }}, null, true);
290             var tr = tb.getElementsByTagName('tr')[0];
291             for(var i = 0, len = this.buttons.length; i < len; i++) {
292                 var b = this.buttons[i];
293                 var td = document.createElement('td');
294                 td.className = 'x-form-btn-td';
295                 b.render(tr.appendChild(td));
296             }
297         }
298         if(this.monitorValid){ // initialize after render
299             this.startMonitoring();
300         }
301         this.fireEvent('rendered', this);
302         return this;
303     },
304
305     /**
306      * Adds a button to the footer of the form - this <b>must</b> be called before the form is rendered.
307      * @param {String/Object} config A string becomes the button text, an object can either be a Button config
308      * object or a valid Roo.DomHelper element config
309      * @param {Function} handler The function called when the button is clicked
310      * @param {Object} scope (optional) The scope of the handler function
311      * @return {Roo.Button}
312      */
313     addButton : function(config, handler, scope){
314         var bc = {
315             handler: handler,
316             scope: scope,
317             minWidth: this.minButtonWidth,
318             hideParent:true
319         };
320         if(typeof config == "string"){
321             bc.text = config;
322         }else{
323             Roo.apply(bc, config);
324         }
325         var btn = new Roo.Button(null, bc);
326         this.buttons.push(btn);
327         return btn;
328     },
329
330      /**
331      * Adds a series of form elements (using the xtype property as the factory method.
332      * Valid xtypes are:  TextField, TextArea .... Button, Layout, FieldSet, Column, (and 'end' to close a block)
333      * @param {Object} config 
334      */
335     
336     addxtype : function()
337     {
338         var ar = Array.prototype.slice.call(arguments, 0);
339         var ret = false;
340         for(var i = 0; i < ar.length; i++) {
341             if (!ar[i]) {
342                 continue; // skip -- if this happends something invalid got sent, we 
343                 // should ignore it, as basically that interface element will not show up
344                 // and that should be pretty obvious!!
345             }
346             
347             if (Roo.form[ar[i].xtype]) {
348                 ar[i].form = this;
349                 var fe = Roo.factory(ar[i], Roo.form);
350                 if (!ret) {
351                     ret = fe;
352                 }
353                 fe.form = this;
354                 if (fe.store) {
355                     fe.store.form = this;
356                 }
357                 if (fe.isLayout) {  
358                          
359                     this.start(fe);
360                     this.allItems.push(fe);
361                     if (fe.items && fe.addxtype) {
362                         fe.addxtype.apply(fe, fe.items);
363                         delete fe.items;
364                     }
365                      this.end();
366                     continue;
367                 }
368                 
369                 
370                  
371                 this.add(fe);
372               //  console.log('adding ' + ar[i].xtype);
373             }
374             if (ar[i].xtype == 'Button') {  
375                 //console.log('adding button');
376                 //console.log(ar[i]);
377                 this.addButton(ar[i]);
378                 this.allItems.push(fe);
379                 continue;
380             }
381             
382             if (ar[i].xtype == 'end') { // so we can add fieldsets... / layout etc.
383                 alert('end is not supported on xtype any more, use items');
384             //    this.end();
385             //    //console.log('adding end');
386             }
387             
388         }
389         return ret;
390     },
391     
392     /**
393      * Starts monitoring of the valid state of this form. Usually this is done by passing the config
394      * option "monitorValid"
395      */
396     startMonitoring : function(){
397         if(!this.bound){
398             this.bound = true;
399             Roo.TaskMgr.start({
400                 run : this.bindHandler,
401                 interval : this.monitorPoll || 200,
402                 scope: this
403             });
404         }
405     },
406
407     /**
408      * Stops monitoring of the valid state of this form
409      */
410     stopMonitoring : function(){
411         this.bound = false;
412     },
413
414     // private
415     bindHandler : function(){
416         if(!this.bound){
417             return false; // stops binding
418         }
419         var valid = true;
420         this.items.each(function(f){
421             if(!f.isValid(true)){
422                 valid = false;
423                 return false;
424             }
425         });
426         for(var i = 0, len = this.buttons.length; i < len; i++){
427             var btn = this.buttons[i];
428             if(btn.formBind === true && btn.disabled === valid){
429                 btn.setDisabled(!valid);
430             }
431         }
432         this.fireEvent('clientvalidation', this, valid);
433     }
434     
435     
436     
437     
438     
439     
440     
441     
442 });
443
444
445 // back compat
446 Roo.Form = Roo.form.Form;
447
448
449 Roo.apply(Roo.form.Form, {
450     
451     popover : {
452         
453         padding : 5,
454         
455         isApplied : false,
456         
457         isMasked : false,
458         
459         form : false,
460         
461         target : false,
462         
463         toolTip : false,
464         
465         intervalID : false,
466         
467         maskEl : false,
468         
469         apply : function()
470         {
471             if(this.isApplied){
472                 return;
473             }
474             
475             this.maskEl = {
476                 top : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-top-mask" }, true),
477                 left : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-left-mask" }, true),
478                 bottom : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-bottom-mask" }, true),
479                 right : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-right-mask" }, true)
480             };
481             
482             this.maskEl.top.enableDisplayMode("block");
483             this.maskEl.left.enableDisplayMode("block");
484             this.maskEl.bottom.enableDisplayMode("block");
485             this.maskEl.right.enableDisplayMode("block");
486             
487             this.toolTip = new Roo.bootstrap.Tooltip({
488                 cls : 'roo-form-error-popover',
489                 alignment : {
490                     'left' : ['r-l', [-2,0], 'right'],
491                     'right' : ['l-r', [2,0], 'left'],
492                     'bottom' : ['tl-bl', [0,2], 'top'],
493                     'top' : [ 'bl-tl', [0,-2], 'bottom']
494                 }
495             });
496             
497             this.toolTip.render(Roo.get(document.body));
498
499             this.toolTip.el.enableDisplayMode("block");
500             
501             Roo.get(document.body).on('click', function(){
502                 this.unmask();
503             }, this);
504             
505             Roo.get(document.body).on('touchstart', function(){
506                 this.unmask();
507             }, this);
508             
509             this.isApplied = true
510         },
511         
512         mask : function(form, target)
513         {
514             this.form = form;
515             
516             this.target = target;
517             
518             if(!this.form.errorMask || !target.el){
519                 return;
520             }
521             
522             var scrollable = this.target.el.findScrollableParent() || this.target.el.findParent('div.modal', 100, true) || Roo.get(document.body);
523             
524             Roo.log(scrollable);
525             
526             var ot = this.target.el.calcOffsetsTo(scrollable);
527             
528             var scrollTo = ot[1] - this.form.maskOffset;
529             
530             scrollTo = Math.min(scrollTo, scrollable.dom.scrollHeight);
531             
532             scrollable.scrollTo('top', scrollTo);
533             
534             var box = this.target.el.getBox();
535             Roo.log(box);
536             var zIndex = Roo.bootstrap.Modal.zIndex++;
537
538             
539             this.maskEl.top.setStyle('position', 'absolute');
540             this.maskEl.top.setStyle('z-index', zIndex);
541             this.maskEl.top.setSize(Roo.lib.Dom.getDocumentWidth(), box.y - this.padding);
542             this.maskEl.top.setLeft(0);
543             this.maskEl.top.setTop(0);
544             this.maskEl.top.show();
545             
546             this.maskEl.left.setStyle('position', 'absolute');
547             this.maskEl.left.setStyle('z-index', zIndex);
548             this.maskEl.left.setSize(box.x - this.padding, box.height + this.padding * 2);
549             this.maskEl.left.setLeft(0);
550             this.maskEl.left.setTop(box.y - this.padding);
551             this.maskEl.left.show();
552
553             this.maskEl.bottom.setStyle('position', 'absolute');
554             this.maskEl.bottom.setStyle('z-index', zIndex);
555             this.maskEl.bottom.setSize(Roo.lib.Dom.getDocumentWidth(), Roo.lib.Dom.getDocumentHeight() - box.bottom - this.padding);
556             this.maskEl.bottom.setLeft(0);
557             this.maskEl.bottom.setTop(box.bottom + this.padding);
558             this.maskEl.bottom.show();
559
560             this.maskEl.right.setStyle('position', 'absolute');
561             this.maskEl.right.setStyle('z-index', zIndex);
562             this.maskEl.right.setSize(Roo.lib.Dom.getDocumentWidth() - box.right - this.padding, box.height + this.padding * 2);
563             this.maskEl.right.setLeft(box.right + this.padding);
564             this.maskEl.right.setTop(box.y - this.padding);
565             this.maskEl.right.show();
566
567             this.toolTip.bindEl = this.target.el;
568
569             this.toolTip.el.setStyle('z-index', Roo.bootstrap.Modal.zIndex++);
570
571             var tip = this.target.blankText;
572
573             if(this.target.getValue() !== '' ) {
574                 
575                 if (this.target.invalidText.length) {
576                     tip = this.target.invalidText;
577                 } else if (this.target.regexText.length){
578                     tip = this.target.regexText;
579                 }
580             }
581
582             this.toolTip.show(tip);
583
584             this.intervalID = window.setInterval(function() {
585                 Roo.bootstrap.Form.popover.unmask();
586             }, 10000);
587
588             window.onwheel = function(){ return false;};
589             
590             (function(){ this.isMasked = true; }).defer(500, this);
591             
592         },
593         
594         unmask : function()
595         {
596             if(!this.isApplied || !this.isMasked || !this.form || !this.target || !this.form.errorMask){
597                 return;
598             }
599             
600             this.maskEl.top.setStyle('position', 'absolute');
601             this.maskEl.top.setSize(0, 0).setXY([0, 0]);
602             this.maskEl.top.hide();
603
604             this.maskEl.left.setStyle('position', 'absolute');
605             this.maskEl.left.setSize(0, 0).setXY([0, 0]);
606             this.maskEl.left.hide();
607
608             this.maskEl.bottom.setStyle('position', 'absolute');
609             this.maskEl.bottom.setSize(0, 0).setXY([0, 0]);
610             this.maskEl.bottom.hide();
611
612             this.maskEl.right.setStyle('position', 'absolute');
613             this.maskEl.right.setSize(0, 0).setXY([0, 0]);
614             this.maskEl.right.hide();
615             
616             this.toolTip.hide();
617             
618             this.toolTip.el.hide();
619             
620             window.onwheel = function(){ return true;};
621             
622             if(this.intervalID){
623                 window.clearInterval(this.intervalID);
624                 this.intervalID = false;
625             }
626             
627             this.isMasked = false;
628             
629         }
630         
631     }
632     
633 });