diff --git a/etemplate/js/et2_core_DOMWidget.js b/etemplate/js/et2_core_DOMWidget.js
index 07a21448bc..e08192b269 100644
--- a/etemplate/js/et2_core_DOMWidget.js
+++ b/etemplate/js/et2_core_DOMWidget.js
@@ -13,32 +13,10 @@
"use strict";
/*egw:uses
+ et2_core_interfaces;
et2_core_widget;
*/
-/**
- * Interface for all widget classes, which are based on a DOM node.
- */
-var et2_IDOMNode = new Interface({
- /**
- * 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.
- * This is used in the grid. Normally the _sender parameter can be omitted
- * in most implementations of the getDOMNode function.
- * However, you should always define the _sender parameter when calling
- * getDOMNode!
- */
- getDOMNode: function(_sender) {}
-});
-
/**
* Abstract widget class which can be inserted into the DOM. All widget classes
* deriving from this class have to care about implementing the "getDOMNode"
@@ -110,7 +88,7 @@ var et2_DOMWidget = et2_widget.extend(et2_IDOMNode, {
if (this._surroundingsMgr)
{
- this._surroundingsMgr.destroy();
+ this._surroundingsMgr.free();
this._surroundingsMgr = null;
}
diff --git a/etemplate/js/et2_core_common.js b/etemplate/js/et2_core_common.js
index fb320d1efa..72e8f5dc3d 100644
--- a/etemplate/js/et2_core_common.js
+++ b/etemplate/js/et2_core_common.js
@@ -594,3 +594,13 @@ function et2_hasChild(_nodes, _child)
return false;
}
+/**
+ * Generates a localy unique id and returns it
+ */
+var _et2_uniqueId = 0;
+
+function et2_uniqueId()
+{
+ return _et2_uniqueId++;
+}
+
diff --git a/etemplate/js/et2_core_inheritance.js b/etemplate/js/et2_core_inheritance.js
index 7b50cdc5d7..2e7a4e5a9e 100644
--- a/etemplate/js/et2_core_inheritance.js
+++ b/etemplate/js/et2_core_inheritance.js
@@ -72,7 +72,14 @@
*/
// Inspired by base2 and Prototype
(function(){
- var initializing = false
+ var initializing = false;
+
+ /**
+ * Turn this to "true" to track creation and destruction of elements
+ */
+ var getMem_freeMem_trace = false;
+
+ var tracedObjects = {};
// Check whether "function decompilation" works - fnTest is normally used to
// check whether a
@@ -257,6 +264,18 @@
}
}
+ // Do some tracing of the getMem_freeMem_trace is activated
+ if (getMem_freeMem_trace)
+ {
+ this.__OBJ_UID = "obj_" + et2_uniqueId();
+ var className = this.className();
+ tracedObjects[this.__OBJ_UID] = {
+ "created": new Date().getTime(),
+ "class": className
+ }
+ et2_debug("log", "*" + this.__OBJ_UID + " (" + className + ")");
+ }
+
if (this.init)
{
this.init.apply(this, arguments);
@@ -289,6 +308,61 @@
// Add the basic functions
+ /**
+ * Destructor function - it calls "destroy" if it has been defined and then
+ * deletes all keys of this element, so that any access to this element will
+ * eventually throw an exception, making it easier to hunt down memory leaks.
+ */
+ Class.prototype.free = function() {
+ if (this.destroy)
+ {
+ this.destroy();
+ }
+
+ // Trace the freeing of the object
+ if (getMem_freeMem_trace)
+ {
+ delete(tracedObjects[this.__OBJ_UID]);
+ et2_debug("log", "-" + this.__OBJ_UID);
+ }
+
+ // Delete every object entry
+ for (var key in this)
+ {
+ delete(this[key]);
+ }
+
+ // Don't raise an exception when attempting to free an element multiple
+ // times.
+ this.free = function() {};
+ };
+
+ // Some debug functions for memory leak hunting
+ if (getMem_freeMem_trace)
+ {
+ /**
+ * Prints a list of all objects UIDs which have not been freed yet.
+ */
+ Class.prototype.showTrace = function() {
+ console.log(tracedObjects);
+ },
+
+ /**
+ * VERY slow - for debugging only!
+ */
+ Class.prototype.className = function() {
+ for (var key in window)
+ {
+ if (key.substr(0, 3) == "et2" && this.constructor == window[key])
+ {
+ return key;
+ }
+ }
+
+ return "?";
+ }
+ }
+
/**
* Returns the value of the given attribute. If the property does not
* exist, an error message is issued.
@@ -310,7 +384,7 @@
{
et2_debug("error", this, "Attribute '" + _name + "' does not exist!");
}
- }
+ };
/**
* The setAttribute function sets the attribute with the given name to
@@ -345,7 +419,7 @@
{
et2_debug("warn", this, "Attribute '" + _name + "' does not exist!");
}
- }
+ };
/**
* generateAttributeSet sanitizes the given associative array of attributes
@@ -392,7 +466,7 @@
}
return _attrs;
- }
+ };
/**
* The initAttributes function sets the attributes to their default
@@ -408,22 +482,22 @@
this.setAttribute(key, _attrs[key], false);
}
}
- }
+ };
/**
* The implements function can be used to check whether the object
* implements the given interface.
*/
Class.prototype.implements = function(_iface) {
- for (var key in _iface)
+ for (var key in _iface)
+ {
+ if (this._ifacefuncs.indexOf(key) < 0)
{
- if (this._ifacefuncs.indexOf(key) < 0)
- {
- return false;
- }
+ return false;
}
- return true;
}
+ return true;
+ };
/**
* The instanceOf function can be used to check for both - classes and
@@ -431,15 +505,15 @@
* affects IE and Opera support.
*/
Class.prototype.instanceOf = function(_obj) {
- if (_obj instanceof Interface)
- {
+ if (_obj instanceof Interface)
+ {
return this.implements(_obj);
}
else
{
return this instanceof _obj;
}
- }
+ };
}).call(window);
diff --git a/etemplate/js/et2_core_inputWidget.js b/etemplate/js/et2_core_inputWidget.js
index 6473f42f6c..d6dfda1bce 100644
--- a/etemplate/js/et2_core_inputWidget.js
+++ b/etemplate/js/et2_core_inputWidget.js
@@ -14,30 +14,10 @@
/*egw:uses
jquery.jquery;
+ et2_core_interfaces;
et2_core_valueWidget;
*/
-/**
- * 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
diff --git a/etemplate/js/et2_core_interfaces.js b/etemplate/js/et2_core_interfaces.js
new file mode 100644
index 0000000000..106738ea9a
--- /dev/null
+++ b/etemplate/js/et2_core_interfaces.js
@@ -0,0 +1,74 @@
+/**
+ * eGroupWare eTemplate2 - File which contains all interfaces
+ *
+ * @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
+ et2_core_inheritance;
+*/
+
+/**
+ * Interface for all widget classes, which are based on a DOM node.
+ */
+var et2_IDOMNode = new Interface({
+ /**
+ * 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.
+ * This is used in the grid. Normally the _sender parameter can be omitted
+ * in most implementations of the getDOMNode function.
+ * However, you should always define the _sender parameter when calling
+ * getDOMNode!
+ */
+ getDOMNode: function(_sender) {}
+});
+
+/**
+ * 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() {}
+});
+
+/**
+ * Interface for widgets which should be automatically resized
+ */
+var et2_IResizeable = new Interface({
+ /**
+ * Called whenever the window is resized
+ */
+ resize: function() {}
+});
+
+
+
diff --git a/etemplate/js/et2_core_stylesheet.js b/etemplate/js/et2_core_stylesheet.js
new file mode 100644
index 0000000000..03c1da6ea2
--- /dev/null
+++ b/etemplate/js/et2_core_stylesheet.js
@@ -0,0 +1,95 @@
+/**
+ * eGroupWare eTemplate2 - Stylesheet 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$
+ */
+
+"use strict"
+
+/**
+ * Contains the egwDynStyleSheet class which allows dynamic generation of stylesheet
+ * rules - updating a single stylesheet rule is way more efficient than updating
+ * the element style of many objects.
+ */
+var EGW_DYNAMIC_STYLESHEET = null;
+
+/**
+ * Main egwDynStyleSheet class - all egwDynStyleSheets share the same stylesheet
+ * which is dynamically inserted into the head section of the DOM-Tree.
+ * This stylesheet is created with the first egwDynStyleSheet class.
+ */
+function et2_dynStyleSheet()
+{
+ // Check whether the EGW_DYNAMIC_STYLESHEET has already be created
+ if (!EGW_DYNAMIC_STYLESHEET)
+ {
+ var style = document.createElement("style");
+ document.getElementsByTagName("head")[0].appendChild(style);
+
+ this.styleSheet = style.sheet ? style.sheet : style.styleSheet;
+ this.selectors = {};
+ this.selectorCount = 0;
+
+ EGW_DYNAMIC_STYLESHEET = this;
+
+ return this;
+ }
+ else
+ {
+ return EGW_DYNAMIC_STYLESHEET;
+ }
+}
+
+/**
+ * Creates/Updates the given stylesheet rule. Example call:
+ *
+ * styleSheet.updateRule("#container", "background-color: blue; font-family: sans;")
+ *
+ * @param string _selector is the css selector to which the given rule should apply
+ * @param string _rule is the rule which is bound to the selector.
+ */
+et2_dynStyleSheet.prototype.updateRule = function (_selector, _rule)
+{
+ var ruleObj = {
+ "index": this.selectorCount
+ }
+
+ // Remove any existing rule first
+ if (typeof this.selectors[_selector] !== "undefined")
+ {
+ var ruleObj = this.selectors[_selector];
+ if (typeof this.styleSheet.removeRule !== "undefined")
+ {
+ this.styleSheet.removeRule(ruleObj.index);
+ }
+ else
+ {
+ this.styleSheet.deleteRule(ruleObj.index);
+ }
+
+ delete (this.selectors[_selector]);
+ }
+ else
+ {
+ this.selectorCount++;
+ }
+
+ // Add the rule to the stylesheet
+ if (typeof this.styleSheet.addRule !== "undefined")
+ {
+ this.styleSheet.addRule(_selector, _rule, ruleObj.index);
+ }
+ else
+ {
+ this.styleSheet.insertRule(_selector + "{" + _rule + "}", ruleObj.index);
+ }
+
+ this.selectors[_selector] = ruleObj;
+}
+
diff --git a/etemplate/js/et2_core_widget.js b/etemplate/js/et2_core_widget.js
index 4234dbf043..8837fca440 100644
--- a/etemplate/js/et2_core_widget.js
+++ b/etemplate/js/et2_core_widget.js
@@ -220,7 +220,7 @@ var et2_widget = Class.extend({
// Call the destructor of all children
for (var i = this._children.length - 1; i >= 0; i--)
{
- this._children[i].destroy();
+ this._children[i].free();
}
// Remove this element from the parent
@@ -322,7 +322,13 @@ var et2_widget = Class.extend({
// Check whether the node is one of the supported widget classes.
if (this.isOfSupportedWidgetClass(_node))
{
- _node.parent = this;
+ // Remove the node from its original parent
+ if (_node._parent)
+ {
+ _node._parent.removeChild(_node);
+ }
+
+ _node._parent = this;
this._children.splice(_idx, 0, _node);
}
else
diff --git a/etemplate/js/et2_dataview_view_gridcontainer.js b/etemplate/js/et2_dataview_view_gridcontainer.js
new file mode 100644
index 0000000000..3e4b7da65e
--- /dev/null
+++ b/etemplate/js/et2_dataview_view_gridcontainer.js
@@ -0,0 +1,439 @@
+/**
+ * eGroupWare eTemplate2 - Class which generates the outer container for the grid
+ *
+ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
+ * @package etemplate
+ * @subpackage dataview
+ * @link http://www.egroupware.org
+ * @author Andreas Stöckel
+ * @copyright Stylite 2011
+ * @version $Id$
+ */
+
+"use strict"
+
+/*egw:uses
+ jquery.jquery;
+ et2_core_common;
+ et2_core_stylesheet;
+*/
+
+/**
+ * Base view class which is responsible for displaying a grid view element.
+ */
+var et2_dataview_gridContainer = Class.extend({
+
+ /**
+ * Constant which regulates the column padding.
+ */
+ columnPadding: 2,
+
+ /**
+ * Some browser dependant variables which will be calculated on creation of
+ * the first gridContainer object.
+ */
+ scrollbarWidth: false,
+ headerBorderWidth: false,
+ columnBorderWidth: false,
+
+ /**
+ * Constructor for the grid container
+ * @param object _parentNode is the DOM-Node into which the grid view will be inserted
+ */
+ init: function(_parentNode) {
+
+ // Copy the parent node parameter
+ this.parentNode = $j(_parentNode);
+
+ // Initialize some variables
+ this.columnNodes = []; // Array with the header containers
+ this.columns = [];
+ this.columnMgr = null;
+
+ this.width = 0;
+ this.height = 0;
+
+ // Build the base nodes
+ this._createElements();
+
+ // Read the browser dependant variables
+ this._getDepVars();
+ },
+
+ /**
+ * Destroys the object, removes all dom nodes and clears all references.
+ */
+ destroy: function() {
+ // Clear the columns
+ this._clearHeader();
+
+ // Detatch the outer element
+ this.table.remove();
+ },
+
+ /**
+ * Returns the column container node for the given column index
+ *
+ * @param _columnIdx the integer column index
+ */
+ getHeaderContainerNode: function(_columnIdx) {
+ if (typeof this.columnNodes[_columnIdx] != "undefined")
+ {
+ return this.columnNodes[_columnIdx].container[0];
+ }
+
+ return null;
+ },
+
+ /**
+ * Sets the column descriptors and creates the column header according to it.
+ * The inner grid will be emptied if it has already been built.
+ */
+ setColumns: function(_columnData) {
+ // Free all column objects which have been created till this moment
+ this.headTr.empty();
+
+ // Create the column manager and pass the _columnData to it
+ this.columns = _columnData; //XXX
+ //this.columnMgr = new et2_dataview_columnsMgr(_columnData);
+
+ // Build the header row
+ this._buildHeader();
+ },
+
+ /**
+ * Resizes the grid
+ */
+ resize: function(_w, _h) {
+ if (this.width != _w)
+ {
+ this.width = _w;
+
+ // Rebuild the column stylesheets
+ this._updateColumns();
+ }
+
+ if (this.height != _h)
+ {
+ // Set the height of the grid.
+ // TODO
+ }
+ },
+
+
+ /* --- PRIVATE FUNCTIONS --- */
+
+ /* --- Code for building the grid container DOM-Tree elements ---- */
+
+ /**
+ * Builds the base DOM-Tree elements
+ */
+ _createElements: function() {
+ /*
+ Structure:
+
+
+
[HEAD]
+
+
+
[GRID CONTAINER]
+
+
+ */
+
+ this.containerTr = $j(document.createElement("tr"));
+ this.headTr = $j(document.createElement("tr"));
+
+ this.thead = $j(document.createElement("thead"))
+ .append(this.headTr);
+ this.tbody = $j(document.createElement("tbody"))
+ .append(this.containerTr);
+
+ this.table = $j(document.createElement("table"))
+ .addClass("egwGridView_outer")
+ .append(this.thead, this.tbody)
+ .appendTo(this.parentNode);
+ },
+
+
+ /* --- Code for building the header row --- */
+
+ /**
+ * Clears the header row
+ */
+ _clearHeader: function() {
+ // Destroy the column manager if it had been created
+ if (this.columnMgr)
+ {
+ this.columnMgr.free();
+ this.columnMgr = null;
+ }
+
+ // Reset the headerColumns array and empty the table row
+ this.columnNodes = [];
+ this.headTr.empty();
+ },
+
+ /**
+ * Builds the containers for the header row
+ */
+ _buildHeader: function() {
+ for (var i = 0; i < this.columns.length; i++)
+ {
+ var col = this.columns[i];
+
+ // Create the column header and the container element
+ var cont = $j(document.createElement("div"))
+ .addClass("innerContainer")
+ .addClass(col.divClass);
+ var column = $j(document.createElement("th"))
+ .addClass(col.tdClass)
+ .append(cont)
+ .appendTo(this.headTr);
+
+ // Store both nodes in the columnNodes array
+ this.columnNodes.push({
+ "column": column,
+ "container": cont
+ });
+ }
+
+ this._buildSelectCol();
+ },
+
+ /**
+ * Builds the select cols column
+ */
+ _buildSelectCol: function() {
+ // Build the "select columns" icon
+ this.selectColIcon = $j(document.createElement("span"))
+ .addClass("selectcols");
+
+ // Build the option column
+ this.selectCol = $j(document.createElement("th"))
+ .addClass("optcol")
+ .append(this.selectColIcon)
+ .appendTo(this.headTr);
+
+ this.selectCol.css("width", this.scrollbarWidth - this.selectCol.outerWidth()
+ + this.selectCol.width() + 1);
+ },
+
+
+ /* --- Code for calculating the browser/css depending widths --- */
+
+ /**
+ * Reads the browser dependant variables
+ */
+ _getDepVars: function() {
+ if (this.scrollbarWidth === false)
+ {
+ // Clone the table and attach it to the outer body tag
+ var clone = this.table.clone();
+ $j(window.top.document.getElementsByTagName("body")[0])
+ .append(clone);
+
+ // Read the scrollbar width
+ this.scrollbarWidth = this.constructor.prototype.scrollbarWidth =
+ this._getScrollbarWidth(clone);
+
+ // Read the header border width
+ this.headerBorderWidth = this.constructor.prototype.headerBorderWidth =
+ this._getHeaderBorderWidth(clone);
+
+ // Read the column border width
+ this.columnBorderWidth = this.constructor.prototype.columnBorderWidth =
+ this._getColumnBorderWidth(clone);
+
+ // Remove the cloned DOM-Node again from the outer body
+ clone.remove();
+ }
+ },
+
+ /**
+ * Reads the scrollbar width
+ */
+ _getScrollbarWidth: function(_table) {
+ // Create a temporary td and two divs, which are inserted into the
+ // DOM-Tree. The outer div has a fixed size and "overflow" set to auto.
+ // When the second div is inserted, it will be forced to display a scrollbar.
+ var div_inner = $j(document.createElement("div"))
+ .css("height", "1000px");
+ var div_outer = $j(document.createElement("div"))
+ .css("height", "100px")
+ .css("width", "100px")
+ .css("overflow", "auto")
+ .append(div_inner);
+ var td = $j(document.createElement("td"))
+ .append(div_outer);
+
+ // Store the scrollbar width statically.
+ $j("tbody tr", _table).append(td);
+ var width = Math.max(10, div_outer.outerWidth() - div_inner.outerWidth());
+
+ // Remove the elements again
+ div_outer.remove();
+
+ return width;
+ },
+
+ /**
+ * Calculates the total width of the header column border
+ */
+ _getHeaderBorderWidth: function(_table) {
+ // Create a temporary th which is appended to the outer thead row
+ var cont = $j(document.createElement("div"))
+ .addClass("innerContainer");
+
+ var th = $j(document.createElement("th"))
+ .append(cont);
+
+ // Insert the th into the document tree
+ $j("thead tr", _table).append(th);
+
+ // Calculate the total border width
+ var width = th.outerWidth(true) - cont.width();
+
+ // Remove the appended element again
+ th.remove();
+
+ return width;
+ },
+
+ /**
+ * Calculates the total width of the column border
+ */
+ _getColumnBorderWidth : function(_table) {
+ // Create a temporary th which is appended to the outer thead row
+ var cont = $j(document.createElement("div"))
+ .addClass("innerContainer");
+
+ var td = $j(document.createElement("td"))
+ .append(cont);
+
+ // Insert the th into the document tree
+ $j("tbody tr", _table).append(td);
+
+ // Calculate the total border width
+ _table.addClass("egwGridView_grid");
+ var width = td.outerWidth(true) - cont.width();
+
+ // Remove the appended element again
+ td.remove();
+
+ return width;
+ }
+
+});
+
+
+
+
+/*
+ * Sets the column data which is retrieved by calling egwGridColumns.getColumnData.
+ * The columns will be updated.
+ */
+/*egwGridViewOuter.prototype.updateColumns = function(_columns)
+{
+ // Copy the columns data
+ this.columns = _columns;
+
+ var first = true;
+
+ // Count the visible rows
+ var total_cnt = 0;
+ for (var i = 0; i < this.columns.length; i++)
+ {
+ if (this.columns[i].visible)
+ {
+ total_cnt++;
+ }
+ }
+
+ var vis_col = this.visibleColumnCount = 0;
+ var totalWidth = 0;
+
+ // Set the grid column styles
+ for (var i = 0; i < this.columns.length; i++)
+ {
+ var col = this.columns[i];
+
+ col.tdClass = this.uniqueId + "_td_" + col.id;
+ col.divClass = this.uniqueId + "_div_" + col.id;
+
+ if (col.visible)
+ {
+ vis_col++;
+ this.visibleColumnCount++;
+
+ this.styleSheet.updateRule("." + col.tdClass,
+ "display: " + (col.visible ? "table-cell" : "none") + "; " +
+ ((vis_col == total_cnt) ? "border-right-width: 0 " : "border-right-width: 1px ") +
+ "!important;");
+
+ this.styleSheet.updateRule(".egwGridView_outer ." + col.divClass,
+ "width: " + (col.width - this.headerBorderWidth) + "px;");
+
+ // Ugly browser dependant code - each browser seems to treat the
+ // right (collapsed) border of the row differently
+ addBorder = 0;
+ if ($j.browser.mozilla)
+ {
+ var maj = $j.browser.version.split(".")[0];
+ if (maj < 2) {
+ addBorder = 1; // Versions <= FF 3.6
+ }
+ }
+ if ($j.browser.webkit && !first)
+ {
+ addBorder = 1;
+ }
+ if (($j.browser.msie || $j.browser.opera) && first)
+ {
+ addBorder = -1;
+ }
+
+ // Make the last columns one pixel smaller, to prevent a horizontal
+ // scrollbar from showing up
+ if (vis_col == total_cnt)
+ {
+ addBorder += 1;
+ }
+
+ var width = (col.width - this.columnBorderWidth - addBorder);
+
+ this.styleSheet.updateRule(".egwGridView_grid ." + col.divClass,
+ "width: " + width + "px;");
+
+ totalWidth += col.width;
+
+ first = false;
+ }
+ else
+ {
+ this.styleSheet.updateRule("." + col.tdClass,
+ "display: " + (col.visible ? "table-cell" : "none") + ";");
+ }
+ }
+
+ // Add the full row and spacer class
+ this.styleSheet.updateRule(".egwGridView_grid ." + this.uniqueId + "_div_fullRow",
+ "width: " + (totalWidth - this.columnBorderWidth - 1) + "px; border-right-width: 0 !important;");
+ this.styleSheet.updateRule(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow",
+ "width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;");
+
+ // Build the header if this hasn't been done yet
+ this.buildBaseHeader();
+
+ // Update the grid
+ this.grid.updateColumns(this.columns);
+}
+
+
+
+egwGridViewOuter.prototype.setHeight = function(_h)
+{
+ this.grid.setScrollHeight(_h - this.outer_thead.outerHeight());
+}
+
+});*/
diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js
new file mode 100644
index 0000000000..047a8a31b0
--- /dev/null
+++ b/etemplate/js/et2_extension_nextmatch.js
@@ -0,0 +1,351 @@
+/**
+ * eGroupWare eTemplate2 - JS Nextmatch 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;
+ et2_core_interfaces;
+ et2_core_DOMWidget;
+ et2_widget_template;
+ et2_widget_grid;
+ et2_widget_selectbox;
+*/
+
+/**
+ * Interface all special nextmatch header elements have to implement.
+ */
+var et2_INextmatchHeader = new Interface({
+
+ /**
+ * The 'setNextmatch' function is called by the parent nextmatch widget
+ * and tells the nextmatch header widgets which widget they should direct
+ * their 'sort', 'search' or 'filter' calls to.
+ */
+ setNextmatch: function(_nextmatch) {}
+});
+
+var et2_INextmatchSortable = new Interface({
+
+ setSortmode: function(_mode) {}
+
+});
+
+/**
+ * Class which implements the "nextmatch" XET-Tag
+ */
+var et2_nextmatch = et2_DOMWidget.extend({
+
+ attributes: {
+ "template": {
+ "name": "Template",
+ "type": "string",
+ "description": "The id of the template which contains the grid layout."
+ }
+ },
+
+ legacyOptions: ["template"],
+
+ init: function() {
+ this._super.apply(this, arguments);
+
+ this.div = $j(document.createElement("div"))
+ .addClass("et2_nextmatch");
+
+ // Create the outer grid container
+ this.dataviewContainer = new et2_dataview_gridContainer(this.div);
+
+ this.columns = [];
+ this.activeFilters = {};
+ },
+
+ destroy: function() {
+ // Destroy the dataview objects
+ this.dataviewContainer.free();
+
+ this._super.apply(this, arguments);
+ },
+
+ /**
+ * Sorts the nextmatch widget by the given ID.
+ *
+ * @param _id is the id of the data entry which should be sorted.
+ * @param _asc if true, the elements are sorted ascending, otherwise
+ * descending. If not set, the sort direction will be determined
+ * automatically.
+ */
+ sortBy: function(_id, _asc) {
+ // Create the "sort" entry in the active filters if it did not exist
+ // yet.
+ if (typeof this.activeFilters["sort"] == "undefined")
+ {
+ this.activeFilters["sort"] = {
+ "id": null,
+ "asc": true
+ };
+ }
+
+ // Determine the sort direction automatically if it is not set
+ if (typeof _asc == "undefined")
+ {
+ if (this.activeFilters["sort"].id == _id)
+ {
+ _asc = !this.activeFilters["sort"].asc;
+ }
+ }
+
+ // Update the entry in the activeFilters object
+ this.activeFilters["sort"] = {
+ "id": _id,
+ "asc": _asc
+ }
+
+ // Set the sortmode display
+ this.iterateOver(function(_widget) {
+ _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc": "desc") : "none");
+ }, this, et2_INextmatchSortable);
+
+ et2_debug("info", "Sorting nextmatch by '" + _id + "' in direction '" +
+ (_asc ? "asc" : "desc") + "'");
+ },
+
+ resetSort: function() {
+ // Check whether the nextmatch widget is currently sorted
+ if (typeof this.activeFilters["sort"] != "undefined")
+ {
+ // Reset the sortmode
+ this.iterateOver(function(_widget) {
+ _widget.setSortmode("none");
+ }, this, et2_INextmatchSortable);
+
+ // If yes, delete the "sort" filter
+ delete(this.activeFilters["sort"]);
+ this.applyFilters();
+ }
+ },
+
+ applyFilters: function() {
+ et2_debug("info", "Changing nextmatch filters to ", this.activeFilters);
+ },
+
+ /**
+ * Generates the column name for the given column widget
+ */
+ _genColumnName: function(_widget) {
+ var result = null;
+
+ _widget.iterateOver(function(_widget) {
+ if (!result)
+ {
+ result = _widget.options.label;
+ }
+ else
+ {
+ result += ", " + _widget.options.label;
+ }
+ }, this, et2_INextmatchHeader);
+
+ return result;
+ },
+
+ _parseHeaderRow: function(_row) {
+ // Go over the header row and create the column entries
+ this.columns = new Array(_row.length);
+ for (var x = 0; x < _row.length; x++)
+ {
+ this.columns[x] = {
+ "widget": _row[x].widget,
+ "name": this._genColumnName(_row[x].widget),
+ "disabled": false,
+ "canDisable": true,
+ "width": "auto"
+ }
+
+ // Append the widget to this container
+ this.addChild(_row[x].widget);
+ }
+
+ this.dataviewContainer.setColumns(this.columns);
+ },
+
+ _parseGrid: function(_grid) {
+ // Search the rows for a header-row - if one is found, parse it
+ for (var y = 0; y < _grid.rowData.length; y++)
+ {
+ if (_grid.rowData[y]["class"] == "th")
+ {
+ this._parseHeaderRow(_grid.cells[y]);
+ }
+ }
+ },
+
+ /**
+ * When the template attribute is set, the nextmatch widget tries to load
+ * that template and to fetch the grid which is inside of it. It then calls
+ * _parseGrid in order to get the information for the column headers etc.
+ */
+ set_template: function(_value) {
+ if (!this.template)
+ {
+ // Load the template
+ var template = et2_createWidget("template", {"id": _value}, this);
+
+ if (!template.proxiedTemplate)
+ {
+ et2_debug("error", "Error while loading definition template for" +
+ "nextmatch widget.");
+ return;
+ }
+
+ // Fetch the grid element and parse it
+ var definitionGrid = template.proxiedTemplate.getChildren()[0];
+ if (definitionGrid && definitionGrid instanceof et2_grid)
+ {
+ this._parseGrid(definitionGrid);
+ }
+ else
+ {
+ et2_debug("error", "Nextmatch widget expects a grid to be the " +
+ "first child of the defined template.");
+ return;
+ }
+
+ // Free the template again
+ template.free();
+
+ // Call the "setNextmatch" function of all registered
+ // INextmatchHeader widgets.
+ this.iterateOver(function (_node) {
+ _node.setNextmatch(this);
+ }, this, et2_INextmatchHeader);
+ }
+ },
+
+ getDOMNode: function(_sender) {
+ if (_sender == this)
+ {
+ return this.div[0];
+ }
+
+ for (var i = 0; i < this.columns.length; i++)
+ {
+ if (_sender == this.columns[i].widget)
+ {
+ return this.dataviewContainer.getHeaderContainerNode(i);
+ }
+ }
+
+ return null;
+ }
+
+});
+
+et2_register_widget(et2_nextmatch, ["nextmatch"]);
+
+
+/**
+ * Classes for the nextmatch sortheaders etc.
+ */
+var et2_nextmatch_header = et2_baseWidget.extend(et2_INextmatchHeader, {
+
+ attributes: {
+ "label": {
+ "name": "Caption",
+ "type": "string",
+ "description": "Caption for the nextmatch header",
+ "translate": true
+ }
+ },
+
+ init: function() {
+ this._super.apply(this, arguments);
+
+ this.labelNode = $j(document.createElement("span"));
+ this.nextmatch = null;
+
+ this.setDOMNode(this.labelNode[0]);
+ },
+
+ destroy: function() {
+ this._super.apply(this, arguments);
+ },
+
+ /**
+ * Set nextmatch is the function which has to be implemented for the
+ * et2_INextmatchHeader interface.
+ */
+ setNextmatch: function(_nextmatch) {
+ this.nextmatch = _nextmatch;
+ },
+
+ set_label: function(_value) {
+ this.label = _value;
+
+ this.labelNode.text(_value);
+ }
+
+});
+
+et2_register_widget(et2_nextmatch_header, ['nextmatch-header',
+ 'nextmatch-accountfilter', 'nextmatch-customfilter', 'nextmatch-customfields']);
+
+var et2_nextmatch_sortheader = et2_nextmatch_header.extend(et2_INextmatchSortable, {
+
+ init: function() {
+ this._super.apply(this, arguments);
+
+ this.sortmode = "none";
+
+ this.labelNode.addClass("nextmatch_sortheader none");
+ },
+
+ click: function() {
+ if (this.nextmatch && this._super.apply(this, arguments))
+ {
+ this.nextmatch.sortBy(this.id);
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Function which implements the et2_INextmatchSortable function.
+ */
+ setSortmode: function(_mode) {
+ // Remove the last sortmode class and add the new one
+ this.labelNode.removeClass(this.sortmode)
+ .addClass(_mode);
+
+ this.sortmode = _mode;
+ }
+
+});
+
+et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']);
+
+
+var et2_nextmatch_filterheader = et2_selectbox.extend(et2_INextmatchHeader, {
+
+ /**
+ * Set nextmatch is the function which has to be implemented for the
+ * et2_INextmatchHeader interface.
+ */
+ setNextmatch: function(_nextmatch) {
+ this.nextmatch = _nextmatch;
+ }
+
+});
+
+et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']);
+
diff --git a/etemplate/js/et2_widget_button.js b/etemplate/js/et2_widget_button.js
index 699c757376..aea1095df9 100644
--- a/etemplate/js/et2_widget_button.js
+++ b/etemplate/js/et2_widget_button.js
@@ -14,7 +14,7 @@
/*egw:uses
jquery.jquery;
- et2_core_inputWidget;
+ et2_core_interfaces;
et2_core_baseWidget;
*/
diff --git a/etemplate/js/et2_widget_grid.js b/etemplate/js/et2_widget_grid.js
index 3c3a6712c4..a51c88f69f 100644
--- a/etemplate/js/et2_widget_grid.js
+++ b/etemplate/js/et2_widget_grid.js
@@ -39,17 +39,12 @@ var et2_grid = et2_DOMWidget.extend({
// 2D-Array which holds references to the DOM td tags
this.cells = [];
+ this.rowData = [];
this.managementArray = [];
},
destroy: function() {
this._super.call(this, arguments);
-
- // Delete all references to cells or widgets
- this.cells = null;
- this.managementArray = null;
- this.table = null;
- this.tbody = null;
},
_initCells: function(_colData, _rowData) {
@@ -72,6 +67,7 @@ var et2_grid = et2_DOMWidget.extend({
"colData": _colData[x],
"rowData": _rowData[y],
"disabled": _colData[x].disabled || _rowData[y].disabled,
+ "class": _colData[x]["class"],
"colSpan": 1,
"autoColSpan": false,
"rowSpan": 1,
@@ -369,7 +365,7 @@ var et2_grid = et2_DOMWidget.extend({
this._expandLastCells(cells);
// Create the table rows
- this.createTableFromCells(cells);
+ this.createTableFromCells(cells, rowData);
}
else
{
@@ -377,20 +373,26 @@ var et2_grid = et2_DOMWidget.extend({
}
},
- createTableFromCells: function(_cells) {
+ createTableFromCells: function(_cells, _rowData) {
// Set the rowCount and columnCount variables
var h = this.rowCount = _cells.length;
var w = this.columnCount = (h > 0) ? _cells[0].length : 0;
this.managementArray = [];
this.cells = _cells;
+ this.rowData = _rowData;
// Create the table rows.
for (var y = 0; y < h; y++)
{
var row = _cells[y];
- var tr = $j(document.createElement("tr")).appendTo(this.tbody);
- var row_hidden = true;
+ var tr = $j(document.createElement("tr")).appendTo(this.tbody)
+ .addClass(this.rowData[y]["class"]);
+
+ if (this.rowData[y].disabled)
+ {
+ tr.hide();
+ }
// Create the cells. x is incremented by the colSpan value of the
// cell.
@@ -402,16 +404,12 @@ var et2_grid = et2_DOMWidget.extend({
if (cell.td == null && cell.widget != null)
{
// Create the cell
- var td = $j(document.createElement("td")).appendTo(tr);
+ var td = $j(document.createElement("td")).appendTo(tr)
+ .addClass(cell["class"]);
if (cell.disabled)
{
td.hide();
- //td.css("border", "2px solid red");
- }
- else
- {
- row_hidden = false;
}
// Add the entry for the widget to the management array
@@ -450,11 +448,6 @@ var et2_grid = et2_DOMWidget.extend({
x++;
}
}
-
- if (row_hidden)
- {
- tr.hide();
- }
}
},
@@ -468,6 +461,16 @@ var et2_grid = et2_DOMWidget.extend({
// Remember all widgets which have already been instanciated
var instances = [];
+ // Copy the some data from the rowData array
+ var rowData = new Array(_obj.rowData.length);
+ for (var y = 0; y < _obj.rowData.length; y++)
+ {
+ rowData[y] = {
+ "disabled": _obj.rowData[y].disabled,
+ "class": _obj.rowData[y]["class"]
+ }
+ }
+
// Copy the cells array of the other grid and clone the widgets
// inside of it
var cells = new Array(_obj.cells.length);
@@ -508,13 +511,14 @@ var et2_grid = et2_DOMWidget.extend({
"td": null,
"colSpan": srcCell.colSpan,
"rowSpan": srcCell.rowSpan,
- "disabled": srcCell.disabled
+ "disabled": srcCell.disabled,
+ "class": srcCell["class"]
}
}
}
// Create the table
- this.createTableFromCells(cells);
+ this.createTableFromCells(cells, rowData);
// Copy a reference to the content array manager
if (_obj._mgr)
diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js
index 344ade39dc..5dd51c540d 100644
--- a/etemplate/js/etemplate2.js
+++ b/etemplate/js/etemplate2.js
@@ -29,6 +29,8 @@
et2_widget_tabs;
et2_widget_hrule;
+ et2_extension_nextmatch;
+
// Requirements for the etemplate2 object
et2_core_xml;
et2_core_arrayMgr;
@@ -67,7 +69,7 @@ etemplate2.prototype.clear = function()
if (this.widgetContainer != null)
{
// $j(':input',this.DOMContainer).validator().data("validator").destroy();
- this.widgetContainer.destroy();
+ this.widgetContainer.free();
this.widgetContainer = null;
}
}
diff --git a/etemplate/js/test/et2_test_nextmatch.xet b/etemplate/js/test/et2_test_nextmatch.xet
new file mode 100644
index 0000000000..26dd087b91
--- /dev/null
+++ b/etemplate/js/test/et2_test_nextmatch.xet
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/etemplate/js/test/gfx/ajax-loader.gif b/etemplate/js/test/gfx/ajax-loader.gif
new file mode 100644
index 0000000000..9ef515f1e3
Binary files /dev/null and b/etemplate/js/test/gfx/ajax-loader.gif differ
diff --git a/etemplate/js/test/gfx/arrows.png b/etemplate/js/test/gfx/arrows.png
new file mode 100644
index 0000000000..e8c4d9fdbd
Binary files /dev/null and b/etemplate/js/test/gfx/arrows.png differ
diff --git a/etemplate/js/test/gfx/down.png b/etemplate/js/test/gfx/down.png
new file mode 100755
index 0000000000..bad2281a4e
Binary files /dev/null and b/etemplate/js/test/gfx/down.png differ
diff --git a/etemplate/js/test/gfx/focused_hatching.png b/etemplate/js/test/gfx/focused_hatching.png
new file mode 100644
index 0000000000..3c352a4a44
Binary files /dev/null and b/etemplate/js/test/gfx/focused_hatching.png differ
diff --git a/etemplate/js/test/gfx/header_overlay.png b/etemplate/js/test/gfx/header_overlay.png
new file mode 100644
index 0000000000..9ce3828a50
Binary files /dev/null and b/etemplate/js/test/gfx/header_overlay.png differ
diff --git a/etemplate/js/test/gfx/non_loaded_bg.png b/etemplate/js/test/gfx/non_loaded_bg.png
new file mode 100644
index 0000000000..bb87fe5a5f
Binary files /dev/null and b/etemplate/js/test/gfx/non_loaded_bg.png differ
diff --git a/etemplate/js/test/gfx/selectcols.png b/etemplate/js/test/gfx/selectcols.png
new file mode 100644
index 0000000000..57bd26f88e
Binary files /dev/null and b/etemplate/js/test/gfx/selectcols.png differ
diff --git a/etemplate/js/test/gfx/up.png b/etemplate/js/test/gfx/up.png
new file mode 100755
index 0000000000..20da24d9bb
Binary files /dev/null and b/etemplate/js/test/gfx/up.png differ
diff --git a/etemplate/js/test/grid.css b/etemplate/js/test/grid.css
new file mode 100644
index 0000000000..829f59ad72
--- /dev/null
+++ b/etemplate/js/test/grid.css
@@ -0,0 +1,220 @@
+
+.egwGridView_grid {
+ table-layout: fixed;
+ border-spacing: 0;
+ border-collapse: collapse;
+}
+
+.egwGridView_outer div.innerContainer.queued {
+ background-image: url(gfx/ajax-loader.gif);
+ background-position: center;
+ background-repeat: no-repeat;
+ height: 19px;
+}
+
+.egwGridView_grid tr.focused td {
+ background-image: url(gfx/focused_hatching.png);
+ background-repeat: repeat;
+}
+
+.egwGridView_grid tr.selected td {
+ background-color: #b7c3ff;
+}
+
+.egwGridView_grid tr.draggedOver td {
+ background-color: #ffd09c !important;
+}
+
+/*.egwGridView_grid tr.selected.odd td {
+ background-color: #9dadff;
+}*/
+
+.egwGridView_scrollarea {
+ width: 100%;
+ overflow: auto;
+}
+
+.egwGridView_spacer {
+ background-image: url(gfx/non_loaded_bg.png);
+ background-position: top left;
+}
+
+.egwGridView_outer {
+ table-layout: fixed;
+ border-spacing: 0;
+ border-collapse: collapse;
+ padding: 0;
+}
+
+.egwGridView_outer td, .egwGridView_outer tr {
+ padding: 0;
+ margin: 0;
+}
+
+.egwGridView_grid td {
+ border-right: 1px solid silver;
+ border-bottom: 1px solid #e0e0e0;
+ padding: 2px 3px 2px 4px;
+ margin: 0;
+}
+
+.egwGridView_outer th div.innerContainer,
+.egwGridView_grid td div.innerContainer {
+ margin: 0;
+ padding: 0;
+ display: block;
+ overflow: hidden;
+}
+
+.egwGridView_grid tr.fullRow {
+ font-style: italic;
+}
+
+.egwGridView_grid tr.row:hover {
+ background-color: #f0f0ff;
+}
+
+.egwGridView_grid tr {
+ padding: 2px 3px 2px 4px;
+ margin: 0;
+}
+
+.egwGridView_grid tr.hidden {
+ display: none;
+}
+
+/*.egwGridView_grid tr.odd {
+ background-color: #F1F1F1;
+}*/
+
+.egwGridView_grid span.indentation {
+ display: inline-block;
+}
+
+.egwGridView_grid span {
+ vertical-align: middle;
+}
+
+.egwGridView_grid img.icon {
+ vertical-align: middle;
+ margin: 2px 5px 2px 2px;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ user-select: none;
+}
+
+.egwGridView_grid span.arrow {
+ display: inline-block;
+ vertical-align: middle;
+ width: 8px;
+ height: 8px;
+ background-repeat: no-repeat;
+ margin-right: 2px;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ user-select: none;
+}
+
+.egwGridView_grid span.arrow.opened {
+ cursor: pointer;
+ background-image: url(gfx/arrows.png);
+ background-position: -8px 0;
+}
+
+.egwGridView_grid span.arrow.closed {
+ cursor: pointer;
+ background-image: url(gfx/arrows.png);
+ background-position: 0 0;
+}
+
+.egwGridView_grid span.arrow.loading {
+ cursor: pointer;
+ background-image: url(gfx/ajax-loader.gif);
+ background-position: 0 0;
+}
+
+.egwGridView_grid span.iconContainer {
+ display: inline-block;
+ padding: 0;
+ margin: 0;
+ text-align: center;
+}
+
+.egwGridView_grid span.caption {
+ cursor: default;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ user-select: none;
+}
+
+.egwGridView_outer thead th {
+ background-color: #E0E0E0;
+ font-weight: normal;
+ padding: 5px;
+ text-align: left;
+ border-left: 1px solid silver;
+ border-top: 1px solid silver;
+ border-right: 1px solid gray;
+ border-bottom: 1px solid gray;
+ background-image: url(gfx/header_overlay.png);
+ background-position: center;
+ background-repeat: repeat-x;
+ -webkit-user-select: none;
+ cursor: default;
+}
+
+.egwGridView_outer thead th:hover {
+ background-color: #F0F0F0;
+}
+
+.egwGridView_outer thead th.optcol {
+ padding: 0;
+ text-align: center;
+}
+
+.egwGridView_outer thead th.optcol:active {
+ background-color: #D0D0D0;
+ border-left: 1px solid gray;
+ border-top: 1px solid gray;
+ border-right: 1px solid silver;
+ border-bottom: 1px solid silver;
+}
+
+.selectcols {
+ display: inline-block;
+ width: 10px;
+ height: 9px;
+ margin: 0;
+ padding: 0;
+ vertical-align: middle;
+ background-image: url(gfx/selectcols.png);
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.egwGridView_grid td.frame,
+.egwGridView_outer td.frame,
+.egwGridView_grid td.egwGridView_spacer {
+ padding: 0 !important;
+ border-right: 0 none silver !important;
+ border-bottom: 0 none silver !important;
+}
+
+.egwGridView_outer span.sort {
+ display: inline-block;
+ width: 7px;
+ height: 7px;
+ background-repeat: no-repeat;
+ background-position: center;
+ margin: 2px;
+ vertical-align: middle;
+}
+
+.egwGridView_outer span.sort.asc {
+ background-image: url(gfx/up.png);
+}
+
+.egwGridView_outer span.sort.desc {
+ background-image: url(gfx/down.png);
+}
+
diff --git a/etemplate/js/test/test.css b/etemplate/js/test/test.css
index 6c5daf3aa9..99df9f5087 100644
--- a/etemplate/js/test/test.css
+++ b/etemplate/js/test/test.css
@@ -316,4 +316,35 @@ label input, label span, label div, label select, label textarea {
background-image:url(gfx/hint.png);
}
+/**
+ * Nextmatch widget
+ */
+
+.et2_nextmatch {
+ background-color: silver;
+}
+
+.nextmatch_sortheader {
+ color: #003075;
+ cursor: pointer;
+ padding-right: 18px;
+ margin-right: 10px;
+ background-repeat: no-repeat;
+ background-position: right center;
+}
+
+.nextmatch_sortheader:hover {
+ text-decoration: underline;
+}
+
+.nextmatch_sortheader.asc {
+ font-weight: bold;
+ background-image: url(gfx/up.png);
+}
+
+.nextmatch_sortheader.desc {
+ font-weight: bold;
+ background-image: url(gfx/down.png);
+}
+
diff --git a/etemplate/js/test/test_xml.html b/etemplate/js/test/test_xml.html
index f148a78435..6565b07082 100644
--- a/etemplate/js/test/test_xml.html
+++ b/etemplate/js/test/test_xml.html
@@ -5,6 +5,8 @@
+
+
@@ -29,6 +31,9 @@
+
+
+
@@ -42,6 +47,7 @@
+