4 * Copyright(c) 2006-2007, Ext JS, LLC.
6 * Originally Released Under LGPL - original licence link has changed is not relivant.
9 * <script type="text/javascript">
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.
17 * @param {String/HTMLElement/Roo.Element} el The form element or its id
18 * @param {Object} config Configuration options
20 Roo.form.BasicForm = function(el, config){
23 Roo.apply(this, config);
25 * The Roo.form.Field items in this form.
26 * @type MixedCollection
30 this.items = new Roo.util.MixedCollection(false, function(o){
31 return o.id || (o.id = Roo.id());
36 * Fires before any action is performed. Return false to cancel the action.
38 * @param {Action} action The action to be performed
43 * Fires when an action fails.
45 * @param {Action} action The action that failed
49 * @event actioncomplete
50 * Fires when an action is completed.
52 * @param {Action} action The action that completed
59 Roo.form.BasicForm.superclass.constructor.call(this);
61 Roo.form.BasicForm.popover.apply();
64 Roo.extend(Roo.form.BasicForm, Roo.util.Observable, {
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.
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.
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.
81 * The URL to use for form actions if one isn't supplied in the action options.
84 * @cfg {Boolean} fileUpload
85 * Set to true if this form is a file upload.
89 * @cfg {Object} baseParams
90 * Parameters to pass with all requests. e.g. baseParams: {id: '123', foo: 'bar'}.
95 * @cfg {Number} timeout Timeout for form actions in seconds (default is 30 seconds).
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.
106 trackResetOnLoad : false,
110 * childForms - used for multi-tab forms
116 * allItems - full list of fields.
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.
126 waitMsgTarget : false,
134 * @cfg {Boolean} errorMask (true|false) default false
139 * @cfg {Number} maskOffset Default 100
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');
152 onSubmit : function(e){
157 * Returns true if client-side validation on the form is successful.
160 isValid : function(){
163 this.items.each(function(f){
170 if(!target && f.el.isVisible(true)){
175 if(this.errorMask && !valid){
176 Roo.form.BasicForm.popover.mask(this, target);
182 * Returns array of invalid form fields.
186 invalidFields : function()
189 this.items.each(function(f){
202 * DEPRICATED Returns true if any fields in this form have changed since their original load.
205 isDirty : function(){
207 this.items.each(function(f){
217 * Returns true if any fields in this form have changed since their original load. (New version)
221 hasChanged : function()
224 this.items.each(function(f){
234 * Resets all hasChanged to 'false' -
235 * The old 'isDirty' used 'original value..' however this breaks reset() and a few other things.
236 * So hasChanged storage is only to be used for this purpose
239 resetHasChanged : function()
241 this.items.each(function(f){
249 * Performs a predefined action (submit or load) or custom actions you define on this form.
250 * @param {String} actionName The name of the action type
251 * @param {Object} options (optional) The options to pass to the action. All of the config options listed
252 * below are supported by both the submit and load actions unless otherwise noted (custom actions could also
253 * accept other config options):
255 Property Type Description
256 ---------------- --------------- ----------------------------------------------------------------------------------
257 url String The url for the action (defaults to the form's url)
258 method String The form method to use (defaults to the form's method, or POST if not defined)
259 params String/Object The params to pass (defaults to the form's baseParams, or none if not defined)
260 clientValidation Boolean Applies to submit only. Pass true to call form.isValid() prior to posting to
261 validate the form on the client (defaults to false)
263 * @return {BasicForm} this
265 doAction : function(action, options){
266 if(typeof action == 'string'){
267 action = new Roo.form.Action.ACTION_TYPES[action](this, options);
269 if(this.fireEvent('beforeaction', this, action) !== false){
270 this.beforeAction(action);
271 action.run.defer(100, action);
277 * Shortcut to do a submit action.
278 * @param {Object} options The options to pass to the action (see {@link #doAction} for details)
279 * @return {BasicForm} this
281 submit : function(options){
282 this.doAction('submit', options);
287 * Shortcut to do a load action.
288 * @param {Object} options The options to pass to the action (see {@link #doAction} for details)
289 * @return {BasicForm} this
291 load : function(options){
292 this.doAction('load', options);
297 * Persists the values in this form into the passed Roo.data.Record object in a beginEdit/endEdit block.
298 * @param {Record} record The record to edit
299 * @return {BasicForm} this
301 updateRecord : function(record){
303 var fs = record.fields;
305 var field = this.findField(f.name);
307 record.set(f.name, field.getValue());
315 * Loads an Roo.data.Record into this form.
316 * @param {Record} record The record to load
317 * @return {BasicForm} this
319 loadRecord : function(record){
320 this.setValues(record.data);
325 beforeAction : function(action){
326 var o = action.options;
328 if(!this.disableMask) {
329 if(this.waitMsgTarget === true){
330 this.el.mask(o.waitMsg || "Sending", 'x-mask-loading');
331 }else if(this.waitMsgTarget){
332 this.waitMsgTarget = Roo.get(this.waitMsgTarget);
333 this.waitMsgTarget.mask(o.waitMsg || "Sending", 'x-mask-loading');
335 Roo.MessageBox.wait(o.waitMsg || "Sending", o.waitTitle || this.waitTitle || 'Please Wait...');
343 afterAction : function(action, success){
344 this.activeAction = null;
345 var o = action.options;
347 if(!this.disableMask) {
348 if(this.waitMsgTarget === true){
350 }else if(this.waitMsgTarget){
351 this.waitMsgTarget.unmask();
353 Roo.MessageBox.updateProgress(1);
354 Roo.MessageBox.hide();
362 Roo.callback(o.success, o.scope, [this, action]);
363 this.fireEvent('actioncomplete', this, action);
367 // failure condition..
368 // we have a scenario where updates need confirming.
369 // eg. if a locking scenario exists..
370 // we look for { errors : { needs_confirm : true }} in the response.
372 (typeof(action.result) != 'undefined') &&
373 (typeof(action.result.errors) != 'undefined') &&
374 (typeof(action.result.errors.needs_confirm) != 'undefined')
377 Roo.MessageBox.confirm(
378 "Change requires confirmation",
379 action.result.errorMsg,
384 _t.doAction('submit', { params : { _submit_confirmed : 1 } } );
394 Roo.callback(o.failure, o.scope, [this, action]);
395 // show an error message if no failed handler is set..
396 if (!this.hasListener('actionfailed')) {
397 Roo.MessageBox.alert("Error",
398 (typeof(action.result) != 'undefined' && typeof(action.result.errorMsg) != 'undefined') ?
399 action.result.errorMsg :
400 "Saving Failed, please check your entries or try again"
404 this.fireEvent('actionfailed', this, action);
410 * Find a Roo.form.Field in this form by id, dataIndex, name or hiddenName
411 * @param {String} id The value to search for
414 findField : function(id){
415 var field = this.items.get(id);
417 this.items.each(function(f){
418 if(f.isFormField && (f.dataIndex == id || f.id == id || f.getName() == id)){
424 return field || null;
428 * Add a secondary form to this one,
429 * Used to provide tabbed forms. One form is primary, with hidden values
430 * which mirror the elements from the other forms.
432 * @param {Roo.form.Form} form to add.
435 addForm : function(form)
438 if (this.childForms.indexOf(form) > -1) {
442 this.childForms.push(form);
444 Roo.each(form.allItems, function (fe) {
446 n = typeof(fe.getName) == 'undefined' ? fe.name : fe.getName();
447 if (this.findField(n)) { // already added..
450 var add = new Roo.form.Hidden({
460 * Mark fields in this form invalid in bulk.
461 * @param {Array/Object} errors Either an array in the form [{id:'fieldId', msg:'The message'},...] or an object hash of {id: msg, id2: msg2}
462 * @return {BasicForm} this
464 markInvalid : function(errors){
465 if(errors instanceof Array){
466 for(var i = 0, len = errors.length; i < len; i++){
467 var fieldError = errors[i];
468 var f = this.findField(fieldError.id);
470 f.markInvalid(fieldError.msg);
476 if(typeof errors[id] != 'function' && (field = this.findField(id))){
477 field.markInvalid(errors[id]);
481 Roo.each(this.childForms || [], function (f) {
482 f.markInvalid(errors);
489 * Set values for fields in this form in bulk.
490 * @param {Array/Object} values Either an array in the form [{id:'fieldId', value:'foo'},...] or an object hash of {id: value, id2: value2}
491 * @return {BasicForm} this
493 setValues : function(values){
494 if(values instanceof Array){ // array of objects
495 for(var i = 0, len = values.length; i < len; i++){
497 var f = this.findField(v.id);
500 if(this.trackResetOnLoad){
501 f.originalValue = f.getValue();
505 }else{ // object hash
508 if(typeof values[id] != 'function' && (field = this.findField(id))){
510 if (field.setFromData &&
512 field.displayField &&
513 // combos' with local stores can
514 // be queried via setValue()
515 // to set their value..
516 (field.store && !field.store.isLocal)
520 sd[field.valueField] = typeof(values[field.hiddenName]) == 'undefined' ? '' : values[field.hiddenName];
521 sd[field.displayField] = typeof(values[field.name]) == 'undefined' ? '' : values[field.name];
522 field.setFromData(sd);
525 field.setValue(values[id]);
529 if(this.trackResetOnLoad){
530 field.originalValue = field.getValue();
535 this.resetHasChanged();
538 Roo.each(this.childForms || [], function (f) {
547 * Returns the fields in this form as an object with key/value pairs. If multiple fields exist with the same name
548 * they are returned as an array.
549 * @param {Boolean} asString
552 getValues : function(asString)
554 if (this.childForms) {
555 // copy values from the child forms
556 Roo.each(this.childForms, function (f) {
557 this.setValues(f.getFieldValues()); // get the full set of data, as we might be copying comboboxes from external into this one.
562 if (typeof(FormData) != 'undefined' && asString !== true) {
563 // this relies on a 'recent' version of chrome apparently...
565 var fd = (new FormData(this.el.dom)).entries();
569 ret[ent.value[0]] = ent.value[1]; // not sure how this will handle duplicates..
580 var fs = Roo.lib.Ajax.serializeForm(this.el.dom);
581 if(asString === true){
584 return Roo.urlDecode(fs);
588 * Returns the fields in this form as an object with key/value pairs.
589 * This differs from getValues as it calls getValue on each child item, rather than using dom data.
590 * Normally this will not return readOnly data
591 * @param {Boolean} with_readonly return readonly field data.
594 getFieldValues : function(with_readonly)
596 if (this.childForms) {
597 // copy values from the child forms
598 // should this call getFieldValues - probably not as we do not currently copy
599 // hidden fields when we generate..
600 Roo.each(this.childForms, function (f) {
601 this.setValues(f.getFieldValues());
606 this.items.each(function(f){
608 if (f.readOnly && with_readonly !== true) {
609 return; // skip read only values. - this is in theory to stop 'old' values being copied over new ones
610 // if a subform contains a copy of them.
611 // if you have subforms with the same editable data, you will need to copy the data back
618 var v = f.getValue();
619 if (f.inputType =='radio') {
620 if (typeof(ret[f.getName()]) == 'undefined') {
621 ret[f.getName()] = ''; // empty..
624 if (!f.el.dom.checked) {
632 // not sure if this supported any more..
633 if ((typeof(v) == 'object') && f.getRawValue) {
634 v = f.getRawValue() ; // dates..
636 // combo boxes where name != hiddenName...
637 if (f.name != f.getName()) {
638 ret[f.name] = f.getRawValue();
640 ret[f.getName()] = v;
647 * Clears all invalid messages in this form.
648 * @return {BasicForm} this
650 clearInvalid : function(){
651 this.items.each(function(f){
655 Roo.each(this.childForms || [], function (f) {
665 * @return {BasicForm} this
668 this.items.each(function(f){
672 Roo.each(this.childForms || [], function (f) {
675 this.resetHasChanged();
681 * Add Roo.form components to this form.
682 * @param {Field} field1
683 * @param {Field} field2 (optional)
684 * @param {Field} etc (optional)
685 * @return {BasicForm} this
688 this.items.addAll(Array.prototype.slice.call(arguments, 0));
694 * Removes a field from the items collection (does NOT remove its markup).
695 * @param {Field} field
696 * @return {BasicForm} this
698 remove : function(field){
699 this.items.remove(field);
704 * Looks at the fields in this form, checks them for an id attribute,
705 * and calls applyTo on the existing dom element with that id.
706 * @return {BasicForm} this
709 this.items.each(function(f){
710 if(f.isFormField && !f.rendered && document.getElementById(f.id)){ // if the element exists
718 * Calls {@link Ext#apply} for all fields in this form with the passed object.
719 * @param {Object} values
720 * @return {BasicForm} this
722 applyToFields : function(o){
723 this.items.each(function(f){
730 * Calls {@link Ext#applyIf} for all field in this form with the passed object.
731 * @param {Object} values
732 * @return {BasicForm} this
734 applyIfToFields : function(o){
735 this.items.each(function(f){
743 Roo.BasicForm = Roo.form.BasicForm;
745 Roo.apply(Roo.form.BasicForm, {
770 top : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-top-mask" }, true),
771 left : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-left-mask" }, true),
772 bottom : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-bottom-mask" }, true),
773 right : Roo.DomHelper.append(Roo.get(document.body), { tag: "div", cls:"x-dlg-mask roo-form-right-mask" }, true)
776 this.maskEl.top.enableDisplayMode("block");
777 this.maskEl.left.enableDisplayMode("block");
778 this.maskEl.bottom.enableDisplayMode("block");
779 this.maskEl.right.enableDisplayMode("block");
781 Roo.get(document.body).on('click', function(){
785 Roo.get(document.body).on('touchstart', function(){
789 this.isApplied = true
792 mask : function(form, target)
796 this.target = target;
798 if(!this.form.errorMask || !target.el){
802 var scrollable = this.target.el.findScrollableParent() || this.target.el.findParent('div.x-layout-active-content', 100, true) || Roo.get(document.body);
804 var ot = this.target.el.calcOffsetsTo(scrollable);
806 var scrollTo = ot[1] - this.form.maskOffset;
808 scrollTo = Math.min(scrollTo, scrollable.dom.scrollHeight);
810 scrollable.scrollTo('top', scrollTo);
812 var el = this.target.wrap || this.target.el;
814 var box = el.getBox();
816 this.maskEl.top.setStyle('position', 'absolute');
817 this.maskEl.top.setStyle('z-index', 10000);
818 this.maskEl.top.setSize(Roo.lib.Dom.getDocumentWidth(), box.y - this.padding);
819 this.maskEl.top.setLeft(0);
820 this.maskEl.top.setTop(0);
821 this.maskEl.top.show();
823 this.maskEl.left.setStyle('position', 'absolute');
824 this.maskEl.left.setStyle('z-index', 10000);
825 this.maskEl.left.setSize(box.x - this.padding, box.height + this.padding * 2);
826 this.maskEl.left.setLeft(0);
827 this.maskEl.left.setTop(box.y - this.padding);
828 this.maskEl.left.show();
830 this.maskEl.bottom.setStyle('position', 'absolute');
831 this.maskEl.bottom.setStyle('z-index', 10000);
832 this.maskEl.bottom.setSize(Roo.lib.Dom.getDocumentWidth(), Roo.lib.Dom.getDocumentHeight() - box.bottom - this.padding);
833 this.maskEl.bottom.setLeft(0);
834 this.maskEl.bottom.setTop(box.bottom + this.padding);
835 this.maskEl.bottom.show();
837 this.maskEl.right.setStyle('position', 'absolute');
838 this.maskEl.right.setStyle('z-index', 10000);
839 this.maskEl.right.setSize(Roo.lib.Dom.getDocumentWidth() - box.right - this.padding, box.height + this.padding * 2);
840 this.maskEl.right.setLeft(box.right + this.padding);
841 this.maskEl.right.setTop(box.y - this.padding);
842 this.maskEl.right.show();
844 this.intervalID = window.setInterval(function() {
845 Roo.form.BasicForm.popover.unmask();
848 window.onwheel = function(){ return false;};
850 (function(){ this.isMasked = true; }).defer(500, this);
856 if(!this.isApplied || !this.isMasked || !this.form || !this.target || !this.form.errorMask){
860 this.maskEl.top.setStyle('position', 'absolute');
861 this.maskEl.top.setSize(0, 0).setXY([0, 0]);
862 this.maskEl.top.hide();
864 this.maskEl.left.setStyle('position', 'absolute');
865 this.maskEl.left.setSize(0, 0).setXY([0, 0]);
866 this.maskEl.left.hide();
868 this.maskEl.bottom.setStyle('position', 'absolute');
869 this.maskEl.bottom.setSize(0, 0).setXY([0, 0]);
870 this.maskEl.bottom.hide();
872 this.maskEl.right.setStyle('position', 'absolute');
873 this.maskEl.right.setSize(0, 0).setXY([0, 0]);
874 this.maskEl.right.hide();
876 window.onwheel = function(){ return true;};
879 window.clearInterval(this.intervalID);
880 this.intervalID = false;
883 this.isMasked = false;