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