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 @@ +