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