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