mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-27 10:23:28 +01:00
775 lines
18 KiB
JavaScript
775 lines
18 KiB
JavaScript
/**
|
|
* 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
|
|
*
|
|
* @augments et2_DOMWidget
|
|
*/
|
|
var et2_grid = et2_DOMWidget.extend([et2_IDetachedDOM, et2_IAligned],
|
|
{
|
|
createNamespace: true,
|
|
|
|
attributes: {
|
|
// Better to use CSS, no need to warn about it
|
|
"border": {
|
|
"ignore": true
|
|
},
|
|
"align": {
|
|
"name": "Align",
|
|
"type": "string",
|
|
"default": "left",
|
|
"description": "Position of this element in the parent hbox"
|
|
},
|
|
"spacing": {
|
|
"ignore": true
|
|
},
|
|
"padding": {
|
|
"ignore": true
|
|
},
|
|
"sortable": {
|
|
"name": "Sortable callback",
|
|
"type": "string",
|
|
"default": et2_no_init,
|
|
"description": "PHP function called when user sorts the grid. Setting this enables sorting the grid rows. The callback will be passed the ID of the grid and the new order of the rows."
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @memberOf et2_grid
|
|
*/
|
|
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");
|
|
|
|
var id = et2_readAttrWithDefault(node, "id", "");
|
|
if(id)
|
|
{
|
|
rowDataEntry["id"] = id;
|
|
}
|
|
}
|
|
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
|
|
if(this.getArrayMgr("content"))
|
|
{
|
|
var content = this.getArrayMgr("content");
|
|
var rowDataEntry = rowData[rowData.length-1];
|
|
var rowIndex = rowData.length-1;
|
|
// Find out if we have any content rows, and how many
|
|
while(true)
|
|
{
|
|
if(content.data[rowIndex])
|
|
{
|
|
rowData[rowIndex] = jQuery.extend({}, rowDataEntry);
|
|
|
|
rowIndex++;
|
|
}
|
|
else
|
|
{
|
|
// No more rows, stop
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(rowIndex <= rowData.length - 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)
|
|
{
|
|
if(nodeName != "description")
|
|
{
|
|
// Only notify it skipping other than description,
|
|
// description used to pad
|
|
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
|
|
if(!cell.disabled)
|
|
{
|
|
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(this._getCell(cells, x,y).rowData.id)
|
|
{
|
|
this.getArrayMgr("content").expandName(this.rowData[y].id);
|
|
}
|
|
// 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;
|
|
}
|
|
}
|
|
if(this._getCell(cells, x, y).rowData.id)
|
|
{
|
|
this._getCell(cells, x, y).rowData.id = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.id);
|
|
}
|
|
if(this._getCell(cells, x, y).rowData.class)
|
|
{
|
|
this._getCell(cells, x, y).rowData.class = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.class);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if(this.rowData[y].id)
|
|
{
|
|
tr.attr("id", this.rowData[y].id);
|
|
}
|
|
// 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();
|
|
cell.widget.options = cell.disabled;
|
|
}
|
|
|
|
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 || typeof _sender == 'undefined')
|
|
{
|
|
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);
|
|
},
|
|
|
|
set_align: function(_value) {
|
|
this.align = _value;
|
|
},
|
|
|
|
get_align: function(_value) {
|
|
return this.align;
|
|
},
|
|
|
|
/**
|
|
* Sortable allows you to reorder grid rows using the mouse.
|
|
* The new order is returned as part of the value of the
|
|
* grid, in 'sort_order'.
|
|
*
|
|
* @param {boolean|function} sortable Callback or false to disable
|
|
*/
|
|
set_sortable: function(sortable) {
|
|
if(!sortable)
|
|
{
|
|
this.tbody.sortable("destroy");
|
|
return;
|
|
}
|
|
|
|
// Make sure rows have IDs, so sortable has something to return
|
|
$j('tr', this.tbody).each(function(index) {
|
|
var $this = $j(this);
|
|
|
|
// Header does not participate in sorting
|
|
if($this.hasClass('th')) return;
|
|
|
|
// If row doesn't have an ID, assign the index as ID
|
|
if(!$this.attr("id")) $this.attr("id", index);
|
|
});
|
|
|
|
var self = this;
|
|
|
|
// Set up sortable
|
|
this.tbody.sortable({
|
|
// Header does not participate in sorting
|
|
items: "tr:not(.th)",
|
|
distance: 15,
|
|
stop: function(event, ui) {
|
|
self.egw().jsonq(sortable,[self.id, self.tbody.sortable("toArray")],
|
|
null,
|
|
self
|
|
);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Override parent to apply actions on each row
|
|
*
|
|
* @param Object[ {ID: attributes..}+] as for set_actions
|
|
*/
|
|
_link_actions: function(actions)
|
|
{
|
|
// Get the top level element for the tree
|
|
var objectManager = egw_getAppObjectManager(true);
|
|
var widget_object = objectManager.getObjectById(this.id);
|
|
if (widget_object == null) {
|
|
// Add a new container to the object manager which will hold the widget
|
|
// objects
|
|
widget_object = objectManager.insertObject(false, new egwActionObject(
|
|
this.id, objectManager, new et2_action_object_impl(this),
|
|
objectManager.manager.getActionById(this.id) || objectManager.manager
|
|
));
|
|
}
|
|
|
|
// Delete all old objects
|
|
widget_object.clear();
|
|
|
|
// Go over the widget & add links - this is where we decide which actions are
|
|
// 'allowed' for this widget at this time
|
|
var action_links = this._get_action_links(actions);
|
|
|
|
// Deal with each row
|
|
for(var i = 0; i < this.rowData.length; i++)
|
|
{
|
|
// Add a new action object to the object manager
|
|
var aoi = new et2_action_object_impl(this);
|
|
var row = $j('tr', this.tbody)[i];
|
|
aoi.doGetDOMNode = function() { return row;};
|
|
var obj = widget_object.addObject("row_"+i, aoi);
|
|
obj.updateActionLinks(action_links);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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"]); |