Roo/form/BasicForm.js
[roojs1] / Roo / form / BasicForm.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.BasicForm
14  * @extends Roo.util.Observable
15  * Supplies the functionality to do "actions" on forms and initialize Roo.form.Field types on existing markup.
16  * @constructor
17  * @param {String/HTMLElement/Roo.Element} el The form element or its id
18  * @param {Object} config Configuration options
19  */
20 Roo.form.BasicForm = function(el, config){
21     this.allItems = [];
22     this.childForms = [];
23     Roo.apply(this, config);
24     /*
25      * The Roo.form.Field items in this form.
26      * @type MixedCollection
27      */
28      
29      
30     this.items = new Roo.util.MixedCollection(false, function(o){
31         return o.id || (o.id = Roo.id());
32     });
33     this.addEvents({
34         /**
35          * @event beforeaction
36          * Fires before any action is performed. Return false to cancel the action.
37          * @param {Form} this
38          * @param {Action} action The action to be performed
39          */
40         beforeaction: true,
41         /**
42          * @event actionfailed
43          * Fires when an action fails.
44          * @param {Form} this
45          * @param {Action} action The action that failed
46          */
47         actionfailed : true,
48         /**
49          * @event actioncomplete
50          * Fires when an action is completed.
51          * @param {Form} this
52          * @param {Action} action The action that completed
53          */
54         actioncomplete : true
55     });
56     if(el){
57         this.initEl(el);
58     }
59     Roo.form.BasicForm.superclass.constructor.call(this);
60     
61     Roo.form.BasicForm.popover.apply();
62 };
63
64 Roo.extend(Roo.form.BasicForm, Roo.util.Observable, {
65     /**
66      * @cfg {String} method
67      * The request method to use (GET or POST) for form actions if one isn't supplied in the action options.
68      */
69     /**
70      * @cfg {DataReader} reader
71      * An Roo.data.DataReader (e.g. {@link Roo.data.XmlReader}) to be used to read data when executing "load" actions.
72      * This is optional as there is built-in support for processing JSON.
73      */
74     /**
75      * @cfg {DataReader} errorReader
76      * An Roo.data.DataReader (e.g. {@link Roo.data.XmlReader}) to be used to read data when reading validation errors on "submit" actions.
77      * This is completely optional as there is built-in support for processing JSON.
78      */
79     /**
80      * @cfg {String} url
81      * The URL to use for form actions if one isn't supplied in the action options.
82      */
83     /**
84      * @cfg {Boolean} fileUpload
85      * Set to true if this form is a file upload.
86      */
87      
88     /**
89      * @cfg {Object} baseParams
90      * Parameters to pass with all requests. e.g. baseParams: {id: '123', foo: 'bar'}.
91      */
92      /**
93      
94     /**
95      * @cfg {Number} timeout Timeout for form actions in seconds (default is 30 seconds).
96      */
97     timeout: 30,
98
99     // private
100     activeAction : null,
101
102     /**
103      * @cfg {Boolean} trackResetOnLoad If set to true, form.reset() resets to the last loaded
104      * or setValues() data instead of when the form was first created.
105      */
106     trackResetOnLoad : false,
107     
108     
109     /**
110      * childForms - used for multi-tab forms
111      * @type {Array}
112      */
113     childForms : false,
114     
115     /**
116      * allItems - full list of fields.
117      * @type {Array}
118      */
119     allItems : false,
120     
121     /**
122      * By default wait messages are displayed with Roo.MessageBox.wait. You can target a specific
123      * element by passing it or its id or mask the form itself by passing in true.
124      * @type Mixed
125      */
126     waitMsgTarget : false,
127     
128     /**
129      * @type Boolean
130      */
131     disableMask : false,
132     
133     /**
134      * @cfg {Boolean} errorMask (true|false) default false
135      */
136     errorMask : false,
137     
138     /**
139      * @cfg {Number} maskOffset Default 100
140      */
141     maskOffset : 100,
142
143     // private
144     initEl : function(el){
145         this.el = Roo.get(el);
146         this.id = this.el.id || Roo.id();
147         this.el.on('submit', this.onSubmit, this);
148         this.el.addClass('x-form');
149     },
150
151     // private
152     onSubmit : function(e){
153         e.stopEvent();
154     },
155
156     /**
157      * Returns true if client-side validation on the form is successful.
158      * @return Boolean
159      */
160     isValid : function(){
161         var valid = true;
162         var target = false;
163         this.items.each(function(f){
164             if(f.validate()){
165                 return;
166             }
167             
168             valid = false;
169                 
170             if(!target && f.el.isVisible(true)){
171                 target = f;
172             }
173         });
174         
175         if(this.errorMask && !valid){
176             Roo.form.BasicForm.popover.mask(this, target);
177         }
178         
179         return valid;
180     },
181
182     /**
183      * DEPRICATED Returns true if any fields in this form have changed since their original load. 
184      * @return Boolean
185      */
186     isDirty : function(){
187         var dirty = false;
188         this.items.each(function(f){
189            if(f.isDirty()){
190                dirty = true;
191                return false;
192            }
193         });
194         return dirty;
195     },
196     
197     /**
198      * Returns true if any fields in this form have changed since their original load. (New version)
199      * @return Boolean
200      */
201     
202     hasChanged : function()
203     {
204         var dirty = false;
205         this.items.each(function(f){
206            if(f.hasChanged()){
207                dirty = true;
208                return false;
209            }
210         });
211         return dirty;
212         
213     },
214     /**
215      * Resets all hasChanged to 'false' -
216      * The old 'isDirty' used 'original value..' however this breaks reset() and a few other things.
217      * So hasChanged storage is only to be used for this purpose
218      * @return Boolean
219      */
220     resetHasChanged : function()
221     {
222         this.items.each(function(f){
223            f.resetHasChanged();
224         });
225         
226     },
227     
228     
229     /**
230      * Performs a predefined action (submit or load) or custom actions you define on this form.
231      * @param {String} actionName The name of the action type
232      * @param {Object} options (optional) The options to pass to the action.  All of the config options listed
233      * below are supported by both the submit and load actions unless otherwise noted (custom actions could also
234      * accept other config options):
235      * <pre>
236 Property          Type             Description
237 ----------------  ---------------  ----------------------------------------------------------------------------------
238 url               String           The url for the action (defaults to the form's url)
239 method            String           The form method to use (defaults to the form's method, or POST if not defined)
240 params            String/Object    The params to pass (defaults to the form's baseParams, or none if not defined)
241 clientValidation  Boolean          Applies to submit only.  Pass true to call form.isValid() prior to posting to
242                                    validate the form on the client (defaults to false)
243      * </pre>
244      * @return {BasicForm} this
245      */
246     doAction : function(action, options){
247         if(typeof action == 'string'){
248             action = new Roo.form.Action.ACTION_TYPES[action](this, options);
249         }
250         if(this.fireEvent('beforeaction', this, action) !== false){
251             this.beforeAction(action);
252             action.run.defer(100, action);
253         }
254         return this;
255     },
256
257     /**
258      * Shortcut to do a submit action.
259      * @param {Object} options The options to pass to the action (see {@link #doAction} for details)
260      * @return {BasicForm} this
261      */
262     submit : function(options){
263         this.doAction('submit', options);
264         return this;
265     },
266
267     /**
268      * Shortcut to do a load action.
269      * @param {Object} options The options to pass to the action (see {@link #doAction} for details)
270      * @return {BasicForm} this
271      */
272     load : function(options){
273         this.doAction('load', options);
274         return this;
275     },
276
277     /**
278      * Persists the values in this form into the passed Roo.data.Record object in a beginEdit/endEdit block.
279      * @param {Record} record The record to edit
280      * @return {BasicForm} this
281      */
282     updateRecord : function(record){
283         record.beginEdit();
284         var fs = record.fields;
285         fs.each(function(f){
286             var field = this.findField(f.name);
287             if(field){
288                 record.set(f.name, field.getValue());
289             }
290         }, this);
291         record.endEdit();
292         return this;
293     },
294
295     /**
296      * Loads an Roo.data.Record into this form.
297      * @param {Record} record The record to load
298      * @return {BasicForm} this
299      */
300     loadRecord : function(record){
301         this.setValues(record.data);
302         return this;
303     },
304
305     // private
306     beforeAction : function(action){
307         var o = action.options;
308         
309         if(!this.disableMask) {
310             if(this.waitMsgTarget === true){
311                 this.el.mask(o.waitMsg || "Sending", 'x-mask-loading');
312             }else if(this.waitMsgTarget){
313                 this.waitMsgTarget = Roo.get(this.waitMsgTarget);
314                 this.waitMsgTarget.mask(o.waitMsg || "Sending", 'x-mask-loading');
315             }else {
316                 Roo.MessageBox.wait(o.waitMsg || "Sending", o.waitTitle || this.waitTitle || 'Please Wait...');
317             }
318         }
319         
320          
321     },
322
323     // private
324     afterAction : function(action, success){
325         this.activeAction = null;
326         var o = action.options;
327         
328         if(!this.disableMask) {
329             if(this.waitMsgTarget === true){
330                 this.el.unmask();
331             }else if(this.waitMsgTarget){
332                 this.waitMsgTarget.unmask();
333             }else{
334                 Roo.MessageBox.updateProgress(1);
335                 Roo.MessageBox.hide();
336             }
337         }
338         
339         if(success){
340             if(o.reset){
341                 this.reset();
342             }
343             Roo.callback(o.success, o.scope, [this, action]);
344             this.fireEvent('actioncomplete', this, action);
345             
346         }else{
347             
348             // failure condition..
349             // we have a scenario where updates need confirming.
350             // eg. if a locking scenario exists..
351             // we look for { errors : { needs_confirm : true }} in the response.
352             if (
353                 (typeof(action.result) != 'undefined')  &&
354                 (typeof(action.result.errors) != 'undefined')  &&
355                 (typeof(action.result.errors.needs_confirm) != 'undefined')
356            ){
357                 var _t = this;
358                 Roo.MessageBox.confirm(
359                     "Change requires confirmation",
360                     action.result.errorMsg,
361                     function(r) {
362                         if (r != 'yes') {
363                             return;
364                         }
365                         _t.doAction('submit', { params :  { _submit_confirmed : 1 } }  );
366                     }
367                     
368                 );
369                 
370                 
371                 
372                 return;
373             }
374             
375             Roo.callback(o.failure, o.scope, [this, action]);
376             // show an error message if no failed handler is set..
377             if (!this.hasListener('actionfailed')) {
378                 Roo.MessageBox.alert("Error",
379                     (typeof(action.result) != 'undefined' && typeof(action.result.errorMsg) != 'undefined') ?
380                         action.result.errorMsg :
381                         "Saving Failed, please check your entries or try again"
382                 );
383             }
384             
385             this.fireEvent('actionfailed', this, action);
386         }
387         
388     },
389
390     /**
391      * Find a Roo.form.Field in this form by id, dataIndex, name or hiddenName
392      * @param {String} id The value to search for
393      * @return Field
394      */
395     findField : function(id){
396         var field = this.items.get(id);
397         if(!field){
398             this.items.each(function(f){
399                 if(f.isFormField && (f.dataIndex == id || f.id == id || f.getName() == id)){
400                     field = f;
401                     return false;
402                 }
403             });
404         }
405         return field || null;
406     },
407
408     /**
409      * Add a secondary form to this one, 
410      * Used to provide tabbed forms. One form is primary, with hidden values 
411      * which mirror the elements from the other forms.
412      * 
413      * @param {Roo.form.Form} form to add.
414      * 
415      */
416     addForm : function(form)
417     {
418        
419         if (this.childForms.indexOf(form) > -1) {
420             // already added..
421             return;
422         }
423         this.childForms.push(form);
424         var n = '';
425         Roo.each(form.allItems, function (fe) {
426             
427             n = typeof(fe.getName) == 'undefined' ? fe.name : fe.getName();
428             if (this.findField(n)) { // already added..
429                 return;
430             }
431             var add = new Roo.form.Hidden({
432                 name : n
433             });
434             add.render(this.el);
435             
436             this.add( add );
437         }, this);
438         
439     },
440     /**
441      * Mark fields in this form invalid in bulk.
442      * @param {Array/Object} errors Either an array in the form [{id:'fieldId', msg:'The message'},...] or an object hash of {id: msg, id2: msg2}
443      * @return {BasicForm} this
444      */
445     markInvalid : function(errors){
446         if(errors instanceof Array){
447             for(var i = 0, len = errors.length; i < len; i++){
448                 var fieldError = errors[i];
449                 var f = this.findField(fieldError.id);
450                 if(f){
451                     f.markInvalid(fieldError.msg);
452                 }
453             }
454         }else{
455             var field, id;
456             for(id in errors){
457                 if(typeof errors[id] != 'function' && (field = this.findField(id))){
458                     field.markInvalid(errors[id]);
459                 }
460             }
461         }
462         Roo.each(this.childForms || [], function (f) {
463             f.markInvalid(errors);
464         });
465         
466         return this;
467     },
468
469     /**
470      * Set values for fields in this form in bulk.
471      * @param {Array/Object} values Either an array in the form [{id:'fieldId', value:'foo'},...] or an object hash of {id: value, id2: value2}
472      * @return {BasicForm} this
473      */
474     setValues : function(values){
475         if(values instanceof Array){ // array of objects
476             for(var i = 0, len = values.length; i < len; i++){
477                 var v = values[i];
478                 var f = this.findField(v.id);
479                 if(f){
480                     f.setValue(v.value);
481                     if(this.trackResetOnLoad){
482                         f.originalValue = f.getValue();
483                     }
484                 }
485             }
486         }else{ // object hash
487             var field, id;
488             for(id in values){
489                 if(typeof values[id] != 'function' && (field = this.findField(id))){
490                     
491                     if (field.setFromData && 
492                         field.valueField && 
493                         field.displayField &&
494                         // combos' with local stores can 
495                         // be queried via setValue()
496                         // to set their value..
497                         (field.store && !field.store.isLocal)
498                         ) {
499                         // it's a combo
500                         var sd = { };
501                         sd[field.valueField] = typeof(values[field.hiddenName]) == 'undefined' ? '' : values[field.hiddenName];
502                         sd[field.displayField] = typeof(values[field.name]) == 'undefined' ? '' : values[field.name];
503                         field.setFromData(sd);
504                         
505                     } else {
506                         field.setValue(values[id]);
507                     }
508                     
509                     
510                     if(this.trackResetOnLoad){
511                         field.originalValue = field.getValue();
512                     }
513                 }
514             }
515         }
516         this.resetHasChanged();
517         
518         
519         Roo.each(this.childForms || [], function (f) {
520             f.setValues(values);
521             f.resetHasChanged();
522         });
523                 
524         return this;
525     },
526
527     /**
528      * Returns the fields in this form as an object with key/value pairs. If multiple fields exist with the same name
529      * they are returned as an array.
530      * @param {Boolean} asString
531      * @return {Object}
532      */
533     getValues : function(asString){
534         if (this.childForms) {
535             // copy values from the child forms
536             Roo.each(this.childForms, function (f) {
537                 this.setValues(f.getValues());
538             }, this);
539         }
540         
541         // use formdata
542         if (typeof(FormData) != 'undefined' && asString !== true) {
543             var fd = new FormData(this.el.dom);
544             var ret = {};
545             while (pair = fd.entries().next()) {
546                 if (pair.done) {
547                     break;
548                 }
549                 ret[pair.value[0]] = pair.value[1]; // not sure how this will handle duplicates..
550             };
551             return ret;
552         }
553         
554         
555         var fs = Roo.lib.Ajax.serializeForm(this.el.dom);
556         if(asString === true){
557             return fs;
558         }
559         return Roo.urlDecode(fs);
560     },
561     
562     /**
563      * Returns the fields in this form as an object with key/value pairs. 
564      * This differs from getValues as it calls getValue on each child item, rather than using dom data.
565      * @return {Object}
566      */
567     getFieldValues : function(with_hidden)
568     {
569         if (this.childForms) {
570             // copy values from the child forms
571             // should this call getFieldValues - probably not as we do not currently copy
572             // hidden fields when we generate..
573             Roo.each(this.childForms, function (f) {
574                 this.setValues(f.getValues());
575             }, this);
576         }
577         
578         var ret = {};
579         this.items.each(function(f){
580             if (!f.getName()) {
581                 return;
582             }
583             var v = f.getValue();
584             if (f.inputType =='radio') {
585                 if (typeof(ret[f.getName()]) == 'undefined') {
586                     ret[f.getName()] = ''; // empty..
587                 }
588                 
589                 if (!f.el.dom.checked) {
590                     return;
591                     
592                 }
593                 v = f.el.dom.value;
594                 
595             }
596             
597             // not sure if this supported any more..
598             if ((typeof(v) == 'object') && f.getRawValue) {
599                 v = f.getRawValue() ; // dates..
600             }
601             // combo boxes where name != hiddenName...
602             if (f.name != f.getName()) {
603                 ret[f.name] = f.getRawValue();
604             }
605             ret[f.getName()] = v;
606         });
607         
608         return ret;
609     },
610
611     /**
612      * Clears all invalid messages in this form.
613      * @return {BasicForm} this
614      */
615     clearInvalid : function(){
616         this.items.each(function(f){
617            f.clearInvalid();
618         });
619         
620         Roo.each(this.childForms || [], function (f) {
621             f.clearInvalid();
622         });
623         
624         
625         return this;
626     },
627
628     /**
629      * Resets this form.
630      * @return {BasicForm} this
631      */
632     reset : function(){
633         this.items.each(function(f){
634             f.reset();
635         });
636         
637         Roo.each(this.childForms || [], function (f) {
638             f.reset();
639         });
640         this.resetHasChanged();
641         
642         return this;
643     },
644
645     /**
646      * Add Roo.form components to this form.
647      * @param {Field} field1
648      * @param {Field} field2 (optional)
649      * @param {Field} etc (optional)
650      * @return {BasicForm} this
651      */
652     add : function(){
653         this.items.addAll(Array.prototype.slice.call(arguments, 0));
654         return this;
655     },
656
657
658     /**
659      * Removes a field from the items collection (does NOT remove its markup).
660      * @param {Field} field
661      * @return {BasicForm} this
662      */
663     remove : function(field){
664         this.items.remove(field);
665         return this;
666     },
667
668     /**
669      * Looks at the fields in this form, checks them for an id attribute,
670      * and calls applyTo on the existing dom element with that id.
671      * @return {BasicForm} this
672      */
673     render : function(){
674         this.items.each(function(f){
675             if(f.isFormField && !f.rendered && document.getElementById(f.id)){ // if the element exists
676                 f.applyTo(f.id);
677             }
678         });
679         return this;
680     },
681
682     /**
683      * Calls {@link Ext#apply} for all fields in this form with the passed object.
684      * @param {Object} values
685      * @return {BasicForm} this
686      */
687     applyToFields : function(o){
688         this.items.each(function(f){
689            Roo.apply(f, o);
690         });
691         return this;
692     },
693
694     /**
695      * Calls {@link Ext#applyIf} for all field in this form with the passed object.
696      * @param {Object} values
697      * @return {BasicForm} this
698      */
699     applyIfToFields : function(o){
700         this.items.each(function(f){
701            Roo.applyIf(f, o);
702         });
703         return this;
704     }
705 });
706
707 // back compat
708 Roo.BasicForm = Roo.form.BasicForm;
709
710 Roo.apply(Roo.form.BasicForm, {
711     
712     popover : {
713         
714         padding : 5,
715         
716         isApplied : false,
717         
718         isMasked : false,
719         
720         form : false,
721         
722         target : false,
723         
724         intervalID : false,
725         
726         maskEl : false,
727         
728         apply : function()
729         {
730             if(this.isApplied){
731                 return;
732             }
733             
734             this.maskEl = {
735                 top : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-top-mask" }, true),
736                 left : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-left-mask" }, true),
737                 bottom : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-bottom-mask" }, true),
738                 right : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-right-mask" }, true)
739             };
740             
741             this.maskEl.top.enableDisplayMode("block");
742             this.maskEl.left.enableDisplayMode("block");
743             this.maskEl.bottom.enableDisplayMode("block");
744             this.maskEl.right.enableDisplayMode("block");
745             
746             Roo.get(document.body).on('click', function(){
747                 this.unmask();
748             }, this);
749             
750             Roo.get(document.body).on('touchstart', function(){
751                 this.unmask();
752             }, this);
753             
754             this.isApplied = true
755         },
756         
757         mask : function(form, target)
758         {
759             this.form = form;
760             
761             this.target = target;
762             
763             if(!this.form.errorMask || !target.el){
764                 return;
765             }
766             
767             var scrollable = this.target.el.findScrollableParent() || this.target.el.findParent('div.x-layout-active-content', 100, true) || Roo.get(document.body);
768             
769             var ot = this.target.el.calcOffsetsTo(scrollable);
770             
771             var scrollTo = ot[1] - this.form.maskOffset;
772             
773             scrollTo = Math.min(scrollTo, scrollable.dom.scrollHeight);
774             
775             scrollable.scrollTo('top', scrollTo);
776             
777             var el = this.target.wrap || this.target.el;
778             
779             var box = el.getBox();
780             
781             this.maskEl.top.setStyle('position', 'absolute');
782             this.maskEl.top.setStyle('z-index', 10000);
783             this.maskEl.top.setSize(Roo.lib.Dom.getDocumentWidth(), box.y - this.padding);
784             this.maskEl.top.setLeft(0);
785             this.maskEl.top.setTop(0);
786             this.maskEl.top.show();
787             
788             this.maskEl.left.setStyle('position', 'absolute');
789             this.maskEl.left.setStyle('z-index', 10000);
790             this.maskEl.left.setSize(box.x - this.padding, box.height + this.padding * 2);
791             this.maskEl.left.setLeft(0);
792             this.maskEl.left.setTop(box.y - this.padding);
793             this.maskEl.left.show();
794
795             this.maskEl.bottom.setStyle('position', 'absolute');
796             this.maskEl.bottom.setStyle('z-index', 10000);
797             this.maskEl.bottom.setSize(Roo.lib.Dom.getDocumentWidth(), Roo.lib.Dom.getDocumentHeight() - box.bottom - this.padding);
798             this.maskEl.bottom.setLeft(0);
799             this.maskEl.bottom.setTop(box.bottom + this.padding);
800             this.maskEl.bottom.show();
801
802             this.maskEl.right.setStyle('position', 'absolute');
803             this.maskEl.right.setStyle('z-index', 10000);
804             this.maskEl.right.setSize(Roo.lib.Dom.getDocumentWidth() - box.right - this.padding, box.height + this.padding * 2);
805             this.maskEl.right.setLeft(box.right + this.padding);
806             this.maskEl.right.setTop(box.y - this.padding);
807             this.maskEl.right.show();
808
809             this.intervalID = window.setInterval(function() {
810                 Roo.form.BasicForm.popover.unmask();
811             }, 10000);
812
813             window.onwheel = function(){ return false;};
814             
815             (function(){ this.isMasked = true; }).defer(500, this);
816             
817         },
818         
819         unmask : function()
820         {
821             if(!this.isApplied || !this.isMasked || !this.form || !this.target || !this.form.errorMask){
822                 return;
823             }
824             
825             this.maskEl.top.setStyle('position', 'absolute');
826             this.maskEl.top.setSize(0, 0).setXY([0, 0]);
827             this.maskEl.top.hide();
828
829             this.maskEl.left.setStyle('position', 'absolute');
830             this.maskEl.left.setSize(0, 0).setXY([0, 0]);
831             this.maskEl.left.hide();
832
833             this.maskEl.bottom.setStyle('position', 'absolute');
834             this.maskEl.bottom.setSize(0, 0).setXY([0, 0]);
835             this.maskEl.bottom.hide();
836
837             this.maskEl.right.setStyle('position', 'absolute');
838             this.maskEl.right.setSize(0, 0).setXY([0, 0]);
839             this.maskEl.right.hide();
840             
841             window.onwheel = function(){ return true;};
842             
843             if(this.intervalID){
844                 window.clearInterval(this.intervalID);
845                 this.intervalID = false;
846             }
847             
848             this.isMasked = false;
849             
850         }
851         
852     }
853     
854 });