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