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