4904b46af095e4a4127f0af260a9727c65c0b20f
[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             
544             var ret = {};
545             (new FormData(this.el.dom)).entries().forEach(function(pair) {
546                 ret[pair[0]] = pair[1]; // not sure how this will handle duplicates..
547             });
548             return ret;
549         }
550         
551         
552         var fs = Roo.lib.Ajax.serializeForm(this.el.dom);
553         if(asString === true){
554             return fs;
555         }
556         return Roo.urlDecode(fs);
557     },
558     
559     /**
560      * Returns the fields in this form as an object with key/value pairs. 
561      * This differs from getValues as it calls getValue on each child item, rather than using dom data.
562      * @return {Object}
563      */
564     getFieldValues : function(with_hidden)
565     {
566         if (this.childForms) {
567             // copy values from the child forms
568             // should this call getFieldValues - probably not as we do not currently copy
569             // hidden fields when we generate..
570             Roo.each(this.childForms, function (f) {
571                 this.setValues(f.getValues());
572             }, this);
573         }
574         
575         var ret = {};
576         this.items.each(function(f){
577             if (!f.getName()) {
578                 return;
579             }
580             var v = f.getValue();
581             if (f.inputType =='radio') {
582                 if (typeof(ret[f.getName()]) == 'undefined') {
583                     ret[f.getName()] = ''; // empty..
584                 }
585                 
586                 if (!f.el.dom.checked) {
587                     return;
588                     
589                 }
590                 v = f.el.dom.value;
591                 
592             }
593             
594             // not sure if this supported any more..
595             if ((typeof(v) == 'object') && f.getRawValue) {
596                 v = f.getRawValue() ; // dates..
597             }
598             // combo boxes where name != hiddenName...
599             if (f.name != f.getName()) {
600                 ret[f.name] = f.getRawValue();
601             }
602             ret[f.getName()] = v;
603         });
604         
605         return ret;
606     },
607
608     /**
609      * Clears all invalid messages in this form.
610      * @return {BasicForm} this
611      */
612     clearInvalid : function(){
613         this.items.each(function(f){
614            f.clearInvalid();
615         });
616         
617         Roo.each(this.childForms || [], function (f) {
618             f.clearInvalid();
619         });
620         
621         
622         return this;
623     },
624
625     /**
626      * Resets this form.
627      * @return {BasicForm} this
628      */
629     reset : function(){
630         this.items.each(function(f){
631             f.reset();
632         });
633         
634         Roo.each(this.childForms || [], function (f) {
635             f.reset();
636         });
637         this.resetHasChanged();
638         
639         return this;
640     },
641
642     /**
643      * Add Roo.form components to this form.
644      * @param {Field} field1
645      * @param {Field} field2 (optional)
646      * @param {Field} etc (optional)
647      * @return {BasicForm} this
648      */
649     add : function(){
650         this.items.addAll(Array.prototype.slice.call(arguments, 0));
651         return this;
652     },
653
654
655     /**
656      * Removes a field from the items collection (does NOT remove its markup).
657      * @param {Field} field
658      * @return {BasicForm} this
659      */
660     remove : function(field){
661         this.items.remove(field);
662         return this;
663     },
664
665     /**
666      * Looks at the fields in this form, checks them for an id attribute,
667      * and calls applyTo on the existing dom element with that id.
668      * @return {BasicForm} this
669      */
670     render : function(){
671         this.items.each(function(f){
672             if(f.isFormField && !f.rendered && document.getElementById(f.id)){ // if the element exists
673                 f.applyTo(f.id);
674             }
675         });
676         return this;
677     },
678
679     /**
680      * Calls {@link Ext#apply} for all fields in this form with the passed object.
681      * @param {Object} values
682      * @return {BasicForm} this
683      */
684     applyToFields : function(o){
685         this.items.each(function(f){
686            Roo.apply(f, o);
687         });
688         return this;
689     },
690
691     /**
692      * Calls {@link Ext#applyIf} for all field in this form with the passed object.
693      * @param {Object} values
694      * @return {BasicForm} this
695      */
696     applyIfToFields : function(o){
697         this.items.each(function(f){
698            Roo.applyIf(f, o);
699         });
700         return this;
701     }
702 });
703
704 // back compat
705 Roo.BasicForm = Roo.form.BasicForm;
706
707 Roo.apply(Roo.form.BasicForm, {
708     
709     popover : {
710         
711         padding : 5,
712         
713         isApplied : false,
714         
715         isMasked : false,
716         
717         form : false,
718         
719         target : false,
720         
721         intervalID : false,
722         
723         maskEl : false,
724         
725         apply : function()
726         {
727             if(this.isApplied){
728                 return;
729             }
730             
731             this.maskEl = {
732                 top : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-top-mask" }, true),
733                 left : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-left-mask" }, true),
734                 bottom : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-bottom-mask" }, true),
735                 right : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-right-mask" }, true)
736             };
737             
738             this.maskEl.top.enableDisplayMode("block");
739             this.maskEl.left.enableDisplayMode("block");
740             this.maskEl.bottom.enableDisplayMode("block");
741             this.maskEl.right.enableDisplayMode("block");
742             
743             Roo.get(document.body).on('click', function(){
744                 this.unmask();
745             }, this);
746             
747             Roo.get(document.body).on('touchstart', function(){
748                 this.unmask();
749             }, this);
750             
751             this.isApplied = true
752         },
753         
754         mask : function(form, target)
755         {
756             this.form = form;
757             
758             this.target = target;
759             
760             if(!this.form.errorMask || !target.el){
761                 return;
762             }
763             
764             var scrollable = this.target.el.findScrollableParent() || this.target.el.findParent('div.x-layout-active-content', 100, true) || Roo.get(document.body);
765             
766             var ot = this.target.el.calcOffsetsTo(scrollable);
767             
768             var scrollTo = ot[1] - this.form.maskOffset;
769             
770             scrollTo = Math.min(scrollTo, scrollable.dom.scrollHeight);
771             
772             scrollable.scrollTo('top', scrollTo);
773             
774             var el = this.target.wrap || this.target.el;
775             
776             var box = el.getBox();
777             
778             this.maskEl.top.setStyle('position', 'absolute');
779             this.maskEl.top.setStyle('z-index', 10000);
780             this.maskEl.top.setSize(Roo.lib.Dom.getDocumentWidth(), box.y - this.padding);
781             this.maskEl.top.setLeft(0);
782             this.maskEl.top.setTop(0);
783             this.maskEl.top.show();
784             
785             this.maskEl.left.setStyle('position', 'absolute');
786             this.maskEl.left.setStyle('z-index', 10000);
787             this.maskEl.left.setSize(box.x - this.padding, box.height + this.padding * 2);
788             this.maskEl.left.setLeft(0);
789             this.maskEl.left.setTop(box.y - this.padding);
790             this.maskEl.left.show();
791
792             this.maskEl.bottom.setStyle('position', 'absolute');
793             this.maskEl.bottom.setStyle('z-index', 10000);
794             this.maskEl.bottom.setSize(Roo.lib.Dom.getDocumentWidth(), Roo.lib.Dom.getDocumentHeight() - box.bottom - this.padding);
795             this.maskEl.bottom.setLeft(0);
796             this.maskEl.bottom.setTop(box.bottom + this.padding);
797             this.maskEl.bottom.show();
798
799             this.maskEl.right.setStyle('position', 'absolute');
800             this.maskEl.right.setStyle('z-index', 10000);
801             this.maskEl.right.setSize(Roo.lib.Dom.getDocumentWidth() - box.right - this.padding, box.height + this.padding * 2);
802             this.maskEl.right.setLeft(box.right + this.padding);
803             this.maskEl.right.setTop(box.y - this.padding);
804             this.maskEl.right.show();
805
806             this.intervalID = window.setInterval(function() {
807                 Roo.form.BasicForm.popover.unmask();
808             }, 10000);
809
810             window.onwheel = function(){ return false;};
811             
812             (function(){ this.isMasked = true; }).defer(500, this);
813             
814         },
815         
816         unmask : function()
817         {
818             if(!this.isApplied || !this.isMasked || !this.form || !this.target || !this.form.errorMask){
819                 return;
820             }
821             
822             this.maskEl.top.setStyle('position', 'absolute');
823             this.maskEl.top.setSize(0, 0).setXY([0, 0]);
824             this.maskEl.top.hide();
825
826             this.maskEl.left.setStyle('position', 'absolute');
827             this.maskEl.left.setSize(0, 0).setXY([0, 0]);
828             this.maskEl.left.hide();
829
830             this.maskEl.bottom.setStyle('position', 'absolute');
831             this.maskEl.bottom.setSize(0, 0).setXY([0, 0]);
832             this.maskEl.bottom.hide();
833
834             this.maskEl.right.setStyle('position', 'absolute');
835             this.maskEl.right.setSize(0, 0).setXY([0, 0]);
836             this.maskEl.right.hide();
837             
838             window.onwheel = function(){ return true;};
839             
840             if(this.intervalID){
841                 window.clearInterval(this.intervalID);
842                 this.intervalID = false;
843             }
844             
845             this.isMasked = false;
846             
847         }
848         
849     }
850     
851 });