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 });