1 /*
  2  * All content on this website (including text, images, source
  3  * code and any other original works), unless otherwise noted,
  4  * is licensed under a Creative Commons License.
  5  * 
  6  * http://creativecommons.org/licenses/by-nc-sa/2.5/
  7  * 
  8  * Copyright (C) 2004-2010 Open-Xchange, Inc.
  9  * Mail: info@open-xchange.com
 10  */
 11  
 12 /** 
 13  * @fileOverview Open-Xchange GUI Plugin API
 14  * @author Viktor Pracht <Viktor.Pracht@open-xchange.org>
 15  */
 16 
 17 /**
 18  * @namespace
 19  */
 20 var ox = {
 21 
 22     /**
 23      * @namespace
 24      */
 25     UI: {},
 26     
 27     /**
 28      * @namespace
 29      */
 30     Configuration: {
 31         
 32         /**
 33          * Displays an error message to the user.
 34          * @param {I18nString} text The error message to display.
 35          */
 36         error: function(text) {
 37             setTimeout(function() {
 38                 triggerEvent("OX_New_Error", 4, text());
 39             }, 0);
 40         },
 41 
 42         /**
 43          * Displays an information message to the user.
 44          * @param {I18nString} text The information message to display.
 45          */
 46         info: function(text) {
 47             if (typeof text != "function") {
 48                 if (debug) {
 49                     (console.warn || console.log || alert)(format(
 50                         "The string \"%s\" is not internationalized!", text));
 51                 }
 52                 triggerEvent("OX_New_Info", 4, text);
 53             } else {
 54                 triggerEvent("OX_New_Info", 4, text());
 55             }
 56         },
 57 
 58         /**
 59          * Adds a new node to the configuration tree.
 60          * The parent node must already exist.
 61          * @class Leaf node in the configuration tree
 62          * @param {String} path The path in the configuration tree.
 63          * Must start with "configuration" and contain at least one more
 64          * element. Path elements are separated by a slash ("/"). There must be
 65          * no slash at the end. If the parent of the node does not already
 66          * exist, an Error exception is thrown.
 67          * @param {I18nString} name The name of the page.
 68          */
 69         LeafNode: function(path, name) {
 70             var Self = new ox.Configuration.Node(path, name, false, function() {
 71                 if (Self.click) return Self.click();
 72             });
 73             return Self;
 74         },
 75 
 76         /**
 77          * This callback is called when the user clicks on the node.
 78          * @name ox.Configuration.LeafNode.prototype.click
 79          * @type Function
 80          */
 81 
 82         /**
 83          * Adds a new inner node to the configuration tree.
 84          * The parent node must already exist.
 85          * @class Inner node in the configuration tree.
 86          * @param {String} path The path in the configuration tree.
 87          * Must start with "configuration" and contain at least one more
 88          * element. Path elements are separated by a slash ("/"). There must be
 89          * no slash at the end. If the parent of the node does not already
 90          * exist, an Error exception is thrown.
 91          * @param {I18nString} name The name of the page.
 92          */
 93         InnerNode: function(path, name) {
 94             return new ox.Configuration.Node(path, name, true);
 95         },
 96         
 97         /**
 98          * @class
 99          * @private
100          */
101         Node: function(path, name, folder, click) {
102             if (path in ox.Configuration.nodes)
103                 return ox.Configuration.nodes[path];
104             var index = path.indexOf("/");
105             if (index < 0) throw Error("Invalid path: '" + path + "'");
106             this.id = folder ? path
107                 : "configuration/modules/" + ox.Configuration.id++;
108             var txtNode = typeof(name) == "function" || typeof(name) == "string"
109                         ? addTranslated(name) : name; 
110             var obj = new cConfigObject(this.id, txtNode, folder);
111             if (click) obj.onClick = click;
112             if ("configTree" in window && configTree) {
113                 var parent = configTree;
114                 while (index >= 0) {
115                     parent = parent.children[path.substring(0, index)];
116                     if (!parent) throw Error("Invalid path: '" +
117                                              path.substring(0, index) + "'");
118                     index = path.indexOf("/", index + 1);
119                 }
120                 drawTreeNode(parent, obj);
121             } else {
122                 var parent = oConfigTreeDef;
123                 index = path.indexOf("/", index + 1);
124                 while (index >= 0) {
125                     var subpath = path.substring(0, index);
126                     search: {
127                         for (var j = 0; j < parent.aObjects.length; j++) {
128                             if (parent.aObjects[j].sId == subpath) {
129                                 parent = parent.aObjects[j];
130                                 break search;
131                             }
132                         }
133                         throw Error("Invalid path: '" +
134                                     path.substring(0, index) + "'");
135                     }
136                     index = path.indexOf("/", index + 1);
137                 }
138                 parent.pushObject(obj);
139             }
140             ox.Configuration.nodes[path] = this;
141         },
142     
143         /**
144          * A map from paths to configuration tree nodes.
145          * @private
146          */
147         nodes: {},
148         
149         /**
150          * A counter for unique node IDs.
151          * @private
152          */
153         id: 0,
154         
155         /**
156          * An array with all created configuration pages.
157          * @private
158          */
159         pages: []
160         
161     },
162     
163     /**
164      * @namespace
165      */
166     JSON: {}
167 };
168 
169 /**
170  * Asynchronously requests a JSON object from the server.
171  * This method retrieves a JSON object from the server by issuing an HTTP
172  * GET request to the specified URI and calling the specified callback when
173  * the retrieval is complete.
174  * @param {String} uri The URI for the HTTP GET request.
175  * @param {Function} ok A callback function which is called with the
176  * received JSON object or raw data as parameter. If there was any error
177  * then this function is not called.
178  * @param {Function} error An optional callback function wihch is called when
179  * the server returns an error. The function takes two parameters: result and
180  * status. If the HTTP status code was 200, then result is the JSON object and
181  * status is not set. If the HTTP status was not 200, then result is the status
182  * string and status is the HTTP status code. The function should return true
183  * when it handles the error. Otherwise, the default error handler specified by
184  * JSON.errorHandler will be called after this function returns. If this
185  * parameter is not specified, the default error handler is called directly.
186  * @param {Boolean} raw Specifies whether the response data should be
187  * passed to the callback as-is or first parsed as a JSON object. Defaults
188  * to the latter.
189  */
190 ox.JSON.get = function(uri, ok, error, raw) {
191     (new JSON()).get(uri, null, ok, error, raw);
192 };
193 
194 /**
195  * Asynchronously posts url-encoded data and retrieves a JSON object from
196  * the server.
197  * This method posts an object to the server by issuing an HTTP
198  * POST request to the specified URI and calling the specified callback when
199  * the reply arrives.
200  * @param {String} uri The URI for the HTTP POST request.
201  * @param {Object} data An object which is serialized using the
202  * application/x-www-form-urlencoded encoding and sent as the body of the
203  * request.
204  * @param {Function} ok A callback function which is called with the received
205  * JSON object or raw data as parameter. If there was any error then this
206  * function is not called.
207  * @param {Function} error An optional callback function wihch is called when
208  * the server returns an error. The function takes two parameters: result and
209  * status. If the HTTP status code was 200, then result is the JSON object and
210  * status is not set. If the HTTP status was not 200, then result is the status
211  * string and status is the HTTP status code. The function should return true
212  * when it handles the error. Otherwise, the default error handler specified by
213  * JSON.errorHandler will be called after this function returns. If this
214  * parameter is not specified, the default error handler is called directly.
215  * @param {Boolean} raw Specifies whether the response data should be
216  * passed to the callback as-is or first parsed as a JSON object. Defaults
217  * to the latter.
218  */
219 ox.JSON.post = function(uri, data, ok, error, raw) {
220     (new JSON()).post(uri, data, null, ok, error, raw);
221 };
222 
223 /**
224  * Asynchronously sends a JSON object and retrieves a JSON object from
225  * the server.
226  * This method sends a JSON object to the server by issuing an HTTP
227  * PUT request to the specified URI and calling the specified callback when
228  * the reply arrives.
229  * @param {String} uri The URI for the HTTP POST request.
230  * @param {Object} data An object which is serialized using JSON syntax and
231  * sent as the body of the request.
232  * @param {Function} ok A callback function which is called with the received
233  * JSON object or raw data as parameter. If there was any error then this
234  * function is not called.
235  * @param {Function} error An optional callback function wihch is called when
236  * the server returns an error. The function takes two parameters: result and
237  * status. If the HTTP status code was 200, then result is the JSON object and
238  * status is not set. If the HTTP status was not 200, then result is the status
239  * string and status is the HTTP status code. The function should return true
240  * when it handles the error. Otherwise, the default error handler specified by
241  * JSON.errorHandler will be called after this function returns. If this
242  * parameter is not specified, the default error handler is called directly.
243  * @param {Boolean} raw Specifies whether the response data should be
244  * passed to the callback as-is or first parsed as a JSON object. Defaults
245  * to the latter.
246  */
247 ox.JSON.put = function(uri, data, ok, error, raw) {
248     (new JSON()).put(uri, data, null, ok, error, raw);
249 };
250 
251 /**
252  * @class Abstract base class of all widgets.
253  */
254 ox.UI.Widget = function() {
255     /**
256      * Specifies whether the widget was already initialized.
257      * @type Boolean
258      * @default false
259      */
260     this.initialized = false;
261 };
262 
263 /**
264  * The default value of this widget, which is used when the model does
265  * not contain the field for this widget.
266  * @name ox.UI.Widget.prototype.default_value
267  */
268 
269 /**
270  * The topmost DOM node of the widget. It is used by the default implementations
271  * of {@link #show}, {@link #hide} and {@link #remove}.
272  * @name ox.UI.Widget.prototype.node
273  * @type DOM Node
274  */
275 
276 /**
277  * A DOM node which is used by the default implementation of {@link #enable} and
278  * {@link #disable} to control the disabled state of the widget. The DOM node
279  * should have a property named "disabled". If the value equals false, disabling
280  * will have no effect besides updating the {@link #enabled} field and applying
281  * the disabled CSS.
282  * @name ox.UI.Widget.prototype.formnode
283  * @type DOM Node
284  */
285 
286 /**
287  * The width of the widget as a CSS length specification.
288  * If not specified, defaults to the {@link ox.UI.Container#childWidth} of
289  * the parent.
290  * @name ox.UI.Widget.prototype.width
291  * @type String
292  */
293 
294 ox.UI.Widget.setDisabledClass = classNameSetter("font-color-disabled");
295 
296 ox.UI.Widget.prototype = {
297 
298     /**
299      * Adds the DOM nodes of this widget to its parent container.
300      * @param {String} node_id The ID of the current page. This ID is required
301      * for adding menu entries.
302      */
303     addContent: function(node_id) {
304         this.initialized = true;
305         if (!this.isEnabled) this.applyEnabled();
306         if (!this.isVisible) this.applyVisible();
307     },
308     
309     /**
310      * Returns the current value of the widget. The returned type depends on
311      * the actual widget class.
312      */
313     get: function() {},
314     
315     /**
316      * Sets the value which is displayed by the widget.
317      * @param value The new value of the widget. The type depends on the actual
318      * widget class.
319      */
320     set: function(value) {},
321     
322     /**
323      * Resizes the widget.
324      */
325     resize: function() {},
326     
327     /**
328      * Removes the widget's DOM nodes from the form.
329      */
330     remove: function() {
331         if (this.node) this.node.parentNode.removeChild(this.node);
332     },
333     
334     /**
335      * Displays the widget if it was previously hidden.
336      */
337     show: function() { this.setVisible(true); },
338     
339     /**
340      * Hides the widget.
341      */
342     hide: function() { this.setVisible(false); },
343     
344     /**
345      * Sets the visibility of the widget.
346      * @param {Boolean} visible The new visibility status.
347      */
348     setVisible: function(visible) {
349         this.visible = visible;
350         visible = visible && (!this.parent || this.parent.isVisible);
351         if (this.isVisible != visible) {
352             this.isVisible = visible;
353             if (this.initialized) this.applyVisible();
354         }
355     },
356     
357     /**
358      * Applies the visibility status to the actual widget.
359      * Only called when the widget is initialized.
360      * @protected
361      */
362     applyVisible: function() {
363         if (this.node) this.node.style.display = this.isVisible ? "" : "none";
364     },
365     
366     /**
367      * Specifies whether the widget is visible.
368      * @type Boolean
369      * @default true
370      */
371     visible: true,
372     
373     /**
374      * The actual visibility of the widget, which can be false even if visible
375      * is set to true, because the widget's container is invisible.
376      */
377     isVisible: true,
378 
379     /**
380      * Enables the widget for user interaction.
381      */
382     enable: function() { this.setEnabled(true); },
383     
384     /**
385      * Disables the widget for user interaction. 
386      */
387     disable: function() { this.setEnabled(false); },
388     
389     /**
390      * Enables or disables the widget.
391      * @param {Boolean} enabled The new enabled status.
392      */
393     setEnabled: function(enabled) {
394         this.enabled = enabled;
395         enabled = enabled && (!this.parent || this.parent.isEnabled);
396         if (this.isEnabled != enabled) {
397             this.isEnabled = enabled;
398             if (this.initialized) this.applyEnabled();
399         }
400     },
401     
402     /**
403      * Applies the value of this.isEnabled to the actual widget.
404      * When this method is called, the widget is already initialized.
405      * @protected
406      */
407     applyEnabled: function() {
408         if (this.formnode) this.formnode.disabled = !this.isEnabled;
409         if (this.node)
410             ox.UI.Widget.setDisabledClass(this.node, !this.isEnabled);
411     },
412     
413     /**
414      * Specifies whether the widget is enabled.
415      * @type Boolean
416      * @default true
417      */
418     enabled: true,
419     
420     /**
421      * The actual enabled state of the widget, which can be false even if
422      * enabled is set to true, because the widget's container is disabled.
423      */
424     isEnabled: true,
425     
426     /**
427      * Sets the widget's container.
428      * @param {ox.UI.Container} parent The new parent container.
429      */
430     setParent: function(parent) {
431         this.parent = parent;
432         this.setEnabled(this.enabled);
433         this.setVisible(this.visible);
434     },
435     
436     /**
437      * The widget which contains this widget. null if this widget is not a child
438      * of another widget.
439      * @default null
440      */
441     parent: null
442 
443 };
444 
445 /**
446  * @class Static explanation text.
447  * Do not specify a field name when adding instances of this class to
448  * containers.
449  * @augments ox.UI.Widget
450  * @param {I18nString} text The displayed text.
451  */
452 ox.UI.Text = function(text) {
453     ox.UI.Widget.apply(this);
454     this.text = text;
455 };
456 
457 ox.UI.Text.prototype = extend(ox.UI.Widget,
458     /** @lends ox.UI.Text.prototype */
459     {
460 
461         addContent: function(node_id) {
462             this.node = this.parent.addRow(addTranslated(this.text), false);
463             ox.UI.Widget.prototype.addContent.apply(this, arguments);
464         }
465 
466     });
467 
468 /**
469  * @class A text input field.
470  * @param {I18nString} label The label for the input field.
471  */
472 ox.UI.Input = function(label) {
473     ox.UI.Widget.apply(this);
474     this.label = label;
475 };
476 
477 ox.UI.Input.prototype = extend(ox.UI.Widget,
478     /** @lends ox.UI.Input.prototype */
479     {
480 
481         /**
482          * The default value of the input field, which is used when the model
483          * does not contain the field for this input field.
484          * @default an empty string
485          */
486         default_value: "",
487     
488         addContent: function(node_id) {
489             this.formnode =
490                 newnode("input",
491                         { width: this.width || this.parent.childWidth },
492                         { type: this.inputType });
493             this.node = this.parent.addCells(this.label, this.formnode);
494             ox.UI.Widget.prototype.addContent.apply(this, arguments);
495         },
496     
497         /**
498          * Returns the current value of the input field.
499          * @type String
500          */
501         get: function() { return this.formnode.value; },
502     
503         /**
504          * Sets the value of the input field.
505          * @param {String} value
506          */
507         set: function(value) { this.formnode.value = value; },
508         
509         /**
510          * The type attribute of the HTML input element.
511          * @type String
512          * @default "text"
513          */
514         inputType: "text"
515         
516     });
517 
518 /**
519  * @class A password input field.
520  * @augments ox.UI.Input
521  * @param {I18nString} label The label for the input field.
522  */
523 ox.UI.Password = function(label) {
524     ox.UI.Input.apply(this, arguments);
525 }
526 
527 ox.UI.Password.prototype = extend(ox.UI.Input,
528     /** @lends ox.UI.Password.prototype */
529     { inputType: "password" });
530 
531 /**
532  * @class a multi-line text input area.
533  * @param {I18nString} label The label for the input field.
534  */
535 ox.UI.TextArea = function(label) {
536     ox.UI.Widget.apply(this);
537     this.label = label;
538 }
539 
540 ox.UI.TextArea.prototype = extend(ox.UI.Widget,
541     /** @lends ox.UI.TextArea.prototype */
542     {
543     
544         default_value: "",
545     
546         addContent: function(node_id) {
547             this.formnode = newnode("textarea",
548                 { width: this.width || this.parent.childWidth }, 0);
549             if (this.height) this.formnode.style.height = this.height;
550             this.node = this.parent.addCells(this.label, this.formnode);
551             ox.UI.Widget.prototype.addContent.apply(this, arguments);
552         },
553         
554         /**
555          * Returns the current value of the text area.
556          * @type String
557          */
558         get: function() { return this.formnode.value; },
559         
560         /**
561          * Sets the value of the text area.
562          * @param {String} value
563          */
564         set: function(value) { this.formnode.value = value; }
565         
566     });
567 
568 /**
569  * @class A check box for a single boolean field.
570  * @augments ox.UI.Widget
571  * @param {I18nString} label The label for the checkbox.
572  */
573 ox.UI.CheckBox = function(label) {
574     ox.UI.Widget.apply(this);
575     this.label = label;
576 };
577 
578 /**
579  * @private
580  */
581 ox.UI.CheckBox.id = 0;
582 
583 /**
584  * This callback is called when the user changes the state of the CheckBox.
585  * @name ox.UI.CheckBox.prototype.changed
586  * @type Function
587  */
588 
589 ox.UI.CheckBox.prototype = extend(ox.UI.Widget,
590     /** @lends ox.UI.CheckBox.prototype */
591     {
592     
593         addContent: function(node_id) {
594             var Self = this;
595             var id = "ox.UI.CheckBox." + ox.UI.CheckBox.id++;
596             this.formnode = newnode("input", 0, { type: "checkbox", id: id });
597             this.node = this.parent.addRow(newnode("label", 0, { htmlFor: id }, [
598                 this.formnode,
599                 document.createTextNode(" "),
600                 addTranslated(this.label)
601             ]), true);
602             addDOMEvent(this.formnode, "click", function() {
603                 if (Self.changed) Self.changed();
604             });
605             ox.UI.Widget.prototype.addContent.apply(this, arguments);
606         },
607         
608         /**
609          * Returns the current value of the CheckBox.
610          * @type Boolean
611          */
612         get: function() { return this.formnode.checked; },
613         
614         /**
615          * Sets the value of the CheckBox
616          * @param {Boolean} value
617          */
618         set: function(value) { this.formnode.checked = value; }
619 
620     });
621 
622 /**
623  * @class Abstract base class of selection widgets.
624  * This class handles the association between displayed objects and value
625  * objects.
626  * @augments ox.UI.Widget
627  */
628 ox.UI.Selection = function() {
629     ox.UI.Widget.apply(this);
630     this.values = [];
631     this.display_values = [];
632 };
633 
634 /**
635  * This callback is called when the user selects a different value.
636  * @name ox.UI.Selection.prototype.changed
637  * @type Function
638  */
639 
640 ox.UI.Selection.prototype = extend(ox.UI.Widget,
641     /** @lends ox.UI.Selection.prototype */
642     {
643     
644         /**
645          * Sets the list of possible values and the corresponding displayed
646          * values.
647          * The parameters are used directly, without copying. Therefore,
648          * the arrays should not be modified after being passed to this method.
649          * An exception is modifying the arrays and immediately calling this
650          * method again to update the displayed widget.
651          * @param {Array} values An array of possible values.
652          * @param {Array} display_values An array of displayed values.
653          * Each element represents the value in the values array with the same
654          * index. The type of elements depends on the actual selection class.
655          */
656         setEntries: function(values, display_values) {
657             this.values = values;
658             this.display_values = display_values;
659         }
660     
661     });
662 
663 /**
664  * @class A combination of a text input field and a drop-down list.
665  * @augments ox.UI.Selection
666  * @param {I18nString} label The label of the ComboBox.
667  * @param {Boolean} editable Specifies whether the text input field can contain
668  * values which are not in the drop-down list. Defaults to false.
669  * Not implemented yet.
670  *
671  */
672 ox.UI.ComboBox = function(label, editable) {
673     ox.UI.Selection.apply(this);
674     this.label = label;
675     this.editable = editable;
676 }
677 
678 ox.UI.ComboBox.prototype = extend(ox.UI.Selection,
679     /** @lends ox.UI.ComboBox.prototype */
680     {
681         
682         /**
683          * @private
684          */
685         recreate: function() {
686             var nokey = {};
687             var key = this.combobox ? this.combobox.getKey() : nokey;
688             removeChildNodes(this.div);
689             var Self = this;
690             this.combobox = new ComboBox3.impl(window, this.div,
691                 this.width || this.parent.childWidth, 0, true,
692                 Math.min(8, this.values.length),
693                 function(value) {
694                     if (Self.changed) Self.changed(value);
695                 });
696             if (this.values.length) {
697                 for (var i = 0; i < this.values.length; i++) {
698                 var f = this.display_values[i];
699                     if (typeof f != "function")
700                         f = (function(x) {
701                             return function() { return x; };
702                         })(f);
703                     this.combobox.addElement(f(), this.values[i], f);
704                 }
705             } else {
706                 this.combobox.addElement("\xa0", null);
707             }
708             this.combobox.getDomNode();
709             if (key != nokey) this.combobox.setKey(key);
710             if (!this.enabled) this.combobox.disable();
711         },
712 
713         addContent: function(node_id) {
714             this.div = newnode("div");
715             this.node = this.parent.addCells(this.label, this.div);
716             this.recreate();
717             ox.UI.Selection.prototype.addContent.apply(this, arguments);
718         },
719         
720         resize: function() { this.recreate(); },
721         
722         /**
723          * Sets the list of possible values and the corresponding displayed
724          * textual descriptions.
725          * The parameters are used directly, without copying. Therefore,
726          * the arrays should not be modified after being passed to this method.
727          * An exception is modifying the arrays and immediately calling this
728          * method again to update the displayed widget.
729          * @param {Array} values An array of possible values.
730          * @param {I18nString[]} display_values An array of displayed texts.
731          * Each element represents the value in the values array with the same
732          * index.
733          */
734         setEntries: function(values, display_values) {
735             ox.UI.Selection.prototype.setEntries.apply(this, arguments);
736             if (this.initialized) this.recreate();
737         },
738         
739         /**
740          * Returns the current value of the ComboBox.
741          */
742         get: function() {
743             return this.combobox.getKey();
744         },
745         
746         /**
747          * Sets the value of the ComboBox.
748          */
749         set: function(value) {
750             this.combobox.setKey(value);
751         },
752         
753         applyEnabled: function() {
754             if (this.isEnabled)
755                 this.combobox.enable();
756             else
757                 this.combobox.disable();
758             ox.UI.Widget.setDisabledClass(this.node, !this.isEnabled);
759         }
760 
761     });
762 
763 /**
764  * @class A group of radio buttons. Only one option can be selected at a time.
765  * @augments ox.UI.Selection
766  */
767 ox.UI.RadioButtons = function() {
768     ox.UI.Selection.apply(this);
769     this.name = ++ox.UI.RadioButtons.lastName;
770 }
771 
772 ox.UI.RadioButtons.lastName = 0;
773 
774 ox.UI.RadioButtons.prototype = extend(ox.UI.Selection,
775     /** @lends ox.UI.RadioButtons.prototype */
776     {
777     
778         /**
779          * @private
780          */
781         recreate: function() {
782             removeChildNodes(this.div);
783             this.nodes = new Array(this.values.length);
784             var Self = this;
785             for (var i = 0; i < this.values.length; i++) {
786                 this.nodes[i] = newnode("input", 0,
787                     { type: "radio", name: this.name, value: this.values[i],
788                       className: "noborder nobackground" });
789                 addDOMEvent(this.nodes[i], "change", function() {
790                     if (Self.changed) Self.changed();
791                 });
792                 this.div.appendChild(newnode("label", 0,
793                     { htmlFor: "ox.UI.RadioButtons." + this.name + "." + i },
794                     [
795                         this.nodes[i],
796                         document.createTextNode(" "),
797                         addTranslated(this.display_values[i])
798                     ]));
799                 this.div.appendChild(newnode("br"));
800             }
801         },
802         
803         addContent: function(node_id) {
804             this.div = newnode("div");
805             this.node = this.parent.addRow(this.div, true);
806             this.recreate();
807             ox.UI.Selection.prototype.addContent.apply(this, arguments);
808         },
809     
810         /**
811          * Sets the list of possible values and the corresponding displayed
812          * labels.
813          * The parameters are used directly, without copying. Therefore,
814          * the arrays should not be modified after being passed to this method.
815          * An exception is modifying the arrays and immediately calling this
816          * method again to update the displayed widget.
817          * @param {Array} values An array of possible values.
818          * @param {String[]} display_values An array of displayed labels.
819          * Each element represents the value in the values array with the same
820          * index.
821          */
822         setEntries: function() {
823             ox.UI.Selection.prototype.setEntries.apply(this, arguments);
824             if (this.initialized) this.recreate();
825         },
826         
827         /**
828          * Returns the current value of the RadioButtons.
829          */
830         get: function() {
831             for (var i = 0; i < this.values.length; i++)
832                 if (this.nodes[i].checked) return this.values[i];
833         },
834         
835         /**
836          * Sets the value of the RadioButtons.
837          */
838         set: function(value) {
839             for (var i = 0; i < this.values.length; i++)
840                 this.nodes[i].checked = this.values[i] == value;
841         },
842         
843         applyEnabled: function() {
844             for (var i = 0; i < this.nodes.length; i++)
845                 this.nodes[i].disabled = !this.isEnabled;
846             ox.UI.Widget.setDisabledClass(this.node, !this.isEnabled);
847         }
848         
849     });
850 
851 /**
852  * @class A button.
853  * @augments ox.UI.Widget
854  * @para {I18nStirng} text The text of the button.
855  */
856 ox.UI.Button = function(text) {
857     ox.UI.Widget.apply(this);
858     this.text = text;
859 };
860 
861 /**
862  * A callback which is called when the button is clicked.
863  * @name ox.UI.Button.prototype.click
864  * @type Function
865  */
866 
867 ox.UI.Button.prototype = extend(ox.UI.Widget,
868     /** @lends ox.UI.Button.prototype */
869     {
870     
871         addContent: function(node_id) {
872             this.formnode = newnode("button", 0, 0, [addTranslated(this.text)]);
873             var Self = this;
874             addDOMEvent(this.formnode, "click", function() {
875                 if (Self.click) Self.click();
876             });
877             this.node = this.parent.addRow(this.formnode, false);
878             ox.UI.Widget.prototype.addContent.apply(this, arguments);
879         }
880     
881     });
882 
883 /**
884  * @class Abstract base class of all controllers.
885  * Controllers are responsible for connecting widgets to the data model.
886  */
887 ox.UI.Controller = {};
888 
889 /**
890  * @class An exception object which can be thrown by ox.UI.Controller.set to
891  * indicate an attempt to set an invalid value.
892  * @param {String} message An optional message which, if specified, is assigned
893  * to the property message of the created instance.
894  */
895 ox.UI.Controller.InvalidData = function(message) {
896     if (message != undefined) this.message = message;
897 },
898 
899 ox.UI.Controller.InvalidData.prototype = new Error("Invalid data");
900 ox.UI.Controller.InvalidData.prototype.constructor =    
901     ox.UI.Controller.InvalidData;
902 ox.UI.Controller.InvalidData.prototype.name = "ox.UI.Controller.InvalidData";
903 
904 /**
905  * Adds a child value to a container value.
906  * @name ox.UI.Controller.prototype.get
907  * @function
908  * @param data The value of the container.
909  * @param value The value of the child widget.
910  */
911 
912 /**
913  * Extracts a child value from a container value.
914  * Throws ox.UI.Controller.InvalidData if the extracted data is invalid.
915  * @name ox.UI.Controller.prototype.set
916  * @function
917  * @param data The value of the container.
918  * @return The value of the child widget, or undefined if the default value of
919  * the widget should be used.
920  */
921 
922 /**
923  * @class Abstract base class of all containers.
924  * @augments ox.UI.Widget
925  */
926 ox.UI.Container = function() {
927     ox.UI.Widget.apply(this);
928     this.children = [];
929     this.names = {};
930 };
931 
932 /**
933  * Adds a content row to the container.
934  * @name ox.UI.Container.prototype.addRow
935  * @function
936  * @param {DOM Node} child A DOM node which is or contains the chlid widget.
937  * @param {Boolean} table Specifies whether the contents of this row should
938  * align themselves with the content of other rows (if true), or not (if false).
939  * @type DOM node
940  * @return The DOM TR or DIV element of the added row.
941  */
942 
943 /**
944  * Adds a row consisting of two cells: a label and a form element.
945  * The form element will align itself with other form elements 
946  * @name ox.UI.Container.prototype.addCells
947  * @function
948  * @param {I18nString} label An optional label text for the form element.
949  * @param {DOM node} input The DOM node which contains the form element. 
950  * @type DOM node
951  * @return The DOM TR element of the added row.
952  */
953 
954 ox.UI.Container.prototype = extend(ox.UI.Widget,
955     /** @lends ox.UI.Container.prototype */
956     {
957         addContent: function(node_id) {
958             this.node_id = node_id;
959             for (var i = 0; i < this.children.length; i++)
960                 this.children[i].addContent(node_id);
961             ox.UI.Widget.prototype.addContent.apply(this, arguments);
962         },
963         
964         default_value: {},
965         
966         /**
967          * Adds a widget to this container as a new child.
968          * The new widget appears behind all previously added widgets.
969          * The value of the container is an object which contains the values of
970          * all children. Each child value appears in the value of the container
971          * as a field with the name specified by the parameter name.
972          * @param {Widget} widget The widget to add.
973          * @param {String or ox.UI.Controller} name If a string, the field name
974          * under which the child value will appear in the container's value.
975          * If an {@link ox.UI.Controller}, the controller which is responsible
976          * for building and parsing the container value. If not specified,
977          * the child value will not be connected to the container value.
978          * @see ox.Configuration.Group.NoField
979          */
980         addWidget: function(widget, name) {
981             if (name !== undefined) this.names[this.children.length] = name;
982             this.children.push(widget);
983             widget.setParent(this);
984             if (this.initialized) widget.addContent(this.node_id);
985         },
986         
987         /**
988          * Removes a previously added widget.
989          * @param {Widget} widget The widget to remove.
990          */
991         deleteWidget: function(widget) {
992             for (var i = 0; i < this.children.length; i++) {
993                 if (this.children[i] == widget) {
994                     this.children[i].remove(this);
995                     this.children.splice(i, 1);
996                     widget.setParent(null);
997                     break;
998                 }
999             }
1000         },
1001         
1002         /**
1003          * Returns an object with all child values.
1004          * Only children with specified field names are queried.
1005          * @type Object
1006          */
1007         get: function() {
1008             var data = {};
1009             for (var i = 0; i < this.children.length; i++) {
1010                 if (i in this.names) {
1011                     var child = this.children[i];
1012                     if (child.isVisible && child.isEnabled) {
1013                         var value = child.get();
1014                         if (typeof this.names[i] == "string") {
1015                             data[this.names[i]] = value;
1016                         } else {
1017                             this.names[i].get(data, value);
1018                         }
1019                     }
1020                 }
1021             }
1022             return data;
1023         },
1024         
1025         /**
1026          * Sets the values of all children.
1027          * @param {Object} data An object with a field for every child.
1028          * If a child was added with a field name, but the object does not
1029          * contain that field, the child will be set to its default value.
1030          */
1031         set: function(value) {
1032             value = value || {};
1033             for (var i = 0; i < this.children.length; i++) {
1034                 if (i in this.names) {
1035                     if (typeof this.names[i] == "string") {
1036                         var val = value[this.names[i]];
1037                     } else {
1038                         var val = this.names[i].set(value);
1039                     }
1040                     if (val === undefined || val === null)
1041                         val = this.children[i].default_value;
1042                     this.children[i].set(val);
1043                 }
1044             }
1045         },
1046         
1047         resize: function() {
1048             for (var i = 0; i < this.children.length; i++)
1049                 this.children[i].resize();
1050         },
1051         
1052         applyVisible: function() {
1053             ox.UI.Widget.prototype.applyVisible.call(this);
1054             for (var i = 0; i < this.children.length; i++) {
1055                 var child = this.children[i];
1056                 child.setVisible(child.visible);
1057             }
1058         },
1059         
1060         applyEnabled: function() {
1061             ox.UI.Widget.prototype.applyEnabled.call(this);
1062             for (var i = 0; i < this.children.length; i++) {
1063                 var child = this.children[i];
1064                 child.setEnabled(child.enabled);
1065             }
1066         }
1067 
1068     });
1069 
1070 registerView("configuration/modules",
1071     function() { showNode("modules"); },
1072     null, null,
1073     function() { hideNode("modules"); });
1074 
1075 register("Loaded", function() {
1076     ox.Configuration.View.i18n = new I18nNode(emptyFunction);
1077     ox.Configuration.View.i18n.disable();
1078     $("modules.header").appendChild(ox.Configuration.View.i18n.node);
1079 });
1080 
1081 resizeEvents.register("Resized", function() {
1082     var current = ox.Configuration.View.current;
1083     if (current) setTimeout(function() { current.resize(); }, 0);
1084 });
1085 
1086 /**
1087  * Attaches a content page to a leaf node in the configuration tree.
1088  * The order of events for a view is <ol>
1089  * <li>{@link #init} (first time only),</li>
1090  * <li>{@link #addContent} (first time only),</li>
1091  * <li>{@link #enter},</li>
1092  * <li>user edits the view,</li>
1093  * <li>{@link #viewModified}</li>
1094  * <li>optionally, either {@link #saveView} or {@link #cancelView},</li>
1095  * <li>{@link #leave}.</li></ol>
1096  * @class Abstract base class of configuration pages.
1097  * @augments ox.UI.Container
1098  * @param {ox.Configuration.LeafNode} node A {@link ox.Configuration.LeafNode}
1099  * which will be configured to open this page.
1100  * @param {I18nString} title The page title.
1101  */
1102 ox.Configuration.View = function(node, title) {
1103     ox.UI.Container.apply(this);
1104     var Self = this;
1105     var view = node.id;
1106     var old_configuration_changed;
1107     registerView(view, function() {
1108         if (!Self.initialized) {
1109             if (Self.init) Self.init();
1110             Self.addContent(view);
1111         }
1112         with (ox.Configuration.View) {
1113             if (typeof title != "function") {
1114                 if (debug) {
1115                     (console.warn || console.log || alert)(format(
1116                         "The string \"%s\" is not internationalized!",
1117                         title));
1118                 }
1119                 i18n.callback = function() { return _(title); };
1120             } else {
1121                 i18n.callback = title;
1122             }
1123             i18n.update();
1124             i18n.enable();
1125         }
1126         $("modules.content").appendChild(Self.content);
1127     }, function() {
1128         ox.Configuration.View.current = Self;
1129         old_configuration_changed = configuration_changed;
1130         configuration_changed = modified;
1131         register("OX_SAVE_OBJECT", save);
1132         register("OX_Cancel_Object", cancel);
1133         Self.enter();
1134     }, function() {
1135         Self.leave();
1136         unregister("OX_SAVE_OBJECT", save);
1137         unregister("OX_Cancel_Object", cancel);
1138         configuration_changed = old_configuration_changed;
1139         ox.Configuration.View.current = null;
1140     }, function() {
1141         $("modules.content").removeChild(Self.content);
1142         ox.Configuration.View.i18n.disable();
1143     });
1144     ox.Configuration.pages.push(this);
1145     node.click = function() {
1146         configuration_askforSave(function() {
1147             triggerEvent("OX_Switch_View", node.id); 
1148         });     
1149     };
1150     
1151     function modified() { return Self.viewModified(); }
1152     
1153     function save() { if (Self.viewModified()) Self.saveView(); }
1154     
1155     function cancel() { Self.cancelView(); }
1156 };
1157 
1158 /**
1159  * A callback which is called before the view is opened for the first time.
1160  * Usually, child elements are created and added here.
1161  * @name ox.Configuration.View.prototype.init
1162  * @type Function
1163  */
1164 
1165 /**
1166  * DOM node with the content of the view.
1167  * @name ox.Configuration.View.prototype.content
1168  * @type DOM node
1169  */
1170 
1171 /**
1172  * Returns whether the view contents were modified and may need to be saved.
1173  * @name ox.Configuration.View.prototype.viewModified
1174  * @function
1175  * @type Boolean
1176  * @return true if the view contents were modified.
1177  */
1178 
1179 ox.Configuration.View.prototype = extend(ox.UI.Container,
1180     /** @lends ox.Configuration.View.prototype */
1181     {
1182     
1183         /**
1184          * Performs initialization when the user enters the view.
1185          */
1186         enter: function() {},
1187         
1188         /**
1189          * Performs cleanup when the user leaves the view.
1190          */
1191         leave: function() {},
1192         
1193         /**
1194          * Saves view contents.
1195          * @param {Function} callback An optional callback functi which is
1196          * called after the view is saved. 
1197          */
1198         saveView: function(callback) {},
1199         
1200         /**
1201          * Reverts modifications made by the user.
1202          */
1203         cancelView: function() {},
1204         
1205         /**
1206          * Width of child widgets, as a CSS length value.
1207          * @type String
1208          * @default "20em"
1209          */
1210         childWidth: "20em"
1211         
1212     });
1213 
1214 /**
1215  * @class Resizable vertical split view.
1216  * All sizes are specified as a fraction of the total available space.
1217  * @augments ox.Configuration.View
1218  * @param {ox.Configuration.LeafNode} node An {@link ox.Configuration.LeafNode}
1219  * which will be configured to open this page.
1220  * @param {I18nString} title The page title.
1221  * @param {Number} size The width of the left panel.
1222  * @param {Boolean} new_button Specifies whether the big button in the menu
1223  * should be a "New" button instead of a "Save" button.
1224  */
1225 ox.Configuration.VSplit = function(node, title, size, new_button) {
1226     ox.Configuration.View.call(this, node, title);
1227 
1228     /**
1229      * Current width of the left panel.
1230      * @type Number
1231      * @private
1232      */
1233     this.size = size;
1234     
1235     /**
1236      * Specifies whether the currently edited data is a new list entry.
1237      * @type Boolean
1238      * @private
1239      */
1240     this.isNew = false;
1241     
1242     var Self = this;
1243     if (new_button) {
1244         this.new_click = function() { if (Self.onNew) Self.onNew(); }
1245         changeDisplay(node.id, "menu_new");
1246     } else {
1247         changeDisplay(node.id, "menu_save");
1248     }
1249 };
1250 
1251 /**
1252  * A callback which is called when the big "New" button in the menu is clicked.
1253  * @name ox.Configuration.VSplit.prototype.onNew
1254  * @type Function
1255  */
1256 
1257 /**
1258  * The LiveGrid which is displayed in the left panel. It must be set before
1259  * the view is entered for the first time.
1260  * @name ox.Configuration.VSplit.prototype.list
1261  * @type LiveGrid
1262  * @deprecated
1263  */
1264 
1265 /**
1266  * Specifies whether the list view was modified and needs to be saved.
1267  * @name ox.Configuration.VSplit.prototype.listModified
1268  * @type Boolean
1269  * @default false
1270  */
1271 
1272 /**
1273  * Enables the list view when the view is entered. Disabling is done
1274  * automatically by calling the disable() method of the list. 
1275  * @name ox.Configuration.VSplit.prototype.enableList
1276  * @function
1277  */
1278 
1279 /**
1280  * A callback which is called to get the data for the detail view.
1281  * It has a continuation function as parameter, which should be called with
1282  * the data as parameter when the data becomes available. If the continuation
1283  * function is called without a parameter, the detail view is set to the default
1284  * value and disabled.
1285  * @name ox.Configuration.VSplit.prototype.load
1286  * @type Function
1287  */
1288 
1289 /**
1290  * A callback which is called to save the data of the detail view.
1291  * It has two parameters: data and cont. data is the data object to save.
1292  * cont is a continuation functions which should be called after the data was
1293  * saved successfully. If the continuation function function is called with
1294  * a parameter, the value of the parameter is used as the new value.
1295  * @name ox.Configuration.VSplit.prototype.save
1296  * @type Function
1297  */
1298 
1299 /**
1300  * An unmodified copy of the data object which is edited in the detail view.
1301  * @name ox.Configuration.VSplit.prototype.original
1302  */
1303 
1304 ox.Configuration.VSplit.prototype = extend(ox.Configuration.View,
1305     /** @lends ox.Configuration.VSplit.prototype */
1306     {
1307     
1308         addContent: function(node_id) {
1309             var selection = this.list.selection;
1310             selection.events.register("Selected", function(count) {
1311                 menuselectedfolders = [];
1312                 triggerEvent("OX_SELECTED_ITEMS_CHANGED", selection.length);
1313                 triggerEvent("OX_SELECTION_CHANGED", selection.length);
1314             });
1315             var list_parent = newnode("div", {
1316                 position: "absolute", top: "1.6em", bottom: 0, overflow: "auto",
1317                 width: "100%"
1318             });
1319             this.left = newnode("div", {
1320                 position: "absolute", left: 0, top: 0, width: 0, height: "100%",
1321                 overflow: "hidden"
1322             }, 0, [this.list.getHeader(), list_parent]);
1323             this.list.getTable(list_parent);
1324             this.split = newnode("div", {
1325                 position: "absolute", left: 0, top: 0, height: "100%",
1326                 width: this.split_size + "px", cursor: "e-resize"
1327             }, { className: "sizesplit-vline" }, [
1328                 newnode("img", { top: "50%", marginTop: "-10px" },
1329                     { src: getFullImgSrc("img/split_grip_v.gif") })
1330             ]);
1331             addDOMEvent(this.split, "mousedown", d);
1332             this.right = this.table = newnode("div", {
1333                 position: "absolute", right: 0, top: 0, height: "100%",
1334                 left: this.split_size + "px", overflow: "auto"
1335             });
1336             this.content = newnode("div", {
1337                 position: "absolute", left: 0, top: 0, width: "100%",
1338                 height: "100%"
1339             }, 0, [this.left, this.split, this.right]);
1340             ox.Configuration.View.prototype.addContent.apply(this, arguments);
1341 
1342             var Self = this;
1343 
1344             function d(e) {
1345                 hideIFrames();
1346                 Self.content.style.cursor = "e-resize";
1347                 addDOMEvent(body, "mousemove", m);
1348                 addDOMEvent(body, "mouseup", u);
1349                 var pxsize = Self.getPixel(Self.size);
1350                 var offset = pxsize - e.clientX;
1351                 if (!Self.animated) {
1352                     var movingSplit = Self.split.cloneNode(true);
1353                     movingSplit.style.left = pxsize + "px";
1354                     movingSplit.className = movingSplit.className + " moving";
1355                     Self.content.appendChild(movingSplit);
1356                 }
1357                 cancelDefault(e);
1358                 
1359                 function getFraction(x) {
1360                     return (x + offset) /
1361                            (Self.content.clientWidth - Self.split_size);
1362                 }
1363 
1364                 function m(e) {
1365                     stopEvent(e);
1366                     Self.size = getFraction(e.clientX);
1367                     if (Self.animated) {
1368                         Self.setSize(Self.size);
1369                     } else {
1370                         movingSplit.style.left = (e.clientX + offset) + "px";
1371                     }
1372                 }
1373     
1374                 function u(e) {
1375                     showIFrames();
1376                     removeDOMEvent(body, "mousemove", m);
1377                     removeDOMEvent(body, "mouseup", u);
1378                     Self.content.style.cursor = "";
1379                     if (!Self.animated) {
1380                         Self.content.removeChild(movingSplit);
1381                         movingSplit = null;
1382                         Self.setSize(Self.size);
1383                     }
1384                 }
1385     
1386             }
1387 
1388         },
1389         
1390         /**
1391          * Computes the width of the left panel in pixels.
1392          * @param {Number} size The width of the left panel as a fraction of
1393          * the total available space.
1394          * @type Number
1395          * @return The width of the left panel in pixels.
1396          * @private
1397          */
1398         getPixel: function(size) {
1399             size = size < this.min ? this.min
1400                  : size > this.max ? this.max
1401                                    : size;
1402             return size * (this.content.clientWidth - this.split_size);
1403         },
1404         
1405         addRow: function(child, table) {
1406             if (table) {
1407                 var row = newnode("tr", 0, 0, [
1408                     newnode("td", { paddingLeft: "20px" }, { colSpan: 2 },
1409                             child ? [child] : 0)
1410                 ]);
1411                 if (this.lastRow) {
1412                     this.lastRow.appendChild(row);
1413                 } else {
1414                     this.lastRow = newnode("tbody", { vAlign: "top" }, 0,
1415                                            [row]);
1416                     this.table.appendChild(newnode("table", 0, 0,
1417                                                    [this.lastRow]));
1418                 }
1419                 return row;
1420             } else {
1421                 var row = newnode("div", { paddingLeft: "20px" }, 0,
1422                                   child ? [child] : 0);
1423                 this.table.appendChild(row);
1424                 this.lastRow = null;
1425                 return row;
1426             }
1427         },
1428         
1429         addCells: function(label, input) {
1430             var tr = this.addRow(label ? addTranslated(label) : null, true);
1431             tr.firstChild.colSpan = 1;
1432             tr.appendChild(newnode("td", { paddingLeft: "10px" }, 0,
1433                                    input ? [input] : 0));
1434             return tr;
1435         },
1436 
1437         viewModified: function() {
1438             return this.listModified || !equals(this.get(), this.original);
1439         },
1440 
1441         enter: function() {
1442             if (this.new_click)
1443                 addDOMEvent($("menu_big_new"), "click", this.new_click);
1444             if (this.enableList) {
1445                 var Self = this;
1446                 this.selected_cb = selected;
1447                 this.list.selection.events.register("Selected", selected);
1448                 this.enableList();
1449                 if (this.list.selection.count == 1) {
1450                     this.enable();
1451                 } else {
1452                     this.set(this.default_value);
1453                     this.id = undefined;
1454                     this.disable();
1455                 }
1456             }
1457             this.original = this.get();
1458 
1459             function selected(count) {
1460                 if (!count && Self.isNew) return;
1461                 Self.isNew = false;
1462                 var data = Self.get();
1463                 if (Self.enabled && !equals(data, Self.original) && Self.save) {
1464                     Self.afterSave = saved;
1465                     configuration_askforSave();
1466                 } else {
1467                     saved();
1468                 }
1469 
1470                 function saved() {
1471                     Self.afterSave = null;
1472                     if (count == 1) {
1473                         if (Self.load) {
1474                             try {
1475                                 Self.load(function(data) {
1476                                     if (data == undefined) {
1477                                         Self.set(Self.default_value);
1478                                         Self.id = undefined;
1479                                         Self.disable();
1480                                     } else {
1481                                         Self.set(data);
1482                                         Self.enable();
1483                                         Self.id = data.id;
1484                                     }
1485                                     Self.original = Self.get();
1486                                 });
1487                             } catch (e) {
1488                                 if (e instanceof ox.UI.Controller.InvalidData) {
1489                                     Self.set(Self.default_value);
1490                                     Self.id = undefined;
1491                                     Self.disable();
1492                                 } else throw e;
1493                             }
1494                         } else {
1495                             Self.enable();
1496                             Self.original = Self.get();
1497                         }
1498                     } else {
1499                         Self.set(Self.default_value);
1500                         Self.id = undefined;
1501                         Self.disable();
1502                         Self.original = Self.get();
1503                     }
1504                 }
1505             }
1506             
1507         },
1508         
1509         leave: function() {
1510             if (this.new_click)
1511                 removeDOMEvent($("menu_big_new"), "click", this.new_click);
1512             this.list.selection.events.unregister("Selected", this.selected_cb);
1513             this.list.disable();
1514         },
1515         
1516         saveView: function(callback) {
1517             this.isNew = false;
1518             if (this.save) {
1519                 var data = this.get();
1520                 var data2 = clone(data);
1521                 if (this.id != undefined) data2.id = this.id;
1522                 var Self = this;
1523                 this.save(data2, function(data3) {
1524                     if (Self.afterSave) {
1525                         Self.afterSave();
1526                     } else if (data3) {
1527                         Self.set(data3);
1528                         Self.id = data3.id;
1529                         Self.original = Self.get();
1530                     } else {
1531                         Self.original = data;
1532                     }
1533                     if (callback) callback();
1534                 });
1535             } else if (callback) {
1536                 callback();
1537             }
1538         },
1539 
1540         cancelView: function() {
1541             this.isNew = false;
1542             if (this.afterSave) {
1543                 this.afterSave();
1544             } else {
1545                 this.set(this.original);
1546             }
1547         },
1548 
1549         /**
1550          * Changes the width of the left panel.
1551          * @param {Number} size The new width of the left panel.
1552          */
1553         setSize: function(size) {
1554             this.size = size;
1555             size = this.getPixel(size);
1556             this.left.style.width = size + "px";
1557             this.split.style.left = size + "px";
1558             this.right.style.left = (size + this.split_size) + "px";
1559             if (IE6) {
1560                 var h = this.content.parentNode.clientHeight + "px";
1561                 this.content.style.height = h;
1562                 this.left.style.height = h;
1563                 this.split.style.height = h;
1564                 this.right.style.height = h;
1565                 this.right.style.width =
1566                     (this.content.parentNode.clientWidth
1567                     - this.getPixel(this.size) - this.split_size) + "px";
1568             }
1569         },
1570         
1571         resize: function() {
1572             this.setSize(this.size);
1573         },
1574     
1575         /**
1576          * Width of the split handle in pixels.
1577          */
1578         split_size: 7,
1579         
1580         /**
1581          * Specifies whether resizing takes effect immediately during dragging
1582          * (true) or only at the end (false).
1583          * @type Boolean
1584          * @default true
1585          */
1586         animated: true,
1587     
1588         /**
1589          * Minimum width of the left panel.
1590          * @type Number
1591          * @default 0
1592          */
1593         min: 0,
1594         
1595         /**
1596          * Maximum width of the left panel.
1597          * @type Number
1598          * @default 1
1599          */
1600         max: 1,
1601         
1602         original: {},
1603         
1604         addNew: function(data) {
1605             this.isNew = true;
1606             this.list.selection.reset();
1607             this.set(data);
1608             this.enable();
1609             this.original = this.get();
1610             this.id = data.id;
1611         }
1612         
1613     });
1614 
1615 /**
1616  * @class A configuration page with a single form.
1617  * @augments ox.Configuration.View
1618  * @param {ox.Configuration.LeafNode} node A {@link ox.Configuration.LeafNode}
1619  * which will be configured to open this page.
1620  * @param {I18nString} title The page title.
1621  * @param {Boolean} save_button Whether the default save button should be
1622  * displayed in the menu. Defaults to true. Without the save button,
1623  * viewModified always returns false. 
1624  */
1625 ox.Configuration.Page = function(node, title, save_button) {
1626     ox.Configuration.View.apply(this, arguments);
1627     if (save_button == undefined) save_button = true;
1628     changeDisplay(node.id, save_button ? "menu_save" : "menu_nothing");
1629     if (!save_button) this.viewModified = ox.Configuration.IFrame.viewModified;
1630 };
1631 
1632 /**
1633  * A callback which is called to get the data for the view.
1634  * It has a continuation function as parameter, which should be called with
1635  * the data as parameter when the data becomes available. If the continuation is
1636  * called without a parameter, the view is set to the default value and
1637  * disabled.
1638  * @name ox.Configuration.Page.prototype.load
1639  * @type Function
1640  */
1641 
1642 /**
1643  * A callback which is called to save the page's data.
1644  * It has two parameters: data and cont. data is the data object to save.
1645  * cont is a continuation functions which should be called after the data was
1646  * saved successfully.
1647  * @name ox.Configuration.Page.prototype.save
1648  * @type Function
1649  */
1650 
1651 /**
1652  * An unmodified copy of the currently edited data object.
1653  * @name ox.Configuration.Page.prototype.original
1654  */
1655 
1656 ox.Configuration.Page.prototype = extend(ox.Configuration.View,
1657     /** @lends ox.Configuration.Page.prototype */
1658     {
1659 
1660         addContent: function(node_id) {
1661             // TODO: Framework above Page
1662             this.content = this.table = newnode("div");
1663             ox.Configuration.View.prototype.addContent.apply(this, arguments);
1664         },
1665         
1666         resize: function() {},
1667     
1668         addRow: ox.Configuration.VSplit.prototype.addRow,
1669         
1670         addCells: ox.Configuration.VSplit.prototype.addCells,
1671         
1672         viewModified: function() {
1673             return !equals(this.get(), this.original);
1674         },
1675 
1676         enter: function() {
1677             if (this.load) {
1678                 var Self = this;
1679                 this.load(function(data) {
1680                     if (data == undefined) {
1681                         Self.set(Self.default_value);
1682                         Self.disable();
1683                     } else {
1684                         Self.set(data);
1685                         Self.enable();
1686                     }
1687                     Self.original = Self.get();
1688                 });
1689             } else {
1690                 this.original = this.get();
1691             }
1692         },
1693 
1694         saveView: function(callback) {
1695             if (this.save) {
1696                 var data = this.get();
1697                 var Self = this;
1698                 this.save(data, function() {
1699                     Self.original = data;
1700                     if (callback) callback();
1701                 });
1702             }
1703         },
1704 
1705         cancelView: function() {
1706             this.set(this.original);
1707         }
1708 
1709     });
1710 
1711 /**
1712  * Creates a new widget group for a configuration page.
1713  * @class A widget group in a configuration page.
1714  * @augments ox.UI.Container
1715  * @param {I18nString} title The title of the group.
1716  */
1717 ox.Configuration.Group = function(title) {
1718     ox.UI.Container.apply(this);
1719     this.title = title;
1720 };
1721 
1722 ox.Configuration.Group.prototype = extend(ox.UI.Container,
1723     /** @lends ox.Configuration.Group.prototype */
1724     {
1725     
1726         addContent: function(node_id) {
1727             if (this.title) {
1728                 this.node = this.parent.addRow(addTranslated(this.title), true);
1729                 this.node.style.paddingTop = "1.6em";
1730                 this.node.className = "height16 font-color-default " +
1731                                       "font-weight-high font-style-low";
1732             }
1733             this.childWidth = this.parent.childWidth;
1734             ox.UI.Container.prototype.addContent.apply(this, arguments);
1735         },
1736         
1737         addRow: function(child, table) {
1738             return this.parent.addRow(child, table);
1739         },
1740         
1741         addCells: function(label, input) {
1742             return this.parent.addCells(label, input);
1743         }
1744         
1745     });
1746 
1747 /**
1748  * A pre-defined controller object for use as the second parameter to
1749  * ox.UI.Container#addWidget. It inserts child values directly into the parent's
1750  * value object, without a nested object.
1751  */
1752 ox.Configuration.Group.NoField = {
1753     get: function(data, value) { for (var i in value) data[i] = value[i]; },
1754     set: function(data) { return data; }
1755 };
1756 
1757 /**
1758  * @class A group which contains an array as value. Children are usually added
1759  * and remoevd at runtime by the user.
1760  * @augments ox.UI.Container
1761  * @param {I18nString} title An optional title of the group.
1762  */
1763 ox.Configuration.ArrayGroup = function(title) {
1764     ox.UI.Container.apply(this);
1765     this.title = title;
1766 };
1767 
1768 /**
1769  * A callback which is caled to add the widget for a new array element.
1770  * @name ox.Configuration.ArrayGroup.prototype.addElement
1771  * @type Function
1772  */
1773 
1774 ox.Configuration.ArrayGroup.prototype = extend(ox.UI.Container,
1775     /** @lends ox.Configuration.ArrayGroup */
1776     {
1777     
1778         default_value: [],
1779     
1780         addContent: function(node_id) {
1781             if (this.title) {
1782                 this.tnode = this.parent.addRow(
1783                     this.title ? addTranslated(this.title) : null, true);
1784                 this.tnode.style.paddingTop = "1.6em";
1785                 this.tnode.className = "height16 font-color-default " +
1786                                        "font-weight-high font-style-low";
1787             }
1788             this.childWidth = this.parent.childWidth;
1789             this.tbody = newnode("tbody");
1790             this.node = this.parent.addRow(newnode("table", 0, 0, [this.tbody]),
1791                                            false);
1792             ox.UI.Container.prototype.addContent.apply(this, arguments);
1793         },
1794         
1795         remove: function() {
1796             if (this.tnode) this.tnode.parentNode.removeChild(this.tnode);
1797             this.node.parentNode.removeChild(this.node);
1798         },
1799         
1800         applyVisible: function() {
1801             if (this.tnode)
1802                 this.tnode.style.display = this.isVisible ? "" : "none";
1803             this.node.style.display = this.isVisible ? "" : "none";
1804         },
1805         
1806         addRow: function(child, table) {
1807             var row = newnode("tr", 0, 0, [
1808                 newnode("td", { paddingLeft: "20px" }, { colSpan: 2 },
1809                         child ? [child] : 0)
1810             ]);
1811             this.tbody.appendChild(row);
1812             return row;
1813         },
1814         
1815         addCells: ox.Configuration.VSplit.prototype.addCells,
1816 
1817         get: function() {
1818             var value = new Array(this.children.length);
1819             for (var i = 0; i < this.children.length; i++)
1820                 value[i] = this.children[i].get();
1821             return value;
1822         },
1823         
1824         set: function(value) {
1825             for (var i = this.children.length - 1; i >= value.length; i--)
1826                 this.deleteWidget(this.children[i]);
1827             for (var i = this.children.length; i < value.length; i++)
1828                 this.addElement();
1829             for (var i = 0; i < this.children.length; i++)
1830                 this.children[i].set(value[i]);
1831         }
1832 
1833     });
1834 
1835 /**
1836  * @class A container which arranges its children horizontally.
1837  */
1838 ox.Configuration.HLayout = function() {
1839     ox.UI.Container.apply(this);
1840 }
1841 
1842 /**
1843  * @private
1844  */
1845 ox.Configuration.HLayout.id = 0;
1846 
1847 ox.Configuration.HLayout.prototype = extend(ox.UI.Container,
1848     /** @lends ox.Configuration.HLayout.prototype */
1849     {
1850     
1851         addContent: function(node_id) {
1852             this.tr = newnode("tr");
1853             this.node = this.parent.addRow(
1854                 newnode("table", 0, { cellpadding: "5px" },
1855                         [newnode("tbody", 0, 0, [this.tr])]),
1856                 true);
1857             ox.UI.Container.prototype.addContent.apply(this, arguments);
1858         },
1859         
1860         applyVisible: ox.UI.Widget.prototype.applyVisible,
1861         
1862         addRow: function(child, table) {
1863             var td = newnode("td", 0, 0, [child]);
1864             this.tr.appendChild(td);
1865             return td;
1866         },
1867         
1868         addCells: function(label, input) {
1869             var td = newnode("td", 0, 0, label
1870                 ? [addTranslated(label), input]
1871                 : [input]);
1872             this.tr.appendChild(td);
1873             return td;
1874         },
1875         
1876         childWidth: "10em"
1877     
1878     });
1879 
1880 /**
1881  * @class An editable list with "Add" and "Remove" buttons in the menu.
1882  * @augments ox.UI.Widget
1883  * @param {I18nString} section Name of the menu section.
1884  * @param {String} height The height of the list as a CSS length.
1885  * @param {I18nString} label An optional label for the list.
1886  */
1887 ox.Configuration.EditableList = function(section, height, label) {
1888     ox.UI.Widget.apply(this);
1889     this.section = section;
1890     this.height = height;
1891     this.label = label;
1892     this.storage = new Storage(0, []);
1893     var Self = this;
1894     this.storage.events.register("Changed", function() {
1895         var newValues = [];
1896         Self.storage.newIterate(Self.storage.ids, emptyFunction,
1897             function(i, data) { newValues[i] = Self.values[data[0]]; },
1898             function() { Self.values = newValues; });
1899     });
1900 };
1901 
1902 /**
1903  * @private
1904  */
1905 ox.Configuration.EditableList.id = 31;
1906 
1907 /**
1908  * This method is called when the "Add" button is clicked.
1909  * @name ox.Configuration.EditableList.prototype.add
1910  * @function
1911  * @param {Function} cont A callback function which should be called with
1912  * an array of new elements as parameter.
1913  */
1914 
1915 ox.Configuration.EditableList.prototype = extend(ox.UI.Widget,
1916     /** @lends ox.Configuration.EditableList.prototype */
1917     {
1918         
1919         addContent: function(node_id) {
1920             var selection = new Selection();
1921             this.grid = new LiveGrid([{
1922                 text: addTranslated(this.label),
1923                 index: 1,
1924                 clear: LiveGrid.makeClear(""),
1925                 set: LiveGrid.defaultSet
1926             }], selection);
1927             this.grid.emptylivegridtext = this.emptyText();
1928             this.head = newnode("div");
1929             this.body = newnode("div",
1930                 { height: this.height, position: "relative" });
1931             this.head.appendChild(this.grid.getHeader());
1932             this.grid.getTable(this.body);
1933             
1934             this.node = this.parent.addRow(newnode("div", 0, 0,
1935                 [this.head, this.body]));
1936             var id = "ox.Configuration.EditableList."
1937                      + ox.Configuration.EditableList.id++;
1938             var menu = MenuNodes.createSmallButtonContext(id, this.section);
1939             var Self = this;
1940             MenuNodes.createSmallButton(menu, id + ".add", "Add", /*i18n*/
1941                 getFullImgSrc("img/dummy.gif"), getFullImgSrc("img/dummy.gif"),
1942                 function() {
1943                     if (Self.add) Self.add(function(values) {
1944                         if (!values.length) return;
1945                         var oldlen = Self.values.length;
1946                         Self.values = Self.values.concat(values);
1947                         var data = new Array(values.length);
1948                         for (var i = 0; i < values.length; i++)
1949                             data[i] = [i + oldlen, Self.getText(values[i])];
1950                         Self.storage.append(data);
1951                         Self.grid.focus = oldlen;
1952                         Self.grid.showFocus();
1953                     });
1954                 });
1955             MenuNodes.createSmallButton(menu, id + ".remove", "Remove", /*i18n*/
1956                 getFullImgSrc("img/dummy.gif"), getFullImgSrc("img/dummy.gif"),
1957                 del);
1958             this.grid.events.register("Deleted", del);
1959             function del() {
1960                 Self.storage.removeIDs(selection.getSelected());
1961             } 
1962             addMenuNode(menu.node, MenuNodes.FIXED,
1963                         ox.Configuration.EditableList.id);
1964             changeDisplay(node_id, id);
1965             selection.events.register("Selected", function(count) {
1966                 menuselectedfolders = [];
1967                 triggerEvent("OX_SELECTED_ITEMS_CHANGED", count);
1968                 triggerEvent("OX_SELECTION_CHANGED", count);
1969             });
1970             menuarrows[node_id] = {};
1971             register("OX_SELECTED_ITEMS_CHANGED", function() {
1972                 menuglobalzaehler = 0;
1973                 menuarrows[node_id][id] = [];
1974                 menu_display_contents(node_id, id, true, id + ".add");
1975                 menu_display_contents(node_id, id, selection.count > 0,
1976                     id + ".remove");
1977             });
1978             ox.UI.Widget.prototype.addContent.apply(this, arguments);
1979         },
1980         
1981         /**
1982          * The text which is displayed in the list when it contains no entries.
1983          * @type I18nString
1984          */
1985         emptyText: function() { return ""; },
1986         
1987         default_value: [],
1988         
1989         get: function() { return this.values; },
1990         
1991         set: function(value) {
1992             this.values = value;
1993             var data = new Array(value.length);
1994             for (var i = 0; i < value.length; i++)
1995                 data[i] = [i, this.getText(value[i])];
1996             this.storage.remove(0, this.storage.ids.length);
1997             this.storage.append(data);
1998         },
1999         
2000         /**
2001          * A callback function which is used to extract a human-readable
2002          * description from an array element of the value. This description is
2003          * displayed in the list. The default implementation handles I18nString
2004          * objects and plain strings.
2005          * @param elem The array element from which to extract the description.
2006          * @type String
2007          * @return A textual description of the specified element.
2008          */
2009         getText: function(elem) {
2010             return typeof elem == "function" ? elem() : elem;
2011         },
2012         
2013         applyEnabled: function() {
2014             if (this.isEnabled) {
2015                 this.grid.enable(this.storage);
2016             } else {
2017                 this.grid.disable();
2018             }
2019         }
2020         
2021     });
2022 
2023 /**
2024  * @class A group of widgets with a common caption (legend).
2025  * The group is represented by a <fieldset> element.
2026  * @param {I18nString} legend An optional legend text. If not specified,
2027  * #getLegend must be overwritten to return the legend as an array of DOM nodes.
2028  */
2029 ox.UI.FieldSet = function(legend) {
2030     ox.UI.Container.apply(this);
2031     if (legend) this.legend = legend;
2032 };
2033 
2034 ox.UI.FieldSet.prototype = extend(ox.UI.Container,
2035     /** @lends ox.UI.FieldSet.prototype */
2036     {
2037         
2038         addContent: function(node_id) {
2039             this.table = this.fieldset = newnode("fieldset", 0, 0,
2040                 [newnode("legend", 0, 0, this.getLegend())]);
2041             this.node = this.parent.addRow(this.fieldset, false);
2042             this.childWidth = this.parent.childWidth;
2043             ox.UI.Container.prototype.addContent.apply(this, arguments);
2044         },
2045         
2046         /**
2047          * Builds the legend of the FieldSet. Descendants can overwrite this
2048          * method to create different legends.
2049          * @type Array
2050          * @return An array of DOM nodes which constitute the children of the
2051          * <legend> node.
2052          * @protected
2053          */
2054         getLegend: function() { return [addTranslated(this.legend)]; },
2055         
2056         applyVisible: ox.UI.Widget.prototype.applyVisible,
2057         
2058         addRow: ox.Configuration.VSplit.prototype.addRow,
2059         
2060         addCells: ox.Configuration.VSplit.prototype.addCells
2061 /*
2062         addRow: function(child, table) {
2063             var td = newnode("td", 0, 0, [child]);
2064             this.tr.appendChild(td);
2065             return td;
2066         },
2067         
2068         addCells: function(label, input) {
2069             var td = newnode("td", 0, 0, label
2070                 ? [addTranslated(label), input]
2071                 : [input]);
2072             this.tr.appendChild(td);
2073             return td;
2074         }
2075 */
2076     });
2077 
2078 /**
2079  * @class A FieldSet which can be enabled and disabled by a checkbox in
2080  * the legend.
2081  * @param {I18nString} legend The text of CheckBox in the legend.
2082  */
2083 ox.UI.CheckedFieldSet = function(legend) {
2084     ox.UI.FieldSet.apply(this, arguments);
2085 };
2086 
2087 ox.UI.CheckedFieldSet.id = 0;
2088 
2089 ox.UI.CheckedFieldSet.prototype = extend(ox.UI.FieldSet,
2090     /** @lends ox.UI.CheckedFieldSet.prototype */
2091     {
2092     
2093         getLegend : function() {
2094             var checkbox = newnode("input", 0, {
2095                 type: "checkbox",
2096                 id: "ox.UI.CheckedFieldSet." + ox.UI.CheckedFieldSet.id++
2097             });
2098             var Self = this;
2099             addDOMEvent(checkbox, "click", function() {
2100                 Self.setEnabled(checkbox.checked);
2101             });
2102             return [checkbox, newnode("label", 0, { htmlFor: checkbox.id },
2103                                       [addTranslated(this.legend)])];
2104         }
2105         
2106     });
2107 
2108 /**
2109  * Attaches an iframe page to a leaf node in the configuration tree.
2110  * @class A configuration page with external content in an iframe.
2111  * @augments ox.Configuration.View
2112  * @param {ox.Configuration.LeafNode} node A {@link ox.Configuration.LeafNode}
2113  * which will be configured to open this page.
2114  * @param {I18nString} title The page title.
2115  * @param {String} src The URI of the iframe content.
2116  * @param {Boolean} save_button Whether the default save button should be
2117  * displayed in the menu.
2118  */
2119 ox.Configuration.IFrame = function(node, title, src, save_button) {
2120     ox.Configuration.View.apply(this, [node, title]);
2121     this.src = src;
2122     changeDisplay(node.id, save_button ? "menu_save" : "menu_nothing");
2123 };
2124 
2125 ox.Configuration.IFrame.prototype = extend(ox.Configuration.View,
2126     /** @lends ox.Configuration.IFrame.prototype */
2127     {
2128     
2129         addContent: function(node_id) {
2130             this.content = newnode("iframe",
2131                 { position: "absolute", // stupid IE7
2132                   width: "100%", height: "100%", border: 0 },
2133                 { src: this.src });
2134             initialized = true;
2135         },
2136         
2137         resize: function() {},
2138         
2139         viewModified: function() { return false; }
2140     
2141     });
2142 
2143 /**
2144  * @name ox.Configuration.IFrame.prototype.addWidget
2145  * @private
2146  */
2147 delete ox.Configuration.IFrame.prototype.addWidget;