      1 /*
      2  *  Copyright (C) 2008-2009 WaveMaker Software, Inc.
      3  *
      4  *  This file is part of the WaveMaker Client Runtime.
      5  *
      6  *  Licensed under the Apache License, Version 2.0 (the "License");
      7  *  you may not use this file except in compliance with the License.
      8  *  You may obtain a copy of the License at
      9  *
     10  *      http://www.apache.org/licenses/LICENSE-2.0
     11  *
     12  *  Unless required by applicable law or agreed to in writing, software
     13  *  distributed under the License is distributed on an "AS IS" BASIS,
     14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15  *  See the License for the specific language governing permissions and
     16  *  limitations under the License.
     17  */
     18 dojo.provide("wm.base.widget.LiveForm");
     19 dojo.require('wm.base.lib.data');
     20
     21 wm.getLiveForms = function(inPage) {
     22 	var forms = [];
     23 	wm.forEachWidget(inPage.root, function(w) {
     24 		if (w instanceof wm.LiveForm)
     25 			forms.push(w);
     26 	})
     27 	return forms;
     28 }
     29
     30 wm.getMatchingFormWidgets = function(inForm, inMatch) {
     31 	var match = [];
     32 	wm.forEach(inForm.widgets, function(w) {
     33 			if (inMatch(w))
     34 				match.push(w);
     35 			if ((w instanceof wm.Container) && !(w instanceof wm.LiveFormBase))
     36 				match = match.concat(wm.getMatchingFormWidgets(w, inMatch));
     37 		});
     38 		return match;
     39 };
     40
     41 wm.getParentForm = function(inWidget) {
     42 	var w = inWidget.parent, r = inWidget.getRoot(), r = r && r.root;
     43 	while (w && w != r) {
     44 		if (w instanceof wm.LiveFormBase)
     45 			return w;
     46 		w = w.parent;
     47 	}
     48 }
     49
     50 wm.getFormLiveView = function(inForm) {
     51 	var lv = inForm && inForm.getLiveVariable();
     52 	return lv && lv.liveView;
     53 }
     54
     55 wm.getFormField = function(inWidget) {
     56 	var a = [], w = inWidget;
     57 	while (w && !(w instanceof wm.LiveForm)) {
     58 		if (w.formField)
     59 			a.unshift(w.formField);
     60 		w = wm.getParentForm(w);
     61 	}
     62 	return a.join('.');
     63 }
     64
     65 wm.focusContainer = function(inContainer) {
     66 	wm.onidle(function() {
     67 		wm.forEachWidget(inContainer, function(w) {
     68 			if (w.focus && (!w.canFocus || w.canFocus())) {
     69 				w.focus();
     70 				return false;
     71 			}
     72 		});
     73 	});
     74 }
     75
     76 /**
     77 	Base class for {@link wm.LiveForm}.
     78 	@name wm.LiveFormBase
     79 	@class
     80 	@extends wm.Panel
     81 	@noindex
     82 */
     83 dojo.declare("wm.LiveFormBase", wm.Panel, {
     84 	/** @lends wm.LiveFormBase.prototype */
     85 	editorHeight: "26px",
     86 	editorWidth: "100%",
     87 	captionSize: "50%",
     88 	captionAlign: "right",
     89 	captionPosition: "left",
     90 	height: "228px",
     91 	width: "100%",
     92 	//fitToContent: true,
     93 	layoutKind: "top-to-bottom",
     94 	readonly: false,
     95 	/**
     96 		The dataSet the LiveForm uses for source data.
     97
     98 		Typically dataSet is bound to a grid's <i>selectedItem</i> (where the grid is showing the output of a liveVariable) or directly to a liveVariable.
     99
    100 		When dataSet uses a LiveView, LiveForm generates editors for each of the fields in its related liveView.
    101 		When dataSet uses a LiveTable, a default view is created which contains all top level properties in the data type (not including composite key fields).
    102
    103 		Although it is possible to setup a LiveForm to do automatic CRUD operations when using a LiveTable, it's easiest
    104 		to set up a LiveView instead.
    105
    106 		@type Variable
    107 	*/
    108 	dataSet: null,
    109 	/**
    110 		Data as modified by the form editors is available as <i>dataOutput</i>.
    111 		@type Variable
    112 	*/
    113 	dataOutput: null,
    114 	init: function() {
    115 		this.dataOutput = new wm.Variable({name: "dataOutput", owner: this});
    116 		this.inherited(arguments);
    117 	},
    118 	postInit: function() {
    119 		this.inherited(arguments);
    120 		this.dataOutput = this.$.dataOutput;
    121 		if (wm.pasting)
    122 			wm.fire(this, "designPasted");
    123 		this.populateEditors();
    124 	},
    125 	//===========================================================================
    126 	// Form data
    127 	//===========================================================================
    128 	setDataSet: function(inDataSet) {
    129 		this.beginEditUpdate();
    130 		this.dataSet = inDataSet;
    131 		var d = this.getItemData();
    132 		this.populateEditors();
    133 		this.setDataOutput(d);
    134 		this.endEditUpdate();
    135 	},
    136 	setDataOutput: function(inDataSet) {
    137 		this.dataOutput.setDataSet(inDataSet);
    138 	},
    139 	clearDataOutput: function() {
    140 		// FIXME: handle related editors specially
    141 		dojo.forEach(this.getRelatedEditorsArray(), function(e) {
    142 				e.clearDataOutput();
    143 		});
    144 		this.dataOutput.setData({});
    145 	},
    146 	getItemData: function() {
    147 		return wm.fire(this.dataSet, "getCursorItem");
    148 	},
    149 	// FIXME: store this explicitly?
    150 	_getDataType: function() {
    151 		var t = (this.dataSet || 0).type;
    152 		if (!wm.typeManager.isStructuredType(t)) {
    153 			var v = this.getLiveVariable();
    154 			t = v && v.type;
    155 		}
    156 		if (wm.typeManager.isStructuredType(t))
    157 			return t;
    158 	},
    159 	// get the liveVariable related to this dataSet
    160 	// currently just checks if dataSet is a liveVariable
    161 	// currently does not check for subNards that may be part of a dataSet
    162 	getLiveVariable: function() {
    163 		var
    164 			s = this.dataSet,
    165 			o = s && s.owner;
    166 		o = o && !(o instanceof wm.Variable) ? o : null;
    167 		// if source not owned by a variable but it has a dataSet, use it if it's a LiveVariable
    168 		if (o && o.dataSet && o.dataSet instanceof wm.LiveVariable)
    169 			return o.dataSet;
    170 		// otherwise walk owners to look for a LiveVariable
    171 		while (s) {
    172 			if (s instanceof wm.LiveVariable)
    173 				return s;
    174 			s = s.owner;
    175 			if (!(s.owner instanceof wm.Variable))
    176 				break;
    177 		}
    178 	},
    179 	beginEditUpdate: function() {
    180 		this.dataOutput.beginUpdate();
    181 		dojo.forEach(this.getFormEditorsArray(), function(e) {
    182 			wm.fire(e, "beginEditUpdate");
    183 		});
    184 	},
    185 	endEditUpdate: function() {
    186 		this.dataOutput.endUpdate();
    187 		dojo.forEach(this.getFormEditorsArray(), function(e) {
    188 			wm.fire(e, "endEditUpdate");
    189 		});
    190 	},
    191 	//===========================================================================
    192 	// Editor management
    193 	//===========================================================================
    194 	populateEditors: function() {
    195 		var i = this.getItemData(), data = i ? i.getData() : null;
    196 		dojo.forEach(this.getFormEditorsArray(), function(e) {
    197 			if (e instanceof wm.LiveFormBase)
    198 				wm.fire(e, "populateEditors");
    199 			else {
    200 				wm.fire(e, "setDataValue", [e.formField && data ? data[e.formField] : data]);
    201 			}
    202 		});
    203 	},
    204 	populateDataOutput: function() {
    205 		var d = this.dataOutput;
    206 		dojo.forEach(this.getFormEditorsArray(), function(e) {
    207 			if (e instanceof wm.LiveFormBase)
    208 				wm.fire(e, "populateDataOutput");
    209 			else if (e.formField)
    210 				d.setValue(e.formField, e.getDataValue());
    211 		});
    212 	},
    213 	editStarting: function() {
    214 		dojo.forEach(this.getFormEditorsArray(), function(e) {
    215 			wm.fire(e, "editStarting");
    216 		});
    217 	},
    218 	editCancelling: function() {
    219 		dojo.forEach(this.getFormEditorsArray(), function(e) {
    220 			wm.fire(e, "editCancelling");
    221 		});
    222 	},
    223 	/**
    224 		Clear all editors.
    225 		As usual, the data clear is a change propagated via
    226 		bindings. So, typically, <i>dataOutput</i> is cleared too.
    227 	*/
    228 	clearData: function() {
    229 		dojo.forEach(this.getFormEditorsArray(), function(e) {
    230 			wm.fire(e, "clear");
    231 		});
    232 		// FIXME: handle related editors specially
    233 		dojo.forEach(this.getRelatedEditorsArray(), function(e) {
    234 			wm.fire(e, "clearData");
    235 		});
    236 	},
    237 	getEditorsArray: function() {
    238 		return wm.getMatchingFormWidgets(this, function(w) {
    239 			return (w instanceof wm.Editor || w.constructor.superclass.declaredClass == "wm.Editor");
    240 		});
    241 	},
    242 	// FIXME: handle related editors specially
    243 	getRelatedEditorsArray: function(inContainer) {
    244 		return wm.getMatchingFormWidgets(this, function(w) {
    245 			return (w instanceof wm.RelatedEditor);
    246 		});
    247 	},
    248 	getFormEditorsArray: function() {
    249 		return wm.getMatchingFormWidgets(this, function(w) {
    250 			//return ((w instanceof wm.Editor && w.formField !== undefined) || w instanceof wm.RelatedEditor);
    251 			return (w.formField !== undefined);
    252 		});
    253 	},
    254 	_getEditorBindSource: function(inSourceId) {
    255 		var parts = (inSourceId || "").split(".");
    256 		parts.pop();
    257 		var
    258 			s = parts.join('.'),
    259 			v = this.getValueById(s);
    260 		if (v instanceof wm.Editor || v instanceof wm.RelatedEditor)
    261 			return v;
    262 	},
    263 	// get editors bound to dataOutput
    264 	getBoundEditorsArray: function() {
    265 		var editors = [];
    266 		// get editors bound to dataOutput
    267 		var wires = this.$.binding.wires;
    268 		for (var i in wires) {
    269 			w = wires[i];
    270 			if (!w.targetId && w.targetProperty.indexOf("dataOutput") == 0) {
    271 				e = this._getEditorBindSource(w.source);
    272 				if (e)
    273 					editors.push(e);
    274 			}
    275 		}
    276 		return editors;
    277 	},
    278 	//===========================================================================
    279 	// Editor setters
    280 	//===========================================================================
    281 	canChangeEditorReadonly: function(inEditor, inReadonly, inCanChangeFunc) {
    282 		var c = dojo.isFunction(inCanChangeFunc);
    283 		return !c || inCanChangeFunc(inEditor, this, inReadonly);
    284 	},
    285 	_setReadonly: function(inReadonly, inCanChangeFunc) {
    286 		dojo.forEach(this.getFormEditorsArray(), function(e) {
    287 			if (this.canChangeEditorReadonly(e, inReadonly, inCanChangeFunc))
    288 				e.setReadonly(inReadonly);
    289 		}, this);
    290 		// FIXME: handle related editors specially
    291 		dojo.forEach(this.getRelatedEditorsArray(), function(e) {
    292 			if (this.canChangeEditorReadonly(e, inReadonly, inCanChangeFunc))
    293 				e._setReadonly(inReadonly, inCanChangeFunc);
    294 		}, this);
    295 	},
    296 	setReadonly: function(inReadonly) {
    297 		this.readonly = inReadonly;
    298 		this._setReadonly(inReadonly);
    299 	},
    300 	setCaptionSize: function(inSize) {
    301 		this.captionSize = inSize;
    302 		dojo.forEach(this.getEditorsArray(), function(e) {
    303 			e.setCaptionSize(inSize);
    304 		});
    305 	},
    306 	setCaptionUnits: function(inUnits) {
    307 		this.captionUnits = inUnits;
    308 		dojo.forEach(this.getEditorsArray(), function(e) {
    309 			e.setCaptionUnits(inUnits);
    310 		});
    311 	},
    312 	setCaptionAlign: function(inAlign) {
    313 		this.captionAlign = inAlign;
    314 		dojo.forEach(this.getEditorsArray(), function(e) {
    315 			e.setCaptionAlign(inAlign);
    316 		});
    317 	},
    318 	setCaptionPosition: function(inPosition) {
    319 		this.captionPosition = inPosition;
    320 		dojo.forEach(this.getEditorsArray(), function(e) {
    321 			e.setCaptionPosition(inPosition);
    322 		});
    323 	},
    324 	setEditorWidth: function(inEditorWidth) {
    325 		this.editorWidth = inEditorWidth;
    326 		dojo.forEach(this.getEditorsArray(), function(e) {
    327 			e.setWidth(inEditorWidth);
    328 		});
    329 	},
    330 	setEditorHeight: function(inEditorHeight) {
    331 		this.editorHeight = inEditorHeight;
    332 		dojo.forEach(this.getEditorsArray(), function(e) {
    333 			e.setHeight(inEditorHeight);
    334 		});
    335 	},
    336 	valueChanged: function(inProp, inValue) {
    337 		// FIXME: disallow change messages from being set on our variable properties
    338 		// they send these messages themselves when they change...
    339 		if (this[inProp] instanceof wm.Variable)
    340 			return;
    341 		else
    342 			this.inherited(arguments);
    343 	},
    344 	getViewDataIndex: function(inFormField) {
    345 		return inFormField;
    346 	},
    347 	//===========================================================================
    348 	// Data Navigation API
    349 	//===========================================================================
    350 	getRecordCount: function() {
    351 		return wm.fire(this.getDataSource(), "getCount");
    352 	},
    353 	getDataSource: function() {
    354 		if (!this._dataSource) {
    355 			var
    356 				b = this.$ && this.$.binding,
    357 				v = (b && b.wires["dataSet"] || 0).source;
    358 			this._dataSource = v && this.getValueById(v);
    359 		}
    360 		return this._dataSource;
    361 	},
    362 	setRecord: function(inIndex) {
    363 		wm.fire(this.getDataSource(), "setCursor", [inIndex]);
    364 	},
    365 	setNext: function() {
    366 		wm.fire(this.getDataSource(), "setNext");
    367 	},
    368 	setPrevious: function() {
    369 		wm.fire(this.getDataSource(), "setPrevious");
    370 	},
    371 	setFirst: function() {
    372 		wm.fire(this.getDataSource(), "setFirst");
    373 	},
    374 	setLast: function() {
    375 		wm.fire(this.getDataSource(), "setLast");
    376 	},
    377 	getIndex: function() {
    378 		return (this.getDataSource() || 0).cursor || 0;
    379 	}
    380 });
    381
    382 /**
    383 	LiveForm displays a set of editors for a data type.
    384 	LiveForm's editors display the data in its <i>dataSet</i> property.
    385 	Output data is provided via the <i>dataOutput</i> property.
    386 	LiveForm can directly perform operations on the dataOuput without the need for additional services,
    387 	it generates a set of editors automatically.
    388 	An additional <i>editPanel</i> widget is added by default so that operations are available without additional user setup.
    389 	@name wm.LiveForm
    390 	@class
    391 	@extends wm.LiveFormBase
    392 */
    393 dojo.declare("wm.LiveForm", wm.LiveFormBase, {
    394 	/**
    395 		@lends wm.LiveForm.prototype
    396 	*/
    397 	defaultButton: "",
    398 	displayErrors: true,
    399 	// process editing via liveData API
    400 	// if this setting is off, users can manually handle editing events
    401 	// and editor readonly/required states are not managed automatically
    402 	// other than being toggled on/off when editing starts/stops
    403 	liveEditing: true,
    404 	liveSaving: true,
    405 	liveVariable: null,
    406 	_confirmDelete: true,
    407 	_formMessages: {
    408 		confirmDelete: "Are you sure you want to delete this data?"
    409 	},
    410 	_controlSubForms: false,
    411 	destroy: function() {
    412 		this._cancelDefaultButton();
    413 		this.inherited(arguments);
    414 	},
    415 	init: function() {
    416 		this.connect(this.domNode, "keyup", this, "keyup");
    417 		// bc
    418 		this.canBeginEdit = this.hasEditableData;
    419 		this.inherited(arguments);
    420 	},
    421 	postInit: function() {
    422 		this.inherited(arguments);
    423 		this.initLiveVariable();
    424 		// BC: if captionSize contains only digits, append units
    425 		if (String(this.captionSize).search(/\D/) == -1)
    426 			this.captionSize += this.captionUnits;
    427 		// BC: if editorSize contains only digits, append units
    428 		if (String(this.editorSize).search(/\D/) == -1)
    429 			this.editorSize += this.editorSizeUnits;
    430 		//
    431 		if (this.liveEditing && !this.isDesignLoaded())
    432 			this.setReadonly(this.readonly);
    433 	},
    434 	initLiveVariable: function() {
    435 		var lv = this.liveVariable = new wm.LiveVariable({
    436 			name: "liveVariable",
    437 			owner: this,
    438 			liveSource: (this.dataSet || 0).type,
    439 			autoUpdate: false
    440 		});
    441 		this.connect(lv, "onBeforeUpdate", this, "beforeOperation");
    442 		this.connect(lv, "onSuccess", this, "operationSucceeded");
    443 		this.connect(lv, "onResult", this, "onResult");
    444 		this.connect(lv, "onError", this, "onError");
    445 	},
    446 	//===========================================================================
    447 	// Form data
    448 	//===========================================================================
    449 	setDataSet: function(inDataSet) {
    450 		if (this.operation)
    451 			return;
    452 		this._cancelDefaultButton();
    453 		this.inherited(arguments, [inDataSet]);
    454 	},
    455 	//===========================================================================
    456 	// Edit API
    457 	//===========================================================================
    458 	/**
    459 		Clear the form's editors and make them editable.
    460 		Fires <i>onBeginInsert</i> event.
    461 	*/
    462 	beginDataInsert: function() {
    463 		// Note: must clear dataOutput so that it's in a fresh state for insert
    464 		// this is because we may have stale data from a previous setting that's not
    465 		// cleared via clearing editors.
    466 		// Because of this, any statically set / bound value to dataOutput will be blown away.
    467 		this.clearDataOutput();
    468 		this.beginEditUpdate();
    469 		this.clearData();
    470 		this.endEditUpdate();
    471 		this.beginEdit("insert");
    472 		this.onBeginInsert();
    473 		this.validate();
    474 		return true;
    475 	},
    476 	/**
    477 		Make the form's editors and editable.
    478 		Fires <i>onBeginUpdate</i> event.
    479 	*/
    480 	beginDataUpdate: function() {
    481 		this.beginEdit("update");
    482 		this.onBeginUpdate();
    483 		return true;
    484 	},
    485 	beginEdit: function(inOperation) {
    486 		this.editStarting();
    487 		this.operation = inOperation;
    488 		if (this.liveEditing) {
    489 			if (this.hasLiveService())
    490 				this._setReadonly(false, dojo.hitch(this, "_canChangeEditorReadonly", [inOperation]));
    491 			else
    492 				this.setReadonly(false);
    493 		}
    494 	},
    495 	endEdit: function() {
    496 		if (this.liveEditing)
    497 			this.setReadonly(true);
    498 		this.operation = null;
    499 	},
    500 	/**
    501 		Cancels an edit by restoring the editors to the data from the <i>dataSet</i> property.
    502 	*/
    503 	cancelEdit: function() {
    504 		this.editCancelling();
    505 		var d = this.getItemData();
    506 		this.beginEditUpdate();
    507 		//this.clearData();
    508 		this.dataOutput.setData(d);
    509 		this.endEditUpdate();
    510 		//wm.fire(this.dataSet, "notify");
    511 		this.populateEditors();
    512 		this.onCancelEdit();
    513 		this.endEdit();
    514 	},
    515 	// editors that should not be changed during an edit should remain readonly
    516 	_canChangeEditorReadonly: function(inOperations, inEditor, inForm, inReadonly) {
    517 		if (inEditor instanceof wm.Editor && inEditor.formField) {
    518 			var
    519 				f = inEditor.formField,
    520 				dt = inForm.dataSet.type,
    521 				s = wm.typeManager.getTypeSchema(dt),
    522 				pi = wm.typeManager.getPropertyInfoFromSchema(s, f),
    523 				ops = inOperations;
    524 			if (!f)
    525 				return true;
    526 			// NOTE: if an editor should be excluded or not changed
    527 			// for given operation then it should remain read only.
    528 			//
    529 			// NOTE: exclude is use for inserts only so
    530 			// we can simply leave it read only since the editor will be blank
    531 			var
    532 				// this field should not be changed for the given operations
    533 				noChange = pi && dojo.some(pi.noChange, function(i) { return (dojo.indexOf(ops, i) > -1)}),
    534 				// this field should not be excluded for the given operations
    535 				exclude = pi && dojo.some(pi.exclude, function(i) { return (dojo.indexOf(ops, i) > -1)});
    536 			if (!inReadonly && (noChange || exclude))
    537 				return false;
    538 		}
    539 		return true;
    540 	},
    541 	//===========================================================================
    542 	// Data Verification
    543 	//===========================================================================
    544 	hasLiveService: function() {
    545 		return Boolean(wm.typeManager.getLiveService((this.dataSet || 0).type));
    546 	},
    547 	hasEditableData: function() {
    548 		var v = this.dataOutput;
    549 		return !this.liveEditing || (v && wm.typeManager.getLiveService(v.type) && wm.data.hasIncludeData(v.type, v.getData()));
    550 	},
    551 	//===========================================================================
    552 	// Editing server interaction
    553 	//===========================================================================
    554 	_getDeferredSuccess: function() {
    555 		var d = new dojo.Deferred();
    556 		d.callback(true);
    557 		return d;
    558 	},
    559 	saveData: function() {
    560 		if (this.operation == "insert")
    561 			return this.insertData();
    562 		if (this.operation == "update")
    563 			return this.updateData();
    564 	},
    565 	/**
    566 		Performs an insert operation based on the data in the
    567 		<i>dataOutput</i> property.
    568 	*/
    569 	insertData: function() {
    570 		return this.doOperation("insert");
    571 	},
    572 	/**
    573 		Performs an update operation based on the data in the
    574 		<i>dataOutput</i> property.
    575 	*/
    576 	updateData: function() {
    577 		return this.doOperation("update");
    578 	},
    579 	/**
    580 		Performs a delete operation based on the data in the
    581 		<i>dataOutput</i> property.
    582 	*/
    583 	deleteData: function() {
    584 		if (!this._confirmDelete || confirm(this._formMessages.confirmDelete)) {
    585 			this.onBeginDelete()
    586 			return this.doOperation("delete");
    587 		} else {
    588 			this.cancelEdit();
    589 		}
    590 	},
    591 	doOperation: function(inOperation) {
    592 		this.populateDataOutput();
    593 		var data = this.dataOutput.getData();
    594 		if (this.liveSaving) {
    595 			var lv = this.liveVariable;
    596 			lv.setOperation(inOperation);
    597 			lv.sourceData.setData(this.dataOutput.getData());
    598 			return lv.update();
    599 		} else {
    600 			switch (this.operation) {
    601 				case "insert":
    602 					this.onInsertData();
    603 					break;
    604 				case "update":
    605 					this.onUpdateData();
    606 					break;
    607 				case "delete":
    608 					this.onDeleteData();
    609 					break;
    610 			}
    611 			this.endEdit();
    612 			return this._getDeferredSuccess();
    613 		}
    614 	},
    615 	operationSucceeded: function(inResult) {
    616 		// if we get result as an array, take the frist one
    617 		if (dojo.isArray(inResult))
    618 			inResult = inResult[0];
    619 		var op = this.liveVariable.operation;
    620 		//
    621 		if (op == "insert" || op == "delete")
    622 			this.dataSet.cursor = 0;
    623 		if (op == "insert" || op == "update") {
    624 			wm.fire(this.getItemData(), "setData", [inResult]);
    625 			wm.fire(this.dataSet, "notify");
    626 		}
    627 		//
    628 		switch (op) {
    629 			case "insert":
    630 				this.onInsertData(inResult);
    631 				break;
    632 			case "update":
    633 				this.onUpdateData(inResult);
    634 				break;
    635 			case "delete":
    636 				this.beginEditUpdate();
    637 				this.clearData();
    638 				this.endEditUpdate();
    639 				this.onDeleteData(inResult);
    640 				break;
    641 		}
    642 		this.onSuccess(inResult);
    643 		this.endEdit();
    644 	},
    645 	beforeOperation: function() {
    646 		this.onBeforeOperation(this.liveVariable.operation);
    647 	},
    648 	//===========================================================================
    649 	// Form management
    650 	//===========================================================================
    651 	getSubFormsArray: function() {
    652 		var forms = [], w;
    653 		for (var i in this.widgets) {
    654 			w = this.widgets[i];
    655 			if (w instanceof wm.LiveForm) {
    656 				forms.push(w);
    657 				forms = forms.concat(w.getSubFormsArray());
    658 			}
    659 		}
    660 		return forms;
    661 	},
    662 	clearData: function() {
    663 		this.inherited(arguments);
    664 		if (this._controlSubForms)
    665 			dojo.forEach(this.getSubFormsArray(), function(f) {
    666 				f.clearData();
    667 			});
    668 	},
    669 	_setReadonly: function(inReadonly, inCanChangeFunc) {
    670 		this.inherited(arguments);
    671 		if (this._controlSubForms)
    672 			dojo.forEach(this.getSubFormsArray(), function(f) {
    673 				f.setReadonly(inReadonly);
    674 			});
    675 	},
    676 	//===========================================================================
    677 	// Default Button Processing
    678 	//===========================================================================
    679 	forceValidation: function() {
    680 		dojo.forEach(this.getEditorsArray(), function(e) {
    681 			wm.fire(e.editor, "changed");
    682 		});
    683 		this.validate();
    684 	},
    685 	keyup: function(e) {
    686 		// don't process enter for textareas
    687 		if (e.keyCode == dojo.keys.ENTER && e.target.tagName != "TEXTAREA") {
    688 			this._defaultButtonHandle = setTimeout(dojo.hitch(this, "_doDefaultButton"), 50);
    689 		}
    690 	},
    691 	_doDefaultButton: function() {
    692 		this._defaultButtonHandle = null;
    693 		var d = this.defaultButton;
    694 		if (d) {
    695 			this.forceValidation();
    696 			if (!d.disabled)
    697 				wm.fire(d, "onclick");
    698 		}
    699 	},
    700 	_cancelDefaultButton: function() {
    701 		if (this._defaultButtonHandle) {
    702 			clearTimeout(this._defaultButtonHandle);
    703 			this._defaultButtonHandle = null;
    704 		}
    705 	},
    706 	//===========================================================================
    707 	// Events
    708 	//===========================================================================
    709 	onBeginInsert: function() {
    710 	},
    711 	onInsertData: function() {
    712 	},
    713 	onBeginUpdate: function() {
    714 	},
    715 	onUpdateData: function() {
    716 	},
    717 	onBeginDelete: function() {
    718 	},
    719 	onDeleteData: function() {
    720 	},
    721 	onCancelEdit: function() {
    722 	},
    723 	onBeforeOperation: function(inOperation) {
    724 	},
    725 	onSuccess: function(inData) {
    726 	},
    727 	onResult: function(inData) {
    728 	},
    729 	onError: function(inError) {
    730 		wm.logging && console.error(inError);
    731 		if (this.displayErrors) {
    732 			var m = dojo.isString(inError) ? inError : (inError.message ? "Error: " + inError.message : "Unspecified Error");
    733 			alert(m);
    734 		}
    735 	}
    736 });