/** * This layout implements the column arrangement for {@link Ext.form.CheckboxGroup} and * {@link Ext.form.RadioGroup}. It groups the component's sub-items into columns * based on the component's {@link Ext.form.CheckboxGroup#columns columns} and * {@link Ext.form.CheckboxGroup#vertical} config properties. */Ext.define('Ext.layout.container.CheckboxGroup', { extend: 'Ext.layout.container.Container', alias: ['layout.checkboxgroup'], /** * @cfg {Boolean} [autoFlex=true] * By default, CheckboxGroup allocates all available space to the configured columns * meaning that column are evenly spaced across the container. * * To have each column only be wide enough to fit the container Checkboxes (or Radios), * set `autoFlex` to `false` */ autoFlex: true, type: 'checkboxgroup', createsInnerCt: true, childEls: [ 'innerCt' ], /* eslint-disable indent, max-len */ renderTpl: '<table id="{ownerId}-innerCt" data-ref="innerCt" class="' + Ext.baseCSSPrefix + 'table-plain" cellpadding="0"' + 'role="presentation" style="{tableStyle}">' + '<tbody role="presentation">' + '<tr role="presentation">' + '<tpl for="columns">' + '<td class="{parent.colCls}" valign="top" style="{style}" role="presentation">' + '{% this.renderColumn(out,parent,xindex-1) %}' + '</td>' + '</tpl>' + '</tr>' + '</tbody>' + '</table>', /* eslint-enable indent, max-len */ lastOwnerItemsGeneration: null, initLayout: function() { var me = this, owner = me.owner; me.columnsArray = Ext.isArray(owner.columns); me.autoColumns = !owner.columns || owner.columns === 'auto'; // Auto layout is always horizontal if (!me.autoColumns) { // ... but one column is always vertical me.vertical = owner.vertical || (owner.columns === 1 || owner.columns.length === 1); } me.callParent(); }, beginLayout: function(ownerContext) { var me = this, autoFlex = me.autoFlex, innerCtStyle = me.innerCt.dom.style, totalFlex = 0, flexedCols = 0, columns, numCols, i, width, cwidth; me.callParent(arguments); columns = me.rowNodes[0].children; ownerContext.innerCtContext = ownerContext.getEl('innerCt', me); // The columns config may be an array of widths. Any value < 1 is taken to be a fraction: if (!ownerContext.widthModel.shrinkWrap) { numCols = columns.length; // If columns is an array of numeric widths if (me.columnsArray) { // first calculate total flex for (i = 0; i < numCols; i++) { width = me.owner.columns[i]; if (width < 1) { totalFlex += width; flexedCols++; } } // now apply widths for (i = 0; i < numCols; i++) { width = me.owner.columns[i]; if (width < 1) { cwidth = ((width / totalFlex) * 100) + '%'; } else { cwidth = width + 'px'; } columns[i].style.width = cwidth; } } // Otherwise it's the *number* of columns, so distributed the widths evenly else { for (i = 0; i < numCols; i++) { // autoFlex: true will automatically calculate % widths // autoFlex: false allows the table to decide (shrinkWrap, in effect) // on a per-column basis cwidth = autoFlex ? (1 / numCols * 100) + '%' : ''; columns[i].style.width = cwidth; flexedCols++; } } // no flexed cols -- all widths are fixed if (!flexedCols) { innerCtStyle.tableLayout = 'fixed'; innerCtStyle.width = ''; // some flexed cols -- need to fix some } else if (flexedCols < numCols) { innerCtStyle.tableLayout = 'fixed'; innerCtStyle.width = '100%'; // let the table decide } else { innerCtStyle.tableLayout = 'auto'; // if autoFlex, fill available space, else compact down if (autoFlex) { innerCtStyle.width = '100%'; } else { innerCtStyle.width = ''; } } } else { innerCtStyle.tableLayout = 'auto'; innerCtStyle.width = ''; } }, cacheElements: function() { var me = this; // Grab defined childEls me.callParent(); me.rowNodes = me.innerCt.query('tr', true); // There always should be at least one row me.tBodyNode = me.rowNodes[0].parentNode; }, /* * Just wait for the child items to all lay themselves out in the width we are configured * to make available to them. Then we can measure our height. */ calculate: function(ownerContext) { var me = this, targetContext, widthShrinkWrap, heightShrinkWrap, shrinkWrap, table, targetPadding; // The column nodes are widthed using their own width attributes, we just need to wait // for all children to have arranged themselves in that width, and then collect our height. if (!ownerContext.getDomProp('containerChildrenSizeDone')) { me.done = false; } else { targetContext = ownerContext.innerCtContext; widthShrinkWrap = ownerContext.widthModel.shrinkWrap; heightShrinkWrap = ownerContext.heightModel.shrinkWrap; shrinkWrap = heightShrinkWrap || widthShrinkWrap; table = targetContext.el.dom; targetPadding = shrinkWrap && targetContext.getPaddingInfo(); if (widthShrinkWrap) { ownerContext.setContentWidth(table.offsetWidth + targetPadding.width, true); } if (heightShrinkWrap) { ownerContext.setContentHeight(table.offsetHeight + targetPadding.height, true); } } }, doRenderColumn: function(out, renderData, columnIndex) { // Careful! This method is bolted on to the renderTpl so all we get for context is // the renderData! The "this" pointer is the renderTpl instance! var me = renderData.$layout, owner = me.owner, columnCount = renderData.columnCount, items = owner.items.items, itemCount = items.length, item, itemIndex, rowCount, increment, tree; // Example: // columnCount = 3 // items.length = 10 if (owner.vertical) { // For vertical layouts we're using only one row // with items rendered "vertically" into table cells. // This is to ensure proper DOM order for native // keyboard navigation. // // 0 1 2 // +---+---+---+ // 0 | 0 | 4 | 8 | // 1 | 1 | 5 | 9 | // 2 | 2 | 6 | | // 3 | 3 | 7 | | // +---+---+---+ rowCount = Math.ceil(itemCount / columnCount); // = 4 itemIndex = columnIndex * rowCount; itemCount = Math.min(itemCount, itemIndex + rowCount); increment = 1; } else { // For horizontal layouts we're using table with rows // and cells, each cell holding one item. // // 0 1 2 // +---+---+---+ // 0 | 0 | 1 | 2 | // +---+---+---+ // 1 | 3 | 4 | 5 | // +---+---+---+ // 2 | 6 | 7 | 8 | // +---+---+---+ // 3 | 9 | | | // +---+---+---+ itemIndex = columnIndex; increment = columnCount; } for (; itemIndex < itemCount; itemIndex += increment) { item = items[itemIndex]; me.configureItem(item); tree = item.getRenderTree(); Ext.DomHelper.generateMarkup(tree, out); } }, /** * Returns the number of columns in the checkbox group. * @private */ getColumnCount: function() { var me = this, owner = me.owner, ownerColumns = owner.columns; // Our columns config is an array of numeric widths. // Calculate our total width if (me.columnsArray) { return ownerColumns.length; } if (Ext.isNumber(ownerColumns)) { return ownerColumns; } return owner.items.length; }, getItemSizePolicy: function(item) { return this.autoSizePolicy; }, getRenderData: function() { var me = this, data = me.callParent(), owner = me.owner, columns = me.getColumnCount(), autoFlex = me.autoFlex, totalFlex = 0, flexedCols = 0, width, column, cwidth, i; // calculate total flex if (me.columnsArray) { for (i = 0; i < columns; i++) { width = me.owner.columns[i]; if (width < 1) { totalFlex += width; flexedCols++; } } } data.colCls = owner.groupCls; data.columnCount = columns; data.columns = []; for (i = 0; i < columns; i++) { column = (data.columns[i] = {}); if (me.columnsArray) { width = me.owner.columns[i]; if (width < 1) { cwidth = ((width / totalFlex) * 100) + '%'; } else { cwidth = width + 'px'; } column.style = 'width:' + cwidth; } else { column.style = 'width:' + (1 / columns * 100) + '%'; flexedCols++; } } /* eslint-disable indent, multiline-ternary, no-multi-spaces */ // If the columns config was an array of column widths, allow table to auto width data.tableStyle = !flexedCols ? 'table-layout:fixed;' : (flexedCols < columns) ? 'table-layout:fixed;width:100%' : (autoFlex) ? 'table-layout:auto;width:100%' : 'table-layout:auto;'; /* eslint-enable indent, multiline-ternary, no-multi-spaces */ return data; }, // Always valid. beginLayout ensures the encapsulating elements of all children // are in the correct place isValidParent: Ext.returnTrue, setupRenderTpl: function(renderTpl) { this.callParent(arguments); renderTpl.renderColumn = this.doRenderColumn; }, renderChildren: function() { var me = this, generation = me.owner.items.generation; if (me.lastOwnerItemsGeneration !== generation) { me.lastOwnerItemsGeneration = generation; me.renderItems(me.getLayoutItems()); } }, /** * Iterates over all passed items, ensuring they are rendered. If the items * are already rendered, also determines if the items are in the proper place in the dom. * @protected */ renderItems: function(items) { var me = this, itemCount = items.length, item, rowCount, columnCount, rowIndex, columnIndex, i; if (itemCount) { Ext.suspendLayouts(); // We operate on "virtual" row and column counts here, which is the same // as the actual DOM structure for horizontal layouts but is quite different // for vertical layouts. if (me.autoColumns) { columnCount = itemCount; rowCount = 1; } else { columnCount = me.columnsArray ? me.owner.columns.length : me.owner.columns; rowCount = Math.ceil(itemCount / columnCount); } for (i = 0; i < itemCount; i++) { item = items[i]; rowIndex = me.getRenderRowIndex(i, rowCount, columnCount); columnIndex = me.getRenderColumnIndex(i, rowCount, columnCount); if (!item.rendered) { me.renderItem(item, rowIndex, columnIndex); } else if (!me.isItemAtPosition(item, rowIndex, columnIndex)) { me.moveItem(item, rowIndex, columnIndex); } } me.pruneRows(rowCount, columnCount); Ext.resumeLayouts(true); } }, isItemAtPosition: function(item, rowIndex, columnIndex) { return item.el.dom === this.getItemNodeAt(rowIndex, columnIndex); }, getRenderColumnIndex: function(itemIndex, rowCount, columnCount) { if (this.vertical) { return Math.floor(itemIndex / rowCount); } else { return itemIndex % columnCount; } }, getRenderRowIndex: function(itemIndex, rowCount, columnCount) { if (this.vertical) { return itemIndex % rowCount; } else { return Math.floor(itemIndex / columnCount); } }, getItemNodeAt: function(rowIndex, columnIndex) { var column = this.getColumnNodeAt(rowIndex, columnIndex); return this.vertical ? column.children[rowIndex] : column.children[0]; }, getRowNodeAt: function(rowIndex) { var me = this, row; // Vertical layout uses only one row with several columns, // each column containing one or more items, thus simulating "rows" rowIndex = me.vertical ? 0 : rowIndex; row = me.rowNodes[rowIndex]; if (!row) { row = me.rowNodes[rowIndex] = document.createElement('tr'); row.role = 'presentation'; me.tBodyNode.appendChild(row); } return row; }, getColumnNodeAt: function(rowIndex, columnIndex, row) { var column; row = row || this.getRowNodeAt(rowIndex); column = row.children[columnIndex]; if (!column) { column = Ext.fly(row).appendChild({ tag: 'td', cls: this.owner.groupCls, vAlign: 'top', role: 'presentation' }, true); } return column; }, pruneRows: function(rowCount, columnCount) { var me = this, rows = me.tBodyNode.children, columns, row, column, i, j; rowCount = me.vertical ? 1 : rowCount; while (rows.length > rowCount) { row = rows[rows.length - 1]; while (row.children.length) { Ext.get(row.children[0]).destroy(); } // We don't create Element instances for rows row.parentNode.removeChild(row); } for (i = rowCount - 1; i >= 0; i--) { row = rows[i]; columns = row.children; while (columns.length > columnCount) { column = columns[columns.length - 1]; Ext.get(column).destroy(); } // We only prune empty cells on 2nd and subsequent rows; // the first row needs to have all cells up to columnCount // to establish the structure. if (i > 0) { for (j = columns.length - 1; j >= 0; j--) { column = columns[j]; // We only need to test for the last cells that can be empty // due to item removal. As soon as we reach a non-empty column // there's no point in continuing the loop. if (column.children.length === 0) { Ext.get(column).destroy(); } else { break; } } } } }, /** * Renders the given Component into the specified row and column * @param {Ext.Component} item The Component to render * @param {number} rowIndex row index * @param {number} columnIndex column index * @private */ renderItem: function(item, rowIndex, columnIndex) { var me = this, column, itemIndex; me.configureItem(item); itemIndex = me.vertical ? rowIndex : 0; column = Ext.get(me.getColumnNodeAt(rowIndex, columnIndex)); item.render(column, itemIndex); }, /** * Moves the given already rendered Component to the specified row and column * @param {Ext.Component} item The Component to move * @param {number} rowIndex row index * @param {number} columnIndex column index * @private */ moveItem: function(item, rowIndex, columnIndex) { var me = this, column, itemIndex, targetNode; itemIndex = me.vertical ? rowIndex : 0; column = me.getColumnNodeAt(rowIndex, columnIndex); targetNode = column.children[itemIndex]; column.insertBefore(item.el.dom, targetNode || null); }, destroy: function() { if (this.owner.rendered) { // eslint-disable-next-line vars-on-top var target = this.getRenderTarget(), cells, i, len; if (target) { cells = target.query('.' + this.owner.groupCls, false); for (i = 0, len = cells.length; i < len; i++) { cells[i].destroy(); } } } this.callParent(); }});