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