diff --git a/etemplate/js/et2_common.js b/etemplate/js/et2_common.js index c3cd9d1616..0152b47aae 100644 --- a/etemplate/js/et2_common.js +++ b/etemplate/js/et2_common.js @@ -31,3 +31,19 @@ function et2_debug(_level, _msg) } } +/** + * IE Fix for array.indexOf + */ +if (typeof Array.prototype.indexOf == "undefined") +{ + Array.prototype.indexOf = function(_elem) { + for (var i = 0; i < this.length; i++) + { + if (this[i] === _elem) + return i; + } + return -1; + }; +} + + diff --git a/etemplate/js/et2_description.js b/etemplate/js/et2_description.js new file mode 100644 index 0000000000..11cd82742f --- /dev/null +++ b/etemplate/js/et2_description.js @@ -0,0 +1,47 @@ +/** + * eGroupWare eTemplate2 - JS Template 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$ + */ + +/*egw:uses + jquery.jquery; + et2_widget; +*/ + +/** + * Class which implements the "description" XET-Tag + */ +et2_description = et2_DOMWidget.extend({ + + init: function(_parent) { + this.span = $j(document.createElement("span")); + + this._super.apply(this, arguments); + this.value = ""; + }, + + set_value: function(_value) { + if (_value != this.value) + { + this.value = _value; + + this.span.text(_value); + } + }, + + getDOMNode: function() { + return this.span[0]; + } + +}); + +et2_register_widget(et2_description, ["description"]); + + diff --git a/etemplate/js/et2_inheritance.js b/etemplate/js/et2_inheritance.js index 0782a31f09..0ffeffb15d 100644 --- a/etemplate/js/et2_inheritance.js +++ b/etemplate/js/et2_inheritance.js @@ -21,11 +21,11 @@ * where "interfaces" is a single interface or an array of interfaces and * functions an object containing the functions the class implements. * - * A single interface is also a simple object defining (empty) functions. Example: + * An interface has to be created in the following way: * - * IBreathingObject = { + * IBreathingObject = new Interface({ * breath: function() {} - * } + * }); * * Human = Class.extend(IBreathingObject, { * walk: function() { @@ -72,6 +72,15 @@ // check whether a var fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; + // Base "Class" for interfaces - needed to check whether an object is an + // interface + this.Interface = function(fncts) { + for (var key in fncts) + { + this[key] = fncts[key]; + } + }; + /** * The addInterfaceStuff function adds all interface functions the class has * to implement to the class prototype. @@ -84,15 +93,23 @@ prototype["_ifacefuncs"] = []; - for (var i in interfaces) + for (var i = 0; i < interfaces.length; i++) { - for (var key in interfaces[i]) + var iface = interfaces[i]; + if (iface instanceof Interface) { - prototype["_ifacefuncs"].push(key); + for (var key in iface) + { + prototype["_ifacefuncs"].push(key); + } + } + else + { + throw("Interfaces must be instanceof Interface!"); } } - for (var i in ifaces) + for (var i = 0; i < ifaces.length; i++) { prototype["_ifacefuncs"].push(ifaces[i]); } @@ -109,7 +126,21 @@ } return true; } - } + + // The instanceOf function can be used to check for both - classes and + // interfaces. Please don't change the case of this function as this + // affects IE and Opera support. + prototype["instanceOf"] = function(_obj) { + if (_obj instanceof Interface) + { + return this.implements(_obj); + } + else + { + return this instanceof _obj; + } + } + }; // The base Class implementation (does nothing) this.Class = function(){}; @@ -179,13 +210,8 @@ // All construction is actually done in the init method if (!initializing) { - if (this.init) - { - this.init.apply(this, arguments); - } - // Check whether the object implements all interface functions - for (var i in this._ifacefuncs) + for (var i = 0; i < this._ifacefuncs.length; i++) { var func = this._ifacefuncs[i]; if (!(typeof this[func] == "function")) @@ -194,6 +220,11 @@ "function '" + func + "' not implemented."); } } + + if (this.init) + { + this.init.apply(this, arguments); + } } } diff --git a/etemplate/js/et2_template.js b/etemplate/js/et2_template.js new file mode 100644 index 0000000000..9a43b79f31 --- /dev/null +++ b/etemplate/js/et2_template.js @@ -0,0 +1,123 @@ +/** + * eGroupWare eTemplate2 - JS Template 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$ + */ + +/*egw:uses + et2_widget; +*/ + +/** + * Class which implements the "template" XET-Tag. When the id parameter is set, + * the template class checks whether another template with this id already + * exists. If yes, this template is removed from the DOM tree, copied and + * inserted in place of this template. + * + * TODO: Check whether this widget behaves as it should. + */ +et2_template = et2_DOMWidget.extend({ + + /** + * Initializes this template widget as a simple container. + */ + init: function(_parent) { + this.proxiedTemplate = null; + this.isProxied = false; + + this.div = document.createElement("div"); + + this._super.apply(this, arguments); + }, + + /** + * If the parent node is changed, either the DOM-Node of the proxied template + * or the DOM-Node of this template is connected to the parent DOM-Node. + */ + onSetParent: function() { + // Check whether the parent implements the et2_IDOMNode interface. If + // yes, grab the DOM node and create our own. + if (this._parent && this._parent.implements(et2_IDOMNode)) { + var parentNode = this._parent.getDOMNode(); + + if (parentNode) + { + if (this.proxiedTemplate) + { + this.proxiedTemplate.setParentDOMNode(parentNode); + } + else if (!this.isProxied) + { + this.setParentDOMNode(parentNode); + } + } + } + }, + + makeProxied: function() { + if (!this.isProxied) + { + this.detatchFromDOM(); + this.div = null; + this.parentNode = null; + } + + this.isProxied = true; + }, + + set_id: function(_value) { + if (_value != this.id) + { + // Check whether a template with the given name already exists and + // is not a proxy. + var tmpl = this.getRoot().getWidgetById(_value); + if (tmpl instanceof et2_template && tmpl.proxiedTemplate == null && + tmpl != this) + { + // Check whether we still have a proxied template, if yes, + // destroy it + if (this.proxiedTemplate != null) + { + this.proxiedTemplate.destroy(); + this.proxiedTemplate = null; + } + + // This element does not have a node in the tree + this.detatchFromDOM(); + + // Detatch the proxied template from the DOM to and set its + // isProxied property to true + tmpl.makeProxied(); + + // Create a clone of the template and add it as child of this + // template (done by passing "this" to the clone function) + this.proxiedTemplate = tmpl.clone(this); + + // Disallow adding any new node to this template + this.supportedWidgetClasses = []; + + // Call the parent change event function + this.onSetParent(); + } + else + { + this._super(_value); + } + } + }, + + getDOMNode: function(_fromProxy) { + return this.div; + } + +}); + +et2_register_widget(et2_template, ["template"]); + + diff --git a/etemplate/js/et2_widget.js b/etemplate/js/et2_widget.js index 12ca726dfb..8969f377b3 100644 --- a/etemplate/js/et2_widget.js +++ b/etemplate/js/et2_widget.js @@ -11,6 +11,7 @@ */ /*egw:uses + jquery.jquery; et2_xml; et2_common; et2_inheritance; @@ -29,7 +30,7 @@ var et2_registry = {}; function et2_register_widget(_constructor, _types) { // Iterate over all given types and register those - for (var i in _types) + for (var i = 0; i < _types.length; i++) { var type = _types[i].toLowerCase(); @@ -71,6 +72,8 @@ et2_widget = Class.extend({ // Copy the parent parameter and add this widget to its parent children // list. this._parent = _parent; + this.onSetParent(); + if (_parent != null) { this._parent.addChild(this); @@ -79,6 +82,10 @@ et2_widget = Class.extend({ this._children = []; this.id = ""; this.type = _type; + + // The supported widget classes array defines a whitelist for all widget + // classes or interfaces child widgets have to support. + this.supportedWidgetClasses = [et2_widget]; }, /** @@ -92,7 +99,7 @@ et2_widget = Class.extend({ destroy: function() { // Call the destructor of all children - for (var i = this._children.length; i >= 0; i--) + for (var i = this._children.length - 1; i >= 0; i--) { this._children[i].destroy(); } @@ -106,6 +113,41 @@ et2_widget = Class.extend({ // Delete all references to other objects this._children = []; this._parent = null; + this.onSetParent(); + }, + + /** + * Creates a copy of this widget. The parameters given are passed to the + * constructor of the copied object. If the parameters are omitted, _parent + * is defaulted to null + */ + clone: function(_parent, _type) { + + // Default _parent to null + if (typeof _parent == "undefined") + { + _parent = null; + } + + // Create the copy + var copy = new (this.constructor)(_parent, _type); + + // Create a clone of all child elements + for (var i = 0; i < this._children.length; i++) + { + this._children[i].clone(copy, this._children[i].type); + } + + // Copy all properties for which a setter function exists + for (var key in this) + { + if (key != "id" && typeof copy["set_" + key] == "function") + { + copy["set_" + key](this[key]); + } + } + + return copy; }, /** @@ -115,6 +157,14 @@ et2_widget = Class.extend({ return this._parent; }, + /** + * The set parent event is called, whenever the parent of the widget is set. + * Child classes can overwrite this function. Whe onSetParent is called, + * the change of the parent has already taken place. + */ + onSetParent: function() { + }, + /** * Returns the list of children of this widget. */ @@ -154,14 +204,15 @@ et2_widget = Class.extend({ * @param _idx is the position at which the element should be added. */ insertChild: function(_node, _idx) { - if (_node instanceof et2_widget) + // Check whether the node is one of the supported widget classes. + if (this.isOfSupportedWidgetClass(_node)) { _node.parent = this; this._children.splice(_idx, 0, _node); } else { - throw("_node is not an instance of et2_widget!"); + throw("_node is not supported by this widget class!"); } }, @@ -176,6 +227,7 @@ et2_widget = Class.extend({ { // This element is no longer parent of the child _node._parent = null; + _node.onSetParent(); this._children.splice(idx, 1); } @@ -205,6 +257,18 @@ et2_widget = Class.extend({ return null; }, + isOfSupportedWidgetClass: function(_obj) + { + for (var i = 0; i < this.supportedWidgetClasses.length; i++) + { + if (_obj.instanceOf(this.supportedWidgetClasses[i])) + { + return true; + } + } + return false; + }, + /** * Loads the widget tree from an XML node */ @@ -221,6 +285,20 @@ et2_widget = Class.extend({ var node = _node.childNodes[i]; var widgetType = node.nodeName.toLowerCase(); + if (widgetType == "#comment") + { + continue; + } + + if (widgetType == "#text") + { + if (node.data.replace(/^\s+|\s+$/g, '')) + { + this.loadContent(node.data); + } + continue; + } + // Check whether a widget with the given type is registered. var constructor = typeof et2_registry[widgetType] == "undefined" ? et2_placeholder : et2_registry[widgetType]; @@ -247,6 +325,12 @@ et2_widget = Class.extend({ } }, + /** + * Called whenever textNodes are loaded from the XML tree + */ + loadContent: function(_content) { + }, + /** * Calls the setter of each property with its current value, calls the * update function of all child nodes. @@ -263,7 +347,7 @@ et2_widget = Class.extend({ } // Call the update function of all children. - for (var i in this._children) + for (var i = 0; i < this._children.length; i++) { this._children[i].update(); } @@ -285,9 +369,9 @@ et2_widget = Class.extend({ /** * Interface for all widget classes, which are based on a DOM node. */ -et2_IDOMNode = { +et2_IDOMNode = new Interface({ getDOMNode: function() {} -} +}); /** * Abstract widget class which can be inserted into the DOM. All widget classes @@ -301,17 +385,11 @@ et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { * object (if available) and passes it to its own "createDOMNode" function */ init: function(_parent, _type) { + this.parentNode = null; + this.visible = true; // Call the inherited constructor this._super.apply(this, arguments); - - this.parentNode = null; - - // Check whether the parent implements the et2_IDOMNode interface. If - // yes, grab the DOM node and create our own. - if (this._parent && this._parent.implements(et2_IDOMNode)) { - this.setParentDOMNode(this._parent.getDOMNode()); - } }, destroy: function() { @@ -321,14 +399,22 @@ et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { this._super(); }, + onSetParent: function() { + // Check whether the parent implements the et2_IDOMNode interface. If + // yes, grab the DOM node and create our own. + if (this._parent && this._parent.implements(et2_IDOMNode)) { + this.setParentDOMNode(this._parent.getDOMNode()); + } + }, + detatchFromDOM: function() { if (this.parentNode) { var node = this.getDOMNode(); - if (node) { this.parentNode.removeChild(node); + this.parentNode = null; } } }, @@ -366,6 +452,17 @@ et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { { node.setAttribute("id", _value); } + }, + + set_visible: function(_value) { + /*if (_value != this.visible) + { + var node = this.getDOMNode(); + if (node) + { + node.set + } + }*/ } }); @@ -376,13 +473,40 @@ et2_DOMWidget = et2_widget.extend(et2_IDOMNode, { et2_placeholder = et2_DOMWidget.extend({ init: function() { - this.placeDiv = document.createElement("span"); + // Create the placeholder div + this.placeDiv = $j(document.createElement("span")) + .addClass("et2_placeholder"); + + // The attrNodes object will hold the DOM nodes which represent the + // values of this object + this.attrNodes = {}; this._super.apply(this, arguments); + + var headerNode = $j(document.createElement("span")) + .text(this.type) + .addClass("et2_caption"); + $j(this.placeDiv).append(headerNode); + }, + + loadAttributes: function(_attrs) { + for (var i = 0; i < _attrs.length; i++) + { + var attr = _attrs[i]; + + if (typeof this.attrNodes[attr.name] == "undefined") + { + this.attrNodes[attr.name] = $j(document.createElement("span")) + .addClass("et2_attr"); + this.placeDiv.append(this.attrNodes[attr.name]); + } + + this.attrNodes[attr.name].text(attr.name + "=" + attr.value); + } }, getDOMNode: function() { - return this.placeDiv; + return this.placeDiv[0]; } }); @@ -404,3 +528,12 @@ et2_container = et2_DOMWidget.extend({ }); +/** + * Interface for all widgets which support returning a value + */ + +et2_IValue = new Interface({ + getValue: function() {} +}); + + diff --git a/etemplate/js/et2_xml.js b/etemplate/js/et2_xml.js index 5382a20a63..86acefd8f7 100644 --- a/etemplate/js/et2_xml.js +++ b/etemplate/js/et2_xml.js @@ -31,7 +31,21 @@ function et2_loadXMLFromURL(_url, _callback, _context) xmldoc.onreadystatechange = function() { if (xmldoc && xmldoc.readyState == 4) { - _callback.call(_context, xmldoc); + // Find the root node - the root node is the node which is not + // the "xml", not a text node and not a comment node - those nodes + // are marked with an "#" + for (var i = 0; i < xmldoc.childNodes.length; i++) + { + var nodeName = xmldoc.childNodes[i].nodeName; + if (nodeName != "xml" && nodeName.charAt(0) != "#") + { + // Call the callback function and pass the current node + _callback.call(_context, xmldoc.childNodes[i]); + return; + } + } + + throw("Could not find XML root node."); } } diff --git a/etemplate/js/test/et2_test_template.xet b/etemplate/js/test/et2_test_template.xet new file mode 100644 index 0000000000..f272934353 --- /dev/null +++ b/etemplate/js/test/et2_test_template.xet @@ -0,0 +1,13 @@ + + + + +