/**
* This layout allows you to easily render content into an HTML table. The total number of columns
* can be specified, and rowspan and colspan can be used to create complex layouts within the table.
* This class is intended to be extended or created via the `layout: {type: 'table'}`
* {@link Ext.container.Container#layout} config, and should generally not need to be created
* directly via the new keyword.
*
* Note that when creating a layout via config, the layout-specific config properties must be passed
* in via the {@link Ext.container.Container#layout} object which will then be applied internally
* to the layout. In the case of TableLayout, the only valid layout config properties are
* {@link #columns} and {@link #tableAttrs}. However, the items added to a TableLayout can supply
* the following table-specific config properties:
*
* - **rowspan** Applied to the table cell containing the item.
* - **colspan** Applied to the table cell containing the item.
* - **cellCls** A CSS class name added to the table cell containing the item.
*
* The basic concept of building up a TableLayout is conceptually very similar to building up
* a standard HTML table. You simply add each panel (or "cell") that you want to include along
* with any span attributes specified as the special config properties of rowspan and colspan
* which work exactly like their HTML counterparts. Rather than explicitly creating and nesting rows
* and columns as you would in HTML, you simply specify the total column count in the layout config
* and start adding panels in their natural order from left to right, top to bottom. The layout will
* automatically figure out, based on the column count, rowspans and colspans, how to position each
* panel within the table. Just like with HTML tables, your rowspans and colspans must add up
* correctly in your overall layout or you'll end up with missing and/or extra cells!
* Example usage:
*
* @example
* Ext.create('Ext.panel.Panel', {
* title: 'Table Layout',
* width: 300,
* height: 150,
* layout: {
* type: 'table',
* // The total column count must be specified here
* columns: 3
* },
* defaults: {
* // applied to each contained panel
* bodyStyle: 'padding:20px'
* },
* items: [{
* html: 'Cell A content',
* rowspan: 2
* },{
* html: 'Cell B content',
* colspan: 2
* },{
* html: 'Cell C content',
* cellCls: 'highlight'
* },{
* html: 'Cell D content'
* }],
* renderTo: Ext.getBody()
* });
*/
Ext.define('Ext.layout.container.Table', {
extend: 'Ext.layout.container.Container',
alternateClassName: 'Ext.layout.TableLayout',
alias: 'layout.table',
/**
* @cfg {Number} columns
* The total number of columns to create in the table for this layout. If not specified,
* all Components added to this layout will be rendered into a single row using one column
* per Component.
*/
type: 'table',
createsInnerCt: true,
targetCls: Ext.baseCSSPrefix + 'table-layout-ct',
tableCls: Ext.baseCSSPrefix + 'table-layout',
cellCls: Ext.baseCSSPrefix + 'table-layout-cell',
childEls: [ 'table', 'tbody' ],
/**
* @cfg {Object} tableAttrs
* An object containing properties which are added to the {@link Ext.dom.Helper DomHelper}
* specification used to create the layout's `<table>` element. Example:
*
* {
* xtype: 'panel',
* layout: {
* type: 'table',
* columns: 3,
* tableAttrs: {
* style: {
* width: '100%'
* }
* }
* }
* }
*/
tableAttrs: null,
/**
* @cfg {Object} trAttrs
* An object containing properties which are added to the {@link Ext.dom.Helper DomHelper}
* specification used to create the layout's `<tr>` elements.
*/
/**
* @cfg {Object} tdAttrs
* An object containing properties which are added to the {@link Ext.dom.Helper DomHelper}
* specification used to create the layout's `<td>` elements.
*/
getItemSizePolicy: function(item) {
return this.autoSizePolicy;
},
initInheritedState: function(inheritedState, inheritedStateInner) {
inheritedStateInner.inShrinkWrapTable = true;
},
getHiddenItems: function() {
var result = [],
items = this.owner.items.items,
len = items.length,
i, item;
for (i = 0; i < len; ++i) {
item = items[i];
if (item.rendered && item.hidden) {
result.push(item);
}
}
return result;
},
/**
* @private
* Iterates over all passed items, ensuring they are rendered in a cell in the proper
* location in the table structure.
*/
renderChildren: function() {
var me = this,
items = me.getLayoutItems(),
tbody = me.tbody.dom,
rows = tbody.rows,
len = items.length,
hiddenItems = me.getHiddenItems(),
cells, curCell, rowIdx, cellIdx, item, trEl, tdEl, i;
// Calculate the correct cell structure for the current items
cells = me.calculateCells(items);
// Loop over each cell and compare to the current cells in the table, inserting/
// removing/moving cells as needed, and making sure each item is rendered into
// the correct cell.
for (i = 0; i < len; i++) {
curCell = cells[i];
rowIdx = curCell.rowIdx;
cellIdx = curCell.cellIdx;
item = items[i];
// If no row present, create and insert one
trEl = rows[rowIdx];
if (!trEl) {
trEl = tbody.insertRow(rowIdx);
if (me.trAttrs) {
trEl.set(me.trAttrs);
}
}
// If no cell present, create and insert one
tdEl = Ext.get(trEl.cells[cellIdx] || trEl.insertCell(cellIdx));
// Render or move the component into the cell
if (!item.rendered) {
me.renderItem(item, tdEl, 0);
}
else if (!me.isValidParent(item, tdEl, rowIdx, cellIdx, tbody)) {
me.moveItem(item, tdEl, 0);
}
// Set the cell properties
if (me.tdAttrs) {
tdEl.set(me.tdAttrs);
}
if (item.tdAttrs) {
tdEl.set(item.tdAttrs);
}
tdEl.set({
colSpan: item.colspan || 1,
rowSpan: item.rowspan || 1,
cls: me.cellCls + ' ' + (item.cellCls || '')
});
// If at the end of a row, remove any extra cells
if (!cells[i + 1] || cells[i + 1].rowIdx !== rowIdx) {
cellIdx++;
while (trEl.cells[cellIdx]) {
trEl.deleteCell(cellIdx);
}
}
}
// Delete any extra rows
rowIdx++;
while (tbody.rows[rowIdx]) {
tbody.deleteRow(rowIdx);
}
// Check if we've removed any cells that contain a component, we need to move
// them so they don't get cleaned up by the gc
for (i = 0, len = hiddenItems.length; i < len; ++i) {
me.ensureInDocument(hiddenItems[i].getEl());
}
},
ensureInDocument: function(el) {
var dom = el.dom.parentNode;
while (dom) {
if (dom.tagName.toUpperCase() === 'BODY') {
return;
}
dom = dom.parentNode;
}
Ext.getDetachedBody().appendChild(el, true);
},
calculate: function(ownerContext) {
if (!ownerContext.hasDomProp('containerChildrenSizeDone')) {
this.done = false;
}
else {
// eslint-disable-next-line vars-on-top
var targetContext = ownerContext.targetContext,
widthShrinkWrap = ownerContext.widthModel.shrinkWrap,
heightShrinkWrap = ownerContext.heightModel.shrinkWrap,
shrinkWrap = heightShrinkWrap || widthShrinkWrap,
table = shrinkWrap && this.table.dom,
targetPadding = shrinkWrap && targetContext.getPaddingInfo();
if (widthShrinkWrap) {
ownerContext.setContentWidth(table.offsetWidth + targetPadding.width, true);
}
if (heightShrinkWrap) {
ownerContext.setContentHeight(table.offsetHeight + targetPadding.height, true);
}
}
},
/**
* @private
* Determine the row and cell indexes for each component, taking into consideration
* the number of columns and each item's configured colspan/rowspan values.
* @param {Array} items The layout components
* @return {Object[]} List of row and cell indexes for each of the components
*/
calculateCells: function(items) {
var cells = [],
rowIdx = 0,
colIdx = 0,
cellIdx = 0,
totalCols = this.columns || Infinity,
rowspans = [], // rolling list of active rowspans for each column
len = items.length,
item, i, j;
for (i = 0; i < len; i++) {
item = items[i];
// Find the first available row/col slot not taken up by a spanning cell
while (colIdx >= totalCols || rowspans[colIdx] > 0) {
if (colIdx >= totalCols) {
// move down to next row
colIdx = 0;
cellIdx = 0;
rowIdx++;
// decrement all rowspans
for (j = 0; j < totalCols; j++) {
if (rowspans[j] > 0) {
rowspans[j]--;
}
}
}
else {
colIdx++;
}
}
// Add the cell info to the list
cells.push({
rowIdx: rowIdx,
cellIdx: cellIdx
});
// Increment
for (j = item.colspan || 1; j; --j) {
rowspans[colIdx] = item.rowspan || 1;
++colIdx;
}
++cellIdx;
}
return cells;
},
getRenderTree: function() {
var me = this,
items = me.getLayoutItems(),
rows = [],
result = Ext.apply({
tag: 'table',
id: me.owner.id + '-table',
"data-ref": 'table',
role: 'presentation',
cls: me.tableCls,
cellspacing: 0,
cellpadding: 0,
cn: {
tag: 'tbody',
id: me.owner.id + '-tbody',
"data-ref": 'tbody',
role: 'presentation',
cn: rows
}
}, me.tableAttrs),
tdAttrs = me.tdAttrs,
len = items.length,
item, curCell, tr, rowIdx, cellIdx, cell, cells, i;
// Calculate the correct cell structure for the current items
cells = me.calculateCells(items);
for (i = 0; i < len; i++) {
item = items[i];
curCell = cells[i];
rowIdx = curCell.rowIdx;
cellIdx = curCell.cellIdx;
// If no row present, create and insert one
tr = rows[rowIdx];
if (!tr) {
tr = rows[rowIdx] = {
tag: 'tr',
role: 'presentation',
cn: []
};
if (me.trAttrs) {
Ext.apply(tr, me.trAttrs);
}
}
// If no cell present, create and insert one
cell = tr.cn[cellIdx] = {
tag: 'td',
role: 'presentation'
};
if (tdAttrs) {
Ext.apply(cell, tdAttrs);
}
Ext.apply(cell, {
colSpan: item.colspan || 1,
rowSpan: item.rowspan || 1,
cls: me.cellCls + ' ' + (item.cellCls || '')
});
me.configureItem(item);
// The DomHelper config of the item is the cell's sole child
cell.cn = item.getRenderTree();
}
return result;
},
isValidParent: function(item, target, rowIdx, cellIdx) {
// If we were called with the 3 arg signature just check that the item is within our table,
if (arguments.length === 3) {
return this.table.isAncestor(item.el);
}
return item.el.dom.parentNode === this.tbody.dom.rows[rowIdx].cells[cellIdx];
},
destroy: function() {
var targetEl, cells, i, len;
// Table layout cells will be referenced by child items who will create
// Element instances for their container (layout cell). We need to clean up
// these Element instances.
if (this.owner.rendered) {
targetEl = this.getRenderTarget();
if (targetEl) {
cells = targetEl.query('.' + this.cellCls, false);
for (i = 0, len = cells.length; i < len; i++) {
cells[i].destroy();
}
}
}
this.callParent();
}
});