1 /* 2 * Copyright (c) 2007-2009 Per Cederberg & Dynabyte AB. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or 6 * modify it under the terms of the BSD license. 7 * 8 * This program is distributed in the hope that it will be useful, 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 11 */ 12 13 // Check for loaded MochiKit 14 if (typeof(MochiKit) == "undefined") { 15 throw new ReferenceError("MochiKit must be loaded before loading this script"); 16 } 17 18 /** 19 * @namespace The base class for the HTML user interface widgets. 20 * The Widget class shouldn't be instantiated directly, instead 21 * one of the subclasses should be instantiated. 22 */ 23 MochiKit.Widget = function () { 24 throw new ReferenceError("cannot call Widget constructor"); 25 } 26 27 /** 28 * Function to return unique identifiers. 29 * 30 * @return {Number} the next number in the sequence 31 */ 32 MochiKit.Widget._id = MochiKit.Base.counter(); 33 34 /** 35 * Checks if the specified object is a widget. Any non-null object 36 * that looks like a DOM node and has the element class "widget" 37 * will cause this function to return true. Otherwise, false will 38 * be returned. As an option, this function can also check if the 39 * widget has a certain class by checking for an additional CSS 40 * class "widget<className>" (which is a standard followed by all 41 * widgets). 42 * 43 * @param {Object} obj the object to check 44 * @param {String} [className] the optional widget class name 45 * 46 * @return {Boolean} true if the object looks like a widget, or 47 * false otherwise 48 * 49 * @static 50 */ 51 MochiKit.Widget.isWidget = function (obj, className) { 52 if (className != null) { 53 return MochiKit.DOM.isHTML(obj) && 54 MochiKit.DOM.hasElementClass(obj, "widget") && 55 MochiKit.DOM.hasElementClass(obj, "widget" + className); 56 } else { 57 return MochiKit.DOM.isHTML(obj) && 58 MochiKit.DOM.hasElementClass(obj, "widget"); 59 } 60 } 61 62 /** 63 * Checks if the specified object is a form field. Any non-null 64 * object that looks like a DOM node and is either an standard HTML 65 * form field (<input>, <textarea> or <select>) or 66 * one with a "value" property will cause this function to return 67 * true. Otherwise, false will be returned. 68 * 69 * @param {Object} obj the object to check 70 * 71 * @return {Boolean} true if the object looks like a form field, or 72 * false otherwise 73 * 74 * @static 75 */ 76 MochiKit.Widget.isFormField = function (obj) { 77 if (!MochiKit.DOM.isHTML(obj) || typeof(obj.tagName) !== "string") { 78 return false; 79 } 80 var tagName = obj.tagName.toUpperCase(); 81 return tagName == "INPUT" || 82 tagName == "TEXTAREA" || 83 tagName == "SELECT" || 84 MochiKit.Widget.isWidget(obj, "Field"); 85 } 86 87 /** 88 * Adds all functions from a widget class to a DOM node. This will 89 * also convert the DOM node into a widget by adding the "widget" 90 * CSS class and add all the default widget functions from the 91 * standard Widget prototype. Functions are added with setdefault, 92 * ensuring that existing functions will not be overwritten. 93 * 94 * @param {Node} node the DOM node to modify 95 * @param {Object/Function} [...] the widget class or constructor 96 * 97 * @return {Widget} the widget DOM node 98 */ 99 MochiKit.Widget._widgetMixin = function (node/*, objOrClass, ...*/) { 100 MochiKit.DOM.addElementClass(node, "widget"); 101 for (var i = 1; i < arguments.length; i++) { 102 var obj = arguments[i]; 103 if (typeof(obj) === "function") { 104 obj = obj.prototype; 105 } 106 MochiKit.Base.setdefault(node, obj); 107 } 108 MochiKit.Base.setdefault(node, MochiKit.Widget.prototype); 109 return node; 110 } 111 112 /** 113 * Creates a new widget with the specified name, attributes and 114 * child widgets or DOM nodes. The widget class name must have been 115 * registered in the MochiKit.Widget.Classes lookup table, or an 116 * exception will be thrown. This function is identical to calling 117 * the constructor function directly. 118 * 119 * @param {String} name the widget class name 120 * @param {Object} attrs the widget and node attributes 121 * @param {Object} [...] the child widgets or DOM nodes 122 * 123 * @return {Widget} the widget DOM node 124 * 125 * @throws {ReferenceError} if the widget class name couldn't be 126 * found in MochiKit.Widget.Classes 127 * 128 * @static 129 */ 130 MochiKit.Widget.createWidget = function (name, attrs/*, ...*/) { 131 var cls = MochiKit.Widget.Classes[name]; 132 if (cls == null) { 133 throw new ReferenceError("failed to find widget '" + name + 134 "' in MochiKit.Widget.Classes"); 135 } 136 return cls.apply(this, MochiKit.Base.extend([], arguments, 1)); 137 } 138 139 /** 140 * Creates a tree of DOM nodes or widgets from a parsed XML document. 141 * This function will call createWidget() for any XML element node 142 * with a name corresponding to a widget class. Otherwise the 143 * createDOM() function is called. Some basic adjustments will be 144 * performed on the element attributes "id", "style", "class", "w", 145 * "h" and "a", in order to set these values with the appropriate 146 * function instead of as plain attribute strings. Text nodes with 147 * non-whitespace content will be mapped to HTML DOM text nodes. 148 * 149 * @param {Node/NodeList} node the XML document, node or node list 150 * @param {Object} [ids] the optional node id mappings 151 * 152 * @return {Array} an array of the root DOM nodes or widgets created, 153 * or null if no nodes could be created 154 */ 155 MochiKit.Widget.createWidgetTree = function(node, ids) { 156 if (node.documentElement) { 157 return MochiKit.Widget.createWidgetTree(node.documentElement.childNodes, ids); 158 } else if (typeof(node.item) != "undefined" && typeof(node.length) == "number") { 159 var iter = MochiKit.Iter.repeat(ids, node.length); 160 iter = MochiKit.Iter.imap(MochiKit.Widget.createWidgetTree, node, iter); 161 iter = MochiKit.Iter.ifilterfalse(MochiKit.Base.isUndefinedOrNull, iter); 162 return MochiKit.Iter.list(iter); 163 } else if (node.nodeType === 1) { // Node.ELEMENT_NODE 164 try { 165 return [MochiKit.Widget._createWidgetTreeElem(node, ids)]; 166 } catch (e) { 167 MochiKit.Logging.logError("Failed to create DOM node or widget", e); 168 } 169 } else if (node.nodeType === 3) { // Node.TEXT_NODE 170 var str = node.nodeValue; 171 if (str != null && MochiKit.Format.strip(str) != "") { 172 return MochiKit.DOM.createTextNode(str.replace(/\s+/g, " ")); 173 } 174 } 175 // TODO: handling of CDATA nodes to escape text? 176 return null; 177 } 178 179 /** 180 * Creates a DOM node or widget from a parsed XML element. This 181 * function will call createWidget() for any XML element node with a 182 * name corresponding to a widget class. Otherwise the createDOM() 183 * function is called. Some basic adjustments will be performed on 184 * the element attributes "id", "style", "class", "w", "h" and "a", 185 * in order to set these values with the appropriate function 186 * instead of as plain attribute strings. 187 * 188 * @param {Node} node the XML element node 189 * @param {Object} [ids] the optional node id mappings 190 * 191 * @return {Node/Widget} the DOM node or widget created 192 */ 193 MochiKit.Widget._createWidgetTreeElem = function(node, ids) { 194 var name = node.nodeName; 195 var attrs = MochiKit.Base.dict(MochiKit.DOM.attributeArrayNewImpl(node)); 196 var locals = MochiKit.Base.mask(attrs, ["id", "w", "h", "a", "class", "style"]); 197 var children = MochiKit.Widget.createWidgetTree(node.childNodes, ids); 198 if (MochiKit.Widget.Classes[name]) { 199 if (name == "Table" && attrs.multiple) { 200 // TODO: remove deprecated code, eventually... 201 MochiKit.Logging.logWarning("Table 'multiple' attribute is deprecated, use 'select'"); 202 attrs.select = MochiKit.Base.bool(attrs.multiple) ? "multiple" : "one"; 203 delete attrs.multiple; 204 } 205 var widget = MochiKit.Widget.createWidget(name, attrs, children); 206 } else { 207 var widget = MochiKit.DOM.createDOM(name, attrs, children); 208 } 209 if (locals.id) { 210 if (ids) { 211 ids[locals.id] = widget; 212 } else { 213 widget.id = locals.id; 214 } 215 } 216 if (locals.w || locals.h || locals.a) { 217 MochiKit.Style.registerSizeConstraints(widget, locals.w, locals.h, locals.a); 218 } 219 if (locals["class"]) { 220 var classes = MochiKit.Format.strip(locals["class"]).split(" "); 221 if (typeof(widget.addClass) == "function") { 222 widget.addClass.apply(widget, classes); 223 } else { 224 for (var i = 0; i < arguments.length; i++) { 225 MochiKit.DOM.addElementClass(widget, classes[i]); 226 } 227 } 228 } 229 if (locals.style) { 230 var styles = {}; 231 var parts = locals.style.split(";"); 232 for (var i = 0; i < parts.length; i++) { 233 var a = parts[i].split(":"); 234 var k = MochiKit.Format.strip(a[0]); 235 if (k != "" && a.length > 1) { 236 styles[k] = MochiKit.Format.strip(a[1]); 237 } 238 } 239 if (typeof(widget.setAttrs) == "function") { 240 widget.setAttrs({ style: styles }); 241 } else { 242 MochiKit.Style.setStyle(widget, styles); 243 } 244 } 245 return widget; 246 } 247 248 /** 249 * Destroys a widget or a DOM node. This function will remove the DOM 250 * node from the tree, disconnect all signals and call all widget 251 * destructor functions. The same procedure will also be applied 252 * recursively to all child nodes. Once destroyed, all references to 253 * the widget object should be cleared in order for the browser to 254 * be able to reclaim the memory used. 255 * 256 * @param {Widget/Node/Array} node the (widget) DOM node or list 257 * 258 * @static 259 */ 260 MochiKit.Widget.destroyWidget = function (node) { 261 if (node.nodeType != null) { 262 if (typeof(node.destroy) == "function") { 263 node.destroy(); 264 } 265 if (node.parentNode != null) { 266 MochiKit.DOM.removeElement(node); 267 } 268 MochiKit.Signal.disconnectAll(node); 269 while (node.firstChild != null) { 270 MochiKit.Widget.destroyWidget(node.firstChild); 271 } 272 } else if (MochiKit.Base.isArrayLike(node)) { 273 for (var i = node.length - 1; i >= 0; i--) { 274 MochiKit.Widget.destroyWidget(node[i]); 275 } 276 } 277 } 278 279 /** 280 * Creates an event handler function that will forward any calls to 281 * another function. The other function must exist as a property in 282 * a parent widget of the specified class. 283 * 284 * @param {String} className the parent widget class name, or null 285 * to use the same node 286 * @param {String} methodName the name of the method to call 287 * @param {Object} [...] the additional method arguments 288 * 289 * @return {Function} a function that forwards calls as specified 290 */ 291 MochiKit.Widget._eventHandler = function (className, methodName/*, ...*/) { 292 var baseArgs = MochiKit.Base.extend([], arguments, 2); 293 return function (evt) { 294 var node = this; 295 while (!MochiKit.Widget.isWidget(node, className)) { 296 node = node.parentNode; 297 } 298 var e = new MochiKit.Signal.Event(this, evt); 299 return node[methodName].apply(node, baseArgs.concat([e])); 300 }; 301 } 302 303 /** 304 * Emits a signal to any listeners connected with MochiKit.Signal. 305 * This function handles errors by logging them to the default error 306 * log in MochiKit.Logging.<p> 307 * 308 * Note that this function is an internal helper function for the 309 * widgets and shouldn't be called by external code. 310 * 311 * @param {Widget} node the widget DOM node 312 * @param {String} sig the signal name ("onclick" or similar) 313 * @param {Object} [...] the optional signal arguments 314 * 315 * @return {Boolean} true if the signal was processed correctly, or 316 * false if an exception was thrown 317 */ 318 MochiKit.Widget.emitSignal = function (node, sig/*, ...*/) { 319 try { 320 MochiKit.Signal.signal.apply(MochiKit.Signal, arguments); 321 return true; 322 } catch (e) { 323 var msg = "Exception in signal '" + sig + "' handler"; 324 MochiKit.Logging.logError(msg, e); 325 return false; 326 } 327 } 328 329 /** 330 * The internal widget destructor function. This method should only 331 * be called by destroyWidget() and may be overridden by subclasses. 332 * By default this method does nothing. 333 */ 334 MochiKit.Widget.prototype.destroy = function () { 335 // Nothing to do by default 336 } 337 338 /** 339 * Updates the widget or HTML DOM node attributes. This method is 340 * sometimes overridden by individual widgets to allow modification 341 * of widget attributes also available in the constructor. 342 * 343 * @param {Object} attrs the widget and node attributes to set 344 */ 345 MochiKit.Widget.prototype.setAttrs = function (attrs) { 346 MochiKit.DOM.updateNodeAttributes(this, attrs); 347 } 348 349 /** 350 * Updates the CSS styles of this HTML DOM node. This method is 351 * identical to MochiKit.Style.setStyle, but uses "this" as the 352 * first argument. 353 * 354 * @param {Object} styles an object with the styles to set 355 * 356 * @example 357 * widget.setStyle({ "font-size": "bold", "color": "red" }); 358 */ 359 MochiKit.Widget.prototype.setStyle = function (styles) { 360 MochiKit.Style.setStyle(this, styles); 361 } 362 363 /** 364 * Checks if this HTML DOM node has the specified CSS class names. 365 * Note that more than one CSS class name may be checked, in which 366 * case all must be present. 367 * 368 * @param {String} [...] the CSS class names to check 369 * 370 * @return {Boolean} true if all CSS classes were present, or 371 * false otherwise 372 */ 373 MochiKit.Widget.prototype.hasClass = function (/* ... */) { 374 for (var i = 0; i < arguments.length; i++) { 375 if (!MochiKit.DOM.hasElementClass(this, arguments[i])) { 376 return false; 377 } 378 } 379 return true; 380 } 381 382 /** 383 * Adds the specified CSS class names to this HTML DOM node. 384 * 385 * @param {String} [...] the CSS class names to add 386 */ 387 MochiKit.Widget.prototype.addClass = function (/* ... */) { 388 for (var i = 0; i < arguments.length; i++) { 389 MochiKit.DOM.addElementClass(this, arguments[i]); 390 } 391 } 392 393 /** 394 * Removes the specified CSS class names from this HTML DOM node. 395 * 396 * @param {String} [...] the CSS class names to remove 397 */ 398 MochiKit.Widget.prototype.removeClass = function (/* ... */) { 399 for (var i = 0; i < arguments.length; i++) { 400 MochiKit.DOM.removeElementClass(this, arguments[i]); 401 } 402 } 403 404 /** 405 * Toggles adding and removing the specified CSS class names to and 406 * from this HTML DOM node. If all the CSS classes are already set, 407 * they will be removed. Otherwise they will be added. 408 * 409 * @param {String} [...] the CSS class names to remove 410 * 411 * @return {Boolean} true if the CSS classes were added, or 412 * false otherwise 413 */ 414 MochiKit.Widget.prototype.toggleClass = function (/* ... */) { 415 if (this.hasClass.apply(this, arguments)) { 416 this.removeClass.apply(this, arguments); 417 return false; 418 } else { 419 this.addClass.apply(this, arguments); 420 return true; 421 } 422 } 423 424 /** 425 * Checks if this HTML DOM node is hidden (with the hide() method). 426 * This method does NOT check the actual widget visibility (which 427 * will be affected by animations for example), but only checks for 428 * the "widgetHidden" CSS class. 429 * 430 * @return {Boolean} true if the widget is hidden, or 431 * false otherwise 432 */ 433 MochiKit.Widget.prototype.isHidden = function () { 434 return this.hasClass("widgetHidden"); 435 } 436 437 /** 438 * Shows this HTML DOM node if it was previously hidden with the 439 * hide() method. This mechanism is safe for all types of HTML 440 * elements, since it uses a "widgetHidden" CSS class to hide nodes 441 * instead of explicitly setting the CSS display property. 442 */ 443 MochiKit.Widget.prototype.show = function () { 444 this.removeClass("widgetHidden"); 445 } 446 447 /** 448 * Hides this HTML DOM node if it doesn't have an explicit "display" 449 * CSS value. This mechanism is safe for all types of HTML elements, 450 * since it uses a "widgetHidden" CSS class to hide nodes instead of 451 * explicitly setting the CSS display property. 452 */ 453 MochiKit.Widget.prototype.hide = function () { 454 this.addClass("widgetHidden"); 455 } 456 457 /** 458 * Performs a visual effect animation on this widget. This is 459 * implemented using the MochiKit.Visual effect package. All options 460 * sent to this function will be passed on to the appropriate 461 * MochiKit.Visual function. 462 * 463 * @param {Object} opts the visual effect options 464 * @param {String} opts.effect the MochiKit.Visual effect name 465 * @param {String} opts.queue the MochiKit.Visual queue handling, 466 * defaults to "replace" and a unique scope for each widget 467 * (see MochiKit.Visual for full options) 468 * 469 * @example 470 * widget.animate({ effect: "fade", duration: 0.5 }); 471 * widget.animate({ effect: "Move", transition: "spring", y: 300 }); 472 */ 473 MochiKit.Widget.prototype.animate = function (opts) { 474 var queue = { scope: this._animQueueId(), position: "replace" }; 475 opts = MochiKit.Base.updatetree({ queue: queue }, opts); 476 if (typeof(opts.queue) == "string") { 477 queue.position = opts.queue; 478 opts.queue = queue; 479 } 480 var func = MochiKit.Visual[opts.effect]; 481 if (typeof(func) == "function") { 482 func.call(null, this, opts); 483 } 484 } 485 486 /** 487 * Returns the default visual effect queue identifier. 488 * 489 * @return {String} the the default queue identifier 490 */ 491 MochiKit.Widget.prototype._animQueueId = function () { 492 if (this._queueId == null) { 493 this._queueId = this.id || "widget" + MochiKit.Widget._id(); 494 } 495 return this._queueId; 496 } 497 498 /** 499 * Blurs (unfocuses) this DOM node and all relevant child nodes. 500 * This function will recursively blur all A, BUTTON, INPUT, 501 * TEXTAREA and SELECT child nodes found. 502 */ 503 MochiKit.Widget.prototype.blurAll = function () { 504 MochiKit.DOM.blurAll(this); 505 } 506 507 /** 508 * Returns an array with all child DOM nodes. Note that the array is 509 * a real JavaScript array, not a dynamic NodeList. This method is 510 * sometimes overridden by child widgets in order to hide 511 * intermediate DOM nodes required by the widget. 512 * 513 * @return {Array} the array of child DOM nodes 514 */ 515 MochiKit.Widget.prototype.getChildNodes = function () { 516 return MochiKit.Base.extend([], this.childNodes); 517 } 518 519 /** 520 * Adds a single child DOM node to this widget. This method is 521 * sometimes overridden by child widgets in order to hide or control 522 * intermediate DOM nodes required by the widget. 523 * 524 * @param {Widget/Node} child the DOM node to add 525 */ 526 MochiKit.Widget.prototype.addChildNode = function (child) { 527 this.appendChild(child); 528 } 529 530 /** 531 * Removes a single child DOM node from this widget. This method is 532 * sometimes overridden by child widgets in order to hide or control 533 * intermediate DOM nodes required by the widget.<p> 534 * 535 * Note that this method will NOT destroy the removed child widget, 536 * so care must be taken to ensure proper child widget destruction. 537 * 538 * @param {Widget/Node} child the DOM node to remove 539 */ 540 MochiKit.Widget.prototype.removeChildNode = function (child) { 541 this.removeChild(child); 542 } 543 544 /** 545 * Adds one or more children to this widget. This method will 546 * flatten any arrays among the arguments and ignores any null or 547 * undefined argument. Any DOM nodes or widgets will be added to the 548 * end, and other objects will be converted to a text node first. 549 * Subclasses should normally override the addChildNode() method instead 550 * of this one, since that is the basis for DOM node insertion. 551 * 552 * @param {Object} [...] the children to add 553 */ 554 MochiKit.Widget.prototype.addAll = function (/* ... */) { 555 var args = MochiKit.Base.flattenArray(arguments); 556 for (var i = 0; i < args.length; i++) { 557 if (args[i] == null) { 558 // Ignore null values 559 } else if (MochiKit.DOM.isDOM(args[i])) { 560 this.addChildNode(args[i]); 561 // TODO: remove this call for performance 562 MochiKit.Style.resizeElements(args[i]); 563 } else { 564 this.addChildNode(MochiKit.DOM.createTextNode(args[i])); 565 } 566 } 567 } 568 569 /** 570 * Removes all children to this widget. This method will also destroy 571 * and child widgets and disconnect all signal listeners. This method 572 * uses the getChildNodes() and removeChildNode() methods to find and 573 * remove the individual child nodes. 574 */ 575 MochiKit.Widget.prototype.removeAll = function () { 576 var children = this.getChildNodes(); 577 for (var i = children.length - 1; i >= 0; i--) { 578 this.removeChildNode(children[i]); 579 MochiKit.Widget.destroyWidget(children[i]); 580 } 581 } 582 583 /** 584 * Creates a new button widget. 585 * 586 * @constructor 587 * @param {Object} attrs the widget and node attributes 588 * @param {Boolean} [attrs.highlight] the highlight option flag 589 * @param {Object} [...] the child widgets or DOM nodes 590 * 591 * @return {Widget} the widget DOM node 592 * 593 * @class The button widget class. Used to provide a simple push 594 * button, using the <button> HTML element. In particular, 595 * the "onclick" event is usually of interest. 596 * @property {Boolean} disabled The button disabled flag. 597 * @extends MochiKit.Widget 598 * 599 * @example 600 * var widget = MochiKit.Widget.Button({ highlight: true }, "Find"); 601 */ 602 MochiKit.Widget.Button = function (attrs/*, ...*/) { 603 var o = MochiKit.DOM.BUTTON(); 604 MochiKit.Widget._widgetMixin(o, arguments.callee); 605 o.addClass("widgetButton"); 606 o.setAttrs(attrs); 607 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 608 return o; 609 } 610 611 /** 612 * Updates the widget or HTML DOM node attributes. 613 * 614 * @param {Object} attrs the widget and node attributes to set 615 * @param {Boolean} [attrs.highlight] the highlight option flag 616 */ 617 MochiKit.Widget.Button.prototype.setAttrs = function (attrs) { 618 attrs = MochiKit.Base.update({}, attrs); 619 var locals = MochiKit.Base.mask(attrs, ["highlight"]); 620 if (typeof(locals.highlight) != "undefined") { 621 if (MochiKit.Base.bool(locals.highlight)) { 622 this.addClass("widgetButtonHighlight"); 623 } else { 624 this.removeClass("widgetButtonHighlight"); 625 } 626 } 627 MochiKit.DOM.updateNodeAttributes(this, attrs); 628 } 629 630 /** 631 * Creates a new dialog widget. 632 * 633 * @constructor 634 * @param {Object} attrs the widget and node attributes 635 * @param {String} [attrs.title] the dialog title, defaults to "Dialog" 636 * @param {Boolean} [attrs.modal] the modal dialog flag, defaults to false 637 * @param {Boolean} [attrs.center] the center dialog flag, defaults to true 638 * @param {Boolean} [attrs.resizeable] the resize dialog flag, defaults to true 639 * @param {Object} [...] the child widgets or DOM nodes 640 * 641 * @return {Widget} the widget DOM node 642 * 643 * @class The dialog widget class. Used to provide a resizeable and 644 * moveable window within the current page. Internally it uses a 645 * number of <div> HTML elements. 646 * @extends MochiKit.Widget 647 * 648 * @example 649 * var dialog = MochiKit.Widget.Dialog({ title: "My Dialog", modal: true }, 650 * "...dialog content widgets here..."); 651 * parent.addAll(dialog); 652 * dialog.show(); 653 */ 654 MochiKit.Widget.Dialog = function (attrs/*, ... */) { 655 var title = MochiKit.DOM.DIV({ "class": "widgetDialogTitle" }, "Dialog"); 656 var close = new MochiKit.Widget.Icon({ ref: "CLOSE", "class": "widgetDialogClose" }); 657 var resize = new MochiKit.Widget.Icon({ ref: "RESIZE", "class": "widgetDialogResize" }); 658 var content = MochiKit.DOM.DIV({ "class": "widgetDialogContent" }); 659 MochiKit.Style.registerSizeConstraints(content, "100% - 22", "100% - 44"); 660 var o = MochiKit.DOM.DIV({}, title, close, resize, content); 661 MochiKit.Widget._widgetMixin(o, arguments.callee); 662 o.addClass("widgetDialog", "widgetHidden"); 663 o.setAttrs(MochiKit.Base.update({ modal: false, center: true }, attrs)); 664 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 665 title.onmousedown = MochiKit.Widget._eventHandler("Dialog", "_handleMoveStart"); 666 close.onclick = MochiKit.Widget._eventHandler("Dialog", "hide"); 667 resize.onmousedown = MochiKit.Widget._eventHandler("Dialog", "_handleResizeStart"); 668 return o; 669 } 670 671 /** 672 * Emitted when the dialog is shown. 673 * 674 * @name MochiKit.Widget.Dialog#onshow 675 * @event 676 */ 677 678 /** 679 * Emitted when the dialog is hidden. 680 * 681 * @name MochiKit.Widget.Dialog#onhide 682 * @event 683 */ 684 685 /** 686 * Emitted when the dialog is moved. The event will be sent 687 * repeatedly when moving with a mouse drag operation. 688 * 689 * @name MochiKit.Widget.Dialog#onmove 690 * @event 691 */ 692 693 /** 694 * Emitted when the dialog is resized. The event will be sent 695 * repeatedly when resizing with a mouse drag operation. 696 * 697 * @name MochiKit.Widget.Dialog#onresize 698 * @event 699 */ 700 701 /** 702 * Updates the dialog or HTML DOM node attributes. 703 * 704 * @param {Object} attrs the widget and node attributes to set 705 * @param {String} [attrs.title] the dialog title 706 * @param {Boolean} [attrs.modal] the modal dialog flag 707 * @param {Boolean} [attrs.center] the center dialog flag 708 * @param {Boolean} [attrs.resizeable] the resize dialog flag 709 */ 710 MochiKit.Widget.Dialog.prototype.setAttrs = function (attrs) { 711 attrs = MochiKit.Base.update({}, attrs); 712 var locals = MochiKit.Base.mask(attrs, ["title", "modal", "center", "resizeable"]); 713 if (typeof(locals.title) != "undefined") { 714 MochiKit.DOM.replaceChildNodes(this.firstChild, locals.title); 715 } 716 if (typeof(locals.modal) != "undefined") { 717 this.modal = MochiKit.Base.bool(locals.modal); 718 } 719 if (typeof(locals.center) != "undefined") { 720 this.center = MochiKit.Base.bool(locals.center); 721 } 722 if (typeof(locals.resizeable) != "undefined") { 723 var resize = this.childNodes[2]; 724 if (MochiKit.Base.bool(locals.resizeable)) { 725 resize.show(); 726 } else { 727 resize.hide(); 728 } 729 } 730 MochiKit.DOM.updateNodeAttributes(this, attrs); 731 } 732 733 /** 734 * Shows the dialog. 735 */ 736 MochiKit.Widget.Dialog.prototype.show = function () { 737 if (this.parentNode == null) { 738 throw new Error("Cannot show Dialog widget without setting a parent DOM node"); 739 } 740 if (this.modal) { 741 var attrs = { loading: false, message: "", style: { "z-index": "99" } }; 742 this._modalNode = new MochiKit.Widget.Overlay(attrs); 743 this.parentNode.appendChild(this._modalNode); 744 } 745 this.removeClass("widgetHidden"); 746 var dim = MochiKit.Style.getElementDimensions(this); 747 this.resizeTo(dim.w, dim.h); 748 if (this.center) { 749 this.moveToCenter(); 750 } 751 MochiKit.Style.resetScrollOffset(this, true); 752 MochiKit.Widget.emitSignal(this, "onshow"); 753 } 754 755 /** 756 * Hides the dialog. 757 */ 758 MochiKit.Widget.Dialog.prototype.hide = function () { 759 if (this._modalNode != null) { 760 MochiKit.Widget.destroyWidget(this._modalNode); 761 this._modalNode = null; 762 } 763 this.blurAll(); 764 this.addClass("widgetHidden"); 765 MochiKit.Widget.emitSignal(this, "onhide"); 766 } 767 768 /** 769 * Returns an array with all child DOM nodes. Note that the array is 770 * a real JavaScript array, not a dynamic NodeList. 771 * 772 * @return {Array} the array of child DOM nodes 773 */ 774 MochiKit.Widget.Dialog.prototype.getChildNodes = function () { 775 return MochiKit.Base.extend([], this.lastChild.childNodes); 776 } 777 778 /** 779 * Adds a single child DOM node to this widget. 780 * 781 * @param {Widget/Node} child the DOM node to add 782 */ 783 MochiKit.Widget.Dialog.prototype.addChildNode = function (child) { 784 this.lastChild.appendChild(child); 785 } 786 787 /** 788 * Removes a single child DOM node from this widget. 789 * 790 * @param {Widget/Node} child the DOM node to remove 791 */ 792 MochiKit.Widget.Dialog.prototype.removeChildNode = function (child) { 793 this.lastChild.removeChild(child); 794 } 795 796 /** 797 * Moves the dialog to the specified position (relative to the 798 * parent DOM node). The position will be restrained by the parent 799 * DOM node size. 800 * 801 * @param {Number} x the horizontal position (in pixels) 802 * @param {Number} y the vertical position (in pixels) 803 */ 804 MochiKit.Widget.Dialog.prototype.moveTo = function (x, y) { 805 var parentDim = MochiKit.Style.getElementDimensions(this.parentNode); 806 var dim = MochiKit.Style.getElementDimensions(this); 807 var pos = { x: Math.max(0, Math.min(x, parentDim.w - dim.w - 2)), 808 y: Math.max(0, Math.min(y, parentDim.h - dim.h - 2)) }; 809 MochiKit.Style.setElementPosition(this, pos); 810 MochiKit.Widget.emitSignal(this, "onmove", pos); 811 } 812 813 /** 814 * Moves the dialog to the center (relative to the parent DOM node). 815 */ 816 MochiKit.Widget.Dialog.prototype.moveToCenter = function () { 817 var parentDim = MochiKit.Style.getElementDimensions(this.parentNode); 818 var dim = MochiKit.Style.getElementDimensions(this); 819 var pos = { x: Math.round(Math.max(0, (parentDim.w - dim.w) / 2)), 820 y: Math.round(Math.max(0, (parentDim.h - dim.h) / 2)) }; 821 MochiKit.Style.setElementPosition(this, pos); 822 MochiKit.Widget.emitSignal(this, "onmove", pos); 823 } 824 825 /** 826 * Resizes the dialog to the specified size (in pixels). The size 827 * will be restrained by the parent DOM node size. 828 * 829 * @param {Number} width the width (in pixels) 830 * @param {Number} height the height (in pixels) 831 */ 832 MochiKit.Widget.Dialog.prototype.resizeTo = function (width, height) { 833 var parentDim = MochiKit.Style.getElementDimensions(this.parentNode); 834 var pos = MochiKit.Style.getElementPosition(this, this.parentNode); 835 var dim = { w: Math.max(150, Math.min(width, parentDim.w - pos.x - 2)), 836 h: Math.max(100, Math.min(height, parentDim.h - pos.y - 2)) }; 837 MochiKit.Style.setElementDimensions(this, dim); 838 MochiKit.Style.registerSizeConstraints(this, null, null); 839 MochiKit.Base.update(this, dim); 840 MochiKit.Style.resizeElements(this.lastChild); 841 MochiKit.Widget.emitSignal(this, "onresize", dim); 842 } 843 844 /** 845 * Initiates a dialog move drag operation. This will install a mouse 846 * event handler on the parent document. 847 * 848 * @param {Event} evt the MochiKit.Signal.Event object 849 */ 850 MochiKit.Widget.Dialog.prototype._handleMoveStart = function (evt) { 851 var pos = MochiKit.Style.getElementPosition(this.parentNode); 852 this._offsetPos = MochiKit.Style.getElementPosition(this, pos); 853 this._startPos = evt.mouse().page; 854 evt.stop(); 855 MochiKit.Signal.connect(document, "onmousemove", this, "_handleMove"); 856 MochiKit.Signal.connect(document, "onmouseup", this, "_stopDrag"); 857 } 858 859 /** 860 * Handles a dialog move drag operation. 861 * 862 * @param {Event} evt the MochiKit.Signal.Event object 863 */ 864 MochiKit.Widget.Dialog.prototype._handleMove = function (evt) { 865 var pos = evt.mouse().page; 866 this.moveTo(this._offsetPos.x + pos.x - this._startPos.x, 867 this._offsetPos.y + pos.y - this._startPos.y); 868 } 869 870 /** 871 * Initiates a dialog resize drag operation. This will install a 872 * mouse event handler on the parent document. 873 * 874 * @param {Event} evt the MochiKit.Signal.Event object 875 */ 876 MochiKit.Widget.Dialog.prototype._handleResizeStart = function (evt) { 877 this._offsetDim = MochiKit.Style.getElementDimensions(this); 878 this._startPos = evt.mouse().page; 879 evt.stop(); 880 // TODO: correct handling of drag event, since IE seems to get 881 // problems when mouse enters other HTML elements 882 MochiKit.Signal.connect(document, "onmousemove", this, "_handleResize"); 883 MochiKit.Signal.connect(document, "onmousedown", function (evt) { evt.stop(); }); 884 MochiKit.Signal.connect(document, "onmouseup", this, "_stopDrag"); 885 } 886 887 /** 888 * Handles a dialog resize drag operation. 889 * 890 * @param {Event} evt the MochiKit.Signal.Event object 891 */ 892 MochiKit.Widget.Dialog.prototype._handleResize = function (evt) { 893 var pos = evt.mouse().page; 894 this.resizeTo(this._offsetDim.w + pos.x - this._startPos.x, 895 this._offsetDim.h + pos.y - this._startPos.y); 896 } 897 898 /** 899 * Stops a dialog resize or move drag operation. 900 * 901 * @param {Event} evt the MochiKit.Signal.Event object 902 */ 903 MochiKit.Widget.Dialog.prototype._stopDrag = function (evt) { 904 MochiKit.Signal.disconnectAll(document, "onmousemove"); 905 MochiKit.Signal.disconnectAll(document, "onmousedown"); 906 MochiKit.Signal.disconnectAll(document, "onmouseup"); 907 } 908 909 /** 910 * Creates a new field widget. 911 * 912 * @constructor 913 * @param {Object} attrs the widget and node attributes 914 * @param {String} attrs.name the field name 915 * @param {String} [attrs.value] the initial field value, defaults 916 * to an empty string 917 * @param {String} [attrs.format] the field format string, defaults 918 * to "{:s}" 919 * @param {Number} [attrs.maxLength] the maximum data length, 920 * overflow will be displayed as a tooltip, defaults to 921 * -1 (unlimited) 922 * 923 * @return {Widget} the widget DOM node 924 * 925 * @class The field widget class. This widget is useful for providing 926 * visible display of form data, using a <span> HTML 927 * element. 928 * @extends MochiKit.Widget 929 * 930 * @example 931 * var field = MochiKit.Widget.Field({ name: "ratio", format: "Ratio: {:%}" }); 932 * form.addAll(field); 933 * field.setAttrs({ value: 0.23 }); 934 */ 935 MochiKit.Widget.Field = function (attrs) { 936 var o = MochiKit.DOM.SPAN(); 937 MochiKit.Widget._widgetMixin(o, arguments.callee); 938 o.addClass("widgetField"); 939 o.setAttrs(MochiKit.Base.update({ name: "", value: "", maxLength: -1 }, attrs)); 940 o.defaultValue = o.value; 941 return o; 942 } 943 944 /** 945 * Updates the widget or HTML DOM node attributes. 946 * 947 * @param {Object} attrs the widget and node attributes to set 948 * @param {String} [attrs.name] the field name 949 * @param {String} [attrs.value] the field value 950 * @param {String} [attrs.format] the field format string 951 * @param {Number} [attrs.maxLength] the maximum data length, 952 * overflow will be displayed as a tooltip 953 * 954 * @example 955 * field.setAttrs({ value: 0.23 }); 956 */ 957 MochiKit.Widget.Field.prototype.setAttrs = function (attrs) { 958 attrs = MochiKit.Base.update({}, attrs); 959 var locals = MochiKit.Base.mask(attrs, ["name", "value", "format", "maxLength"]); 960 if (typeof(locals.name) != "undefined") { 961 this.name = locals.name; 962 } 963 if (typeof(locals.format) != "undefined") { 964 this.format = locals.format; 965 } 966 if (typeof(locals.maxLength) != "undefined") { 967 this.maxLength = parseInt(locals.maxLength); 968 } 969 if (typeof(locals.value) != "undefined") { 970 var str = this.value = locals.value; 971 if (this.format) { 972 str = MochiKit.Text.format(this.format, str); 973 } else if (str == null) { 974 str = "null"; 975 } else if (typeof(str) != "string") { 976 str = str.toString(); 977 } 978 var longStr = str; 979 if (this.maxLength > 0) { 980 str = MochiKit.Text.truncate(str, this.maxLength, "..."); 981 } 982 MochiKit.DOM.replaceChildNodes(this, str); 983 this.title = (str == longStr) ? null : longStr; 984 } 985 MochiKit.DOM.updateNodeAttributes(this, attrs); 986 } 987 988 /** 989 * Resets the field value to the initial value. 990 */ 991 MochiKit.Widget.Field.prototype.reset = function () { 992 this.setAttrs({ value: this.defaultValue }); 993 } 994 995 /** 996 * Creates a new file streamer widget. 997 * 998 * @constructor 999 * @param {Object} attrs the widget and node attributes 1000 * @param {String} [attrs.url] the URL to post the data to, defaults 1001 * to window.location.href 1002 * @param {String} [attrs.name] the file input field name, 1003 * defaults to 'file' 1004 * @param {String} [attrs.size] the file input size, defaults to '30' 1005 * 1006 * @return {Widget} the widget DOM node 1007 * 1008 * @class The file streamer widget class. This widget is used to 1009 * provide a file upload (file input) control that can stream the 1010 * selected file to the server. The file data is always sent 1011 * asynchronously (i.e. in the background) to allow the rest of 1012 * the web page to remain active also during the potentially long 1013 * delays caused by sending large amounts of data. The widget 1014 * creates its own IFRAME HTML element inside which the actual 1015 * FORM and INPUT elements are created automatically. In addition 1016 * to standard HTML events, the "onselect" and "onupload" events 1017 * are also triggered. 1018 * @extends MochiKit.Widget 1019 * 1020 * @example 1021 * var file = MochiKit.Widget.FileStreamer({ url: "rapidcontext/upload/myid" }); 1022 * form.addAll(file); 1023 * MochiKit.Signal.connect(file, "onselect", function () { 1024 * file.hide(); 1025 * // add code to show progress bar 1026 * }); 1027 */ 1028 MochiKit.Widget.FileStreamer = function (attrs) { 1029 var defs = { src: "about:blank", scrolling: "no", 1030 border: "0", frameborder: "0" }; 1031 var o = MochiKit.DOM.createDOM("iframe", defs); 1032 MochiKit.Widget._widgetMixin(o, arguments.callee); 1033 o.addClass("widgetFileStreamer"); 1034 o.setAttrs(MochiKit.Base.update({ url: "", name: "file", size: "30" }, attrs)); 1035 // TODO: create some kind of utility function for these idioms 1036 var links = MochiKit.Selector.findDocElements("link[href*=widget.css]"); 1037 if (links.length > 0) { 1038 o.cssUrl = links[0].href; 1039 } 1040 o.onload = o._handleLoad; 1041 return o; 1042 } 1043 1044 /** 1045 * Updates the widget or HTML DOM node attributes. 1046 * 1047 * @param {Object} attrs the widget and node attributes to set 1048 * @param {String} [attrs.url] the URL to post the data to 1049 * @param {String} [attrs.name] the file input field name 1050 * @param {String} [attrs.size] the file input size 1051 */ 1052 MochiKit.Widget.FileStreamer.prototype.setAttrs = function (attrs) { 1053 attrs = MochiKit.Base.update({}, attrs); 1054 var locals = MochiKit.Base.mask(attrs, ["url", "name", "size"]); 1055 if (typeof(locals.url) != "undefined") { 1056 this.formUrl = MochiKit.Base.resolveURI(locals.url, window.location.href); 1057 } 1058 if (typeof(locals.name) != "undefined") { 1059 this.inputName = locals.param; 1060 } 1061 if (typeof(locals.size) != "undefined") { 1062 this.inputSize = locals.size; 1063 } 1064 // TODO: update form if already created, or recreate? 1065 MochiKit.DOM.updateNodeAttributes(this, attrs); 1066 } 1067 1068 /** 1069 * Handles the iframe onload event. 1070 */ 1071 MochiKit.Widget.FileStreamer.prototype._handleLoad = function () { 1072 var doc = this.contentDocument; 1073 if (doc.location.href == this.formUrl) { 1074 MochiKit.Widget.emitSignal(this, "onupload"); 1075 } 1076 MochiKit.DOM.withDocument(doc, MochiKit.Base.bind("_initDocument", this)); 1077 } 1078 1079 /** 1080 * Handles the file input onchange event. 1081 */ 1082 MochiKit.Widget.FileStreamer.prototype._handleChange = function () { 1083 MochiKit.Widget.emitSignal(this, "onselect"); 1084 var form = this.contentDocument.getElementsByTagName("form")[0]; 1085 form.submit(); 1086 form.appendChild(MochiKit.Widget.Overlay()); 1087 } 1088 1089 /** 1090 * Creates the document form and file input element. 1091 */ 1092 MochiKit.Widget.FileStreamer.prototype._initDocument = function () { 1093 var doc = this.contentDocument; 1094 var head = doc.getElementsByTagName("head")[0]; 1095 var body = doc.body; 1096 if (head == null) { 1097 head = doc.createElement("head"); 1098 body.parentElement.insertBefore(head, body); 1099 } 1100 var attrs = { rel: "stylesheet", href: this.cssUrl, type: "text/css" }; 1101 var link = MochiKit.DOM.createDOM("link", attrs); 1102 head.appendChild(link); 1103 var attrs = { type: "file", name: this.inputName, size: this.inputSize }; 1104 var input = MochiKit.DOM.INPUT(attrs); 1105 var attrs = { method: "POST", action: this.formUrl, enctype: "multipart/form-data" }; 1106 var form = MochiKit.DOM.FORM(attrs, input); 1107 input.onchange = MochiKit.Base.bind("_handleChange", this); 1108 body.className = "widgetFileStreamer"; 1109 MochiKit.DOM.replaceChildNodes(body, form); 1110 } 1111 1112 /** 1113 * Handles widget resize calls, so that the iframe can be adjusted 1114 * to the file input field. 1115 * 1116 * @private 1117 */ 1118 MochiKit.Widget.FileStreamer.prototype.resizeContent = function () { 1119 var doc = this.contentDocument; 1120 if (doc != null && typeof(doc.getElementsByTagName) === "function") { 1121 var form = doc.getElementsByTagName("form")[0]; 1122 if (form != null) { 1123 var input = form.firstChild; 1124 this.width = input.clientWidth + 2; 1125 this.height = Math.max(24, input.clientHeight); 1126 } 1127 } 1128 } 1129 1130 /** 1131 * Creates a new form widget. 1132 * 1133 * @constructor 1134 * @param {Object} attrs the widget and node attributes 1135 * @param {Object} [...] the child widgets or DOM nodes 1136 * 1137 * @return {Widget} the widget DOM node 1138 * 1139 * @class The form widget class. Provides a grouping for form fields, 1140 * using the <form> HTML element. The form widget supports 1141 * form reset, validation and data retrieval. 1142 * @extends MochiKit.Widget 1143 */ 1144 MochiKit.Widget.Form = function (attrs/*, ...*/) { 1145 var o = MochiKit.DOM.FORM(attrs); 1146 MochiKit.Widget._widgetMixin(o, arguments.callee); 1147 // TODO: Remove this temporary bugfix when the reset() method has 1148 // been renamed or similar... 1149 o.reset = MochiKit.Widget.Form.prototype.reset; 1150 o.addClass("widgetForm"); 1151 o.onsubmit = MochiKit.Widget._eventHandler(null, "_handleSubmit"); 1152 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 1153 return o; 1154 } 1155 1156 /** 1157 * Returns an array with all child DOM nodes containing form fields. 1158 * The child nodes will be returned based on the results of the 1159 * MochiKit.Widget.isFormField() function. 1160 * 1161 * @return {Array} the array of form field elements 1162 */ 1163 MochiKit.Widget.Form.prototype.fields = function () { 1164 var fields = []; 1165 MochiKit.Base.nodeWalk(this, function (elem) { 1166 if (elem.nodeType !== 1) { // !Node.ELEMENT_NODE 1167 return null; 1168 } 1169 if (MochiKit.Widget.isFormField(elem)) { 1170 fields.push(elem); 1171 return null; 1172 } else { 1173 return elem.childNodes; 1174 } 1175 }); 1176 return fields; 1177 } 1178 1179 /** 1180 * Returns a map with all child DOM nodes containing form fields with 1181 * a name attribute. If multiple fields have the same name, the 1182 * returned map will contain an array with all matching fields. 1183 * 1184 * @return {Object} the map of form field elements 1185 */ 1186 MochiKit.Widget.Form.prototype.fieldMap = function () { 1187 var fields = this.fields(); 1188 var map = {}; 1189 for (var i = 0; i < fields.length; i++) { 1190 var name = fields[i].name; 1191 if (typeof(name) == "string") { 1192 if (map[name] instanceof Array) { 1193 map[name].push(fields[i]); 1194 } else if (map[name] != null) { 1195 map[name] = [map[name], fields[i]]; 1196 } else { 1197 map[name] = fields[i]; 1198 } 1199 } 1200 } 1201 return map; 1202 } 1203 1204 /** 1205 * Resets all fields in the form to their default values. 1206 */ 1207 MochiKit.Widget.Form.prototype.reset = function () { 1208 this.validateReset(); 1209 var fields = this.fields(); 1210 for (var i = 0; i < fields.length; i++) { 1211 var elem = fields[i]; 1212 // TODO: generic form field value setting 1213 if (typeof(elem.reset) == "function") { 1214 elem.reset(); 1215 } else if (elem.type == "radio" && typeof(elem.defaultChecked) == "boolean") { 1216 elem.checked = elem.defaultChecked; 1217 } else if (elem.type == "checkbox" && typeof(elem.defaultChecked) == "boolean") { 1218 elem.checked = elem.defaultChecked; 1219 } else if (typeof(elem.defaultValue) == "string") { 1220 if (typeof(elem.setAttrs) == "function") { 1221 elem.setAttrs({ value: elem.defaultValue }); 1222 } else { 1223 elem.value = elem.defaultValue; 1224 } 1225 } else if (elem.options != null) { 1226 for (var j = 0; j < elem.options.length; j++) { 1227 var opt = elem.options[j]; 1228 opt.selected = opt.defaultSelected; 1229 } 1230 } 1231 } 1232 } 1233 1234 /** 1235 * Returns a map with all form field values. If multiple fields have 1236 * the same name, the value will be set to an array of all values. 1237 * Any unchecked checkbox or radiobutton will be also be ignored. 1238 * 1239 * @return {Object} the map of form field values 1240 */ 1241 MochiKit.Widget.Form.prototype.valueMap = function () { 1242 var fields = this.fields(); 1243 var map = {}; 1244 for (var i = 0; i < fields.length; i++) { 1245 var name = fields[i].name; 1246 // TODO: use generic field value retrieval 1247 var value = ""; 1248 if (typeof(fields[i].getValue) == "function") { 1249 value = fields[i].getValue(); 1250 } else { 1251 value = fields[i].value; 1252 } 1253 if (fields[i].type === "radio" || fields[i].type === "checkbox") { 1254 if (fields[i].checked) { 1255 value = value || true; 1256 } else { 1257 value = null; 1258 } 1259 } 1260 if (typeof(name) == "string" && value != null) { 1261 if (map[name] instanceof Array) { 1262 map[name].push(value); 1263 } else if (map[name] != null) { 1264 map[name] = [map[name], value]; 1265 } else { 1266 map[name] = value; 1267 } 1268 } 1269 } 1270 return map; 1271 } 1272 1273 /** 1274 * Updates the fields in this form with a specified map of values. 1275 * If multiple fields have the same name, the value will be set to 1276 * all of them. 1277 * 1278 * @param {Object} values the map of form field values 1279 */ 1280 MochiKit.Widget.Form.prototype.update = function (values) { 1281 var fields = this.fields(); 1282 for (var i = 0; i < fields.length; i++) { 1283 var elem = fields[i]; 1284 if (elem.name in values) { 1285 var value = values[elem.name]; 1286 // TODO: generic form field value setting 1287 if (elem.type === "radio" || elem.type === "checkbox") { 1288 if (value == null) { 1289 elem.checked = false; 1290 } else if (MochiKit.Base.isArrayLike(value)) { 1291 elem.checked = (MochiKit.Base.findValue(value, elem.value) >= 0); 1292 } else { 1293 elem.checked = (elem.value === value || value === true); 1294 } 1295 } else { 1296 if (MochiKit.Base.isArrayLike(value)) { 1297 value = value.join(", "); 1298 } 1299 if (typeof(elem.setAttrs) == "function") { 1300 elem.setAttrs({ value: value }); 1301 } else { 1302 elem.value = value; 1303 } 1304 } 1305 } 1306 } 1307 } 1308 1309 /** 1310 * Returns an array with all child DOM nodes containing form 1311 * validator widgets. 1312 * 1313 * @return {Array} the array of form validator widgets 1314 */ 1315 MochiKit.Widget.Form.prototype.validators = function () { 1316 var res = []; 1317 var elems = this.getElementsByTagName("SPAN"); 1318 for (var i = 0; i < elems.length; i++) { 1319 if (MochiKit.Widget.isWidget(elems[i], "FormValidator")) { 1320 res.push(elems[i]); 1321 } 1322 } 1323 return res; 1324 } 1325 1326 /** 1327 * Validates this form using the form validators found. 1328 * 1329 * @return {Boolean/MochiKit.Async.Deferred} true if the form 1330 * validated successfully, false if the validation failed, 1331 * or a MochiKit.Async.Deferred instance if the validation 1332 * was deferred 1333 */ 1334 MochiKit.Widget.Form.prototype.validate = function () { 1335 var validators = this.validators(); 1336 var fields = this.fields(); 1337 var success = true; 1338 var defers = []; 1339 for (var i = 0; i < validators.length; i++) { 1340 validators[i].reset(); 1341 } 1342 for (var i = 0; i < validators.length; i++) { 1343 for (var j = 0; j < fields.length; j++) { 1344 if (validators[i].name == fields[j].name) { 1345 var res = validators[i].verify(fields[j]); 1346 if (res instanceof MochiKit.Async.Deferred) { 1347 defers.push(res); 1348 } else if (res === false) { 1349 success = false; 1350 } 1351 } 1352 } 1353 } 1354 if (!success) { 1355 return false; 1356 } else if (defers.length > 0) { 1357 return MochiKit.Async.gatherResults(defers); 1358 } else { 1359 return true; 1360 } 1361 } 1362 1363 /** 1364 * Resets all form validators. 1365 */ 1366 MochiKit.Widget.Form.prototype.validateReset = function () { 1367 var validators = this.validators(); 1368 for (var i = 0; i < validators.length; i++) { 1369 validators[i].reset(); 1370 } 1371 } 1372 1373 /** 1374 * Handles the form submit signal. 1375 * 1376 * @param {Event} evt the MochiKit.Signal.Event object 1377 */ 1378 MochiKit.Widget.Form.prototype._handleSubmit = function (evt) { 1379 evt.stop(); 1380 return false; 1381 } 1382 1383 /** 1384 * Creates a new form validator widget. 1385 * 1386 * @constructor 1387 * @param {Object} attrs the widget and node attributes 1388 * @param {String} attrs.name the form field name to validate 1389 * @param {Boolean} [attrs.mandatory] the mandatory field flag, 1390 * defaults to true 1391 * @param {String/RegExp} [attrs.regex] the regular expression to 1392 * match the field value against, defaults to null 1393 * @param {String} [attrs.display] the validator display setting 1394 * (either "none" or "error"), defaults to "error" 1395 * @param {String} [attrs.message] the message to display, defaults 1396 * to the validator function error message 1397 * @param {Function} [attrs.validator] the validator function 1398 * 1399 * @return {Widget} the widget DOM node 1400 * 1401 * @class The form validator widget class. Provides visual feedback 1402 * on form validation failures, using a <span> HTML 1403 * element. It is normally hidden by default and may be 1404 * configured to only modify its related form field. 1405 * @property {String} name The form field name to validate. 1406 * @property {String} message The default validation message. 1407 * @property {Function} validator The validator function in use. 1408 * @extends MochiKit.Widget 1409 */ 1410 MochiKit.Widget.FormValidator = function (attrs) { 1411 var o = MochiKit.DOM.SPAN(); 1412 MochiKit.Widget._widgetMixin(o, arguments.callee); 1413 o.addClass("widgetFormValidator"); 1414 o.setAttrs(MochiKit.Base.update({ name: "", mandatory: true, display: "error", message: null, validator: null }, attrs)); 1415 o.fields = []; 1416 o.hide(); 1417 return o; 1418 } 1419 1420 /** 1421 * Updates the widget or HTML DOM node attributes. 1422 * 1423 * @param {Object} attrs the widget and node attributes to set 1424 * @param {String} [attrs.name] the form field name to validate 1425 * @param {Boolean} [attrs.mandatory] the mandatory field flag 1426 * @param {String/RegExp} [attrs.regex] the regular expression to 1427 * match the field value against 1428 * @param {String} [attrs.display] the validator display setting 1429 * (either "none" or "error") 1430 * @param {String} [attrs.message] the message to display 1431 * @param {Function} [attrs.validator] the validator function 1432 */ 1433 MochiKit.Widget.FormValidator.prototype.setAttrs = function (attrs) { 1434 attrs = MochiKit.Base.update({}, attrs); 1435 var locals = MochiKit.Base.mask(attrs, ["name", "mandatory", "regex", "display", "message", "validator"]); 1436 if (typeof(locals.name) != "undefined") { 1437 this.name = locals.name; 1438 } 1439 if (typeof(locals.mandatory) != "undefined") { 1440 this.mandatory = MochiKit.Base.bool(locals.mandatory); 1441 } 1442 if (typeof(locals.regex) != "undefined") { 1443 if (locals.regex instanceof RegExp) { 1444 this.regex = locals.regex; 1445 } else { 1446 if (locals.regex.indexOf("^") != 0) { 1447 locals.regex = "^" + locals.regex; 1448 } 1449 if (locals.regex.indexOf("$") != locals.regex.length - 1) { 1450 locals.regex += "$"; 1451 } 1452 this.regex = new RegExp(locals.regex); 1453 } 1454 } 1455 if (typeof(locals.display) != "undefined") { 1456 this.display = locals.display; 1457 } 1458 if (typeof(locals.message) != "undefined") { 1459 this.message = locals.message; 1460 } 1461 if (typeof(locals.validator) != "undefined") { 1462 this.validator = locals.validator; 1463 } 1464 MochiKit.DOM.updateNodeAttributes(this, attrs); 1465 } 1466 1467 /** 1468 * Resets this form validator. This will hide any error messages and 1469 * mark all invalidated fields as valid. 1470 */ 1471 MochiKit.Widget.FormValidator.prototype.reset = function () { 1472 for (var i = 0; i < this.fields.length; i++) { 1473 MochiKit.DOM.removeElementClass(this.fields[i], "widgetInvalid"); 1474 } 1475 this.fields = []; 1476 if (this.display === "error") { 1477 this.hide(); 1478 this.removeAll(); 1479 } 1480 } 1481 1482 /** 1483 * Verifies a form field with this validator. If the form field 1484 * value doesn't match this validator, the field will be invalidated 1485 * until this validator is reset. 1486 * 1487 * @param {Widget/Node} field the form field DOM node 1488 * 1489 * @return {Boolean/MochiKit.Async.Deferred} true if the form 1490 * validated successfully, false if the validation failed, 1491 * or a MochiKit.Async.Deferred instance if the validation 1492 * was deferred 1493 */ 1494 MochiKit.Widget.FormValidator.prototype.verify = function (field) { 1495 if (!field.disabled) { 1496 // TODO: use generic field value retrieval 1497 var value = ""; 1498 if (typeof(field.getValue) == "function") { 1499 value = field.getValue(); 1500 } else { 1501 value = field.value; 1502 } 1503 var stripped = MochiKit.Format.strip(value); 1504 if (MochiKit.Format.strip(value) == "") { 1505 if (this.mandatory) { 1506 var msg = "This field is mandatory and cannot be left blank"; 1507 this.addError(field, msg); 1508 return false; 1509 } 1510 } else if (this.regex != null && !this.regex.test(stripped)) { 1511 var msg = "The field format is incorrect"; 1512 this.addError(field, msg); 1513 return false; 1514 } else if (typeof(this.validator) == "function") { 1515 var res = this.validator(value); 1516 if (res instanceof MochiKit.Async.Deferred) { 1517 var self = this; 1518 res.addErrback(function (e) { 1519 self.addError(field, e.message); 1520 return e; 1521 }); 1522 return res; 1523 } else if (typeof(res) == "string") { 1524 this.addError(field, res); 1525 return false; 1526 } else if (res === false) { 1527 this.addError(field, "Field validation failed"); 1528 return false; 1529 } 1530 } 1531 } 1532 return true; 1533 } 1534 1535 /** 1536 * Adds a validation error message for the specified field. If the 1537 * field is already invalid, this method will not do anything. 1538 * 1539 * @param {Widget/Node} field the field DOM node 1540 * @param {String} message the validation error message 1541 */ 1542 MochiKit.Widget.FormValidator.prototype.addError = function (field, message) { 1543 if (!MochiKit.DOM.hasElementClass(field, "widgetInvalid")) { 1544 this.fields.push(field); 1545 MochiKit.DOM.addElementClass(field, "widgetInvalid"); 1546 if (this.display === "error") { 1547 var attrs = { ref: "ERROR", tooltip: this.message || message }; 1548 this.addAll(new MochiKit.Widget.Icon(attrs)); 1549 this.show(); 1550 } 1551 } 1552 } 1553 1554 /** 1555 * Creates a new icon widget. 1556 * 1557 * @constructor 1558 * @param {Object} attrs the widget and node attributes 1559 * @param {String} [attrs.ref] the referenced icon definition 1560 * @param {String} [attrs.src] the icon image source URL (unmodified) 1561 * @param {String} [attrs.url] the icon image file URL, prepended by 1562 * the "baseUrl" (that is inherited from the default icon) 1563 * @param {String} [attrs.baseUrl] the icon image base URL, used only 1564 * to prepend to "url" (normally only specified in the 1565 * default icon) 1566 * @param {String} [attrs.tooltip] the icon tooltip text 1567 * 1568 * @return {Widget} the widget DOM node 1569 * 1570 * @class The icon widget class. Used to provide a small clickable 1571 * image, using the <img> HTML element. In particular, 1572 * the "onclick" event is usually of interest. Predefined icon 1573 * images for variuos purposes are available as constants. 1574 * @extends MochiKit.Widget 1575 */ 1576 MochiKit.Widget.Icon = function (attrs) { 1577 var o = MochiKit.DOM.IMG(); 1578 MochiKit.Widget._widgetMixin(o, arguments.callee); 1579 o.setAttrs(attrs); 1580 o.addClass("widgetIcon"); 1581 return o; 1582 } 1583 1584 /** 1585 * Updates the icon or HTML DOM node attributes. 1586 * 1587 * @param {Object} attrs the widget and node attributes to set 1588 * @param {String} [attrs.ref] the referenced icon definition 1589 * @param {String} [attrs.src] the icon image source URL (unmodified) 1590 * @param {String} [attrs.url] the icon image file URL, prepended by 1591 * the "baseUrl" (that is inherited from the default icon) 1592 * @param {String} [attrs.baseUrl] the icon image base URL, used only 1593 * to prepend to "url" (normally only specified in the 1594 * default icon) 1595 * @param {String} [attrs.tooltip] the icon tooltip text 1596 */ 1597 MochiKit.Widget.Icon.prototype.setAttrs = function (attrs) { 1598 attrs = MochiKit.Base.update({}, attrs); 1599 if (attrs.ref) { 1600 MochiKit.Base.setdefault(attrs, 1601 MochiKit.Widget.Icon[attrs.ref], 1602 MochiKit.Widget.Icon.DEFAULT); 1603 } 1604 var locals = MochiKit.Base.mask(attrs, ["ref", "url", "baseUrl", "tooltip", "width", "height"]); 1605 if (typeof(locals.url) != "undefined") { 1606 MochiKit.Base.setdefault(locals, MochiKit.Widget.Icon.DEFAULT); 1607 attrs.src = locals.baseUrl + locals.url; 1608 } 1609 if (typeof(locals.tooltip) != "undefined") { 1610 attrs.alt = locals.tooltip; 1611 attrs.title = locals.tooltip; 1612 } 1613 /* TODO: Fix width and height for IE, as it seems that the 1614 values set by setAttribute() are ignored. */ 1615 if (typeof(locals.width) != "undefined") { 1616 this.width = locals.width; 1617 this.setStyle({ width: locals.width + "px" }); 1618 } 1619 if (typeof(locals.height) != "undefined") { 1620 this.height = locals.height; 1621 this.setStyle({ height: locals.height + "px" }); 1622 } 1623 MochiKit.DOM.updateNodeAttributes(this, attrs); 1624 } 1625 1626 /** 1627 * @scope MochiKit.Widget.Icon.prototype 1628 */ 1629 MochiKit.Base.update(MochiKit.Widget.Icon, { 1630 /** The default icon definition, inherited by all others. */ 1631 DEFAULT: { baseUrl: "images/icons/", width: "16", height: "16" }, 1632 /** The blank icon definition. */ 1633 BLANK: { url: "blank.gif", style: { cursor: "default" } }, 1634 /** The close icon definition. */ 1635 CLOSE: { url: "close.gif" }, 1636 /** The resize icon definition. */ 1637 RESIZE: { url: "resize-handle.gif", style: { cursor: "se-resize" } }, 1638 /** The ok icon definition. */ 1639 OK: { url: "ok.gif", tooltip: "OK" }, 1640 /** The cancel icon definition. */ 1641 CANCEL: { url: "cancel.gif", tooltip: "Cancel" }, 1642 /** The help icon definition. */ 1643 HELP: { url: "help.gif", tooltip: "Help" }, 1644 /** The error icon definition. */ 1645 ERROR: { url: "error.gif", tooltip: "Error" }, 1646 /** The plus icon definition. */ 1647 PLUS: { url: "plus.gif", tooltip: "Show" }, 1648 /** The minus icon definition. */ 1649 MINUS: { url: "minus.gif", tooltip: "Hide" }, 1650 /** The next icon definition. */ 1651 NEXT: { url: "next.gif", tooltip: "Next" }, 1652 /** The previuos icon definition. */ 1653 PREVIOUS: { url: "previous.gif", tooltip: "Previous" }, 1654 /** The config icon definition. */ 1655 CONFIG: { url: "config.gif", tooltip: "Configure" }, 1656 /** The delay icon definition. */ 1657 DELAY: { url: "delay.gif", tooltip: "Configure Delay" }, 1658 /** The reload icon definition. */ 1659 RELOAD: { url: "reload.gif", tooltip: "Reload" }, 1660 /** The loading icon definition. */ 1661 LOADING: { url: "loading.gif", tooltip: "Loading..." }, 1662 /** The large loading icon definition. */ 1663 LOADING_LARGE: { url: "loading-large.gif", tooltip: "Loading...", width: "32", height: "32" }, 1664 /** The search icon definition. */ 1665 SEARCH: { url: "magnifier.gif", tooltip: "Search" }, 1666 /** The add icon definition. */ 1667 ADD: { url: "add.gif", tooltip: "Add" }, 1668 /** The remove icon definition. */ 1669 REMOVE: { url: "remove.gif", tooltip: "Remove" }, 1670 /** The edit icon definition. */ 1671 EDIT: { url: "edit.gif", tooltip: "Edit" }, 1672 /** The delete icon definition. */ 1673 DELETE: { url: "trash.gif", tooltip: "Clear / Delete" }, 1674 /** The select icon definition. */ 1675 SELECT: { url: "select.gif", tooltip: "Select / Unselect" }, 1676 /** The cut icon definition. */ 1677 CUT: { url: "cut.gif", tooltip: "Cut" }, 1678 /** The config icon definition. */ 1679 DIALOG: { url: "dialog.gif", tooltip: "Open Dialog" }, 1680 /** The export icon definition. */ 1681 EXPORT: { url: "export.gif", tooltip: "Export" }, 1682 /** The expand icon definition. */ 1683 EXPAND: { url: "expand.gif", tooltip: "Expand" }, 1684 /** The up icon definition. */ 1685 UP: { url: "up.gif", tooltip: "Move Up" }, 1686 /** The down icon definition. */ 1687 DOWN: { url: "down.gif", tooltip: "Move Down" }, 1688 /** The left icon definition. */ 1689 LEFT: { url: "left.gif", tooltip: "Move Left" }, 1690 /** The right icon definition. */ 1691 RIGHT: { url: "right.gif", tooltip: "Move Right" }, 1692 /** The comment icon definition. */ 1693 COMMENT: { url: "comment.gif", tooltip: "Comment" }, 1694 /** The calendar icon definition. */ 1695 CALENDAR: { url: "calendar.gif", tooltip: "Calendar" }, 1696 /** The automatic icon definition. */ 1697 AUTOMATIC: { url: "automatic.gif", tooltip: "Automatic Processing" }, 1698 /** The plugin icon definition. */ 1699 PLUGIN: { url: "plugin.gif", tooltip: "Plug-in" }, 1700 /** The folder icon definition. */ 1701 FOLDER: { url: "folder.gif" }, 1702 /** The document icon definition. */ 1703 DOCUMENT: { url: "document.gif" } 1704 }); 1705 1706 /** 1707 * Creates a new overlay widget. 1708 * 1709 * @constructor 1710 * @param {Object} attrs the widget and node attributes 1711 * @param {Boolean} [attrs.loading] the display loading icon flag, 1712 * defaults to true 1713 * @param {String} [attrs.message] the overlay message text, defaults 1714 * to "Working..." 1715 * @param {Widget} [...] the child widgets or DOM nodes 1716 * 1717 * @return {Widget} the widget DOM node 1718 * 1719 * @class The overlay widget class. Used to provide a layer on top 1720 * of the parent node, using a <div> HTML element. This 1721 * widget is useful for disabling the user interface during an 1722 * operation. 1723 * @extends MochiKit.Widget 1724 */ 1725 MochiKit.Widget.Overlay = function (attrs/*, ...*/) { 1726 var msg = MochiKit.DOM.DIV({ "class": "widgetOverlayMessage" }); 1727 var o = MochiKit.DOM.DIV({}, msg); 1728 MochiKit.Widget._widgetMixin(o, arguments.callee); 1729 o.addClass("widgetOverlay"); 1730 attrs = MochiKit.Base.update({ loading: true, message: "Working..." }, attrs); 1731 o.setAttrs(attrs); 1732 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 1733 return o; 1734 } 1735 1736 /** 1737 * Updates the widget or HTML DOM node attributes. 1738 * 1739 * @param {Object} attrs the widget and node attributes to set 1740 * @param {Boolean} [attrs.loading] the display loading icon flag 1741 * @param {String} [attrs.message] the overlay message text 1742 */ 1743 MochiKit.Widget.Overlay.prototype.setAttrs = function (attrs) { 1744 attrs = MochiKit.Base.update({}, attrs); 1745 var locals = MochiKit.Base.mask(attrs, ["loading", "message"]); 1746 if (typeof(locals.loading) != "undefined") { 1747 this.showLoading = MochiKit.Base.bool(locals.loading); 1748 } 1749 if (typeof(locals.message) != "undefined") { 1750 this.message = locals.message; 1751 } 1752 if (typeof(this.showLoading) != "undefined") { 1753 var icon = new MochiKit.Widget.Icon({ ref: "LOADING_LARGE" }); 1754 icon.setStyle({ "padding-right": "20px" }); 1755 } 1756 MochiKit.DOM.replaceChildNodes(this.firstChild, icon, this.message); 1757 MochiKit.DOM.updateNodeAttributes(this, attrs); 1758 } 1759 1760 /** 1761 * Creates a new pane widget. 1762 * 1763 * @constructor 1764 * @param {Object} attrs the widget and node attributes 1765 * @param {String} [attrs.pageTitle] the page title used when inside 1766 * a page container, defaults to "Page" 1767 * @param {String/Object} [attrs.pageStatus] the page status used 1768 * when inside a page container, use one of the predefined 1769 * status constants in this class, defaults to "ANY" 1770 * @param {Boolean} [attrs.pageCloseable] the page closeable flag 1771 * used when inside some page containers, defaults to 1772 * false 1773 * @param {Object} [...] the child widgets or DOM nodes 1774 * 1775 * @return {Widget} the widget DOM node 1776 * 1777 * @class The pane widget class. Used to create the simplest form of 1778 * element container. It is also used inside various types of 1779 * paged containers, such as a TabContainer, a Wizard and 1780 * similar. A pane only uses a <div> HTML element, and 1781 * supports being hidden and shown according to any page 1782 * transitions required by a parent container. In addition to 1783 * standard HTML events, the "onenter" and "onexit" events are 1784 * triggered whenever the pane is used in a parent container 1785 * page transition. 1786 * @property {String} pageTitle [read-only] The current page title. 1787 * @property {Object} pageStatus [read-only] The current page status. 1788 * @property {Boolean} pageCloseable [read-only] The current page 1789 * closeable flag value. 1790 * @extends MochiKit.Widget 1791 */ 1792 MochiKit.Widget.Pane = function (attrs/*, ... */) { 1793 var o = MochiKit.DOM.DIV(); 1794 MochiKit.Widget._widgetMixin(o, arguments.callee); 1795 o.addClass("widgetPane"); 1796 o.setAttrs(MochiKit.Base.update({ pageTitle: "Page", pageStatus: "ANY", pageCloseable: false }, attrs)); 1797 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 1798 return o; 1799 } 1800 1801 /** 1802 * The default page status. Allows page transitions both to the 1803 * previous and the next page. 1804 * 1805 * @memberOf MochiKit.Widget.Pane 1806 * @name ANY 1807 * @static 1808 */ 1809 MochiKit.Widget.Pane.ANY = { previous: true, next: true }; 1810 1811 /** 1812 * The forward-only page status. Allows transitions only to the next 1813 * page. 1814 * 1815 * @memberOf MochiKit.Widget.Pane 1816 * @name FORWARD 1817 * @static 1818 */ 1819 MochiKit.Widget.Pane.FORWARD = { previous: false, next: true }; 1820 1821 /** 1822 * The backward-only page status. Allows transitions only to the 1823 * previous page. 1824 * 1825 * @memberOf MochiKit.Widget.Pane 1826 * @name BACKWARD 1827 * @static 1828 */ 1829 MochiKit.Widget.Pane.BACKWARD = { previous: true, next: false }; 1830 1831 /** 1832 * The working page status. Will disable transitions both to the 1833 * previous and the next page. The page container may also display a 1834 * cancel button to allow user cancellation of the ongoing operation. 1835 * 1836 * @memberOf MochiKit.Widget.Pane 1837 * @name WORKING 1838 * @static 1839 */ 1840 MochiKit.Widget.Pane.WORKING = { previous: false, next: false }; 1841 1842 /** 1843 * Updates the widget or HTML DOM node attributes. 1844 * 1845 * @param {Object} attrs the widget and node attributes to set 1846 * @param {String} [attrs.pageTitle] the page title used when inside 1847 * a page container 1848 * @param {String/Object} [attrs.pageStatus] the page status used 1849 * when inside a page container, use one of the predefined 1850 * status constants in this class 1851 * @param {Boolean} [attrs.pageCloseable] the page closeable flag 1852 * used when inside some page containers 1853 */ 1854 MochiKit.Widget.Pane.prototype.setAttrs = function (attrs) { 1855 attrs = MochiKit.Base.update({}, attrs); 1856 var locals = MochiKit.Base.mask(attrs, ["pageTitle", "pageStatus", "pageCloseable"]); 1857 var modified = false; 1858 if (typeof(locals.pageTitle) != "undefined") { 1859 this.pageTitle = locals.pageTitle; 1860 modified = true; 1861 } 1862 if (typeof(locals.pageStatus) != "undefined") { 1863 if (typeof(locals.pageStatus) == "string") { 1864 locals.pageStatus = MochiKit.Widget.Pane[locals.pageStatus]; 1865 } 1866 this.pageStatus = locals.pageStatus; 1867 modified = true; 1868 } 1869 if (typeof(locals.pageCloseable) != "undefined") { 1870 this.pageCloseable = MochiKit.Base.bool(locals.pageCloseable); 1871 modified = true; 1872 } 1873 if (modified && this.parentNode && 1874 typeof(this.parentNode._updateStatus) == "function") { 1875 this.parentNode._updateStatus(); 1876 } 1877 MochiKit.DOM.updateNodeAttributes(this, attrs); 1878 } 1879 1880 /** 1881 * Handles the page enter event. This method is called by a parent 1882 * page container widget, such as a TabContainer or a Wizard. It will 1883 * reset the form validations (optional), show the pane (optional), 1884 * and finally trigger the "onenter" event. 1885 * 1886 * @param {Object} opts the page transition options 1887 * @param {Boolean} [opts.show] the show pane flag, defaults to true 1888 * @param {Boolean} [opts.validateReset] the form validation reset 1889 * flag, used to clear all form validations in the pane 1890 */ 1891 MochiKit.Widget.Pane.prototype._handleEnter = function (opts) { 1892 opts = MochiKit.Base.update({ show: true, validateReset: false }, opts); 1893 if (MochiKit.Base.bool(opts.validateReset)) { 1894 var forms = this.getElementsByTagName("FORM"); 1895 for (var i = 0; i < forms.length; i++) { 1896 if (typeof(forms[i].validateReset) == "function") { 1897 forms[i].validateReset(); 1898 } 1899 } 1900 } 1901 if (MochiKit.Base.bool(opts.show)) { 1902 this.show(); 1903 MochiKit.Style.resizeElements(this); 1904 } 1905 MochiKit.Widget.emitSignal(this, "onenter"); 1906 } 1907 1908 /** 1909 * Handles the page exit event. This method is called by a parent 1910 * page container widget, such as a TabContainer or a Wizard. It will 1911 * validate the form (optional), unfocus all form fields, hide the 1912 * pane (optional), and finally trigger the "onexit" event. 1913 * 1914 * @param {Object} opts the page transition options 1915 * @param {Boolean} [opts.hide] the hide pane flag, defaults to true 1916 * @param {Boolean} [opts.validate] the form validation flag, used to 1917 * check all forms in the page for valid entries before 1918 * proceeding, defaults to false 1919 * 1920 * @return {Boolean} true if the page exit event completed, or 1921 * false if it was cancelled (due to validation errors) 1922 */ 1923 MochiKit.Widget.Pane.prototype._handleExit = function (opts) { 1924 opts = MochiKit.Base.update({ hide: true, validate: false }, opts); 1925 if (MochiKit.Base.bool(opts.validate)) { 1926 var forms = this.getElementsByTagName("FORM"); 1927 for (var i = 0; i < forms.length; i++) { 1928 if (typeof(forms[i].validate) == "function") { 1929 var res = forms[i].validate(); 1930 // TODO: handle MochiKit.Async.Deferred? 1931 if (!res) { 1932 return false; 1933 } 1934 } 1935 } 1936 } 1937 this.blurAll(); 1938 if (MochiKit.Base.bool(opts.hide)) { 1939 this.hide(); 1940 } 1941 MochiKit.Widget.emitSignal(this, "onexit"); 1942 return true; 1943 } 1944 1945 /** 1946 * Creates a new popup widget. 1947 * 1948 * @constructor 1949 * @param {Object} attrs the widget and node attributes 1950 * @param {Number} [attrs.delay] the widget auto-hide delay in 1951 * milliseconds, defaults to 5000 1952 * @param {Object} [attrs.showAnim] the optional animation options 1953 when showing the popup, defaults to none 1954 * @param {Object} [attrs.hideAnim] the optional animation options 1955 * when hiding the popup, defaults to none 1956 * @param {Widget} [...] the child widgets or DOM nodes 1957 * 1958 * @return {Widget} the widget DOM node 1959 * 1960 * @class The popup widget class. Used to provide a popup menu or 1961 * information area, using a <div> HTML element. The Popup 1962 * widget will automatically disappear after a configurable 1963 * amount of time, unless the user performs keyboard or mouse 1964 * actions related to the popup. In addition to standard HTML 1965 * events, the "onshow" and "onhide" events are triggered when 1966 * the menu has been shown or hidden. 1967 * @extends MochiKit.Widget 1968 */ 1969 MochiKit.Widget.Popup = function (attrs/*, ...*/) { 1970 var o = MochiKit.DOM.DIV(); 1971 MochiKit.Widget._widgetMixin(o, arguments.callee); 1972 o.addClass("widgetPopup", "widgetHidden"); 1973 o.selectedIndex = -1; 1974 o._delayTimer = null; 1975 o.setAttrs(MochiKit.Base.update({ delay: 5000 }, attrs)); 1976 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 1977 MochiKit.Signal.connect(o, "onmousemove", o, "_handleMouseMove"); 1978 MochiKit.Signal.connect(o, "onclick", o, "_handleMouseClick"); 1979 return o; 1980 } 1981 1982 /** 1983 * Updates the widget or HTML DOM node attributes. 1984 * 1985 * @param {Object} attrs the widget and node attributes to set 1986 * @param {Number} [attrs.delay] the widget auto-hide delay in 1987 * milliseconds, defaults to 5000 1988 * @param {Object} [attrs.showAnim] the optional animation options 1989 when showing the popup, defaults to none 1990 * @param {Object} [attrs.hideAnim] the optional animation options 1991 * when hiding the popup, defaults to none 1992 */ 1993 MochiKit.Widget.Popup.prototype.setAttrs = function (attrs) { 1994 attrs = MochiKit.Base.update({}, attrs); 1995 var locals = MochiKit.Base.mask(attrs, ["delay", "showAnim", "hideAnim"]); 1996 if (typeof(locals.delay) != "undefined") { 1997 this.delay = parseInt(locals.delay); 1998 this.resetDelay(); 1999 } 2000 if (typeof(locals.showAnim) != "undefined") { 2001 this.showAnim = locals.showAnim; 2002 } 2003 if (typeof(locals.hideAnim) != "undefined") { 2004 this.hideAnim = locals.hideAnim; 2005 } 2006 MochiKit.DOM.updateNodeAttributes(this, attrs); 2007 } 2008 2009 /** 2010 * Shows the popup. 2011 */ 2012 MochiKit.Widget.Popup.prototype.show = function () { 2013 if (this.isHidden()) { 2014 this.selectChild(-1); 2015 this.removeClass("widgetHidden"); 2016 this.resetDelay(); 2017 if (this.showAnim) { 2018 this.animate(this.showAnim); 2019 } 2020 MochiKit.Style.resetScrollOffset(this, true); 2021 MochiKit.Widget.emitSignal(this, "onshow"); 2022 } else { 2023 this.resetDelay(); 2024 } 2025 } 2026 2027 /** 2028 * Hides the popup. 2029 */ 2030 MochiKit.Widget.Popup.prototype.hide = function () { 2031 if (this.isHidden()) { 2032 this.resetDelay(); 2033 } else { 2034 this.addClass("widgetHidden"); 2035 this.resetDelay(); 2036 if (this.hideAnim) { 2037 this.animate(this.hideAnim); 2038 } 2039 MochiKit.Widget.emitSignal(this, "onhide"); 2040 } 2041 } 2042 2043 /** 2044 * Resets the popup auto-hide timer. Might be called manually when 2045 * receiving events on other widgets related to this one. 2046 */ 2047 MochiKit.Widget.Popup.prototype.resetDelay = function () { 2048 if (this._delayTimer) { 2049 clearTimeout(this._delayTimer); 2050 this._delayTimer = null; 2051 } 2052 if (!this.isHidden() && this.delay > 0) { 2053 this._delayTimer = setTimeout(MochiKit.Base.bind("hide", this), this.delay); 2054 } 2055 } 2056 2057 /** 2058 * Returns the currently selected child node. 2059 * 2060 * @return {Node} the currently selected child node, or 2061 * null if no node is selected 2062 */ 2063 MochiKit.Widget.Popup.prototype.selectedChild = function () { 2064 return MochiKit.DOM.childNode(this, this.selectedIndex); 2065 } 2066 2067 /** 2068 * Marks a popup child as selected. The currently selected child will 2069 * automatically be unselected by this method. 2070 * 2071 * @param {Number/Node} indexOrNode the child node index or DOM node, 2072 * use a negative value to unselect 2073 * 2074 * @return the index of the newly selected child, or 2075 * -1 if none was selected 2076 */ 2077 MochiKit.Widget.Popup.prototype.selectChild = function (indexOrNode) { 2078 var node = this.selectedChild(); 2079 if (node != null) { 2080 MochiKit.DOM.removeElementClass(node, "widgetPopupSelected"); 2081 } 2082 var node = MochiKit.DOM.childNode(this, indexOrNode); 2083 if (typeof(indexOrNode) == "number") { 2084 var index = indexOrNode; 2085 } else { 2086 var index = MochiKit.Base.findIdentical(this.childNodes, node); 2087 } 2088 if (index >= 0 && node != null) { 2089 this.selectedIndex = index; 2090 MochiKit.DOM.addElementClass(node, "widgetPopupSelected"); 2091 var box = { y: node.offsetTop, h: node.offsetHeight + 5 }; 2092 MochiKit.Style.adjustScrollOffset(this, box); 2093 } else { 2094 this.selectedIndex = -1; 2095 } 2096 return this.selectedIndex; 2097 } 2098 2099 /** 2100 * Moves the current selection by a numeric offset. 2101 * 2102 * @param {Number} offset the selection offset (a positive or 2103 * negative number) 2104 * 2105 * @return the index of the newly selected child, or 2106 * -1 if none was selected 2107 */ 2108 MochiKit.Widget.Popup.prototype.selectMove = function (offset) { 2109 var index = this.selectedIndex + offset; 2110 if (index >= this.childNodes.length) { 2111 index = 0; 2112 } 2113 if (index < 0) { 2114 index = this.childNodes.length - 1; 2115 } 2116 return this.selectChild(index); 2117 } 2118 2119 /** 2120 * Handles mouse move events over the popup. 2121 * 2122 * @param {Event} evt the MochiKit.Signal.Event object 2123 */ 2124 MochiKit.Widget.Popup.prototype._handleMouseMove = function (evt) { 2125 this.show(); 2126 var node = MochiKit.DOM.childNode(this, evt.target()); 2127 if (node != null && MochiKit.DOM.hasElementClass(node, "widgetPopupItem")) { 2128 this.selectChild(node); 2129 } else { 2130 this.selectChild(-1); 2131 } 2132 } 2133 2134 /** 2135 * Handles mouse click events on the popup. 2136 * 2137 * @param {Event} evt the MochiKit.Signal.Event object 2138 */ 2139 MochiKit.Widget.Popup.prototype._handleMouseClick = function (evt) { 2140 var node = MochiKit.DOM.childNode(this, evt.target()); 2141 if (node != null && MochiKit.DOM.hasElementClass(node, "widgetPopupItem")) { 2142 this.selectChild(node); 2143 } else { 2144 this.selectChild(-1); 2145 } 2146 } 2147 2148 /** 2149 * Creates a new progress bar widget. 2150 * 2151 * @constructor 2152 * @param {Object} attrs the widget and node attributes 2153 * @param {Number} [attrs.min] the minimum range value, defaults to 0 2154 * @param {Number} [attrs.max] the maximum range value, defaults to 100 2155 * 2156 * @return {Widget} the widget DOM node 2157 * 2158 * @class The progress bar widget class. Used to provide a dynamic 2159 * progress meter, using two <div> HTML elements. The 2160 * progress bar also provides a completion time estimation that 2161 * is displayed in the bar. Whenever the range is modified, the 2162 * time estimation is reset. 2163 * @extends MochiKit.Widget 2164 */ 2165 MochiKit.Widget.ProgressBar = function (attrs) { 2166 var meter = MochiKit.DOM.DIV({ "class": "widgetProgressBarMeter" }); 2167 var text = MochiKit.DOM.DIV({ "class": "widgetProgressBarText" }); 2168 var o = MochiKit.DOM.DIV({}, meter, text); 2169 MochiKit.Widget._widgetMixin(o, arguments.callee); 2170 o.addClass("widgetProgressBar"); 2171 o.setAttrs(MochiKit.Base.update({ min: 0, max: 100 }, attrs)); 2172 o.setValue(0); 2173 return o; 2174 } 2175 2176 /** 2177 * Updates the widget or HTML DOM node attributes. 2178 * 2179 * @param {Object} attrs the widget and node attributes to set 2180 * @param {Number} [attrs.min] the minimum range value, defaults to 0 2181 * @param {Number} [attrs.max] the maximum range value, defaults to 100 2182 */ 2183 MochiKit.Widget.ProgressBar.prototype.setAttrs = function (attrs) { 2184 attrs = MochiKit.Base.update({}, attrs); 2185 var locals = MochiKit.Base.mask(attrs, ["min", "max"]); 2186 if (typeof(locals.min) != "undefined" || typeof(locals.max) != "undefined") { 2187 this.minValue = parseInt(locals.min) || 0; 2188 this.maxValue = parseInt(locals.max) || 100; 2189 this.startTime = new Date().getTime(); 2190 this.lastTime = this.startTime; 2191 this.timeLeft = null; 2192 } 2193 MochiKit.DOM.updateNodeAttributes(this, attrs); 2194 } 2195 2196 /** 2197 * Updates the progress bar completion value. The value should be 2198 * within the range previuosly established by the "min" and "max" 2199 * attributes and is used to calculate a completion ratio. The 2200 * ratio is then used both for updating the progress meter and for 2201 * calculating an approximate remaining time. Any previous progress 2202 * bar text will be replaced by this method. 2203 * 2204 * @param {Number} value the new progress value 2205 * @param {String} [text] the additional information text 2206 */ 2207 MochiKit.Widget.ProgressBar.prototype.setValue = function (value, text) { 2208 value = Math.min(Math.max(value, this.minValue), this.maxValue); 2209 var pos = value - this.minValue; 2210 var total = this.maxValue - this.minValue; 2211 var str = pos + " of " + total; 2212 if (typeof(text) == "string" && text != "") { 2213 str += " \u2014 " + text; 2214 } 2215 this.setRatio(pos / total, str); 2216 } 2217 2218 /** 2219 * Updates the progress bar completion ratio. The ratio value should 2220 * be a floating-point number between 0.0 and 1.0. The ratio is used 2221 * both for updating the progress meter and for calculating an 2222 * approximate remaining time. Any previous progress bar text will 2223 * be replaced by this method. 2224 * 2225 * @param {Number} ratio the new progress ratio, a floating-point number 2226 * between 0.0 and 1.0 2227 * @param {String} [text] the additional information text 2228 */ 2229 MochiKit.Widget.ProgressBar.prototype.setRatio = function (ratio, text) { 2230 var percent = Math.round(ratio * 1000) / 10; 2231 MochiKit.Style.setElementDimensions(this.firstChild, { w: percent }, "%"); 2232 if (percent < 100) { 2233 this.firstChild.className = "widgetProgressBarMeter animated"; 2234 } else { 2235 this.firstChild.className = "widgetProgressBarMeter"; 2236 } 2237 if (typeof(text) == "string" && text != "") { 2238 text = Math.round(percent) + "% \u2014 " + text; 2239 } else { 2240 text = Math.round(percent) + "%"; 2241 } 2242 var nowTime = new Date().getTime(); 2243 if (nowTime - this.lastTime > 1000) { 2244 this.lastTime = nowTime; 2245 var period = nowTime - this.startTime; 2246 period = Math.max(Math.round(period / ratio - period), 0); 2247 this.timeLeft = MochiKit.DateTime.toApproxPeriod(period); 2248 } 2249 if (this.timeLeft != null && percent > 0 && percent < 100) { 2250 text += " \u2014 " + this.timeLeft + " left"; 2251 } 2252 this.setText(text); 2253 } 2254 2255 /** 2256 * Updates the progress bar text. 2257 * 2258 * @param {String} text the new progress bar text 2259 */ 2260 MochiKit.Widget.ProgressBar.prototype.setText = function (text) { 2261 MochiKit.DOM.replaceChildNodes(this.lastChild, text); 2262 } 2263 2264 /** 2265 * Creates a new tab container widget. 2266 * 2267 * @constructor 2268 * @param {Object} attrs the widget and node attributes 2269 * @param {Widget} [...] the child widgets or DOM nodes (should be 2270 * Pane widgets) 2271 * 2272 * @return {Widget} the widget DOM node 2273 * 2274 * @class The tab container widget class. Used to provide a set of 2275 * tabbed pages, where the user can switch page freely. 2276 * Internally it uses a <div> HTML element containing Pane 2277 * widgets that are hidden and shown according to the page 2278 * transitions. If a child Pane widget is "pageCloseable", a 2279 * close button will be available on the tab label and an 2280 * "onclose" signal will be emitted for that node when removed 2281 * from the container. 2282 * @extends MochiKit.Widget 2283 */ 2284 MochiKit.Widget.TabContainer = function (attrs/*, ... */) { 2285 var labels = MochiKit.DOM.DIV({ "class": "widgetTabContainerLabels" }); 2286 var container = MochiKit.DOM.DIV({ "class": "widgetTabContainerContent" }); 2287 var o = MochiKit.DOM.DIV(attrs, labels, container); 2288 MochiKit.Widget._widgetMixin(o, arguments.callee); 2289 o.addClass("widgetTabContainer"); 2290 // TODO: possibly add MSIE size fix? 2291 MochiKit.Style.registerSizeConstraints(container, "100% - 22", "100% - 47"); 2292 container.resizeContent = MochiKit.Base.noop; 2293 o._selectedIndex = -1; 2294 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 2295 return o; 2296 } 2297 2298 /** 2299 * Returns an array with all child pane widgets. Note that the array 2300 * is a real JavaScript array, not a dynamic NodeList. 2301 * 2302 * @return {Array} the array of child DOM nodes 2303 */ 2304 MochiKit.Widget.TabContainer.prototype.getChildNodes = function () { 2305 return MochiKit.Base.extend([], this.lastChild.childNodes); 2306 } 2307 2308 /** 2309 * Adds a single child page widget to this widget. The child widget 2310 * should be a MochiKit.Widget.Pane widget, or it will be added to a 2311 * new one. 2312 * 2313 * @param {Widget} child the page widget to add 2314 */ 2315 MochiKit.Widget.TabContainer.prototype.addChildNode = function (child) { 2316 if (!MochiKit.Widget.isWidget(child, "Pane")) { 2317 child = new MochiKit.Widget.Pane(null, child); 2318 } 2319 MochiKit.Style.registerSizeConstraints(child, "100%", "100%"); 2320 child.hide(); 2321 var text = MochiKit.DOM.SPAN(null, child.pageTitle); 2322 if (child.pageCloseable) { 2323 var icon = new MochiKit.Widget.Icon({ ref: "CLOSE" }); 2324 // TODO: potential memory leak with stale child object references 2325 icon.onclick = MochiKit.Widget._eventHandler("TabContainer", "_handleClose", child); 2326 } 2327 var label = MochiKit.DOM.DIV({ "class": "widgetTabContainerLabel" }, 2328 MochiKit.DOM.DIV({}, text, icon)); 2329 // TODO: potential memory leak with stale child object references 2330 label.onclick = MochiKit.Widget._eventHandler("TabContainer", "selectChild", child); 2331 this.firstChild.appendChild(label); 2332 this.lastChild.appendChild(child); 2333 if (this._selectedIndex < 0) { 2334 this.selectChild(0); 2335 } 2336 } 2337 2338 /** 2339 * Removes a single child DOM node from this widget. This method is 2340 * sometimes overridden by child widgets in order to hide or control 2341 * intermediate DOM nodes required by the widget.<p> 2342 * 2343 * Note that this method will NOT destroy the removed child widget, 2344 * so care must be taken to ensure proper child widget destruction. 2345 * 2346 * @param {Widget/Node} child the DOM node to remove 2347 */ 2348 MochiKit.Widget.TabContainer.prototype.removeChildNode = function (child) { 2349 var children = this.getChildNodes(); 2350 var index = MochiKit.Base.findIdentical(children, child); 2351 if (index < 0) { 2352 throw new Error("Cannot remove DOM node that is not a TabContainer child"); 2353 } 2354 if (this._selectedIndex == index) { 2355 child._handleExit(); 2356 this._selectedIndex = -1; 2357 } 2358 MochiKit.Widget.destroyWidget(this.firstChild.childNodes[index]); 2359 MochiKit.DOM.removeElement(child); 2360 MochiKit.Widget.emitSignal(child, "onclose"); 2361 if (this._selectedIndex > index) { 2362 this._selectedIndex--; 2363 } 2364 if (this._selectedIndex < 0 && this.getChildNodes().length > 0) { 2365 this.selectChild((index == 0) ? 0 : index - 1); 2366 } 2367 } 2368 2369 // TODO: add support for status updates in child pane widget 2370 2371 /** 2372 * Returns the index of the currently selected child in the tab 2373 * container. 2374 * 2375 * @return {Number} the index of the selected child, or 2376 * -1 if no child is selected 2377 */ 2378 MochiKit.Widget.TabContainer.prototype.selectedIndex = function () { 2379 return this._selectedIndex; 2380 } 2381 2382 /** 2383 * Returns the child widget currently selected in the tab container. 2384 * 2385 * @return {Node} the child widget selected, or 2386 * null if no child is selected 2387 */ 2388 MochiKit.Widget.TabContainer.prototype.selectedChild = function () { 2389 var children = this.getChildNodes(); 2390 return (this._selectedIndex < 0) ? null : children[this._selectedIndex]; 2391 } 2392 2393 /** 2394 * Selects a specified child in the tab container. This method can be 2395 * called without arguments to re-select the currently selected tab. 2396 * 2397 * @param {Number/Node} [indexOrChild] the child index or node 2398 */ 2399 MochiKit.Widget.TabContainer.prototype.selectChild = function (indexOrChild) { 2400 var children = this.getChildNodes(); 2401 if (this._selectedIndex >= 0) { 2402 var label = this.firstChild.childNodes[this._selectedIndex]; 2403 MochiKit.DOM.removeElementClass(label, "selected"); 2404 children[this._selectedIndex]._handleExit(); 2405 } 2406 var index = -1; 2407 if (indexOrChild == null) { 2408 index = this._selectedIndex; 2409 } else if (typeof(indexOrChild) == "number") { 2410 index = indexOrChild; 2411 } else { 2412 index = MochiKit.Base.findIdentical(children, indexOrChild); 2413 } 2414 this._selectedIndex = (index < 0 || index >= children.length) ? -1 : index; 2415 if (this._selectedIndex >= 0) { 2416 var label = this.firstChild.childNodes[this._selectedIndex]; 2417 MochiKit.DOM.addElementClass(label, "selected"); 2418 children[this._selectedIndex]._handleEnter(); 2419 } 2420 } 2421 2422 /** 2423 * Resizes the currently selected child. This method need not be called 2424 * directly, but is automatically called whenever a parent node is 2425 * resized. It optimizes the resize chain by only resizing those DOM 2426 * child nodes that are visible, i.e. the currently selected tab 2427 * container child. 2428 */ 2429 MochiKit.Widget.TabContainer.prototype.resizeContent = function () { 2430 MochiKit.Style.resizeElements(this.lastChild); 2431 var child = this.selectedChild(); 2432 if (child != null) { 2433 MochiKit.Style.resizeElements(child); 2434 } 2435 } 2436 2437 /** 2438 * Handles the tab close event. 2439 * 2440 * @param {Node} child the child DOM node 2441 * @param {Event} evt the MochiKit.Signal.Event object 2442 */ 2443 MochiKit.Widget.TabContainer.prototype._handleClose = function (child, evt) { 2444 evt.stop(); 2445 this.removeChildNode(child); 2446 } 2447 2448 /** 2449 * Creates a new data table widget. 2450 * 2451 * @constructor 2452 * @param {Object} attrs the widget and node attributes 2453 * @param {String} [attrs.select] the row selection mode ('none', 'one' or 2454 * 'multiple'), defaults to 'one' 2455 * @param {String} [attrs.key] the unique key identifier column field, 2456 * defaults to null 2457 * @param {Widget} [...] the child table columns 2458 * 2459 * @return {Widget} the widget DOM node 2460 * 2461 * @class The table widget class. Used to provide a sortable and 2462 * scrolling data table, using an outer <div> HTML 2463 * element around a <table>. The Table widget can only 2464 * have TableColumn child nodes, each providing a visible data 2465 * column in the table. In addition to standard HTML events, the 2466 * "onclear" and "onselect" events are triggered when data is 2467 * cleared or selected in the table. 2468 * @extends MochiKit.Widget 2469 * 2470 * @example 2471 * <Table id="exTable" w="50%" h="100%"> 2472 * <TableColumn title="Id" field="id" key="true" type="number" /> 2473 * <TableColumn title="Name" field="name" sort="asc" /> 2474 * <TableColumn title="Creation Date" field="created" type="date" /> 2475 * </Table> 2476 */ 2477 MochiKit.Widget.Table = function (attrs/*, ...*/) { 2478 var thead = MochiKit.DOM.THEAD({}, MochiKit.DOM.TR()); 2479 var tbody = MochiKit.DOM.TBODY(); 2480 tbody.resizeContent = MochiKit.Base.noop; 2481 var table = MochiKit.DOM.TABLE({ "class": "widgetTable" }, thead, tbody); 2482 var o = MochiKit.DOM.DIV({}, table); 2483 MochiKit.Widget._widgetMixin(o, arguments.callee); 2484 o.addClass("widgetTable"); 2485 o._rows = []; 2486 o._data = null; 2487 o._keyField = null; 2488 o._selected = []; 2489 o.setAttrs(MochiKit.Base.update({ select: "one" }, attrs)); 2490 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 2491 tbody.onmousedown = MochiKit.Widget._eventHandler("Table", "_handleSelect"); 2492 return o; 2493 } 2494 2495 /** 2496 * Updates the widget or HTML DOM node attributes. 2497 * 2498 * @param {Object} attrs the widget and node attributes to set 2499 * @param {String} [attrs.select] the row selection mode ('none', 'one' or 2500 * 'multiple') 2501 * @param {String} [attrs.key] the unique key identifier column field 2502 */ 2503 MochiKit.Widget.Table.prototype.setAttrs = function (attrs) { 2504 attrs = MochiKit.Base.update({}, attrs); 2505 var locals = MochiKit.Base.mask(attrs, ["select", "key"]); 2506 if (typeof(locals.select) != "undefined") { 2507 this.select = locals.select; 2508 } 2509 if (typeof(locals.key) != "undefined") { 2510 this.setIdKey(locals.key); 2511 } 2512 MochiKit.DOM.updateNodeAttributes(this, attrs); 2513 } 2514 2515 /** 2516 * Returns an array with all child table column widgets. Note that 2517 * the array is a real JavaScript array, not a dynamic NodeList. 2518 * 2519 * @return {Array} the array of child table column widgets 2520 */ 2521 MochiKit.Widget.Table.prototype.getChildNodes = function () { 2522 var table = this.firstChild; 2523 var thead = table.firstChild; 2524 var tr = thead.firstChild; 2525 return MochiKit.Base.extend([], tr.childNodes); 2526 } 2527 2528 /** 2529 * Adds a single child table column widget to this widget. 2530 * 2531 * @param {Widget} child the table column widget to add 2532 */ 2533 MochiKit.Widget.Table.prototype.addChildNode = function (child) { 2534 if (!MochiKit.Widget.isWidget(child, "TableColumn")) { 2535 throw new Error("Table widget can only have TableColumn children"); 2536 } 2537 this.clear(); 2538 var table = this.firstChild; 2539 var thead = table.firstChild; 2540 var tr = thead.firstChild; 2541 tr.appendChild(child); 2542 } 2543 2544 /** 2545 * Removes a single child table column widget from this widget. 2546 * This will also clear all the data in the table. 2547 * 2548 * @param {Widget} child the table column widget to remove 2549 */ 2550 MochiKit.Widget.Table.prototype.removeChildNode = function (child) { 2551 this.clear(); 2552 var table = this.firstChild; 2553 var thead = table.firstChild; 2554 var tr = thead.firstChild; 2555 tr.removeChild(child); 2556 } 2557 2558 /** 2559 * Returns the column index of a field. 2560 * 2561 * @param {String} field the field name 2562 * 2563 * @return {Number} the column index, or 2564 * -1 if not found 2565 */ 2566 MochiKit.Widget.Table.prototype.getColumnIndex = function (field) { 2567 var cols = this.getChildNodes(); 2568 for (var i = 0; i < cols.length; i++) { 2569 if (cols[i].field === field) { 2570 return i; 2571 } 2572 } 2573 return -1; 2574 } 2575 2576 /** 2577 * Returns the unique key identifier column field, or null if none 2578 * was set. 2579 * 2580 * @return {String} the key column field name, or 2581 * null for none 2582 */ 2583 MochiKit.Widget.Table.prototype.getIdKey = function () { 2584 if (this._keyField) { 2585 return this._keyField; 2586 } 2587 var cols = this.getChildNodes(); 2588 for (var i = 0; i < cols.length; i++) { 2589 if (cols[i].key) { 2590 return cols[i].field; 2591 } 2592 } 2593 return null; 2594 } 2595 2596 /** 2597 * Sets the unique key identifier column field. Note that this 2598 * method will regenerate all row identifiers if the table already 2599 * contains data. 2600 * 2601 * @param {String} key the new key column field name 2602 */ 2603 MochiKit.Widget.Table.prototype.setIdKey = function (key) { 2604 this._keyField = key; 2605 for (var i = 0; this._rows != null && i < this._rows.length; i++) { 2606 var row = this._rows[i]; 2607 if (this._keyField != null && row.$data[this._keyField] != null) { 2608 row.$id = row.$data[this._keyField]; 2609 } 2610 } 2611 } 2612 2613 /** 2614 * Returns the current sort key for the table. 2615 * 2616 * @return {String} the current sort field, or 2617 * null for none 2618 */ 2619 MochiKit.Widget.Table.prototype.getSortKey = function () { 2620 var cols = this.getChildNodes(); 2621 for (var i = 0; i < cols.length; i++) { 2622 if (cols[i].sort != null && cols[i].sort != "none") { 2623 return cols[i].field; 2624 } 2625 } 2626 return null; 2627 } 2628 2629 /** 2630 * Returns a table cell element. 2631 * 2632 * @param {Number} row the row index 2633 * @param {Number} col the column index 2634 * 2635 * @return {Node} the table cell element node, or 2636 * null if not found 2637 */ 2638 MochiKit.Widget.Table.prototype.getCellElem = function (row, col) { 2639 try { 2640 var table = this.firstChild; 2641 var tbody = table.lastChild; 2642 return tbody.childNodes[row].childNodes[col]; 2643 } catch (e) { 2644 return null; 2645 } 2646 } 2647 2648 /** 2649 * Clears all the data in the table. The column headers will not be 2650 * affected by this method. Use removeAll() or removeChildNode() to 2651 * also remove columns. 2652 */ 2653 MochiKit.Widget.Table.prototype.clear = function () { 2654 this.setData([]); 2655 } 2656 2657 /** 2658 * Returns an array with the data in the table. The array returned 2659 * should correspond exactly to the one previously set, i.e. it has 2660 * not been sorted or modified in other ways. 2661 * 2662 * @return {Array} an array with the data in the table 2663 */ 2664 MochiKit.Widget.Table.prototype.getData = function () { 2665 return this._data; 2666 } 2667 2668 /** 2669 * Sets the table data. The table data is an array of objects, each 2670 * having properties corresponding to the table column fields. Any 2671 * object property not mapped to a table column will be ignored (i.e. 2672 * a hidden column). See the TableColumn class for data mapping 2673 * details. 2674 * 2675 * @param {Array} data an array with data objects 2676 * 2677 * @example 2678 * var data = [ 2679 * { id: 1, name: "John Doe", created: "2007-12-31" }, 2680 * { id: 2, name: "First Last", created: "2008-03-01" }, 2681 * { id: 3, name: "Another Name", created: "2009-01-12" } 2682 * ]; 2683 * table.setData(data); 2684 */ 2685 MochiKit.Widget.Table.prototype.setData = function (data) { 2686 var cols = this.getChildNodes(); 2687 var selectedIds = this.getSelectedIds(); 2688 MochiKit.Widget.emitSignal(this, "onclear"); 2689 this._data = data; 2690 this._rows = []; 2691 this._selected = []; 2692 for (var i = 0; data != null && i < data.length; i++) { 2693 var row = { $id: "id" + i, $data: data[i] }; 2694 for (var j = 0; j < cols.length; j++) { 2695 cols[j]._map(data[i], row); 2696 } 2697 if (this._keyField != null && data[i][this._keyField] != null) { 2698 row.$id = data[i][this._keyField]; 2699 } 2700 this._rows.push(row); 2701 } 2702 var key = this.getSortKey(); 2703 if (key) { 2704 this.sortData(key); 2705 } else { 2706 this._renderRows(); 2707 } 2708 if (this.getIdKey() != null) { 2709 this._addSelectedIds(selectedIds); 2710 } 2711 } 2712 2713 /** 2714 * Sorts the table data by field and direction. 2715 * 2716 * @param {String} field the sort field 2717 * @param {String} [direction] the sort direction, either "asc" or 2718 * "desc" 2719 */ 2720 MochiKit.Widget.Table.prototype.sortData = function (field, direction) { 2721 var cols = this.getChildNodes(); 2722 var selectedIds = this.getSelectedIds(); 2723 this._selected = []; 2724 for (var i = 0; i < cols.length; i++) { 2725 if (cols[i].field === field) { 2726 if (cols[i].sort == "none") { 2727 // Skip sorting if not allowed 2728 return; 2729 } else if (direction == null) { 2730 direction = cols[i].sort || "asc"; 2731 } 2732 cols[i].setAttrs({ sort: direction }); 2733 } else if (cols[i].sort != "none") { 2734 cols[i].setAttrs({ sort: null }); 2735 } 2736 } 2737 this._rows.sort(MochiKit.Base.keyComparator(field)); 2738 if (direction == "desc") { 2739 this._rows.reverse(); 2740 } 2741 this._renderRows(); 2742 this._addSelectedIds(selectedIds); 2743 } 2744 2745 /** 2746 * Redraws the table from updated source data. Note that this method 2747 * will not add or remove rows and keeps the current row order 2748 * intact. For a more complete redraw of the table, use setData(). 2749 */ 2750 MochiKit.Widget.Table.prototype.redraw = function () { 2751 var cols = this.getChildNodes(); 2752 for (var i = 0; i < this._rows.length; i++) { 2753 var row = this._rows[i]; 2754 for (var j = 0; j < cols.length; j++) { 2755 cols[j]._map(row.$data, row); 2756 } 2757 } 2758 this._renderRows(); 2759 for (var i = 0; i < this._selected.length; i++) { 2760 this._markSelection(this._selected[i]); 2761 } 2762 } 2763 2764 /** 2765 * Renders the table rows. 2766 */ 2767 MochiKit.Widget.Table.prototype._renderRows = function () { 2768 var cols = this.getChildNodes(); 2769 var tbody = this.firstChild.lastChild; 2770 MochiKit.DOM.replaceChildNodes(tbody); 2771 for (var i = 0; i < this._rows.length; i++) { 2772 var tr = MochiKit.DOM.TR(); 2773 if (i % 2 == 1) { 2774 MochiKit.DOM.addElementClass(tr, "widgetTableAlt"); 2775 } 2776 for (var j = 0; j < cols.length; j++) { 2777 tr.appendChild(cols[j]._render(this._rows[i])); 2778 } 2779 tr.rowNo = i; 2780 tbody.appendChild(tr); 2781 } 2782 if (this._rows.length == 0) { 2783 // Add empty row to avoid browser bugs 2784 tbody.appendChild(MochiKit.DOM.TR()); 2785 } 2786 } 2787 2788 /** 2789 * Returns the currently selected row ids. If no rows are selected, 2790 * an empty array will be returned. The row ids are the data values 2791 * from the key column, or automatically generated internal values 2792 * if no key column is set. 2793 * 2794 * @return {Array} an array with the selected row ids 2795 */ 2796 MochiKit.Widget.Table.prototype.getSelectedIds = function () { 2797 var res = []; 2798 for (var i = 0; i < this._selected.length; i++) { 2799 res.push(this._rows[this._selected[i]].$id); 2800 } 2801 return res; 2802 } 2803 2804 /** 2805 * Returns the currently selected row data. 2806 * 2807 * @return {Object/Array} the data row selected, or 2808 * an array of selected data rows if multiple selection is enabled 2809 */ 2810 MochiKit.Widget.Table.prototype.getSelectedData = function () { 2811 if (this.select === "multiple") { 2812 var res = []; 2813 for (var i = 0; i < this._selected.length; i++) { 2814 res.push(this._rows[this._selected[i]].$data); 2815 } 2816 return res; 2817 } else if (this._selected.length > 0) { 2818 return this._rows[this._selected[0]].$data; 2819 } else { 2820 return null; 2821 } 2822 } 2823 2824 /** 2825 * Sets the selection to the specified row id values. If the current 2826 * selection is changed the select signal will be emitted. 2827 * 2828 * @param {String/Array} [...] the row ids or array with ids to select 2829 * 2830 * @return {Array} an array with the row ids actually modified 2831 */ 2832 MochiKit.Widget.Table.prototype.setSelectedIds = function () { 2833 var args = MochiKit.Base.flattenArguments(arguments); 2834 var ids = MochiKit.Base.dict(args, true); 2835 var oldIds = MochiKit.Base.dict(this.getSelectedIds(), true); 2836 var res = []; 2837 for (var i = 0; i < this._rows.length; i++) { 2838 var rowId = this._rows[i].$id; 2839 if (ids[rowId] && !oldIds[rowId]) { 2840 this._selected.push(i); 2841 this._markSelection(i); 2842 res.push(rowId); 2843 } else if (!ids[rowId] && oldIds[rowId]) { 2844 var pos = MochiKit.Base.findIdentical(this._selected, i); 2845 if (pos >= 0) { 2846 this._selected.splice(pos, 1); 2847 this._unmarkSelection(i); 2848 res.push(rowId); 2849 } 2850 } 2851 } 2852 if (res.length > 0) { 2853 MochiKit.Widget.emitSignal(this, "onselect"); 2854 } 2855 return res; 2856 } 2857 2858 /** 2859 * Adds the specified row id values to the selection. If the current 2860 * selection is changed the select signal will be emitted. 2861 * 2862 * @param {String/Array} [...] the row ids or array with ids to select 2863 * 2864 * @return {Array} an array with the new row ids actually selected 2865 */ 2866 MochiKit.Widget.Table.prototype.addSelectedIds = function () { 2867 var res = this._addSelectedIds(arguments); 2868 if (res.length > 0) { 2869 MochiKit.Widget.emitSignal(this, "onselect"); 2870 } 2871 return res; 2872 } 2873 2874 /** 2875 * Adds the specified row id values to the selection. Note that this 2876 * method does not emit any selection signal. 2877 * 2878 * @param {String/Array} [...] the row ids or array with ids to select 2879 * 2880 * @return {Array} an array with the new row ids actually selected 2881 */ 2882 MochiKit.Widget.Table.prototype._addSelectedIds = function () { 2883 var args = MochiKit.Base.flattenArguments(arguments); 2884 var ids = MochiKit.Base.dict(args, true); 2885 var res = []; 2886 MochiKit.Base.update(ids, MochiKit.Base.dict(this.getSelectedIds(), false)); 2887 for (var i = 0; i < this._rows.length; i++) { 2888 if (ids[this._rows[i].$id]) { 2889 this._selected.push(i); 2890 this._markSelection(i); 2891 res.push(this._rows[i].$id); 2892 } 2893 } 2894 return res; 2895 } 2896 2897 /** 2898 * Removes the specified row id values from the selection. If the 2899 * current selection is changed the select signal will be emitted. 2900 * 2901 * @param {String/Array} [...] the row ids or array with ids to unselect 2902 * 2903 * @return {Array} an array with the row ids actually unselected 2904 */ 2905 MochiKit.Widget.Table.prototype.removeSelectedIds = function () { 2906 var args = MochiKit.Base.flattenArguments(arguments); 2907 var ids = MochiKit.Base.dict(args, true); 2908 var res = []; 2909 for (var i = 0; i < this._rows.length; i++) { 2910 if (ids[this._rows[i].$id]) { 2911 var pos = MochiKit.Base.findIdentical(this._selected, i); 2912 if (pos >= 0) { 2913 this._selected.splice(pos, 1); 2914 this._unmarkSelection(i); 2915 res.push(this._rows[i].$id); 2916 } 2917 } 2918 } 2919 if (res.length > 0) { 2920 MochiKit.Widget.emitSignal(this, "onselect"); 2921 } 2922 return res; 2923 } 2924 2925 /** 2926 * Handles the mouse selection events. 2927 * 2928 * @param {Event} evt the MochiKit.Signal.Event object 2929 */ 2930 MochiKit.Widget.Table.prototype._handleSelect = function (evt) { 2931 var tr = MochiKit.DOM.getFirstParentByTagAndClassName(evt.target(), "TR"); 2932 if (tr == null || tr.rowNo == null || !MochiKit.DOM.isChildNode(tr, this)) { 2933 evt.stop(); 2934 return false; 2935 } 2936 var row = tr.rowNo; 2937 if (this.select === "multiple") { 2938 if (evt.modifier().ctrl || evt.modifier().meta) { 2939 var pos = MochiKit.Base.findIdentical(this._selected, row); 2940 if (pos >= 0) { 2941 this._unmarkSelection(row); 2942 this._selected.splice(pos, 1); 2943 } else { 2944 this._selected.push(row); 2945 this._markSelection(row); 2946 } 2947 } else if (evt.modifier().shift) { 2948 var start = row; 2949 if (this._selected.length > 0) { 2950 start = this._selected[0]; 2951 } 2952 this._unmarkSelection(); 2953 this._selected = []; 2954 if (row >= start) { 2955 for (var i = start; i <= row; i++) { 2956 this._selected.push(i); 2957 } 2958 } else { 2959 for (var i = start; i >= row; i--) { 2960 this._selected.push(i); 2961 } 2962 } 2963 this._markSelection(); 2964 } else { 2965 this._unmarkSelection(); 2966 this._selected = [row]; 2967 this._markSelection(row); 2968 } 2969 } else if (this.select !== "none") { 2970 this._unmarkSelection(); 2971 this._selected = [row]; 2972 this._markSelection(row); 2973 } 2974 evt.stop(); 2975 MochiKit.Widget.emitSignal(this, "onselect"); 2976 return false; 2977 } 2978 2979 /** 2980 * Marks selected rows. 2981 * 2982 * @param {Number} indexOrNull the row index, or null for the array 2983 */ 2984 MochiKit.Widget.Table.prototype._markSelection = function (indexOrNull) { 2985 if (indexOrNull == null) { 2986 for (var i = 0; i < this._selected.length; i++) { 2987 this._markSelection(this._selected[i]); 2988 } 2989 } else { 2990 var tbody = this.firstChild.lastChild; 2991 var tr = tbody.childNodes[indexOrNull]; 2992 MochiKit.DOM.addElementClass(tr, "selected"); 2993 } 2994 } 2995 2996 /** 2997 * Unmarks selected rows. 2998 * 2999 * @param {Number} indexOrNull the row index, or null for the array 3000 */ 3001 MochiKit.Widget.Table.prototype._unmarkSelection = function (indexOrNull) { 3002 if (indexOrNull == null) { 3003 for (var i = 0; i < this._selected.length; i++) { 3004 this._unmarkSelection(this._selected[i]); 3005 } 3006 } else { 3007 var tbody = this.firstChild.lastChild; 3008 var tr = tbody.childNodes[indexOrNull]; 3009 MochiKit.DOM.removeElementClass(tr, "selected"); 3010 } 3011 } 3012 3013 /** 3014 * Creates a new data table column widget. 3015 * 3016 * @constructor 3017 * @param {Object} attrs the widget and node attributes 3018 * @param {String} attrs.title the column title 3019 * @param {String} attrs.field the data property name 3020 * @param {String} [attrs.type] the data property type, one of 3021 * "string", "number", "date", "time", "datetime", 3022 * "boolean" or "object" 3023 * @param {String} [attrs.sort] the sort direction, one of "asc", 3024 * "desc", "none" (disabled) or null (unsorted) 3025 * @param {Number} [attrs.maxLength] the maximum data length, 3026 * overflow will be displayed as a tooltip, only used by 3027 * the default renderer 3028 * @param {Boolean} [attrs.key] the unique key value flag, only to be 3029 * set for a single column per table 3030 * @param {String} [attrs.tooltip] the tooltip text to display on the 3031 * column header 3032 * @param {Function} [attrs.renderer] the function that renders the 3033 * converted data value into a table cell, called with the 3034 * TD DOM node and data value as arguments (in that order) 3035 * 3036 * @return {Widget} the widget DOM node 3037 * 3038 * @class The table column widget class. Used to provide a sortable 3039 * data table column, using a <th> HTML element for the 3040 * header (and rendering data to <td> HTML elements). 3041 * @extends MochiKit.Widget 3042 */ 3043 MochiKit.Widget.TableColumn = function (attrs) { 3044 if (attrs.field == null) { 3045 throw new Error("The 'field' attribute cannot be null for a TableColumn"); 3046 } 3047 var o = MochiKit.DOM.TH(); 3048 MochiKit.Widget._widgetMixin(o, arguments.callee); 3049 o.addClass("widgetTableColumn"); 3050 o.setAttrs(MochiKit.Base.update({ title: attrs.field, type: "string", key: false }, attrs)); 3051 o.onclick = MochiKit.Widget._eventHandler(null, "_handleClick"); 3052 return o; 3053 } 3054 3055 /** 3056 * Updates the widget or HTML DOM node attributes. Note that some 3057 * updates will not take effect until the parent table is cleared 3058 * or data is reloaded. 3059 * 3060 * @param {Object} attrs the widget and node attributes to set 3061 * @param {String} [attrs.title] the column title 3062 * @param {String} [attrs.field] the data property name 3063 * @param {String} [attrs.type] the data property type, one of 3064 * "string", "number", "date", "time", "datetime", 3065 * "boolean" or "object" 3066 * @param {String} [attrs.sort] the sort direction, one of "asc", 3067 * "desc", "none" (disabled) or null (unsorted) 3068 * @param {Number} [attrs.maxLength] the maximum data length, 3069 * overflow will be displayed as a tooltip, only used by 3070 * the default renderer 3071 * @param {Boolean} [attrs.key] the unique key value flag, only to be 3072 * set for a single column per table 3073 * @param {String} [attrs.tooltip] the tooltip text to display on the 3074 * column header 3075 * @param {Function} [attrs.renderer] the function that renders the 3076 * converted data value into a table cell, called with the 3077 * TD DOM node and data value as arguments (in that order) 3078 */ 3079 MochiKit.Widget.TableColumn.prototype.setAttrs = function (attrs) { 3080 attrs = MochiKit.Base.update({}, attrs); 3081 var locals = MochiKit.Base.mask(attrs, ["title", "field", "type", "sort", "maxLength", "key", "tooltip", "renderer"]); 3082 if (typeof(locals.title) !== "undefined") { 3083 MochiKit.DOM.replaceChildNodes(this, locals.title); 3084 } 3085 if (typeof(locals.field) !== "undefined") { 3086 this.field = locals.field; 3087 } 3088 if (typeof(locals.type) !== "undefined") { 3089 this.type = locals.type; 3090 } 3091 if (typeof(locals.sort) !== "undefined") { 3092 this.sort = locals.sort; 3093 if (locals.sort == null || locals.sort == "none") { 3094 MochiKit.DOM.removeElementClass(this, "sortAsc"); 3095 MochiKit.DOM.removeElementClass(this, "sortDesc"); 3096 } else if (locals.sort == "desc") { 3097 MochiKit.DOM.removeElementClass(this, "sortAsc"); 3098 MochiKit.DOM.addElementClass(this, "sortDesc"); 3099 } else { 3100 MochiKit.DOM.removeElementClass(this, "sortDesc"); 3101 MochiKit.DOM.addElementClass(this, "sortAsc"); 3102 } 3103 } 3104 if (typeof(locals.maxLength) !== "undefined") { 3105 this.maxLength = parseInt(locals.maxLength); 3106 } 3107 if (typeof(locals.key) !== "undefined") { 3108 this.key = MochiKit.Base.bool(locals.key); 3109 } 3110 if (typeof(locals.tooltip) !== "undefined") { 3111 this.title = locals.tooltip; 3112 } 3113 if (typeof(locals.renderer) === "function") { 3114 this.renderer = locals.renderer; 3115 } 3116 MochiKit.DOM.updateNodeAttributes(this, attrs); 3117 } 3118 3119 /** 3120 * Maps the column field from one object onto another. This method 3121 * will also convert the data depending on the column data type. 3122 * 3123 * @param src the source object (containing the field) 3124 * @param dst the destination object 3125 */ 3126 MochiKit.Widget.TableColumn.prototype._map = function (src, dst) { 3127 var value = src[this.field]; 3128 if (value != null) { 3129 if (this.key) { 3130 dst.$id = value; 3131 } 3132 switch (this.type) { 3133 case "number": 3134 if (value instanceof Number) { 3135 value = value.valueOf(); 3136 } else if (typeof(value) != "number") { 3137 value = parseFloat(value); 3138 } 3139 break; 3140 case "date": 3141 if (value instanceof Date) { 3142 value = MochiKit.DateTime.toISODate(value); 3143 } else { 3144 value = MochiKit.Text.truncate(value, 10); 3145 } 3146 break; 3147 case "datetime": 3148 if (value instanceof Date) { 3149 value = MochiKit.DateTime.toISOTimestamp(value); 3150 } else { 3151 value = MochiKit.Text.truncate(value, 19); 3152 } 3153 break; 3154 case "time": 3155 if (value instanceof Date) { 3156 value = MochiKit.DateTime.toISOTime(value); 3157 } else { 3158 if (typeof(value) !== "string") { 3159 value = value.toString(); 3160 } 3161 if (value.length > 8) { 3162 value = value.substring(value.length - 8); 3163 } 3164 } 3165 break; 3166 case "boolean": 3167 if (typeof(value) !== "boolean") { 3168 value = MochiKit.Base.bool(value); 3169 } 3170 break; 3171 case "string": 3172 if (typeof(value) !== "string") { 3173 value = value.toString(); 3174 } 3175 break; 3176 } 3177 } 3178 dst[this.field] = value; 3179 } 3180 3181 /** 3182 * Renders the column field value into a table cell. 3183 * 3184 * @param obj the data object (containing the field) 3185 * 3186 * @return the table cell DOM node 3187 */ 3188 MochiKit.Widget.TableColumn.prototype._render = function (obj) { 3189 var td = MochiKit.DOM.TD(); 3190 var value = obj[this.field]; 3191 if (typeof(this.renderer) === "function") { 3192 this.renderer(td, value); 3193 } else { 3194 if (value == null || (typeof(value) == "number" && isNaN(value))) { 3195 value = ""; 3196 } else if (typeof(value) != "string") { 3197 value = value.toString(); 3198 } 3199 if (this.maxLength && this.maxLength < value.length) { 3200 td.title = value; 3201 value = MochiKit.Text.truncate(value, this.maxLength, "..."); 3202 } 3203 td.appendChild(MochiKit.DOM.createTextNode(value)); 3204 } 3205 return td; 3206 } 3207 3208 /** 3209 * Handles click events on the column header. 3210 */ 3211 MochiKit.Widget.TableColumn.prototype._handleClick = function () { 3212 if (this.parentNode != null) { 3213 var dir = (this.sort == "asc") ? "desc" : "asc"; 3214 var tr = this.parentNode; 3215 var thead = tr.parentNode; 3216 var table = thead.parentNode; 3217 table.parentNode.sortData(this.field, dir); 3218 } 3219 } 3220 3221 /** 3222 * Creates a new text area (or text box) widget. 3223 * 3224 * @constructor 3225 * @param {Object} attrs the widget and node attributes 3226 * @param {String} [attrs.helpText] the help text shown on empty 3227 * input, defaults to "" 3228 * @param {String} [attrs.value] the field value, defaults to "" 3229 * @param {Object} [...] the initial text content 3230 * 3231 * @return {Widget} the widget DOM node 3232 * 3233 * @class The text area widget class. Used to provide a text input 3234 * field spanning multiple rows, using the <textarea> HTML 3235 * element. 3236 * @property {Boolean} disabled The widget disabled flag. 3237 * @property {Boolean} focused The read-only widget focused flag. 3238 * @property {String} defaultValue The value to use on form reset. 3239 * @extends MochiKit.Widget 3240 * 3241 * @example 3242 * var field = MochiKit.Widget.TextArea({ helpText: "< Enter Data >" }); 3243 */ 3244 MochiKit.Widget.TextArea = function (attrs/*, ...*/) { 3245 var text = ""; 3246 if (attrs != null && attrs.value != null) { 3247 text = attrs.value; 3248 } 3249 for (var i = 1; i < arguments.length; i++) { 3250 var o = arguments[i]; 3251 if (MochiKit.DOM.isDOM(o)) { 3252 text += MochiKit.DOM.scrapeText(o); 3253 } else if (o != null) { 3254 text += o.toString(); 3255 } 3256 } 3257 var o = MochiKit.DOM.TEXTAREA({ value: text }); 3258 MochiKit.Widget._widgetMixin(o, arguments.callee); 3259 o.addClass("widgetTextArea"); 3260 o.focused = false; 3261 o.setAttrs(MochiKit.Base.update({ helpText: "", value: text }, attrs)); 3262 var focusHandler = MochiKit.Widget._eventHandler(null, "_handleFocus"); 3263 o.onfocus = focusHandler; 3264 o.onblur = focusHandler; 3265 return o; 3266 } 3267 3268 /** 3269 * Updates the widget or HTML DOM node attributes. 3270 * 3271 * @param {Object} attrs the widget and node attributes to set 3272 * @param {String} [attrs.helpText] the help text shown on empty input 3273 * @param {String} [attrs.value] the field value 3274 * 3275 * @example 3276 * var value = field.getValue(); 3277 * var lines = value.split("\n"); 3278 * lines = MochiKit.Base.map(MochiKit.Format.strip, lines); 3279 * value = lines.join("\n"); 3280 * field.setAttrs({ "value": value }); 3281 */ 3282 MochiKit.Widget.TextArea.prototype.setAttrs = function (attrs) { 3283 attrs = MochiKit.Base.update({}, attrs); 3284 var locals = MochiKit.Base.mask(attrs, ["helpText", "value"]); 3285 if (typeof(locals.helpText) != "undefined") { 3286 this.helpText = locals.helpText; 3287 } 3288 if (typeof(locals.value) != "undefined") { 3289 this.value = this.storedValue = locals.value; 3290 } 3291 MochiKit.DOM.updateNodeAttributes(this, attrs); 3292 this._render(); 3293 } 3294 3295 /** 3296 * Resets the text area form value to the initial value. 3297 */ 3298 MochiKit.Widget.TextArea.prototype.reset = function () { 3299 this.setAttrs({ value: this.defaultValue }); 3300 } 3301 3302 /** 3303 * Returns the text area value. This function is slightly different 3304 * from using the "value" property directly, since it will always 3305 * return the actual value string instead of the temporary help text 3306 * displayed when the text area is empty and unfocused. 3307 * 3308 * @return {String} the field value 3309 * 3310 * @example 3311 * var value = field.getValue(); 3312 * var lines = value.split("\n"); 3313 * lines = MochiKit.Base.map(MochiKit.Format.strip, lines); 3314 * value = lines.join("\n"); 3315 * field.setAttrs({ "value": value }); 3316 */ 3317 MochiKit.Widget.TextArea.prototype.getValue = function () { 3318 var str = (this.focused) ? this.value : this.storedValue; 3319 // This is a hack to remove multiple newlines caused by 3320 // platforms inserting or failing to normalize newlines 3321 // within the HTML textarea control. 3322 if (/\r\n\n/.test(str)) { 3323 str = str.replace(/\r\n\n/g, "\n"); 3324 } else if (/\n\n/.test(str) && !/.\n./.test(str)) { 3325 str = str.replace(/\n\n/g, "\n"); 3326 } 3327 if (this.focused && this.value != str) { 3328 this.value = str; 3329 } 3330 return str; 3331 } 3332 3333 /** 3334 * Handles focus and blur events for this widget. 3335 * 3336 * @param evt the MochiKit.Signal.Event object 3337 */ 3338 MochiKit.Widget.TextArea.prototype._handleFocus = function (evt) { 3339 var str = this.getValue(); 3340 if (evt.type() == "focus") { 3341 this.focused = true; 3342 this.value = str 3343 } else if (evt.type() == "blur") { 3344 this.focused = false; 3345 this.storedValue = str; 3346 } 3347 this._render(); 3348 } 3349 3350 /** 3351 * Updates the display of the widget content. 3352 */ 3353 MochiKit.Widget.TextArea.prototype._render = function () { 3354 var strip = MochiKit.Format.strip; 3355 var str = this.getValue(); 3356 if (!this.focused && strip(str) == "" && strip(this.helpText) != "") { 3357 this.value = this.helpText; 3358 this.addClass("widgetTextAreaHelp"); 3359 } else { 3360 this.value = str; 3361 this.removeClass("widgetTextAreaHelp"); 3362 } 3363 } 3364 3365 /** 3366 * Creates a new text field widget. 3367 * 3368 * @constructor 3369 * @param {Object} attrs the widget and node attributes 3370 * @param {String} [attrs.helpText] the help text shown on empty 3371 * input, defaults to "" 3372 * @param {String} [attrs.value] the field value, defaults to "" 3373 * @param {Object} [...] the initial text content 3374 * 3375 * @return {Widget} the widget DOM node 3376 * 3377 * @class The text field widget class. Used to provide a text input 3378 * field for a single line, using the <input> HTML element. 3379 * The text field may also be connected to a popup (for auto- 3380 * suggest or similar) and in that case the "onpopupselect" event 3381 * will be triggered when an element is selected from the popup. 3382 * @property {Boolean} disabled The widget disabled flag. 3383 * @property {Boolean} focused The read-only widget focused flag. 3384 * @property {String} defaultValue The value to use on form reset. 3385 * @extends MochiKit.Widget 3386 * 3387 * @example 3388 * var field = MochiKit.Widget.TextField({ helpText: "< Enter Data >" }); 3389 */ 3390 MochiKit.Widget.TextField = function (attrs/*, ...*/) { 3391 var text = ""; 3392 if (attrs != null && attrs.value != null) { 3393 text = attrs.value; 3394 } 3395 for (var i = 1; i < arguments.length; i++) { 3396 var o = arguments[i]; 3397 if (MochiKit.DOM.isDOM(o)) { 3398 text += MochiKit.DOM.scrapeText(o); 3399 } else if (o != null) { 3400 text += o.toString(); 3401 } 3402 } 3403 var o = MochiKit.DOM.INPUT({ value: text }); 3404 MochiKit.Widget._widgetMixin(o, arguments.callee); 3405 o.addClass("widgetTextField"); 3406 o.focused = false; 3407 o._popupCreated = false; 3408 o.setAttrs(MochiKit.Base.update({ helpText: "", value: text }, attrs)); 3409 var focusHandler = MochiKit.Widget._eventHandler(null, "_handleFocus"); 3410 o.onfocus = focusHandler; 3411 o.onblur = focusHandler; 3412 return o; 3413 } 3414 3415 /** 3416 * Updates the widget or HTML DOM node attributes. 3417 * 3418 * @param {Object} attrs the widget and node attributes to set 3419 * @param {String} [attrs.helpText] the help text shown on empty input 3420 * @param {String} [attrs.value] the field value 3421 * 3422 * @example 3423 * var value = field.getValue(); 3424 * value = MochiKit.Format.strip(value); 3425 * field.setAttrs({ "value": value }); 3426 */ 3427 MochiKit.Widget.TextField.prototype.setAttrs = function (attrs) { 3428 attrs = MochiKit.Base.update({}, attrs); 3429 var locals = MochiKit.Base.mask(attrs, ["helpText", "value"]); 3430 if (typeof(locals.helpText) != "undefined") { 3431 this.helpText = locals.helpText; 3432 } 3433 if (typeof(locals.value) != "undefined") { 3434 this.value = this.storedValue = locals.value; 3435 } 3436 this._render(); 3437 MochiKit.DOM.updateNodeAttributes(this, attrs); 3438 } 3439 3440 /** 3441 * Resets the text area form value to the initial value. 3442 */ 3443 MochiKit.Widget.TextField.prototype.reset = function () { 3444 this.setAttrs({ value: this.defaultValue }); 3445 } 3446 3447 /** 3448 * Returns the text field value. This function is slightly different 3449 * from using the "value" property directly, since it will always 3450 * return the actual value string instead of the temporary help text 3451 * displayed when the text field is empty and unfocused. 3452 * 3453 * @return {String} the field value 3454 * 3455 * @example 3456 * var value = field.getValue(); 3457 * value = MochiKit.Format.strip(value); 3458 * field.setAttrs({ "value": value }); 3459 */ 3460 MochiKit.Widget.TextField.prototype.getValue = function () { 3461 return (this.focused) ? this.value : this.storedValue; 3462 } 3463 3464 /** 3465 * Returns (or creates) a popup for this text field. The popup will 3466 * not be shown by this method, only returned as-is. If the create 3467 * flag is specified, a new popup will be created if none has been 3468 * created previuosly. 3469 * 3470 * @param {Boolean} create the create popup flag 3471 * 3472 * @return {Widget} the popup widget, or 3473 * null if none existed or was created 3474 */ 3475 MochiKit.Widget.TextField.prototype.popup = function (create) { 3476 if (!this._popupCreated && create) { 3477 this.autocomplete = "off"; 3478 this._popupCreated = true; 3479 var style = { "max-height": "300px", "width": "300px" }; 3480 var popup = new MochiKit.Widget.Popup({ style: style }); 3481 MochiKit.DOM.insertSiblingNodesAfter(this, popup); 3482 MochiKit.Style.makePositioned(this.parentNode); 3483 var pos = { x: this.offsetLeft + 1, 3484 y: this.offsetTop + this.offsetHeight + 1 }; 3485 MochiKit.Style.setElementPosition(popup, pos); 3486 MochiKit.Signal.connect(this, "onkeydown", this, "_handleKeyDown"); 3487 MochiKit.Signal.connect(popup, "onclick", this, "_handleClick"); 3488 } 3489 return (this._popupCreated) ? this.nextSibling : null; 3490 } 3491 3492 /** 3493 * Shows a popup for the text field containing the specified items. 3494 * The items specified may be either a list of HTML DOM nodes or 3495 * text strings. 3496 * 3497 * @param {Object} [attrs] the popup attributes to set 3498 * @param {Number} [attrs.delay] the popup auto-hide delay, defaults 3499 * to 30 seconds 3500 * @param {Array} [items] the items to show, or null to keep the 3501 * previuos popup content 3502 */ 3503 MochiKit.Widget.TextField.prototype.showPopup = function (attrs, items) { 3504 var popup = this.popup(true); 3505 if (items) { 3506 popup.hide(); 3507 MochiKit.DOM.replaceChildNodes(popup); 3508 for (var i = 0; i < items.length; i++) { 3509 if (typeof(items[i]) == "string") { 3510 var node = MochiKit.DOM.DIV({ "class": "widgetPopupItem" }, 3511 "\u00BB " + items[i]); 3512 popup.appendChild(node); 3513 } else { 3514 MochiKit.DOM.appendChildNodes(popup, items[i]); 3515 } 3516 } 3517 } 3518 if (popup.childNodes.length > 0) { 3519 popup.setAttrs(MochiKit.Base.update({ delay: 30000 }, attrs)); 3520 popup.show(); 3521 } 3522 } 3523 3524 /** 3525 * Handles focus and blur events for this widget. 3526 * 3527 * @param evt the MochiKit.Signal.Event object 3528 */ 3529 MochiKit.Widget.TextField.prototype._handleFocus = function (evt) { 3530 if (evt.type() == "focus") { 3531 this.focused = true; 3532 this.value = this.storedValue; 3533 } else if (evt.type() == "blur") { 3534 this.focused = false; 3535 this.storedValue = this.value; 3536 var popup = this.popup(); 3537 if (popup != null && !popup.isHidden()) { 3538 popup.setAttrs({ delay: 250 }); 3539 } 3540 } 3541 this._render(); 3542 } 3543 3544 /** 3545 * Handles the key down event for the text field. 3546 * 3547 * @param {Event} evt the MochiKit.Signal.Event object 3548 */ 3549 MochiKit.Widget.TextField.prototype._handleKeyDown = function (evt) { 3550 var popup = this.popup(false); 3551 if (popup != null) { 3552 popup.resetDelay(); 3553 if (popup.isHidden()) { 3554 switch (evt.key().string) { 3555 case "KEY_ESCAPE": 3556 evt.stop(); 3557 break; 3558 case "KEY_ARROW_UP": 3559 case "KEY_ARROW_DOWN": 3560 this.showPopup(); 3561 popup.selectChild(0); 3562 evt.stop(); 3563 break; 3564 } 3565 } else { 3566 switch (evt.key().string) { 3567 case "KEY_TAB": 3568 case "KEY_ENTER": 3569 popup.hide(); 3570 evt.stop(); 3571 if (popup.selectedChild() != null) { 3572 MochiKit.Widget.emitSignal(this, "onpopupselect"); 3573 } 3574 break; 3575 case "KEY_ESCAPE": 3576 popup.hide(); 3577 evt.stop(); 3578 break; 3579 case "KEY_ARROW_UP": 3580 case "KEY_ARROW_DOWN": 3581 popup.selectMove(evt.key().string == "KEY_ARROW_UP" ? -1 : 1); 3582 evt.stop(); 3583 break; 3584 } 3585 } 3586 } 3587 } 3588 3589 /** 3590 * Handles the mouse click event on the popup. 3591 * 3592 * @param evt the MochiKit.Signal.Event object 3593 */ 3594 MochiKit.Widget.TextField.prototype._handleClick = function (evt) { 3595 this.blur(); 3596 this.focus(); 3597 MochiKit.Widget.emitSignal(this, "onpopupselect"); 3598 } 3599 3600 /** 3601 * Updates the display of the widget content. 3602 */ 3603 MochiKit.Widget.TextField.prototype._render = function () { 3604 var strip = MochiKit.Format.strip; 3605 var str = this.getValue(); 3606 if (!this.focused && strip(str) == "" && strip(this.helpText) != "") { 3607 this.value = this.helpText; 3608 this.addClass("widgetTextFieldHelp"); 3609 } else { 3610 this.value = str; 3611 this.removeClass("widgetTextFieldHelp"); 3612 } 3613 } 3614 3615 /** 3616 * Creates a new tree widget. 3617 * 3618 * @constructor 3619 * @param {Object} attrs the widget and node attributes 3620 * @param {Widget} [...] the child tree node widgets 3621 * 3622 * @return {Widget} the widget DOM node 3623 * 3624 * @class The tree widget class. Used to provide a dynamic tree with 3625 * expandable tree nodes, using a number of <div> HTML 3626 * elements. The the "onexpand" and "onselect" event are emitted 3627 * whenever a node is expanded, collapsed or selected. 3628 * @extends MochiKit.Widget 3629 */ 3630 MochiKit.Widget.Tree = function (attrs/*, ...*/) { 3631 var o = MochiKit.DOM.DIV(attrs); 3632 MochiKit.Widget._widgetMixin(o, arguments.callee); 3633 o.addClass("widgetTree"); 3634 o.resizeContent = MochiKit.Base.noop; 3635 o.selectedPath = null; 3636 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 3637 return o; 3638 } 3639 3640 /** 3641 * Adds a single child tree node widget to this widget. 3642 * 3643 * @param {Widget} child the tree node widget to add 3644 */ 3645 MochiKit.Widget.Tree.prototype.addChildNode = function (child) { 3646 if (!MochiKit.Widget.isWidget(child, "TreeNode")) { 3647 throw new Error("Tree widget can only have TreeNode children"); 3648 } 3649 this.appendChild(child); 3650 } 3651 3652 /** 3653 * Removes all tree nodes that are marked as unmodified. When adding 3654 * or updating nodes, they (and their parent nodes) are automatically 3655 * marked as modified. This function makes tree pruning possible, by 3656 * initially marking all tree nodes as unmodified (clearing any 3657 * previous modified flag), touching all nodes to be kept, and 3658 * finally calling this method to remove the remaining nodes. 3659 */ 3660 MochiKit.Widget.Tree.prototype.removeAllMarked = function () { 3661 var children = this.getChildNodes(); 3662 for (var i = 0; i < children.length; i++) { 3663 if (children[i].marked === true) { 3664 this.removeChildNode(children[i]); 3665 } else { 3666 children[i].removeAllMarked(); 3667 } 3668 } 3669 } 3670 3671 /** 3672 * Marks all tree nodes as unmodified. When adding or updating nodes, 3673 * they (and their parent nodes) are automatically marked as 3674 * modified. This function makes tree pruning possible, by initially 3675 * marking all tree nodes (clearing any previous modified flag), 3676 * touching all nodes to be kept, and finally calling the 3677 * removeAllMarked() method to remove the remaining nodes. 3678 */ 3679 MochiKit.Widget.Tree.prototype.markAll = function () { 3680 var children = this.getChildNodes(); 3681 for (var i = 0; i < children.length; i++) { 3682 children[i].markAll(); 3683 } 3684 } 3685 3686 /** 3687 * Finds a root tree node with the specified name. 3688 * 3689 * @param {String} name the root tree node name 3690 * 3691 * @return {Widget} the root tree node found, or 3692 * null if not found 3693 */ 3694 MochiKit.Widget.Tree.prototype.findRoot = function (name) { 3695 var children = this.getChildNodes(); 3696 for (var i = 0; i < children.length; i++) { 3697 if (children[i].name == name) { 3698 return children[i]; 3699 } 3700 } 3701 return null; 3702 } 3703 3704 /** 3705 * Searches for a tree node from the specified path. 3706 * 3707 * @param {Array} path the tree node path (array of names) 3708 * 3709 * @return {Widget} the descendant tree node found, or 3710 * null if not found 3711 */ 3712 MochiKit.Widget.Tree.prototype.findByPath = function (path) { 3713 if (path == null || path.length < 1) { 3714 return null; 3715 } 3716 var root = this.findRoot(path[0]); 3717 if (root != null) { 3718 return root.findByPath(path.slice(1)); 3719 } else { 3720 return null; 3721 } 3722 } 3723 3724 /** 3725 * Returns the currently selected tree node. 3726 * 3727 * @return {Widget} the currently selected tree node, or 3728 * null if no node is selected 3729 */ 3730 MochiKit.Widget.Tree.prototype.selectedChild = function () { 3731 if (this.selectedPath == null) { 3732 return null; 3733 } else { 3734 return this.findByPath(this.selectedPath); 3735 } 3736 } 3737 3738 /** 3739 * Sets the currently selected node in the tree. This method is only 3740 * called from the tree node select() and unselect() methods. 3741 * 3742 * @param {Widget} node the new selected tree node, or null for none 3743 */ 3744 MochiKit.Widget.Tree.prototype._handleSelect = function (node) { 3745 var prev = this.selectedChild(); 3746 if (node == null) { 3747 this.selectedPath = null; 3748 MochiKit.Widget.emitSignal(this, "onselect", null); 3749 } else { 3750 if (prev != null && prev !== node) { 3751 prev.unselect(); 3752 } 3753 this.selectedPath = node.path(); 3754 MochiKit.Widget.emitSignal(this, "onselect", node); 3755 } 3756 } 3757 3758 /** 3759 * Emits a signal when a node has been expanded or collapsed. 3760 * 3761 * @param {Widget} node the affected tree node 3762 */ 3763 MochiKit.Widget.Tree.prototype._emitExpand = function (node) { 3764 MochiKit.Widget.emitSignal(this, "onexpand", node); 3765 } 3766 3767 /** 3768 * Recursively expands all nodes. If a depth is specified, 3769 * expansions will not continue below that depth. 3770 * 3771 * @param {Number} [depth] the optional maximum depth 3772 */ 3773 MochiKit.Widget.Tree.prototype.expandAll = function (depth) { 3774 if (typeof(depth) !== "number") { 3775 depth = 10; 3776 } 3777 var children = this.getChildNodes(); 3778 for (var i = 0; depth > 0 && i < children.length; i++) { 3779 children[i].expandAll(depth - 1); 3780 } 3781 } 3782 3783 /** 3784 * Recursively collapses all nodes. If a depth is specified, only 3785 * nodes below that depth will be collapsed. 3786 * 3787 * @param {Number} [depth] the optional minimum depth 3788 */ 3789 MochiKit.Widget.Tree.prototype.collapseAll = function (depth) { 3790 if (typeof(depth) !== "number") { 3791 depth = 0; 3792 } 3793 var children = this.getChildNodes(); 3794 for (var i = 0; i < children.length; i++) { 3795 children[i].collapseAll(depth - 1); 3796 } 3797 } 3798 3799 /** 3800 * Adds a path to the tree as a recursive list of child nodes. If 3801 * nodes in the specified path already exists, they will be used 3802 * instead of creating new nodes. 3803 * 3804 * @param {Array} path the tree node path (array of names) 3805 * 3806 * @return {Widget} the last node in the path 3807 */ 3808 MochiKit.Widget.Tree.prototype.addPath = function (path) { 3809 if (path == null || path.length < 1) { 3810 return null; 3811 } 3812 var node = this.findRoot(path[0]); 3813 if (node == null) { 3814 node = new MochiKit.Widget.TreeNode({ name: path[0] }); 3815 this.addChildNode(node); 3816 } 3817 node.marked = false; 3818 for (var i = 1; i < path.length; i++) { 3819 var child = node.findChild(path[i]); 3820 if (child == null) { 3821 child = new MochiKit.Widget.TreeNode({ name: path[i] }); 3822 node.addChildNode(child); 3823 } 3824 child.marked = false; 3825 node = child; 3826 } 3827 return node; 3828 } 3829 3830 /** 3831 * Creates a new tree node widget. 3832 * 3833 * @constructor 3834 * @param {Object} attrs the widget and node attributes 3835 * @param {String} attrs.name the tree node name 3836 * @param {Boolean} [attrs.folder] the folder flag, defaults to false 3837 * @param {String} [attrs.icon] the icon reference to use, defaults 3838 * to "FOLDER" for folders and "DOCUMENT" otherwise 3839 * @param {String} [attrs.tooltip] the tooltip text when hovering 3840 * @param {Widget} [...] the child tree node widgets 3841 * 3842 * @return {Widget} the widget DOM node 3843 * 3844 * @class The tree node widget class. Used to provide a tree node in 3845 * a tree, using a number of <div> HTML elements. Note that 3846 * events should normally not be listened for on individual tree 3847 * nodes, but rather on the tree as a whole. 3848 * @extends MochiKit.Widget 3849 */ 3850 MochiKit.Widget.TreeNode = function (attrs/*, ...*/) { 3851 var icon = MochiKit.Widget.Icon({ ref: "BLANK" }); 3852 var label = MochiKit.DOM.SPAN({ "class": "widgetTreeNodeText" }); 3853 var div = MochiKit.DOM.DIV({ "class": "widgetTreeNodeLabel" }, icon, label); 3854 var o = MochiKit.DOM.DIV({}, div); 3855 MochiKit.Widget._widgetMixin(o, arguments.callee); 3856 o.addClass("widgetTreeNode"); 3857 attrs = MochiKit.Base.update({ name: "Tree Node", folder: false }, attrs); 3858 if (typeof(attrs.icon) == "undefined") { 3859 attrs.icon = attrs.folder ? "FOLDER" : "DOCUMENT"; 3860 } 3861 o.setAttrs(attrs); 3862 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 3863 icon.onclick = MochiKit.Widget._eventHandler("TreeNode", "toggle"); 3864 div.onclick = MochiKit.Widget._eventHandler("TreeNode", "select"); 3865 return o; 3866 } 3867 3868 /** 3869 * Returns and/or creates the child container DOM node. If a child 3870 * container is created, it will be hidden by default. 3871 * 3872 * @param {Boolean} create the optional create flag, defaults to false 3873 * 3874 * @return {Node} the child container DOM node found or created 3875 */ 3876 MochiKit.Widget.TreeNode.prototype._container = function (create) { 3877 var container = this.lastChild; 3878 if (MochiKit.DOM.hasElementClass(container, "widgetTreeNodeContainer")) { 3879 return container; 3880 } else if (create) { 3881 container = MochiKit.DOM.DIV({ "class": "widgetTreeNodeContainer widgetHidden" }); 3882 this.appendChild(container); 3883 var imgNode = this.firstChild.firstChild; 3884 imgNode.setAttrs({ ref: "PLUS" }); 3885 this.setAttrs({ icon: "FOLDER" }); 3886 return container; 3887 } else { 3888 return null; 3889 } 3890 } 3891 3892 /** 3893 * Updates the widget or HTML DOM node attributes. 3894 * 3895 * @param {Object} attrs the widget and node attributes to set 3896 * @param {String} [attrs.name] the tree node name 3897 * @param {Boolean} [attrs.folder] the folder flag, cannot be 3898 * reverted to false once set (implicitly or explicitly) 3899 * @param {Icon/Object/String} [attrs.icon] icon the icon to set, or 3900 * null to remove 3901 * @param {String} [attrs.tooltip] the tooltip text when hovering 3902 */ 3903 MochiKit.Widget.TreeNode.prototype.setAttrs = function (attrs) { 3904 attrs = MochiKit.Base.update({}, attrs); 3905 this.marked = false; 3906 var locals = MochiKit.Base.mask(attrs, ["name", "folder", "icon", "tooltip"]); 3907 if (typeof(locals.name) != "undefined") { 3908 this.name = locals.name; 3909 var node = this.firstChild.firstChild; 3910 while (!MochiKit.DOM.hasElementClass(node, "widgetTreeNodeText")) { 3911 node = node.nextSibling; 3912 } 3913 MochiKit.DOM.replaceChildNodes(node, locals.name); 3914 } 3915 if (MochiKit.Base.bool(locals.folder)) { 3916 this._container(true); 3917 } 3918 if (typeof(locals.icon) != "undefined") { 3919 var imgNode = this.firstChild.firstChild; 3920 var iconNode = imgNode.nextSibling; 3921 if (!MochiKit.Widget.isWidget(iconNode, "Icon")) { 3922 iconNode = null; 3923 } 3924 if (iconNode == null && locals.icon != null) { 3925 if (typeof(locals.icon) === "string") { 3926 locals.icon = new MochiKit.Widget.Icon({ ref: locals.icon }); 3927 } else if (!MochiKit.Widget.isWidget(locals.icon, "Icon")) { 3928 locals.icon = new MochiKit.Widget.Icon(locals.icon); 3929 } 3930 MochiKit.DOM.insertSiblingNodesAfter(imgNode, locals.icon); 3931 } else if (iconNode != null && locals.icon != null) { 3932 if (MochiKit.Widget.isWidget(locals.icon, "Icon")) { 3933 MochiKit.DOM.swapDOM(iconNode, locals.icon); 3934 } else if (typeof(locals.icon) === "string") { 3935 iconNode.setAttrs({ ref: locals.icon }); 3936 } else { 3937 iconNode.setAttrs(locals.icon); 3938 } 3939 } else if (iconNode != null && locals.icon == null) { 3940 MochiKit.Widget.destroyWidget(iconNode); 3941 } 3942 } 3943 if (typeof(locals.tooltip) != "undefined") { 3944 this.firstChild.title = locals.tooltip; 3945 } 3946 MochiKit.DOM.updateNodeAttributes(this, attrs); 3947 } 3948 3949 /** 3950 * Returns an array with all child tree node widgets. Note that the 3951 * array is a real JavaScript array, not a dynamic NodeList. 3952 * 3953 * @return {Array} the array of child tree node widgets 3954 */ 3955 MochiKit.Widget.TreeNode.prototype.getChildNodes = function () { 3956 var container = this._container(); 3957 if (container == null) { 3958 return []; 3959 } else { 3960 return MochiKit.Base.extend([], container.childNodes); 3961 } 3962 } 3963 3964 /** 3965 * Adds a single child tree node widget to this widget. 3966 * 3967 * @param {Widget} child the tree node widget to add 3968 */ 3969 MochiKit.Widget.TreeNode.prototype.addChildNode = function (child) { 3970 if (!MochiKit.Widget.isWidget(child, "TreeNode")) { 3971 throw new Error("TreeNode widget can only have TreeNode children"); 3972 } 3973 this._container(true).appendChild(child); 3974 } 3975 3976 /** 3977 * Removes a single child tree node widget from this widget. 3978 * 3979 * @param {Widget} child the tree node widget to remove 3980 */ 3981 MochiKit.Widget.TreeNode.prototype.removeChildNode = function (child) { 3982 var container = this._container(); 3983 if (container != null) { 3984 container.removeChild(child); 3985 } 3986 } 3987 3988 /** 3989 * Removes all tree nodes that are marked as unmodified. When adding 3990 * or updating nodes, they (and their parent nodes) are automatically 3991 * marked as modified. This function makes tree pruning possible, by 3992 * initially marking all tree nodes as unmodified (clearing any 3993 * previous modified flag), touching all nodes to be kept, and 3994 * finally calling this method to remove the remaining nodes. 3995 */ 3996 MochiKit.Widget.TreeNode.prototype.removeAllMarked = function () { 3997 var children = this.getChildNodes(); 3998 for (var i = 0; i < children.length; i++) { 3999 if (children[i].marked === true) { 4000 this.removeChildNode(children[i]); 4001 } else { 4002 children[i].removeAllMarked(); 4003 } 4004 } 4005 } 4006 4007 /** 4008 * Marks all tree nodes as unmodified. When adding or updating nodes, 4009 * they (and their parent nodes) are automatically marked as 4010 * modified. This function makes tree pruning possible, by initially 4011 * marking all tree nodes (clearing any previous modified flag), 4012 * touching all nodes to be kept, and finally calling the 4013 * removeAllMarked() method to remove the remaining nodes. 4014 */ 4015 MochiKit.Widget.TreeNode.prototype.markAll = function () { 4016 this.marked = true; 4017 var children = this.getChildNodes(); 4018 for (var i = 0; i < children.length; i++) { 4019 children[i].markAll(); 4020 } 4021 } 4022 4023 /** 4024 * Checks if this node is a folder. 4025 * 4026 * @return {Boolean} true if this node is a folder, or 4027 * false otherwise 4028 */ 4029 MochiKit.Widget.TreeNode.prototype.isFolder = function () { 4030 return this._container() != null; 4031 } 4032 4033 /** 4034 * Checks if this folder node is expanded. 4035 * 4036 * @return {Boolean} true if this node is expanded, or 4037 * false otherwise 4038 */ 4039 MochiKit.Widget.TreeNode.prototype.isExpanded = function () { 4040 var container = this._container(); 4041 return container != null && 4042 !MochiKit.DOM.hasElementClass(container, "widgetHidden"); 4043 } 4044 4045 /** 4046 * Checks if this node is selected. 4047 * 4048 * @return {Boolean} true if the node is selected, or 4049 * false otherwise 4050 */ 4051 MochiKit.Widget.TreeNode.prototype.isSelected = function () { 4052 return MochiKit.DOM.hasElementClass(this.firstChild, "selected"); 4053 } 4054 4055 /** 4056 * Returns the ancestor tree widget. 4057 * 4058 * @return {Widget} the ancestor tree widget, or 4059 * null if none was found 4060 */ 4061 MochiKit.Widget.TreeNode.prototype.tree = function () { 4062 var parent = this.parent(); 4063 if (parent != null) { 4064 return parent.tree(); 4065 } 4066 if (MochiKit.Widget.isWidget(this.parentNode, "Tree")) { 4067 return this.parentNode; 4068 } else { 4069 return null; 4070 } 4071 } 4072 4073 /** 4074 * Returns the parent tree node widget. 4075 * 4076 * @return {Widget} the parent tree node widget, or 4077 * null if this is a root node 4078 */ 4079 MochiKit.Widget.TreeNode.prototype.parent = function () { 4080 var node = this.parentNode; 4081 if (MochiKit.DOM.hasElementClass(node, "widgetTreeNodeContainer")) { 4082 return node.parentNode; 4083 } else { 4084 return null; 4085 } 4086 } 4087 4088 /** 4089 * Returns the path to this tree node. 4090 * 4091 * @return {Array} the tree node path, i.e an array of node names 4092 */ 4093 MochiKit.Widget.TreeNode.prototype.path = function () { 4094 var parent = this.parent(); 4095 if (parent == null) { 4096 return [this.name]; 4097 } else { 4098 var path = parent.path(); 4099 path.push(this.name); 4100 return path; 4101 } 4102 } 4103 4104 /** 4105 * Finds a child tree node with the specified name. 4106 * 4107 * @param {String} name the child tree node name 4108 * 4109 * @return {Widget} the child tree node found, or 4110 * null if not found 4111 */ 4112 MochiKit.Widget.TreeNode.prototype.findChild = function (name) { 4113 var children = this.getChildNodes(); 4114 for (var i = 0; i < children.length; i++) { 4115 if (children[i].name == name) { 4116 return children[i]; 4117 } 4118 } 4119 return null; 4120 } 4121 4122 /** 4123 * Searches for a descendant tree node from the specified path. 4124 * 4125 * @param {Array} path the tree node path (array of node names) 4126 * 4127 * @return {Widget} the descendant tree node found, or 4128 * null if not found 4129 */ 4130 MochiKit.Widget.TreeNode.prototype.findByPath = function (path) { 4131 var node = this; 4132 4133 for (var i = 0; node != null && path != null && i < path.length; i++) { 4134 node = node.findChild(path[i]); 4135 } 4136 return node; 4137 } 4138 4139 /** 4140 * Selects this tree node. 4141 */ 4142 MochiKit.Widget.TreeNode.prototype.select = function () { 4143 MochiKit.DOM.addElementClass(this.firstChild, "selected"); 4144 var tree = this.tree(); 4145 if (tree != null) { 4146 tree._handleSelect(this); 4147 } 4148 this.expand(); 4149 } 4150 4151 /** 4152 * Unselects this tree node. 4153 */ 4154 MochiKit.Widget.TreeNode.prototype.unselect = function () { 4155 if (this.isSelected()) { 4156 MochiKit.DOM.removeElementClass(this.firstChild, "selected"); 4157 var tree = this.tree(); 4158 if (tree != null) { 4159 tree._handleSelect(null); 4160 } 4161 } 4162 } 4163 4164 /** 4165 * Expands this node to display any child nodes. If the parent node 4166 * is not expanded, it will be expanded as well. 4167 */ 4168 MochiKit.Widget.TreeNode.prototype.expand = function () { 4169 var parent = this.parent(); 4170 if (parent != null && !parent.isExpanded()) { 4171 parent.expand(); 4172 } 4173 var container = this._container(); 4174 if (container != null && !this.isExpanded()) { 4175 var imgNode = this.firstChild.firstChild; 4176 imgNode.setAttrs({ ref: "MINUS" }); 4177 MochiKit.DOM.removeElementClass(container, "widgetHidden"); 4178 var tree = this.tree(); 4179 if (tree != null) { 4180 tree._emitExpand(this); 4181 } 4182 } 4183 } 4184 4185 /** 4186 * Recursively expands this node and all its children. If a depth is 4187 * specified, expansions will not continue below that depth. 4188 * 4189 * @param {Number} [depth] the optional maximum depth 4190 */ 4191 MochiKit.Widget.TreeNode.prototype.expandAll = function (depth) { 4192 if (typeof(depth) !== "number") { 4193 depth = 10; 4194 } 4195 this.expand(); 4196 var children = this.getChildNodes(); 4197 for (var i = 0; depth > 0 && i < children.length; i++) { 4198 children[i].expandAll(depth - 1); 4199 } 4200 } 4201 4202 /** 4203 * Collapses this node to hide any child nodes. 4204 */ 4205 MochiKit.Widget.TreeNode.prototype.collapse = function () { 4206 var container = this._container(); 4207 if (container != null && this.isExpanded()) { 4208 var imgNode = this.firstChild.firstChild; 4209 imgNode.setAttrs({ ref: "PLUS" }); 4210 MochiKit.DOM.addElementClass(container, "widgetHidden"); 4211 var tree = this.tree(); 4212 if (tree != null) { 4213 tree._emitExpand(this); 4214 } 4215 } 4216 } 4217 4218 /** 4219 * Recursively collapses this node and all its children. If a depth 4220 * is specified, only children below that depth will be collapsed. 4221 * 4222 * @param {Number} [depth] the optional minimum depth 4223 */ 4224 MochiKit.Widget.TreeNode.prototype.collapseAll = function (depth) { 4225 if (typeof(depth) !== "number") { 4226 depth = 0; 4227 } 4228 if (depth <= 0) { 4229 this.collapse(); 4230 } 4231 var children = this.getChildNodes(); 4232 for (var i = 0; i < children.length; i++) { 4233 children[i].collapseAll(depth - 1); 4234 } 4235 } 4236 4237 /** 4238 * Toggles expand and collapse for this node. 4239 */ 4240 MochiKit.Widget.TreeNode.prototype.toggle = function (evt) { 4241 if (evt) { 4242 evt.stop(); 4243 } 4244 if (this.isExpanded()) { 4245 this.collapse(); 4246 } else { 4247 this.expand(); 4248 } 4249 } 4250 4251 /** 4252 * Creates a new wizard widget. 4253 * 4254 * @constructor 4255 * @param {Object} attrs the widget and node attributes 4256 * @param {Widget} [...] the child widgets or DOM nodes (should be 4257 * Pane widgets) 4258 * 4259 * @return {Widget} the widget DOM node 4260 * 4261 * @class The wizard widget class. Used to provide a sequence of 4262 * pages, where the user can step forward and backward with 4263 * buttons. Internally it uses a <div> HTML element 4264 * containing Pane widgets that are hidden and shown according 4265 * to the page transitions. In addition to standard HTML events, 4266 * the "onchange" event is triggered on page transitions, the 4267 * "oncancel" event is triggered if a user cancels an operation, 4268 * and the "onclose" event is triggered when the wizard 4269 * completes. 4270 * @extends MochiKit.Widget 4271 * 4272 * @example 4273 * <Dialog id="exDialog" title="Example Dialog" w="80%" h="50%"> 4274 * <Wizard id="exWizard" style="width: 100%; height: 100%;"> 4275 * <Pane pageTitle="The first step"> 4276 * ... 4277 * </Pane> 4278 * <Pane pageTitle="The second step"> 4279 * ... 4280 * </Pane> 4281 * </Wizard> 4282 * </Dialog> 4283 */ 4284 MochiKit.Widget.Wizard = function (attrs/*, ... */) { 4285 var o = MochiKit.DOM.DIV(attrs); 4286 MochiKit.Widget._widgetMixin(o, arguments.callee); 4287 o.addClass("widgetWizard"); 4288 o._selectedIndex = -1; 4289 o.appendChild(MochiKit.DOM.H3({ "class": "widgetWizardTitle" })); 4290 var bCancel = MochiKit.Widget.Button({ style: { "margin-right": "10px" } }, 4291 MochiKit.Widget.Icon({ ref: "CANCEL" }), 4292 " Cancel"); 4293 var bPrev = MochiKit.Widget.Button({ style: { "margin-right": "10px" } }, 4294 MochiKit.Widget.Icon({ ref: "PREVIOUS" }), 4295 " Previous"); 4296 var bNext = MochiKit.Widget.Button({}, 4297 "Next ", 4298 MochiKit.Widget.Icon({ ref: "NEXT" })); 4299 var bDone = MochiKit.Widget.Button({ highlight: true }, 4300 MochiKit.Widget.Icon({ ref: "OK" }), 4301 " Finish"); 4302 bCancel.hide(); 4303 o.appendChild(MochiKit.DOM.DIV({ "class": "widgetWizardButtons" }, 4304 bCancel, bPrev, bNext, bDone)); 4305 MochiKit.Signal.connect(bCancel, "onclick", o, "cancel"); 4306 MochiKit.Signal.connect(bPrev, "onclick", o, "previous"); 4307 MochiKit.Signal.connect(bNext, "onclick", o, "next"); 4308 MochiKit.Signal.connect(bDone, "onclick", o, "done"); 4309 o._updateStatus(); 4310 o.addAll(MochiKit.Base.extend(null, arguments, 1)); 4311 return o; 4312 } 4313 4314 /** 4315 * Returns an array with all child pane widgets. Note that the array 4316 * is a real JavaScript array, not a dynamic NodeList. 4317 * 4318 * @return {Array} the array of child wizard page widgets 4319 */ 4320 MochiKit.Widget.Wizard.prototype.getChildNodes = function () { 4321 return MochiKit.Base.extend([], this.childNodes, 2); 4322 } 4323 4324 /** 4325 * Adds a single child page widget to this widget. The child widget 4326 * should be a MochiKit.Widget.Pane widget, or it will be added to a 4327 * new one. 4328 * 4329 * @param {Widget} child the page widget to add 4330 */ 4331 MochiKit.Widget.Wizard.prototype.addChildNode = function (child) { 4332 if (!MochiKit.Widget.isWidget(child, "Pane")) { 4333 child = new MochiKit.Widget.Pane(null, child); 4334 } 4335 MochiKit.Style.registerSizeConstraints(child, "100%", "100%-65"); 4336 child.hide(); 4337 this.appendChild(child); 4338 child.style.position = "absolute"; 4339 // TODO: remove hard-coded size here... 4340 MochiKit.Style.setElementPosition(child, { x: 0, y: 24 }); 4341 if (this.getChildNodes().length == 1) { 4342 this.activatePage(0); 4343 } else { 4344 this._updateStatus(); 4345 } 4346 } 4347 4348 // TODO: handle removes by possibly selecting new page... 4349 4350 /** 4351 * Updates the wizard status indicators, such as the title and the 4352 * current buttons. 4353 */ 4354 MochiKit.Widget.Wizard.prototype._updateStatus = function () { 4355 var h3 = this.childNodes[0]; 4356 var bCancel = this.childNodes[1].childNodes[0]; 4357 var bPrev = this.childNodes[1].childNodes[1]; 4358 var bNext = this.childNodes[1].childNodes[2]; 4359 var bDone = this.childNodes[1].childNodes[3]; 4360 var page = this.activePage(); 4361 var status = MochiKit.Widget.Pane.FORWARD; 4362 var title = null; 4363 var info = "(No pages available)"; 4364 var icon = null; 4365 if (page != null) { 4366 status = page.pageStatus || MochiKit.Widget.Pane.ANY; 4367 title = page.pageTitle; 4368 info = " (Step " + (this._selectedIndex + 1) + " of " + 4369 this.getChildNodes().length + ")"; 4370 } 4371 if (status === MochiKit.Widget.Pane.WORKING) { 4372 bCancel.show(); 4373 bPrev.hide(); 4374 icon = { ref: "LOADING", "class": "widgetWizardWait" }; 4375 icon = MochiKit.Widget.Icon(icon); 4376 } else { 4377 bCancel.hide(); 4378 bPrev.show(); 4379 } 4380 if (this._selectedIndex >= this.getChildNodes().length - 1) { 4381 bNext.hide(); 4382 bDone.show(); 4383 } else { 4384 bNext.show(); 4385 bDone.hide(); 4386 } 4387 bPrev.disabled = (this._selectedIndex <= 0) || !status.previous; 4388 bNext.disabled = !status.next; 4389 bDone.disabled = !status.next; 4390 info = MochiKit.DOM.SPAN({ "class": "widgetWizardInfo" }, info); 4391 MochiKit.DOM.replaceChildNodes(h3, icon, title, info); 4392 } 4393 4394 /** 4395 * Returns the active page. 4396 * 4397 * @return {Widget} the active page, or 4398 * null if no pages have been added 4399 */ 4400 MochiKit.Widget.Wizard.prototype.activePage = function () { 4401 if (this._selectedIndex >= 0) { 4402 return this.childNodes[this._selectedIndex + 2]; 4403 } else { 4404 return null; 4405 } 4406 } 4407 4408 /** 4409 * Returns the active page index. 4410 * 4411 * @return the active page index, or 4412 * -1 if no page is active 4413 */ 4414 MochiKit.Widget.Wizard.prototype.activePageIndex = function () { 4415 return this._selectedIndex; 4416 } 4417 4418 /** 4419 * Activates a new page. 4420 * 4421 * @param {Number/Widget} indexOrPage the page index or page DOM node 4422 */ 4423 MochiKit.Widget.Wizard.prototype.activatePage = function (indexOrPage) { 4424 if (typeof(indexOrPage) == "number") { 4425 var index = indexOrPage; 4426 var page = this.childNodes[index + 2]; 4427 } else { 4428 var page = indexOrPage; 4429 var index = MochiKit.Base.findIdentical(this.childNodes, page, 2) - 2; 4430 } 4431 if (index < 0 || index >= this.getChildNodes().length) { 4432 throw new RangeError("Page index out of bounds: " + index); 4433 } 4434 var oldIndex = this._selectedIndex; 4435 var oldPage = this.activePage(); 4436 if (oldPage != null && oldPage !== page) { 4437 if (!oldPage._handleExit({ hide: false, validate: this._selectedIndex < index })) { 4438 // Old page blocked page transition 4439 return; 4440 } 4441 } 4442 this._selectedIndex = index; 4443 this._updateStatus(); 4444 if (oldPage != null && oldPage !== page) { 4445 var dim = MochiKit.Style.getElementDimensions(this); 4446 var offset = (oldIndex < index) ? dim.w : -dim.w; 4447 MochiKit.Style.setElementPosition(page, { x: offset }); 4448 page._handleEnter({ validateReset: true }); 4449 var cleanup = function () { 4450 oldPage.hide(); 4451 MochiKit.Style.setElementPosition(oldPage, { x: 0 }); 4452 } 4453 var opts = { duration: 0.5, x: -offset, afterFinish: cleanup }; 4454 MochiKit.Visual.Move(oldPage, opts); 4455 MochiKit.Visual.Move(page, opts); 4456 } else { 4457 page._handleEnter({ validateReset: true }); 4458 } 4459 MochiKit.Widget.emitSignal(this, "onchange", index, page); 4460 } 4461 4462 /** 4463 * Cancels the active page operation. This method will also reset 4464 * the page status of the currently active page to "ANY". 4465 */ 4466 MochiKit.Widget.Wizard.prototype.cancel = function () { 4467 var page = this.activePage(); 4468 page.setAttrs({ pageStatus: MochiKit.Widget.Pane.ANY }); 4469 MochiKit.Widget.emitSignal(this, "oncancel"); 4470 } 4471 4472 /** 4473 * Moves the wizard backward to the previous page. 4474 */ 4475 MochiKit.Widget.Wizard.prototype.previous = function () { 4476 if (this._selectedIndex > 0) { 4477 this.activatePage(this._selectedIndex - 1); 4478 } 4479 } 4480 4481 /** 4482 * Moves the wizard forward to the next page. The page will not be 4483 * changed if the active page fails a validation check. 4484 */ 4485 MochiKit.Widget.Wizard.prototype.next = function () { 4486 if (this._selectedIndex < this.getChildNodes().length - 1) { 4487 this.activatePage(this._selectedIndex + 1); 4488 } 4489 } 4490 4491 /** 4492 * Sends the wizard onclose signal when the user presses the finish 4493 * button. 4494 */ 4495 MochiKit.Widget.Wizard.prototype.done = function () { 4496 var page = this.activePage(); 4497 if (page != null) { 4498 if (!page._handleExit({ validate: true })) { 4499 // Page blocked wizard completion 4500 return; 4501 } 4502 } 4503 MochiKit.Widget.emitSignal(this, "onclose"); 4504 } 4505 4506 /** 4507 * Resizes the current wizard page. This method need not be called 4508 * directly, but is automatically called whenever a parent node is 4509 * resized. It optimizes the resize chain by only resizing those DOM 4510 * child nodes that are visible. 4511 */ 4512 MochiKit.Widget.Wizard.prototype.resizeContent = function () { 4513 var page = this.activePage(); 4514 if (page != null) { 4515 MochiKit.Style.resizeElements(page); 4516 } 4517 } 4518 4519 4520 /** 4521 * The registered widget class names and constructor functions. All 4522 * the default widgets in the MochiKit.Widget namespace are defined 4523 * here, using their constructor function names. It is possible to 4524 * override or add new classes by simply modifying this global data 4525 * object. 4526 */ 4527 MochiKit.Widget.Classes = { 4528 Button: MochiKit.Widget.Button, 4529 Dialog: MochiKit.Widget.Dialog, 4530 Field: MochiKit.Widget.Field, 4531 FileStreamer: MochiKit.Widget.FileStreamer, 4532 Form: MochiKit.Widget.Form, 4533 FormValidator: MochiKit.Widget.FormValidator, 4534 Icon: MochiKit.Widget.Icon, 4535 Overlay: MochiKit.Widget.Overlay, 4536 Popup: MochiKit.Widget.Popup, 4537 Pane: MochiKit.Widget.Pane, 4538 ProgressBar: MochiKit.Widget.ProgressBar, 4539 TabContainer: MochiKit.Widget.TabContainer, 4540 Table: MochiKit.Widget.Table, 4541 TableColumn: MochiKit.Widget.TableColumn, 4542 TextArea: MochiKit.Widget.TextArea, 4543 TextField: MochiKit.Widget.TextField, 4544 Tree: MochiKit.Widget.Tree, 4545 TreeNode: MochiKit.Widget.TreeNode, 4546 Wizard: MochiKit.Widget.Wizard 4547 }; 4548