/** * eGroupWare eTemplate2 - JS Grid 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_core_DOMWidget; et2_core_xml; */ /** * Class which implements the "grid" XET-Tag * * This also includes repeating the last row in the grid and filling * it with content data */ var et2_grid = et2_DOMWidget.extend([et2_IDetachedDOM], { attributes: { // Better to use CSS, no need to warn about it "border": { "ignore": true }, "spacing": { "ignore": true }, "padding": { "ignore": true } }, init: function() { // Create the table body and the table this.tbody = $j(document.createElement("tbody")); this.table = $j(document.createElement("table")) .addClass("et2_grid"); this.table.append(this.tbody); // Call the parent constructor this._super.apply(this, arguments); // Counters for rows and columns this.rowCount = 0; this.columnCount = 0; // 2D-Array which holds references to the DOM td tags this.cells = []; this.rowData = []; this.colData = []; this.managementArray = []; }, destroy: function() { this._super.call(this, arguments); }, _initCells: function(_colData, _rowData) { // Copy the width and height var w = _colData.length; var h = _rowData.length; // Create the 2D-Cells array var cells = new Array(h); for (var y = 0; y < h; y++) { cells[y] = new Array(w); // Initialize the cell description objects for (var x = 0; x < w; x++) { cells[y][x] = { "td": null, "widget": null, "colData": _colData[x], "rowData": _rowData[y], "disabled": _colData[x].disabled || _rowData[y].disabled, "class": _colData[x]["class"], "colSpan": 1, "autoColSpan": false, "rowSpan": 1, "autoRowSpan": false, "width": _colData[x].width, "x": x, "y": y }; } } return cells; }, _getColDataEntry: function() { return { "width": "auto", "class": "", "align": "", "span": "1", "disabled": false }; }, _getRowDataEntry: function() { return { "height": "auto", "class": "", "valign": "top", "span": "1", "disabled": false }; }, _getCell: function(_cells, _x, _y) { if ((0 <= _y) && (_y < _cells.length)) { var row = _cells[_y]; if ((0 <= _x) && (_x < row.length)) { return row[_x]; } } throw("Error while accessing grid cells, invalid element count or span value!"); }, _forceNumber: function(_val) { if (isNaN(_val)) { throw(_val + " is not a number!"); } return parseInt(_val); }, _fetchRowColData: function(columns, rows, colData, rowData) { // Parse the columns tag et2_filteredNodeIterator(columns, function(node, nodeName) { var colDataEntry = this._getColDataEntry(); colDataEntry["disabled"] = this.getArrayMgr("content") .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); if (nodeName == "column") { colDataEntry["width"] = et2_readAttrWithDefault(node, "width", "auto"); colDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); colDataEntry["align"] = et2_readAttrWithDefault(node, "align", ""); colDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); } else { colDataEntry["span"] = "all"; } colData.push(colDataEntry); }, this); // Parse the rows tag et2_filteredNodeIterator(rows, function(node, nodeName) { var rowDataEntry = this._getRowDataEntry(); rowDataEntry["disabled"] = this.getArrayMgr("content") .parseBoolExpression(et2_readAttrWithDefault(node, "disabled", "")); if (nodeName == "row") { // Remember this row for auto-repeat - it'll eventually be the last one this.lastRowNode = node; rowDataEntry["height"] = et2_readAttrWithDefault(node, "height", "auto"); rowDataEntry["class"] = et2_readAttrWithDefault(node, "class", ""); rowDataEntry["valign"] = et2_readAttrWithDefault(node, "valign", ""); rowDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1"); } else { rowDataEntry["span"] = "all"; } rowData.push(rowDataEntry); }, this); // Add in repeated rows // TODO: It would be nice if we could skip header (thead) & footer (tfoot) or treat them separately var rowIndex = 0; if(this.getArrayMgr("content")) { var content = this.getArrayMgr("content"); var rowDataEntry = rowData[rowData.length-1]; // Find out if we have any content rows, and how many while(true) { if(content.data[rowIndex]) { rowData[rowIndex] = rowDataEntry; rowIndex++; } else if (rowIndex == 0) { // Sometimes the header takes a row, so try at 1 rowIndex++; } else { // No more rows, stop break; } } } if(rowIndex <= 1) { // No auto-repeat this.lastRowNode = null; } }, _fillCells: function(cells, columns, rows) { var h = cells.length; var w = (h > 0) ? cells[0].length : 0; var currentPerspective = this.getArrayMgr("content").perspectiveData; // Read the elements inside the columns var x = 0; et2_filteredNodeIterator(columns, function(node, nodeName) { function _readColNode(node, nodeName) { if (y >= h) { this.egw().debug("warn", "Skipped grid cell in column, '" + nodeName + "'"); return; } var cell = this._getCell(cells, x, y); // Read the span value of the element if (node.getAttribute("span")) { cell.rowSpan = node.getAttribute("span"); } else { cell.rowSpan = cell.colData["span"]; cell.autoRowSpan = true; } if (cell.rowSpan == "all") { cell.rowSpan = cells.length; } var span = cell.rowSpan = this._forceNumber(cell.rowSpan); // Create the widget var widget = this.createElementFromNode(node, nodeName); // Fill all cells the widget is spanning for (var i = 0; i < span && y < cells.length; i++, y++) { this._getCell(cells, x, y).widget = widget; } }; // If the node is a column, create the widgets which belong into // the column var y = 0; if (nodeName == "column") { et2_filteredNodeIterator(node, _readColNode, this); } else { _readColNode.call(this, node, nodeName); } x++; }, this); // Read the elements inside the rows var y = 0; var x = 0; var readRowNode; et2_filteredNodeIterator(rows, function(node, nodeName) { readRowNode = function _readRowNode(node, nodeName) { if (x >= w) { this.egw().debug("warn", "Skipped grid cell in row, '" + nodeName + "'"); return; } var cell = this._getCell(cells, x, y); // Read the span value of the element if (node.getAttribute("span")) { cell.colSpan = node.getAttribute("span"); } else { cell.colSpan = cell.rowData["span"]; cell.autoColSpan = true; } if (cell.colSpan == "all") { cell.colSpan = cells[y].length; } var span = cell.colSpan = this._forceNumber(cell.colSpan); // Read the align value of the element if (node.getAttribute("align")) { cell.align = node.getAttribute("align"); } // Apply widget's class to td, for backward compatability if(node.getAttribute("class")) { cell.class += (cell.class ? " " : "") + node.getAttribute("class"); } // Create the element var widget = this.createElementFromNode(node, nodeName); // Fill all cells the widget is spanning for (var i = 0; i < span && x < cells[y].length; i++, x++) { cell = this._getCell(cells, x, y); if (cell.widget == null) { cell.widget = widget; } else { throw("Grid cell collision, two elements " + "defined for cell (" + x + "," + y + ")!"); } } } // If the node is a row, create the widgets which belong into // the row x = 0; if(this.lastRowNode && node == this.lastRowNode) { return; } if (nodeName == "row") { // Adjust for the row for(var name in this.getArrayMgrs()) { //this.getArrayMgr(name).perspectiveData.row = y; } // If row disabled, just skip it var disabled = false; if(node.getAttribute("disabled") == "1") { disabled = true; } if(!disabled) { et2_filteredNodeIterator(node, readRowNode, this); } } else { readRowNode.call(this, node, nodeName); } y++; }, this); // Extra content rows for(y; y < h; y++) { var x = 0; // Adjust for the row var mgrs = this.getArrayMgrs(); for(var name in mgrs) { if(this.getArrayMgr(name).getEntry(y)) { this.getArrayMgr(name).perspectiveData.row = y; } } et2_filteredNodeIterator(this.lastRowNode, readRowNode, this); } // Reset for(var name in this.getArrayMgrs()) { this.getArrayMgr(name).perspectiveData = currentPerspective; } }, _expandLastCells: function(_cells) { var h = _cells.length; var w = (h > 0) ? _cells[0].length : 0; // Determine the last cell in each row and expand its span value if // the span has not been explicitly set. for (var y = 0; y < h; y++) { for (var x = w - 1; x >= 0; x--) { var cell = _cells[y][x]; if (cell.widget != null) { if (cell.autoColSpan) { cell.colSpan = w - x; } break; } } } // Determine the last cell in each column and expand its span value if // the span has not been explicitly set. for (var x = 0; x < w; x++) { for (var y = h - 1; y >= 0; y--) { var cell = _cells[y][x]; if (cell.widget != null) { if (cell.autoRowSpan) { cell.rowSpan = h - y; } break; } } } }, /** * As the does not fit very well into the default widget structure, we're * overwriting the loadFromXML function and doing a two-pass reading - * in the first step the */ loadFromXML: function(_node) { // Get the columns and rows tag var rowsElems = et2_directChildrenByTagName(_node, "rows"); var columnsElems = et2_directChildrenByTagName(_node, "columns"); if (rowsElems.length == 1 && columnsElems.length == 1) { var columns = columnsElems[0]; var rows = rowsElems[0]; var colData = []; var rowData = []; // Fetch the column and row data this._fetchRowColData(columns, rows, colData, rowData); // Initialize the cells var cells = this._initCells(colData, rowData); // Create the widgets inside the cells and read the span values this._fillCells(cells, columns, rows); // Expand the span values of the last cells this._expandLastCells(cells); // Create the table rows this.createTableFromCells(cells, colData, rowData); } else { throw("Error while parsing grid, none or multiple rows or columns tags!"); } }, createTableFromCells: function(_cells, _colData, _rowData) { this.managementArray = []; this.cells = _cells; this.colData = _colData; this.rowData = _rowData; // Set the rowCount and columnCount variables var h = this.rowCount = _cells.length; var w = this.columnCount = (h > 0) ? _cells[0].length : 0; // Create the table rows. for (var y = 0; y < h; y++) { var row = _cells[y]; var tr = $j(document.createElement("tr")).appendTo(this.tbody) .addClass(this.rowData[y]["class"]); if (this.rowData[y].disabled) { tr.hide(); } if (this.rowData[y].height != "auto") { tr.height(this.rowData[y].height); } if (this.rowData[y].valign) { tr.attr("valign", this.rowData[y].valign); } // Create the cells. x is incremented by the colSpan value of the // cell. for (var x = 0; x < w;) { // Fetch a cell from the cells var cell = this._getCell(_cells, x, y); if (cell.td == null && cell.widget != null) { // Create the cell var td = $j(document.createElement("td")).appendTo(tr) .addClass(cell["class"]); if (cell.disabled) { td.hide(); } if (cell.width != "auto") { td.width(cell.width); } if (cell.align) { td.attr("align",cell.align); } // Add the entry for the widget to the management array this.managementArray.push({ "cell": td[0], "widget": cell.widget, "disabled": cell.disabled }); // Set the span values of the cell var cs = (x == w - 1) ? w - x : Math.min(w - x, cell.colSpan); var rs = (y == h - 1) ? h - y : Math.min(h - y, cell.rowSpan); // Set the col and row span values if (cs > 1) { td.attr("colspan", cs); } if (rs > 1) { td.attr("rowspan", rs); } // Assign the td to the cell for (var sx = x; sx < x + cs; sx++) { for (var sy = y; sy < y + rs; sy++) { this._getCell(_cells, sx, sy).td = td; } } x += cell.colSpan; } else { x++; } } } }, getDOMNode: function(_sender) { // If the parent class functions are asking for the DOM-Node, return the // outer table. if (_sender == this) { return this.table[0]; } // Check whether the _sender object exists inside the management array for (var i = 0; i < this.managementArray.length; i++) { if (this.managementArray[i].widget == _sender) { return this.managementArray[i].cell; } } return null; }, isInTree: function(_sender) { var vis = true; if (typeof _sender != "undefined" && _sender != this) { vis = false; // Check whether the _sender object exists inside the management array for (var i = 0; i < this.managementArray.length; i++) { if (this.managementArray[i].widget == _sender) { vis = !(this.managementArray[i].disabled); break; } } } return this._super(this, vis); }, /** * Code for implementing et2_IDetachedDOM * This doesn't need to be implemented. * Individual widgets are detected and handled by the grid, but the interface is needed for this to happen */ getDetachedAttributes: function(_attrs) { }, getDetachedNodes: function() { return [this.getDOMNode()]; }, setDetachedAttributes: function(_nodes, _values) { } }); et2_register_widget(et2_grid, ["grid"]);