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;