diff --git a/etemplate/js/et2_baseWidget.js b/etemplate/js/et2_baseWidget.js new file mode 100644 index 0000000000..21f0769864 --- /dev/null +++ b/etemplate/js/et2_baseWidget.js @@ -0,0 +1,118 @@ +/** + * eGroupWare eTemplate2 - JS Widget base class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id: et2_widget.js 36021 2011-08-07 13:43:46Z igel457 $ + */ + +"use strict"; + +/*egw:uses + jquery.jquery; + lib/tooltip.js; + et2_widget; +*/ + +/** + * Class which manages the DOM node itself. The simpleWidget class is derrived + * from et2_DOMWidget and implements the getDOMNode function. A setDOMNode + * function is provided, which attatches the given node to the DOM if possible. + */ +var et2_baseWidget = et2_DOMWidget.extend({ + + attributes: { + "statustext": { + "name": "Tooltip", + "type": "string", + "description": "Tooltip which is shown for this element" + } + }, + + init: function() { + this._super.apply(this, arguments); + + this.node = null; + this.statustext = ""; + + this._tooltipElem = null; + }, + + detatchFromDOM: function() { + // Detach this node from the tooltip node + if (this._tooltipElem) + { + egw_global_tooltip.unbindFromElement(this._tooltipElem); + this._tooltipElem = null; + } + + this._super.apply(this, arguments); + }, + + attachToDOM: function() { + this._super.apply(this,arguments); + + // Update the statustext + this.set_statustext(this.statustext); + }, + + setDOMNode: function(_node) { + if (_node != this.node) + { + // Deatch the old node from the DOM + this.detatchFromDOM(); + + // Set the new DOM-Node + this.node = _node; + + // Attatch the DOM-Node to the tree + return this.attachToDOM(); + } + + return false; + }, + + getDOMNode: function() { + return this.node; + }, + + getTooltipElement: function() { + return this.getDOMNode(); + }, + + set_statustext: function(_value) { + // Don't execute the code below, if no tooltip will be attached/detached + if (_value == "" && !this._tooltipElem) + { + return; + } + + this.statustext = _value; + + //Get the domnode the tooltip should be attached to + var elem = $j(this.getTooltipElement()); + + if (elem) + { + //If a tooltip is already attached to the element, remove it first + if (this._tooltipElem) + { + egw_global_tooltip.unbindFromElement(this._tooltipElem); + this._tooltipElem = null; + } + + if (_value && _value != '') + { + egw_global_tooltip.bindToElement(elem, _value); + this._tooltipElem = elem; + } + } + } + +}); + + diff --git a/etemplate/js/et2_box.js b/etemplate/js/et2_box.js new file mode 100644 index 0000000000..289e490a2e --- /dev/null +++ b/etemplate/js/et2_box.js @@ -0,0 +1,37 @@ +/** + * eGroupWare eTemplate2 - JS Box object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +"use strict"; + +/*egw:uses + jquery.jquery; + et2_baseWidget; +*/ + +/** + * Class which implements the hbox and vbox tag + */ +var et2_box = et2_baseWidget.extend({ + + init: function(_parent, _type) { + this._super.apply(this, arguments); + + this.div = $j(document.createElement("div")) + .addClass("et2_" + _type); + + this.setDOMNode(this.div[0]); + } + +}); + +et2_register_widget(et2_box, ["hbox", "vbox"]); + diff --git a/etemplate/js/et2_button.js b/etemplate/js/et2_button.js index 27d534fac3..92310ae216 100644 --- a/etemplate/js/et2_button.js +++ b/etemplate/js/et2_button.js @@ -14,20 +14,31 @@ /*egw:uses jquery.jquery; - et2_widget; + et2_baseWidget; */ /** * Class which implements the "button" XET-Tag */ -var et2_button = et2_DOMWidget.extend({ +var et2_button = et2_baseWidget.extend({ + + attributes: { + "label": { + "name": "caption", + "type": "string", + "description": "Label of the button" + } + }, init: function(_parent) { + this._super.apply(this, arguments); + + this.label = ""; + this.btn = $j(document.createElement("button")) .addClass("et2_button"); - this._super.apply(this, arguments); - this.label = ""; + this.setDOMNode(this.btn[0]); }, set_label: function(_value) { @@ -37,10 +48,6 @@ var et2_button = et2_DOMWidget.extend({ this.btn.text(_value); } - }, - - getDOMNode: function() { - return this.btn[0]; } }); diff --git a/etemplate/js/et2_common.js b/etemplate/js/et2_common.js index 0152b47aae..51a19119c2 100644 --- a/etemplate/js/et2_common.js +++ b/etemplate/js/et2_common.js @@ -10,27 +10,6 @@ * @version $Id$ */ -function et2_debug(_level, _msg) -{ - if (typeof console != "undefined") - { - if (_level == "log" && typeof console.log == "function") - { - console.log(_msg); - } - - if (_level == "warn" && typeof console.warn == "function") - { - console.warn(_msg); - } - - if (_level == "error" && typeof console.error == "function") - { - console.error(_msg); - } - } -} - /** * IE Fix for array.indexOf */ @@ -46,4 +25,302 @@ if (typeof Array.prototype.indexOf == "undefined") }; } +/** + * ET2_DEBUGLEVEL specifies which messages are printed to the console. Decrease + * the value of ET2_DEBUGLEVEL to get less messages. + */ +var ET2_DEBUGLEVEL = 0; + +function et2_debug(_level, _msg) +{ + if (typeof console != "undefined") + { + if (_level == "log" && ET2_DEBUGLEVEL >= 4 && + typeof console.log == "function") + { + console.log(_msg); + } + + if (_level == "info" && ET2_DEBUGLEVEL >= 3 && + typeof console.info == "function") + { + console.info(_msg); + } + + if (_level == "warn" && ET2_DEBUGLEVEL >= 2 && + typeof console.warn == "function") + { + console.warn(_msg); + } + + if (_level == "error" && ET2_DEBUGLEVEL >= 1 && + typeof console.error == "function") + { + console.error(_msg); + } + } +} + +/** + * Array with all types supported by the et2_checkType function. + */ +var et2_validTypes = ["boolean", "string", "float", "integer", "any"]; + +/** + * Object whith default values for the above types. Do not specify array or + * objects inside the et2_typeDefaults object, as this instance will be shared + * between all users of it. + */ +var et2_typeDefaults = { + "boolean": false, + "string": "", + "float": 0.0, + "integer": 0, + "any": null +}; + +/** + * Checks whether the given value is of the given type. Strings are converted + * into the corresponding type. The (converted) value is returned. All supported + * types are listed in the et2_validTypes array. + */ +function et2_checkType(_val, _type) +{ + function _err() { + throw("'" + _val + "' is not of specified _type '" + _type + "'"); + } + + // If the type is "any" simply return the value again + if (_type == "any") + { + return _val; + } + + // If the type is boolean, check whether the given value is exactly true or + // false. Otherwise check whether the value is the string "true" or "false". + if (_type == "boolean") + { + if (_val === true || _val === false) + { + return _val; + } + + var lcv = _val.toLowerCase(); + if (lcv === "true" || lcv === "false" || lcv === "") + { + return _val === "true"; + } + + _err(); + } + + // Check whether the given value is of the type "string" + if (_type == "string") + { + if (typeof _val == "string") + { + return _val; + } + + _err(); + } + + // Check whether the value is already a number, otherwise try to convert it + // to one. + if (_type == "float") + { + if (typeof _val == "number") + { + return _val; + } + + if (!isNaN(_val)) + { + return parseFloat(_val); + } + + _err(); + } + + // Check whether the value is an integer by comparing the result of + // parseInt(_val) to the value itself. + if (_type == "integer") + { + if (parseInt(_val) == _val) + { + return parseInt(_val); + } + + _err(); + } + + // We should never come here + throw("Invalid type identifier supplied."); +} + +/** + * Validates the given attribute with the given id. The validation checks for + * the existance of a human name, a description, a type and a default value. + * If the human name defaults to the given id, the description defaults to an + * empty string, the type defaults to any and the default to the corresponding + * type default. + */ +function et2_validateAttrib(_id, _attrib) +{ + // Default ignore to false. + if (typeof _attrib["ignore"] == "undefined") + { + _attrib["ignore"] = false + } + + // Break if "ignore" is set to true. + if (_attrib.ignore) + { + return; + } + + if (typeof _attrib["name"] == "undefined") + { + _attrib["name"] = _id; + et2_debug("log", "Human name ('name'-Field) for attribute '" + + _id + "' has not been supplied, set to '" + _id + "'"); + } + + if (typeof _attrib["description"] == "undefined") + { + _attrib["description"] = ""; + et2_debug("log", "Description for attribute '" + + _id + "' has not been supplied"); + } + + if (typeof _attrib["type"] == "undefined") + { + _attrib["type"] = "any"; + } + else + { + if (et2_validTypes.indexOf(_attrib["type"]) < 0) + { + et2_debug("error", "Invalid type for attribute '" + _id + + "' supplied."); + } + } + + // Set the defaults + if (typeof _attrib["default"] == "undefined") + { + _attrib["default"] = et2_typeDefaults[_attrib["type"]]; + } +} + +/** + * Equivalent to the PHP array_values function + */ +function et2_arrayValues(_arr) +{ + var result = []; + for (var key in _arr) + { + if (parseInt(key) == key) + { + result.push(_arr[key]); + } + } + + return result; +} + +/** + * Equivalent to the PHP substr function, partly take from phpjs, licensed under + * the GPL. + */ +function et2_substr (str, start, len) { + var end = str.length; + + if (start < 0) + { + start += end; + } + end = typeof len === 'undefined' ? end : (len < 0 ? len + end : len + start); + + return start >= str.length || start < 0 || start > end ? "" : str.slice(start, end); +} + +/** + * Split a $delimiter-separated options string, which can contain parts with + * delimiters enclosed in $enclosure. Ported from class.boetemplate.inc.php + * + * Examples: + * - et2_csvSplit('"1,2,3",2,3') === array('1,2,3','2','3') + * - et2_csvSplit('1,2,3',2) === array('1','2,3') + * - et2_csvSplit('"1,2,3",2,3',2) === array('1,2,3','2,3') + * - et2_csvSplit('"a""b,c",d') === array('a"b,c','d') // to escape enclosures double them! + * + * @param string _str + * @param int _num=null in how many parts to split maximal, parts over this + * number end up (unseparated) in the last part + * @param string _delimiter=',' + * @param string _enclosure='"' + * @return array + */ +function et2_csvSplit(_str, _num, _delimiter, _enclosure) +{ + // Default the parameters + if (typeof _num == "undefined") + { + _num == null; + } + + if (typeof _delimiter == "undefined") + { + _delimiter = ","; + } + + if (typeof _enclosure == "undefined") + { + _enclosure = '"'; + } + + // If the _enclosure string does not occur in the string, simply use the + // split function + if (_str.indexOf(_enclosure) == -1) + { + return _num === null ? _str.split(_delimiter) : + _str.split(_delimiter, _num); + } + + // Split the string at the delimiter and join it again, when a enclosure is + // found at the beginning/end of a part + var parts = _str.split(_delimiter); + for (var n = 0; typeof parts[n] != "undefined"; n++) + { + var part = parts[n]; + + if (part.charAt(0) === _enclosure) + { + var m = n; + while (typeof parts[m + 1] != "undefined" && parts[n].substr(-1) !== _enclosure) + { + parts[n] += _delimiter + parts[++m]; + delete(parts[m]); + } + parts[n] = et2_substr(parts[n].replace( + new RegExp(_enclosure + _enclosure, 'g'), _enclosure), 1 , -1); + n = m; + } + } + + // Rebuild the array index + parts = et2_arrayValues(parts); + + // Limit the parts to the given number + if (_num !== null && _num > 0 && _num < parts.length && parts.length > 0) + { + parts[_num - 1] = parts.slice(_num - 1, parts.length).join(_delimiter); + parts = parts.slice(0, _num); + } + + return parts; +} + diff --git a/etemplate/js/et2_description.js b/etemplate/js/et2_description.js index 1ff3dd7bb1..883175afab 100644 --- a/etemplate/js/et2_description.js +++ b/etemplate/js/et2_description.js @@ -14,24 +14,82 @@ /*egw:uses jquery.jquery; - et2_widget; + et2_baseWidget; */ /** * Class which implements the "description" XET-Tag */ -var et2_description = et2_DOMWidget.extend({ +var et2_description = et2_baseWidget.extend({ + + attributes: { + "value": { + "name": "Caption", + "type": "string", + "description": "Displayed text" + }, + + /** + * Options converted from the "options"-attribute. + */ + "font_style": { + "name": "Font Style", + "type": "string", + "description": "Style may be a compositum of \"b\" and \"i\" which " + + " renders the text bold and/or italic." + }, + "href": { + "name": "Link Target", + "type": "string", + "description": "Link URL, empty if you don't wan't to display a link." + }, + "activate_links": { + "name": "Replace URLs", + "type": "boolean", + "default": false, + "description": "If set, URLs in the text are automatically replaced " + + "by links" + }, + "label_for": { + "name": "Label for widget", + "type": "string", + "description": "Marks the text as label for the given widget." + }, + "extra_link_target": { + "name": "Link target", + "type": "string", + "default": "_self", + "description": "Link target descriptor" + }, + "extra_link_popup": { + "name": "Popup", + "type": "string", + "description": "???" + }, + "extra_link_title": { + "name": "Link Title", + "type": "string", + "description": "Link title which is displayed on mouse over." + } + }, + + legacyOptions: ["font_style", "href", "activate_links", "label_for", + "extra_link_target", "extra_link_popup", "extra_link_title"], init: function(_parent) { + this._super.apply(this, arguments); + + this.value = ""; + this.font_style = ""; + this.span = $j(document.createElement("span")) .addClass("et2_label"); - this._super.apply(this, arguments); - this.value = ""; + this.setDOMNode(this.span[0]); }, - set_value: function(_value) { - if (_value != this.value) + set_value: function(_value, _force) { + if (_value != this.value || _force) { this.value = _value; @@ -39,8 +97,14 @@ var et2_description = et2_DOMWidget.extend({ } }, - getDOMNode: function() { - return this.span[0]; + set_font_style: function(_value) { + if (_value != this.font_style) + { + this.font_style = _value; + + this.span.toggleClass("et2_bold", _value.indexOf("b") >= 0); + this.span.toggleClass("et2_italic", _value.indexOf("i") >= 0); + } } }); diff --git a/etemplate/js/et2_grid.js b/etemplate/js/et2_grid.js index 1521d1df93..b129126fe4 100644 --- a/etemplate/js/et2_grid.js +++ b/etemplate/js/et2_grid.js @@ -98,12 +98,6 @@ var et2_grid = et2_DOMWidget.extend({ }; }, - _readAttrWithDefault: function(_node, _name, _default) { - var val = _node.getAttribute(_name); - - return (val === null) ? _default : val; - }, - _getCell: function(_cells, _x, _y) { if ((0 <= _y) && (_y < _cells.length)) { @@ -132,10 +126,10 @@ var et2_grid = et2_DOMWidget.extend({ var colDataEntry = this._getColDataEntry(); if (nodeName == "column") { - colDataEntry["width"] = this._readAttrWithDefault(node, "width", "auto"); - colDataEntry["class"] = this._readAttrWithDefault(node, "class", ""); - colDataEntry["align"] = this._readAttrWithDefault(node, "align", ""); - colDataEntry["span"] = this._readAttrWithDefault(node, "span", "1"); + colDataEntry["width"] = et2_readAttrWithDefault(node, "width", "auto"); + colDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); + colDataEntry["align"] = et2_readAttrWithDefault(node, "align", ""); + colDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); } else { @@ -149,10 +143,10 @@ var et2_grid = et2_DOMWidget.extend({ var rowDataEntry = this._getRowDataEntry(); if (nodeName == "row") { - rowDataEntry["height"] = this._readAttrWithDefault(node, "height", "auto"); - rowDataEntry["class"] = this._readAttrWithDefault(node, "class", ""); - rowDataEntry["valign"] = this._readAttrWithDefault(node, "valign", ""); - rowDataEntry["span"] = this._readAttrWithDefault(node, "span", "1"); + rowDataEntry["height"] = et2_readAttrWithDefault(node, "height", "auto"); + rowDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); + rowDataEntry["valign"] = et2_readAttrWithDefault(node, "valign", ""); + rowDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); } else { diff --git a/etemplate/js/et2_inheritance.js b/etemplate/js/et2_inheritance.js index 377c7c0fbb..9233805d4a 100644 --- a/etemplate/js/et2_inheritance.js +++ b/etemplate/js/et2_inheritance.js @@ -12,6 +12,10 @@ "use strict"; +/*egw:uses + et2_common; +*/ + /** * Usage of the JS inheritance system * ---------------------------------- @@ -84,10 +88,10 @@ }; /** - * The addInterfaceStuff function adds all interface functions the class has + * The addInterfaceFunctions function adds all interface functions the class has * to implement to the class prototype. */ - function addInterfaceStuff(prototype, interfaces) + function addInterfaceFunctions(prototype, interfaces) { // Remember all interface functions in the prototype var ifaces = ((typeof prototype["_ifacefuncs"] == "undefined") ? [] : @@ -107,7 +111,7 @@ } else { - throw("Interfaces must be instanceof Interface!"); + throw("Interfaces must be instance of Interface!"); } } @@ -144,6 +148,118 @@ } }; + function addAttributeFunctions(prototype, _super) + { + var attributes = prototype.attributes; + + // Add the old attributes to the new ones. If the attributes already + // exist, they are merged. + for (var key in _super.attributes) + { + var attrib = _super.attributes[key]; + + if (typeof attributes[key] == "undefined") + { + // In the case that the old attribute has no equivalent in the + // new class, simply create a reference to the old one. + attributes[key] = attrib; + } + else + { + // Otherwise merge the two attribute descriptors. + for (var key2 in attrib) + { + if (typeof attributes[key][key2] == "undefined") + { + attributes[key][key2] = attrib[key2]; + } + } + } + } + + // Validate the attributes + for (var key in attributes) + { + et2_validateAttrib(key, attributes[key]); + } + + /** + * The initAttributes function sets the attributes to their default + * values. The attributes are not overwritten, which means, that the + * default is only set, if either a setter exists or this[propName] does + * not exist yet. + */ + prototype.initAttributes = function() { + for (var key in this.attributes) + { + if (!this.attributes[key].ignore) + { + this.setAttribute(key, this.attributes[key]["default"], + false); + } + } + + this._attrsInitialized = true; + } + + /** + * The setAttribute function sets the attribute with the given name to + * the given value. _override defines, whether this[_name] will be set, + * if this key already exists. _override defaults to true. A warning + * is issued if the attribute does not exist. + */ + prototype.setAttribute = function(_name, _value, _override) { + if (typeof this.attributes[_name] != "undefined") + { + if (!this.attributes[_name].ignore) + { + if (typeof _override == "undefined") + { + _override = true; + } + + var val = et2_checkType(_value, this.attributes[_name].type); + + if (typeof this["set_" + _name] == "function") + { + this["set_" + _name](val); + } + else if (_override || typeof this[_name] == "undefined") + { + this[_name] = val; + } + } + } + else + { + et2_debug("warn", "Attribute '" + _name + "' does not exist!"); + } + } + + /** + * Returns the value of the given attribute. If the property does not + * exist, an error message is issued. + */ + prototype.getAttribute = function(_name) { + if (typeof this.attributes[_name] != "undefined" && + !this.attributes[_name].ignore) + { + if (typeof this["get_" + _name] == "function") + { + return this["get_" + _name](); + } + else + { + return this[_name]; + } + } + else + { + et2_error("error", "Attribute '" + _name + "' does not exist!"); + } + } + }; + function classExtend(interfaces, prop) { if (typeof prop == "undefined") @@ -158,6 +274,11 @@ interfaces = [interfaces]; } + if (typeof prop.attributes == "undefined") + { + prop.attributes = {}; + } + var _super = this.prototype; // Instantiate a base class (but only create the instance, @@ -199,7 +320,11 @@ // Add the interface functions and the "implements" function to the // prototype - addInterfaceStuff(prototype, interfaces); + addInterfaceFunctions(prototype, interfaces); + + // Merge the attributes and create the functions corresponding to the + // attributes + addAttributeFunctions(prototype, _super); // The dummy class constructor function Class() { @@ -221,6 +346,12 @@ { this.init.apply(this, arguments); } + + // Initialize the attributes + if (typeof this._attrsInitialized == "undefined") + { + this.initAttributes(); + } } } @@ -243,5 +374,8 @@ // is an array which defines a set of interfaces the object has to // implement. An interface is simply an object with named functions. Class.extend = classExtend; + + // The base class has no attributes + Class.attributes = {}; }).call(window); diff --git a/etemplate/js/et2_inputWidget.js b/etemplate/js/et2_inputWidget.js new file mode 100644 index 0000000000..7c37e69039 --- /dev/null +++ b/etemplate/js/et2_inputWidget.js @@ -0,0 +1,94 @@ +/** + * eGroupWare eTemplate2 - JS Widget base class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id: et2_widget.js 36021 2011-08-07 13:43:46Z igel457 $ + */ + +"use strict"; + +/*egw:uses + jquery.jquery; + et2_baseWidget; +*/ + +/** + * Interface for all widgets which support returning a value + */ +var et2_IInput = new Interface({ + /** + * getValue has to return the value of the input widget + */ + getValue: function() {}, + + /** + * Is dirty returns true if the value of the widget has changed since it + * was loaded. + */ + isDirty: function() {}, + + /** + * Causes the dirty flag to be reseted. + */ + resetDirty: function() {} +}); + +/** + * et2_inputWidget derrives from et2_simpleWidget and implements the IInput + * interface. When derriving from this class, call setDOMNode with an input + * DOMNode. + */ +var et2_inputWidget = et2_simpleWidget.extend(et2_IInput, { + + attributes: { + "value": { + "name": "Value", + "description": "The value of the widget", + "type": "string", + "default": "" + } + }, + + init: function() { + this._super.apply(this, arguments); + + this._oldValue = ""; + }, + + set_value: function(_value) { + this._oldValue = _value; + + if (this.node) + { + $j(this.node).val(_value); + } + }, + + get_value: function() { + return this.getValue(); + }, + + getValue: function() { + if (this.node) + { + return $j(this.node).val(); + } + + return this._oldValue; + }, + + isDirty: function() { + return this._oldValue != this.getValue(); + }, + + resetDirty: function() { + this._oldValue = this.getValue(); + } + +}); + diff --git a/etemplate/js/et2_tabs.js b/etemplate/js/et2_tabs.js new file mode 100644 index 0000000000..29e3b572bd --- /dev/null +++ b/etemplate/js/et2_tabs.js @@ -0,0 +1,178 @@ +/** + * eGroupWare eTemplate2 - JS Tabs object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +"use strict"; + +/*egw:uses + jquery.jquery; + et2_widget; +*/ + +/** + * Class which implements the tabbox-tag + */ +var et2_tabbox = et2_DOMWidget.extend({ + + init: function(_parent, _type) { + // Create the outer tabbox container + this.container = $j(document.createElement("div")) + .addClass("et2_tabbox"); + + // Create the upper container for the tab flags + var cntr = $j(document.createElement("div")) + .addClass("et2_flags") + .appendTo(this.container); + + this.flagContainer = $j(document.createElement("span")) + .addClass("et2_tabflagcntr") + .appendTo(cntr); + + $j(document.createElement("span")) + .addClass("et2_tabspacer") + .appendTo(cntr); + + // Create the lower tab container + this.tabContainer = $j(document.createElement("div")) + .addClass("et2_tabs") + .appendTo(this.container); + + this._super.apply(this, arguments); + + this.tabData = []; + }, + + destroy: function(_parent, _type) { + + this._super.apply(this, arguments); + + this.container = null; + this.flagContainer = null; + this.tabData = []; + }, + + _readTabs: function(tabData, tabs) { + et2_filteredNodeIterator(tabs, function(node, nodeName) { + if (nodeName == "tab") + { + tabData.push({ + "label": et2_readAttrWithDefault(node, "label", "Tab"), + "widget": null, + "contentDiv": null, + "flagDiv": null + }); + } + else + { + throw("Error while parsing: Invalid tag '" + nodeName + + "' in tabs tag"); + } + }, this); + }, + + _readTabPanels: function(tabData, tabpanels) { + var i = 0; + et2_filteredNodeIterator(tabpanels, function(node, nodeName) { + if (i < tabData.length) + { + // Create the widget corresponding to the given node + tabData[i].widget = this.createElementFromNode(node, + nodeName); + } + else + { + throw("Error while reading tabpanels tag, too many widgets!"); + } + i++; + }, this); + }, + + loadFromXML: function(_node) { + // Get the tabs and tabpanels tags + var tabsElems = et2_directChildrenByTagName(_node, "tabs"); + var tabpanelsElems = et2_directChildrenByTagName(_node, "tabpanels"); + + if (tabsElems.length == 1 && tabpanelsElems.length == 1) + { + var tabs = tabsElems[0]; + var tabpanels = tabpanelsElems[0]; + + var tabData = []; + + // Parse the "tabs" tag + this._readTabs(tabData, tabs); + + // Read and create the widgets defined in the "tabpanels" + this._readTabPanels(tabData, tabpanels); + + // Create the tab DOM-Nodes + this.createTabs(tabData) + } + else + { + throw("Error while parsing tabbox, none or multiple tabs or tabpanels tags!"); + } + }, + + createTabs: function(tabData) { + this.tabData = tabData; + + this.tabContainer.empty(); + this.flagContainer.empty(); + + for (var i = 0; i < this.tabData.length; i++) + { + // Add a spacer to the flag container + $j(document.createElement("span")) + .addClass("et2_flagspacer") + .text("-") + .appendTo(this.flagContainer); + + var entry = this.tabData[i]; + + entry.flagDiv = $j(document.createElement("span")) + .addClass("et2_tabflag") + .text(entry.label) + .appendTo(this.flagContainer); + + entry.contentDiv = $j(document.createElement("div")) + .addClass("et2_tabcntr") + .hide() + .appendTo(this.tabContainer); + + // Let the widget appear on its corresponding page + entry.widget.onSetParent(); + } + }, + + getDOMNode: function(_sender) { + if (_sender == this) + { + return this.container[0]; + } + else + { + for (var i = 0; i < this.tabData.length; i++) + { + if (this.tabData[i].widget == _sender) + { + return this.tabData[i].contentDiv[0]; + } + } + + return null; + } + } + +}); + +et2_register_widget(et2_tabbox, ["tabbox"]); + diff --git a/etemplate/js/et2_template.js b/etemplate/js/et2_template.js index 0a9e5df4fa..a4ad2e270f 100644 --- a/etemplate/js/et2_template.js +++ b/etemplate/js/et2_template.js @@ -26,6 +26,23 @@ */ var et2_template = et2_DOMWidget.extend({ + attributes: { + "template": { + }, + "group": { + }, + "version": { + "name": "Version", + "type": "string", + "description": "Version of the template" + }, + "lang": { + "name": "Language", + "type": "string", + "description": "Language the template is written in" + } + }, + /** * Initializes this template widget as a simple container. */ @@ -34,6 +51,7 @@ var et2_template = et2_DOMWidget.extend({ this.isProxied = false; this.div = document.createElement("div"); + this.id = ""; this._super.apply(this, arguments); }, diff --git a/etemplate/js/et2_textbox.js b/etemplate/js/et2_textbox.js index 9c5e2765bc..14a083e2d6 100644 --- a/etemplate/js/et2_textbox.js +++ b/etemplate/js/et2_textbox.js @@ -14,33 +14,56 @@ /*egw:uses jquery.jquery; - et2_widget; + et2_inputWidget; */ /** * Class which implements the "textbox" XET-Tag */ -var et2_textbox = et2_DOMWidget.extend({ +var et2_textbox = et2_baseWidget.extend({ - init: function(_parent) { - this.input = $j(document.createElement("input")) - .addClass("et2_input"); - - this._super.apply(this, arguments); - this.label = ""; - }, - - set_value: function(_value) { - if (_value != this.value) - { - this.label = _value; - - this.input.attr("value", _value); + attributes: { + "multiline": { + "name": "multiline", + "type": "boolean", + "default": false, + "description": "If true, the textbox is a multiline edit field." } }, - getDOMNode: function() { - return this.input[0]; + init: function(_parent) { + this._super.apply(this, arguments); + + this.input = null; + + this.createInputWidget(); + }, + + createInputWidget: function() { + if (this.multiline) + { + this.input = $j(document.createElement("textarea")); + } + else + { + this.input = $j(document.createElement("input")); + } + + this.input.addClass("et2_textbox"); + + this.setDOMNode(this.input[0]); + }, + + set_multiline: function(_value) { + if (_value != this.multiline) + { + this.multiline = _value; + + this.createInputWidget(); + + // Write all settings again + this.update(); + } } }); diff --git a/etemplate/js/et2_widget.js b/etemplate/js/et2_widget.js index 1c2771c1ce..84d2fc42c9 100644 --- a/etemplate/js/et2_widget.js +++ b/etemplate/js/et2_widget.js @@ -14,6 +14,7 @@ /*egw:uses jquery.jquery; + lib/tooltip.js; et2_xml; et2_common; et2_inheritance; @@ -53,6 +54,26 @@ function et2_register_widget(_constructor, _types) */ var et2_widget = Class.extend({ + attributes: { + "id": { + "name": "ID", + "type": "string", + "description": "Unique identifier of the widget" + }, + + /** + * Ignore the "span" property by default - it is read by the grid and + * other widgets. + */ + "span": { + "ignore": true + } + }, + + // Set the legacyOptions array to the names of the properties the "options" + // attribute defines. + legacyOptions: [], + /** * The init function is the constructor of the widget. When deriving new * classes from the widget base class, always call this constructor unless @@ -82,7 +103,6 @@ var et2_widget = Class.extend({ } this._children = []; - this.id = ""; this.type = _type; // The supported widget classes array defines a whitelist for all widget @@ -148,12 +168,12 @@ var et2_widget = Class.extend({ _obj._children[i].clone(this, _obj._children[i].type); } - // Copy all properties for which a setter function exists - for (var key in _obj) + // Copy all properties + for (var key in _obj.attributes) { - if (key != "id" && typeof this["set_" + key] == "function") + if (!_obj.attributes[key].ignore && key != "id") { - this["set_" + key](_obj[key]); + this.setAttribute(key, _obj.getAttribute(key)); } } }, @@ -265,6 +285,31 @@ var et2_widget = Class.extend({ return null; }, + /** + * Function which allows iterating over the complete widget tree. + * + * @param _callback is the function which should be called for each widget + * @param _context is the context in which the function should be executed + * @param _type is an optional parameter which specifies a class/interface + * the elements have to be instanceOf. + */ + iterateOver: function(_callback, _context, _type) { + if (typeof _type == "undefined") + { + _type = et2_widget; + } + + if (this.instanceOf(_type)) + { + _callback.call(_context, this); + } + + for (var i = 0; i < this._children.length; i++) + { + this._children[i].iterateOver(_callback, _context, _type); + } + }, + isOfSupportedWidgetClass: function(_obj) { for (var i = 0; i < this.supportedWidgetClasses.length; i++) @@ -336,12 +381,19 @@ var et2_widget = Class.extend({ loadAttributes: function(_attrs) { for (var i = 0; i < _attrs.length; i++) { - var attr = _attrs[i]; - - // Check whether a setter exists for the given attribute - if (typeof this["set_" + attr.name] == "function" && attr.name.charAt(0) != "_") + if (_attrs[i].name == "options") { - this["set_" + attr.name](attr.value); + // Parse the legacy options + var splitted = et2_csvSplit(_attrs[i].value); + + for (var i = 0; i < splitted.length && i < this.legacyOptions.length; i++) + { + this.setAttribute(this.legacyOptions[i], splitted[i]); + } + } + else + { + this.setAttribute(_attrs[i].name, _attrs[i].value); } } }, @@ -357,13 +409,14 @@ var et2_widget = Class.extend({ * update function of all child nodes. */ update: function() { + // Go through every property of this object and check whether a // corresponding setter function exists. If yes, it is called. - for (var key in this) + for (var key in this.attributes) { - if (typeof this["set_" + key] == "function" && key.charAt(0) != "_") + if (!this.attributes[key].ignore && key != "id") { - this["set_" + key](this[key]); + this.setAttribute(key, this.getAttribute(key)); } } @@ -372,19 +425,8 @@ var et2_widget = Class.extend({ { this._children[i].update(); } - }, - - get_id: function() { - return this.id; - }, - - set_id: function(_value) { - this.id = _value; - }, - - get_type: function() { - return this.type; } + }); /** @@ -392,7 +434,13 @@ var et2_widget = Class.extend({ */ var et2_IDOMNode = new Interface({ /** - * Returns the DOM-Node of the current widget. + * Returns the DOM-Node of the current widget. The return value has to be + * a plain DOM node. If you want to return an jQuery object as you receive + * it with + * + * obj = $j(node); + * + * simply return obj[0]; * * @param _sender The _sender parameter defines which widget is asking for * the DOMNode.depending on that, the widget may return different nodes. @@ -417,19 +465,32 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { */ init: function(_parent, _type) { this.parentNode = null; - this.visible = true; + + this._attachSet = { + "node": null, + "parent": null + }; // Call the inherited constructor this._super.apply(this, arguments); }, + /** + * Detatches the node from the DOM and clears all references to the parent + * node or the dom node of this widget. + */ destroy: function() { this.detatchFromDOM(); + this.parentNode = null; + this._attachSet = {}; this._super(); }, + /** + * Automatically tries to attach this node to the parent widget. + */ onSetParent: function() { // Check whether the parent implements the et2_IDOMNode interface. If // yes, grab the DOM node and create our own. @@ -438,16 +499,53 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { } }, + /** + * Detaches the widget from the DOM tree, if it had been attached to the + * DOM-Tree using the attachToDOM method. + */ detatchFromDOM: function() { - if (this.parentNode) + + if (this._attachSet.node && this._attachSet.parent) { - var node = this.getDOMNode(this); - if (node) - { - this.parentNode.removeChild(node); - this.parentNode = null; - } + // Remove the current node from the parent node + this._attachSet.parent.removeChild(this._attachSet.node); + + // Reset the "attachSet" + this._attachSet = { + "node": null, + "parent": null + }; + + return true; } + + return false; + }, + + /** + * Attaches the widget to the DOM tree. Fails if the widget is already + * attached to the tree or no parent node or no node for this widget is + * defined. + */ + attachToDOM: function() { + // Attach the DOM node of this widget (if existing) to the new parent + var node = this.getDOMNode(this); + if (node && this.parentNode && + (node != this._attachSet.node || + this.parentNode != this._attachSet.parent)) + { + this.parentNode.appendChild(node); + + // Store the currently attached nodes + this._attachSet = { + "node": node, + "parent": this.parentNode + }; + + return true; + } + + return false; }, /** @@ -462,51 +560,77 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { this.parentNode = _node; - // Attach the DOM node of this widget (if existing) to the new parent - var node = this.getDOMNode(this); - if (node && this.parentNode) - { - this.parentNode.appendChild(node); - } + // And attatch the element to the DOM tree + this.attachToDOM(); } }, + /** + * Returns the parent node. + */ getParentDOMNode: function() { return this.parentNode; }, + /** + * Sets the id of the DOM-Node. + */ set_id: function(_value) { - this._super(_value); + + this.id = _value; var node = this.getDOMNode(this); if (node) { - node.setAttribute("id", _value); + if (_value != "") + { + node.setAttribute("id", _value); + } + else + { + node.removeAttribute("id"); + } } } }); /** - * Container object for not-yet supported widgets + * Common container object */ -var et2_placeholder = et2_DOMWidget.extend({ +var et2_container = et2_simpleWidget.extend({ init: function() { - // Create the placeholder div - this.placeDiv = $j(document.createElement("span")) - .addClass("et2_placeholder"); + this._super.apply(this, arguments); + + this.setDOMNode(document.createElement("div")); + } + +}); + + +/** + * Container object for not-yet supported widgets + */ +var et2_placeholder = et2_baseWidget.extend({ + + init: function() { + this._super.apply(this, arguments); // The attrNodes object will hold the DOM nodes which represent the // values of this object this.attrNodes = {}; - this._super.apply(this, arguments); + // Create the placeholder div + this.placeDiv = $j(document.createElement("span")) + .addClass("et2_placeholder"); var headerNode = $j(document.createElement("span")) .text(this.type) - .addClass("et2_caption"); - $j(this.placeDiv).append(headerNode); + .addClass("et2_caption") + .appendTo(this.placeDiv); + + this.setDOMNode(this.placeDiv[0]); }, loadAttributes: function(_attrs) { @@ -523,37 +647,6 @@ var et2_placeholder = et2_DOMWidget.extend({ this.attrNodes[attr.name].text(attr.name + "=" + attr.value); } - }, - - getDOMNode: function() { - return this.placeDiv[0]; } - }); -/** - * Common container object - */ -var et2_container = et2_DOMWidget.extend({ - - init: function() { - this.div = document.createElement("div"); - - this._super.apply(this, arguments); - }, - - getDOMNode: function() { - return this.div; - } - -}); - -/** - * Interface for all widgets which support returning a value - */ - -var et2_IValue = new Interface({ - getValue: function() {} -}); - - diff --git a/etemplate/js/et2_xml.js b/etemplate/js/et2_xml.js index cc95e3bd13..656306c622 100644 --- a/etemplate/js/et2_xml.js +++ b/etemplate/js/et2_xml.js @@ -114,3 +114,11 @@ function et2_filteredNodeIterator(_node, _callback, _context) } } +function et2_readAttrWithDefault(_node, _name, _default) +{ + var val = _node.getAttribute(_name); + + return (val === null) ? _default : val; +} + + diff --git a/etemplate/js/lib/tooltip.js b/etemplate/js/lib/tooltip.js new file mode 100644 index 0000000000..8c810f7948 --- /dev/null +++ b/etemplate/js/lib/tooltip.js @@ -0,0 +1,180 @@ +/** + * eGroupware2 UI - Tooltip JS + * + * This javascript file contains the JavaScript code for the etemplate2 tooltip + * + * @link http://www.egroupware.org + * @author Andreas Stoeckel (as@stylite.de) + * @version $Id: tooltip.js 31497 2010-07-22 09:50:11Z igel457 $ + */ + +/* Tooltip handling */ +function egw_tooltip() +{ + this.tooltip_div = null; + this.current_elem = null; + this.time_delta = 100; + this.show_delta = 0; + this.show_delay = 800; + this.x = 0; + this.y = 0; +} + +egw_tooltip.prototype.bindToElement = function(_elem, _text) +{ + if (_text != '') + { + var self = this; + _elem.bind('mouseenter.tooltip', function(e) { + if (_elem != self.current_elem) + { + //Prepare the tooltip + self._prepare(_text); + + self.current_elem = _elem; + self.show_delta = 0; + self.x = e.clientX; + self.y = e.clientY; + + window.setTimeout(function() {self.showHintTimeout();}, self.time_delta); + } + + return false; + }); + + _elem.bind('mouseleave.tooltip', function() { + self.current_elem = null; + self.show_delta = 0; + //self.hide(); + if (self.tooltip_div) + { + self.tooltip_div.fadeOut(100); + } + }); + + _elem.bind('mousemove.tooltip', function(e) { + //Calculate the distance the mouse took since the last call of mousemove + var dx = self.x - e.clientX; + var dy = self.y - e.clientY; + var movedist = Math.sqrt(dx * dx + dy * dy); + + //Block appereance of the tooltip on fast movements (with small movedistances) + if (movedist > 2) + self.show_delta = 0; + + self.x = e.clientX; + self.y = e.clientY; + }); + } +} + +egw_tooltip.prototype.unbindFromElement = function(_elem) +{ + _elem.unbind('mouseenter.tooltip'); + _elem.unbind('mouseleave.tooltip'); + _elem.unbind('mousemove.tooltip'); +} + +egw_tooltip.prototype.showHintTimeout = function() +{ + if (this.current_elem != null) + { + this.show_delta += this.time_delta; + if (this.show_delta < this.show_delay) + { + //Repeat the call of timeout + var self = this; + window.setTimeout(function() {self.showHintTimeout();}, this.time_delta); + } + else + { + this.show_delta = 0; + this.show(); + } + } +} + +egw_tooltip.prototype._prepare = function(_text) +{ + var self = this; + + //Remove the old tooltip + if (this.tooltip_div) + this.hide(); + + //Generate the tooltip div, set it's text and append it to the body tag + this.tooltip_div = $j(document.createElement('div')); + this.tooltip_div.hide(); + this.tooltip_div.append(_text); + this.tooltip_div.addClass("egw_tooltip"); + $j('body').append(this.tooltip_div); + + //The tooltip should automatically hide when the mouse comes over it + this.tooltip_div.mouseenter(function() { + self.hide(); + }); +} + +egw_tooltip.prototype.show = function() +{ + if (this.tooltip_div) + { + //Calculate the cursor_rectangle - this is a space the tooltip might + //not overlap with + var cursor_rect = { + left: (this.x - 8), + top: (this.y - 8), + right: (this.x + 8), + bottom: (this.y + 8) + }; + + //Calculate how much space is left on each side of the rectangle + var window_width = $j(document).width(); + var window_height = $j(document).height(); + var space_left = { + left: (cursor_rect.left), + top: (cursor_rect.top), + right: (window_width - cursor_rect.right), + bottom: (window_height - cursor_rect.bottom) + }; + + //Get the width and the height of the tooltip + var tooltip_width = this.tooltip_div.width(); + if (tooltip_width > 300) tooltip_width = 300; + var tooltip_height = this.tooltip_div.height(); + + + if (space_left.right < tooltip_width) { + this.tooltip_div.css('left', cursor_rect.left - tooltip_width); + } else if (space_left.left >= tooltip_width) { + this.tooltip_div.css('left', cursor_rect.right); + } else { + this.tooltip_div.css('left', cursor_rect.right); + this.tooltip_div.css('max-width', space_left.right); + } + + if (space_left.bottom < tooltip_height) { + this.tooltip_div.css('top', cursor_rect.top - tooltip_height); + } else if (space_left.top >= tooltip_height) { + this.tooltip_div.css('top', cursor_rect.bottom); + } else { + this.tooltip_div.css('top', cursor_rect.bottom); + this.tooltip_div.css('max-height', space_left.bottom); + } + + this.tooltip_div.fadeIn(100); + } +} + +egw_tooltip.prototype.hide = function() +{ + if (this.tooltip_div) + { + this.tooltip_div.remove(); + this.tooltip_div = null; + } +} + +//A reference to the current tooltip object +window.egw_global_tooltip = new egw_tooltip(); + diff --git a/etemplate/js/test/et2_test_description.xet b/etemplate/js/test/et2_test_description.xet new file mode 100644 index 0000000000..e5775712ef --- /dev/null +++ b/etemplate/js/test/et2_test_description.xet @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/etemplate/js/test/et2_test_grid.xet b/etemplate/js/test/et2_test_grid.xet index fa4b2d6b2d..2db9ab63a5 100644 --- a/etemplate/js/test/et2_test_grid.xet +++ b/etemplate/js/test/et2_test_grid.xet @@ -25,15 +25,16 @@ + @@ -121,12 +122,12 @@ diff --git a/etemplate/js/test/et2_test_tabbox.xet b/etemplate/js/test/et2_test_tabbox.xet new file mode 100644 index 0000000000..aa310f721b --- /dev/null +++ b/etemplate/js/test/et2_test_tabbox.xet @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/etemplate/js/test/et2_test_textbox.xet b/etemplate/js/test/et2_test_textbox.xet new file mode 100644 index 0000000000..663b17f518 --- /dev/null +++ b/etemplate/js/test/et2_test_textbox.xet @@ -0,0 +1,13 @@ + + + + + + + +