/** * This class is intended to be extended or created via the {@link Ext.container.Container#layout layout} * configuration property. See {@link Ext.container.Container#layout} for additional details. */ Ext.define('Ext.layout.container.Container', { /* Begin Definitions */ extend: 'Ext.layout.Layout', alternateClassName: 'Ext.layout.ContainerLayout', mixins: { elementCt: 'Ext.util.ElementContainer' }, requires: [ 'Ext.XTemplate' ], type: 'container', /* End Definitions */ /** * @cfg {String} itemCls * An optional extra CSS class that will be added to the container. This can be useful for * adding customized styles to the container or any of its children using standard CSS * rules. See {@link Ext.Component}.{@link Ext.Component#componentCls componentCls} also. */ /** * @cfg {Number} [manageOverflow=0] * One of the following values: * * - 0 if the layout should ignore overflow. * - 1 if the layout should be rerun if scrollbars are needed. * - 2 if the layout should also correct padding when overflowed. */ manageOverflow: 0, /** * @private * Called by an owning Panel before the Panel begins its collapse process. * Most layouts will not need to override the default Ext.emptyFn implementation. */ beginCollapse: Ext.emptyFn, /** * @private * Called by an owning Panel before the Panel begins its expand process. * Most layouts will not need to override the default Ext.emptyFn implementation. */ beginExpand: Ext.emptyFn, /** * An object which contains boolean properties specifying which properties are to be * animated upon flush of child Component ContextItems. For example, Accordion would * have: * * { * y: true, * height: true * } * * @private */ animatePolicy: null, childEls: [ /** * @property {Ext.Element} overflowPadderEl * The element used to correct body padding during overflow. */ 'overflowPadderEl' ], renderTpl: [ '{%this.renderBody(out,values)%}' ], usesContainerHeight: true, usesContainerWidth: true, usesHeight: true, usesWidth: true, /** * @cfg {Boolean} [reserveScrollbar=false] * Set to `true` to leave space for a vertical scrollbar (if the OS shows space-consuming scrollbars) regardless * of whether a scrollbar is needed. * * This is useful if content height changes during application usage, but you do not want the calculated width * of child items to change when a scrollbar appears or disappears. The scrollbar will appear in the reserved space, * and the calculated width of child Components will not change. * * @example * Ext.define('Employee', { * extend: 'Ext.data.Model', * fields: [ * {name: 'rating', type: 'int'}, * {name: 'salary', type: 'float'}, * {name: 'name'} * ] * }); * * function createFakeData(count) { * var firstNames = ['Ed', 'Tommy', 'Aaron', 'Abe', 'Jamie', 'Adam', 'Dave', 'David', 'Jay', 'Nicolas', 'Nige'], * lastNames = ['Spencer', 'Maintz', 'Conran', 'Elias', 'Avins', 'Mishcon', 'Kaneda', 'Davis', 'Robinson', 'Ferrero', 'White'], * ratings = [1, 2, 3, 4, 5], * salaries = [100, 400, 900, 1500, 1000000]; * * var data = []; * for (var i = 0; i < (count || 25); i++) { * var ratingId = Math.floor(Math.random() * ratings.length), * salaryId = Math.floor(Math.random() * salaries.length), * firstNameId = Math.floor(Math.random() * firstNames.length), * lastNameId = Math.floor(Math.random() * lastNames.length), * * rating = ratings[ratingId], * salary = salaries[salaryId], * name = Ext.String.format("{0} {1}", firstNames[firstNameId], lastNames[lastNameId]); * * data.push({ * rating: rating, * salary: salary, * name: name * }); * } * store.loadData(data); * } * * // create the Data Store * var store = Ext.create('Ext.data.Store', { * id: 'store', * model: 'Employee', * proxy: { * type: 'memory' * } * }); * createFakeData(10); * * var grid = Ext.create('Ext.grid.Panel', { * title: 'Grid loaded with varying number of records', * anchor: '100%', * store: store, * columns: [{ * xtype: 'rownumberer', * width: 40, * sortable: false * },{ * text: 'Name', * flex: 1, * sortable: true, * dataIndex: 'name' * },{ * text: 'Rating', * width: 125, * sortable: true, * dataIndex: 'rating' * },{ * text: 'Salary', * width: 125, * sortable: true, * dataIndex: 'salary', * align: 'right', * renderer: Ext.util.Format.usMoney * }] * }); * * Ext.create('Ext.panel.Panel', { * renderTo: document.body, * width: 800, * height: 600, * layout: { * type: 'anchor', * reserveScrollbar: true // There will be a gap even when there's no scrollbar * }, * autoScroll: true, * items: grid, * tbar: { * defaults: { * handler: function(b) { * createFakeData(b.count); * } * }, * items: [{ * text: '10 Items', * count: 10 * },{ * text: '100 Items', * count: 100 * },{ * text: '300 Items', * count: 300 * },{ * text: '1000 Items', * count: 1000 * },{ * text: '5000 Items', * count: 5000 * }] * } * }); * */ reserveScrollbar: false, // Begin with no previous adjustments lastOverflowAdjust: { width: 0, height: 0 }, constructor: function () { this.callParent(arguments); this.mixins.elementCt.constructor.call(this); }, destroy : function() { this.callParent(); this.mixins.elementCt.destroy.call(this); }, initLayout: function() { var me = this, scrollbarWidth = Ext.getScrollbarSize().width; me.callParent(); // Create a default lastOverflowAdjust based upon scrolling configuration. // If the Container is to overflow, or we *always* reserve space for a scrollbar // then reserve space for a vertical scrollbar if (scrollbarWidth && me.manageOverflow && !me.hasOwnProperty('lastOverflowAdjust')) { if (me.owner.autoScroll || me.reserveScrollbar) { me.lastOverflowAdjust = { width: scrollbarWidth, height: 0 }; } } }, /** * In addition to work done by our base classes, containers benefit from some extra * cached data. The following properties are added to the ownerContext: * * - visibleItems: the result of {@link #getVisibleItems} * - childItems: the ContextItem[] for each visible item * - targetContext: the ContextItem for the {@link #getTarget} element */ beginLayout: function (ownerContext) { this.callParent(arguments); ownerContext.targetContext = ownerContext.getEl('getTarget', this); this.cacheChildItems(ownerContext); }, beginLayoutCycle: function (ownerContext, firstCycle) { var me = this, padEl = me.overflowPadderEl; me.callParent(arguments); // Begin with the scrollbar adjustment that we used last time - this is more likely to be correct // than beginning with no adjustment at all if (!ownerContext.state.overflowAdjust) { ownerContext.state.overflowAdjust = me.lastOverflowAdjust; } if (firstCycle) { if (me.usesContainerHeight) { ++ownerContext.consumersContainerHeight; } if (me.usesContainerWidth) { ++ownerContext.consumersContainerWidth; } } if (padEl) { padEl.setStyle('display', 'none'); } }, completeLayout: function (ownerContext) { // Cache the scrollbar adjustment this.lastOverflowAdjust = ownerContext.state.overflowAdjust; }, cacheChildItems: function (ownerContext) { var context = ownerContext.context, childItems = [], items = this.getVisibleItems(), length = items.length, i; ownerContext.childItems = childItems; ownerContext.visibleItems = items; for (i = 0; i < length; ++i) { childItems.push(context.getCmp(items[i])); } }, cacheElements: function () { var owner = this.owner; this.applyChildEls(owner.el, owner.id); // from ElementContainer mixin }, calculateContentSize: function (ownerContext, dimensions) { var me = this, containerDimensions = (dimensions || 0) | me.manageOverflow | ((ownerContext.widthModel.shrinkWrap ? 1 : 0) | (ownerContext.heightModel.shrinkWrap ? 2 : 0)), calcWidth = (containerDimensions & 1) || undefined, calcHeight = (containerDimensions & 2) || undefined, childItems = ownerContext.childItems, length = childItems.length, contentHeight = 0, contentWidth = 0, needed = 0, props = ownerContext.props, targetXY, targetX, targetY, targetPadding, borders, child, childContext, childX, childY, height, i, margins, width, xy; if (calcWidth) { if (isNaN(props.contentWidth)) { ++needed; } else { calcWidth = undefined; } } if (calcHeight) { if (isNaN(props.contentHeight)) { ++needed; } else { calcHeight = undefined; } } if (needed) { // TODO - this is rather brute force... maybe a wrapping el or clientHeight/Width // trick might help. Whatever we do, it must either work for Absolute layout or // at least be correctable by an overridden method in that derived class. for (i = 0; i < length; ++i) { childContext = childItems[i]; child = childContext.target; height = calcHeight && childContext.getProp('height'); width = calcWidth && childContext.getProp('width'); margins = childContext.getMarginInfo(); // getXY is the root method here (meaning that we cannot avoid getting both // even if we need only one), so dip into the DOM if something is needed if ((calcWidth && isNaN(child.x)) || (calcHeight && isNaN(child.y))) { xy = child.el.getXY(); if (!targetXY) { targetXY = ownerContext.targetContext.el.getXY(); borders = ownerContext.targetContext.getBorderInfo(); targetX = targetXY[0] + borders.left; targetY = targetXY[1] + borders.top; } // not worth avoiding the possibly useless calculation here: childX = xy[0] - targetX; childY = xy[1] - targetY; } else { // not worth avoiding these either: childX = child.x; childY = child.y; } // XY includes the top/left margin height += margins.bottom; width += margins.right; contentHeight = Math.max(contentHeight, childY + height); contentWidth = Math.max(contentWidth, childX + width); if (isNaN(contentHeight) && isNaN(contentWidth)) { me.done = false; return; } } if (calcWidth || calcHeight) { targetPadding = ownerContext.targetContext.getPaddingInfo(); } if (calcWidth && !ownerContext.setContentWidth(contentWidth + targetPadding.right)) { me.done = false; } if (calcHeight && !ownerContext.setContentHeight(contentHeight + targetPadding.bottom)) { me.done = false; } /* add a '/' to turn on this log ('//* enables, '/*' disables) if (me.done) { var el = ownerContext.targetContext.el.dom; Ext.log(this.owner.id, '.contentSize: ', contentWidth, 'x', contentHeight, ' => scrollSize: ', el.scrollWidth, 'x', el.scrollHeight); }/**/ } }, /** * Handles overflow processing for a container. This should be called once the layout * has determined contentWidth/Height. In addition to the ownerContext passed to the * {@link #calculate} method, this method also needs the containerSize (the object * returned by {@link #getContainerSize}). * * @param {Ext.layout.ContextItem} ownerContext * @param {Object} containerSize * @param {Number} dimensions A bit mask for the overflow managed dimensions. The 0-bit * is for `width` and the 1-bit is for `height`. In other words, a value of 1 would be * only `width`, 2 would be only `height` and 3 would be both. */ calculateOverflow: function (ownerContext, containerSize, dimensions) { var me = this, owner = me.owner, manageOverflow = me.manageOverflow, state = ownerContext.state, overflowAdjust = state.overflowAdjust, padWidth, padHeight, padElContext, padding, scrollRangeFlags, overflow, scrollbarSize, contentW, contentH, ownerW, ownerH, scrollbars, xauto, yauto; if (manageOverflow && !state.secondPass && !me.reserveScrollbar) { // Determine the dimensions that have overflow:auto applied. If these come by // way of component config, this does not require a DOM read: if (owner.autoScroll) { xauto = yauto = true; } else { if (owner.overflowX) { xauto = owner.overflowX == 'auto'; } else { overflow = ownerContext.targetContext.getStyle('overflow-x'); xauto = overflow && overflow != 'hidden' && overflow != 'scroll'; } if (owner.overflowY) { yauto = owner.overflowY == 'auto'; } else { overflow = ownerContext.targetContext.getStyle('overflow-y'); yauto = overflow && overflow != 'hidden' && overflow != 'scroll'; } } // If the container layout is not using width, we don't need to adjust for the // vscroll (likewise for height). Perhaps we don't even need to run the layout // again if the adjustments won't have any effect on the result! if (!containerSize.gotWidth) { xauto = false; } if (!containerSize.gotHeight) { yauto = false; } if (xauto || yauto) { scrollbarSize = Ext.getScrollbarSize(); // as a container we calculate contentWidth/Height, so we don't want // to use getProp and make it look like we are triggered by them... contentW = ownerContext.peek('contentWidth'); contentH = ownerContext.peek('contentHeight'); ownerW = containerSize.width; ownerH = containerSize.height; scrollbars = me.getScrollbarsNeeded(ownerW, ownerH, contentW, contentH); state.overflowState = scrollbars; if (typeof dimensions == 'number') { scrollbars &= ~dimensions; // ignore dimensions that have no effect } overflowAdjust = { width: (xauto && (scrollbars & 2)) ? scrollbarSize.width : 0, height: (yauto && (scrollbars & 1)) ? scrollbarSize.height : 0 }; // We can have 0-sized scrollbars (new Mac OS) and so don't invalidate // the layout unless this will change something... if (overflowAdjust.width !== me.lastOverflowAdjust.width || overflowAdjust.height !== me.lastOverflowAdjust.height) { me.done = false; // we pass overflowAdjust and overflowState in as state for the next // cycle (these are discarded if one of our ownerCt's invalidates): ownerContext.invalidate({ state: { overflowAdjust: overflowAdjust, overflowState: state.overflowState, secondPass: true } }); } } } if (!me.done) { return; } padElContext = ownerContext.padElContext || (ownerContext.padElContext = ownerContext.getEl('overflowPadderEl', me)); // Even if overflow does not effect the layout, we still do need the padEl to be // sized or hidden appropriately... if (padElContext) { scrollbars = state.overflowState; // the true overflow state padWidth = containerSize.width; padHeight = 0;// TODO me.padHeightAdj; // 0 or 1 if (scrollbars) { padding = ownerContext.targetContext.getPaddingInfo(); scrollRangeFlags = me.scrollRangeFlags; if ((scrollbars & 2) && (scrollRangeFlags & 1)) { // if (vscroll and loses bottom) padHeight += padding.bottom; } if ((scrollbars & 1) && (scrollRangeFlags & 4)) { // if (hscroll and loses right) padWidth += padding.right; } padElContext.setProp('display', ''); padElContext.setSize(padWidth, padHeight); } else { padElContext.setProp('display', 'none'); } } }, /** * Adds layout's itemCls and owning Container's itemCls * @protected */ configureItem: function(item) { var me = this, ownerItemCls = me.owner.itemCls, addClasses = [].concat(me.itemCls || []); me.callParent(arguments); if (ownerItemCls) { addClasses = Ext.Array.push(addClasses, ownerItemCls); } item.addCls(addClasses); }, doRenderBody: function (out, renderData) { // 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! this.renderItems(out, renderData); this.renderContent(out, renderData); }, doRenderContainer: function (out, renderData) { // 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.$comp.layout, tpl = me.getRenderTpl(), data = me.getRenderData(); tpl.applyOut(data, out); }, doRenderItems: function (out, renderData) { // 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, tree = me.getRenderTree(); if (tree) { Ext.DomHelper.generateMarkup(tree, out); } }, /** * Creates an element that makes bottom/right body padding consistent across browsers. * This element is sized based on the need for scrollbars in {@link #calculateOverflow}. * If the {@link #manageOverflow} option is false, this element is not created. * * See {@link #getScrollRangeFlags} for more details. */ doRenderPadder: function (out, renderData) { // 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, scrollRangeFlags = me.getScrollRangeFlags(); if (me.manageOverflow == 2) { if (scrollRangeFlags & 5) { // if (loses parent bottom and/or right padding) out.push('
'); me.scrollRangeFlags = scrollRangeFlags; // remember for calculateOverflow } } }, finishRender: function () { var me = this, target, items; me.callParent(); me.cacheElements(); target = me.getRenderTarget(); items = me.getLayoutItems(); if (me.targetCls) { me.getTarget().addCls(me.targetCls); } me.finishRenderItems(target, items); }, /** * @private * Called for every layout in the layout context after all the layouts have been finally flushed */ notifyOwner: function() { this.owner.afterLayout(this); }, /** * Returns the container size (that of the target). Only the fixed-sized dimensions can * be returned because the shrinkWrap dimensions are based on the contentWidth/Height * as determined by the container layout. * * If the {@link #calculateOverflow} method is used and if {@link #manageOverflow} is * true, this may adjust the width/height by the size of scrollbars. * * @param {Ext.layout.ContextItem} ownerContext The owner's context item. * @param {Boolean} [inDom=false] True if the container size must be in the DOM. * @return {Object} The size * @return {Number} return.width The width * @return {Number} return.height The height * @protected */ getContainerSize : function(ownerContext, inDom) { // Subtle But Important: // // We don't want to call getProp/hasProp et.al. unless we in fact need that value // for our results! If we call it and don't need it, the layout manager will think // we depend on it and will schedule us again should it change. var targetContext = ownerContext.targetContext, frameInfo = targetContext.getFrameInfo(), padding = targetContext.getPaddingInfo(), got = 0, needed = 0, overflowAdjust = ownerContext.state.overflowAdjust, gotWidth, gotHeight, width, height; // In an shrinkWrap width/height case, we must not ask for any of these dimensions // because they will be determined by contentWidth/Height which is calculated by // this layout... // Fit/Card layouts are able to set just the width of children, allowing child's // resulting height to autosize the Container. // See examples/tabs/tabs.html for an example of this. if (!ownerContext.widthModel.shrinkWrap) { ++needed; width = inDom ? targetContext.getDomProp('width') : targetContext.getProp('width'); gotWidth = (typeof width == 'number'); if (gotWidth) { ++got; width -= frameInfo.width + padding.width; if (overflowAdjust) { width -= overflowAdjust.width; } } } if (!ownerContext.heightModel.shrinkWrap) { ++needed; height = inDom ? targetContext.getDomProp('height') : targetContext.getProp('height'); gotHeight = (typeof height == 'number'); if (gotHeight) { ++got; height -= frameInfo.height + padding.height; if (overflowAdjust) { height -= overflowAdjust.height; } } } return { width: width, height: height, needed: needed, got: got, gotAll: got == needed, gotWidth: gotWidth, gotHeight: gotHeight }; }, /** * Returns an array of child components either for a render phase (Performed in the beforeLayout * method of the layout's base class), or the layout phase (onLayout). * @return {Ext.Component[]} of child components */ getLayoutItems: function() { var owner = this.owner, items = owner && owner.items; return (items && items.items) || []; }, getRenderData: function () { var comp = this.owner; return { $comp: comp, $layout: this, ownerId: comp.id }; }, /** * @protected * Returns all items that are rendered * @return {Array} All matching items */ getRenderedItems: function() { var me = this, target = me.getRenderTarget(), items = me.getLayoutItems(), ln = items.length, renderedItems = [], i, item; for (i = 0; i < ln; i++) { item = items[i]; if (item.rendered && me.isValidParent(item, target, i)) { renderedItems.push(item); } } return renderedItems; }, /** * Returns the element into which rendering must take place. Defaults to the owner Container's * target element. * * May be overridden in layout managers which implement an inner element. * * @return {Ext.Element} */ getRenderTarget: function() { return this.owner.getTargetEl(); }, /** * Returns the element into which extra functional DOM elements can be inserted. Defaults to the owner Component's encapsulating element. * * May be overridden in Component layout managers which implement a {@link #getRenderTarget component render target} which must only * contain child components. * @return {Ext.Element} */ getElementTarget: function() { return this.getRenderTarget(); }, getRenderTpl: function () { var me = this, renderTpl = Ext.XTemplate.getTpl(this, 'renderTpl'); // Make sure all standard callout methods for the owner component are placed on the // XTemplate instance (but only once please): if (!renderTpl.renderContent) { me.owner.setupRenderTpl(renderTpl); } return renderTpl; }, getRenderTree: function () { var result, items = this.owner.items, itemsGen, renderCfgs = {}; do { itemsGen = items.generation; result = this.getItemsRenderTree(this.getLayoutItems(), renderCfgs); } while (items.generation !== itemsGen); return result; }, getScrollbarsNeeded: function (width, height, contentWidth, contentHeight) { var scrollbarSize = Ext.getScrollbarSize(), hasWidth = typeof width == 'number', hasHeight = typeof height == 'number', needHorz = 0, needVert = 0; // No space-consuming scrollbars. if (!scrollbarSize.width) { return 0; } if (hasHeight && height < contentHeight) { needVert = 2; width -= scrollbarSize.width; } if (hasWidth && width < contentWidth) { needHorz = 1; if (!needVert && hasHeight) { height -= scrollbarSize.height; if (height < contentHeight) { needVert = 2; } } } return needVert + needHorz; }, /** * Returns flags indicating cross-browser handling of scrollHeight/Width. In particular, * IE has issues with padding-bottom in a scrolling element (it does not include that * padding in the scrollHeight). Also, margin-bottom on a child in a scrolling element * can be lost. * * All browsers seem to ignore margin-right on children and padding-right on the parent * element (the one with the overflow) * * This method returns a number with the follow bit positions set based on things not * accounted for in scrollHeight and scrollWidth: * * - 1: Scrolling element's padding-bottom is not included in scrollHeight. * - 2: Last child's margin-bottom is not included in scrollHeight. * - 4: Scrolling element's padding-right is not included in scrollWidth. * - 8: Child's margin-right is not included in scrollWidth. * * To work around the margin-bottom issue, it is sufficient to create a 0px tall last * child that will "hide" the margin. This can also be handled by wrapping the children * in an element, again "hiding" the margin. Wrapping the elements is about the only * way to preserve their right margins. This is the strategy used by Column layout. * * To work around the padding-bottom problem, since it is comes from a style on the * parent element, about the only simple fix is to create a last child with height * equal to padding-bottom. To preserve the right padding, the sizing element needs to * have a width that includes the right padding. */ getScrollRangeFlags: (function () { var flags = -1; return function () { if (flags < 0) { var div = Ext.getBody().createChild({ //cls: 'x-border-box x-hide-offsets', cls: Ext.baseCSSPrefix + 'border-box', style: { width: '100px', height: '100px', padding: '10px', overflow: 'auto' }, children: [{ style: { border: '1px solid red', width: '150px', height: '150px', margin: '0 5px 5px 0' // TRBL } }] }), scrollHeight = div.dom.scrollHeight, scrollWidth = div.dom.scrollWidth, heightFlags = { // right answer, nothing missing: 175: 0, // missing parent padding-bottom: 165: 1, // missing child margin-bottom: 170: 2, // missing both 160: 3 }, widthFlags = { // right answer, nothing missing: 175: 0, // missing parent padding-right: 165: 4, // missing child margin-right: 170: 8, // missing both 160: 12 }; flags = (heightFlags[scrollHeight] || 0) | (widthFlags[scrollWidth] || 0); //Ext.log('flags=',flags.toString(2)); div.remove(); } return flags; }; }()), /** * Returns the owner component's resize element. * @return {Ext.Element} */ getTarget: function() { return this.owner.getTargetEl(); }, /** * @protected * Returns all items that are both rendered and visible * @return {Array} All matching items */ getVisibleItems: function() { var target = this.getRenderTarget(), items = this.getLayoutItems(), ln = items.length, visibleItems = [], i, item; for (i = 0; i < ln; i++) { item = items[i]; if (item.rendered && this.isValidParent(item, target, i) && item.hidden !== true) { visibleItems.push(item); } } return visibleItems; }, setupRenderTpl: function (renderTpl) { var me = this; renderTpl.renderBody = me.doRenderBody; renderTpl.renderContainer = me.doRenderContainer; renderTpl.renderItems = me.doRenderItems; renderTpl.renderPadder = me.doRenderPadder; } });