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