/** * This is a layout that will render form Fields, one under the other all stretched to the Container width. * * @example * Ext.create('Ext.Panel', { * width: 500, * height: 300, * title: "FormLayout Panel", * layout: 'form', * renderTo: Ext.getBody(), * bodyPadding: 5, * defaultType: 'textfield', * items: [{ * fieldLabel: 'First Name', * name: 'first', * allowBlank:false * },{ * fieldLabel: 'Last Name', * name: 'last' * },{ * fieldLabel: 'Company', * name: 'company' * }, { * fieldLabel: 'Email', * name: 'email', * vtype:'email' * }, { * fieldLabel: 'DOB', * name: 'dob', * xtype: 'datefield' * }, { * fieldLabel: 'Age', * name: 'age', * xtype: 'numberfield', * minValue: 0, * maxValue: 100 * }, { * xtype: 'timefield', * fieldLabel: 'Time', * name: 'time', * minValue: '8:00am', * maxValue: '6:00pm' * }] * }); * * Note that any configured {@link Ext.Component#padding padding} will be ignored on items within a Form layout. */ Ext.define('Ext.layout.container.Form', { /* Begin Definitions */ alias: 'layout.form', extend: 'Ext.layout.container.Container', alternateClassName: 'Ext.layout.FormLayout', /* End Definitions */ tableCls: Ext.baseCSSPrefix + 'form-layout-table', type: 'form', createsInnerCt: true, manageOverflow: true, // Begin with no previous adjustments lastOverflowAdjust: { width: 0, height: 0 }, childEls: ['formTable'], padRow: '<tr><td class="' + Ext.baseCSSPrefix + 'form-item-pad" colspan="3"></td></tr>', renderTpl: [ '<table id="{ownerId}-formTable" class="{tableCls}" style="width:100%" cellpadding="0">', '{%this.renderBody(out,values)%}', '</table>', '{%this.renderPadder(out,values)%}' ], getRenderData: function(){ var data = this.callParent(); data.tableCls = this.tableCls; return data; }, calculate : function (ownerContext) { var me = this, containerSize = me.getContainerSize(ownerContext, true), tableWidth, childItems, i = 0, length, shrinkwrapHeight = ownerContext.sizeModel.height.shrinkWrap; if (shrinkwrapHeight) { if (ownerContext.hasDomProp('containerChildrenSizeDone')) { ownerContext.setProp('contentHeight', me.formTable.dom.offsetHeight + ownerContext.targetContext.getPaddingInfo().height); } else { me.done = false; } } // Once we have been widthed, we can impose that width (in a non-dirty setting) upon all children at once if (containerSize.gotWidth) { tableWidth = me.formTable.dom.offsetWidth; childItems = ownerContext.childItems; for (length = childItems.length; i < length; ++i) { childItems[i].setWidth(tableWidth, false); } } else { me.done = false; } }, getRenderTarget: function() { return this.formTable; }, getRenderTree: function() { var me = this, result = me.callParent(arguments), i, len; for (i = 0, len = result.length; i < len; i++) { result[i] = me.transformItemRenderTree(result[i]); } return result; }, transformItemRenderTree: function(item) { if (item.tag && item.tag == 'table') { item.tag = 'tbody'; delete item.cellspacing; delete item.cellpadding; // IE6 doesn't separate cells nicely to provide input field // vertical separation. It also does not support transparent borders // which is how the extra 1px is added to the 2px each side cell spacing. // So it needs a 5px high pad row. if (Ext.isIE6) { item.cn = this.padRow; } return item; } return { tag: 'tbody', cn: { tag: 'tr', cn: { tag: 'td', colspan: 3, style: 'width:100%', cn: item } } }; }, isValidParent: function(item, target, position) { return true; }, isItemShrinkWrap: function(item) { return ((item.shrinkWrap === true) ? 3 : item.shrinkWrap||0) & 2; }, getItemSizePolicy: function(item) { return { setsWidth: 1, setsHeight: 0 }; }, // All of the below methods are old methods moved here from Container layout // TODO: remove these methods once Form layout extends Auto layout beginLayoutCycle: function (ownerContext, firstCycle) { var padEl = this.overflowPadderEl; if (padEl) { padEl.setStyle('display', 'none'); } // 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 = this.lastOverflowAdjust; } }, /** * 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, targetContext = ownerContext.targetContext, manageOverflow = me.manageOverflow, state = ownerContext.state, overflowAdjust = state.overflowAdjust, padWidth, padHeight, padElContext, padding, scrollRangeFlags, 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: xauto = (me.getOverflowXStyle(ownerContext) === 'auto'); yauto = (me.getOverflowYStyle(ownerContext) === 'auto'); // 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'); // The content size includes the target element's padding. Since // the containerSize excludes the target element's padding, we need // to subtract this padding from the content size before checking // to see if scrollbars are needed. padding = targetContext.getPaddingInfo(); contentW -= padding.width; contentH -= padding.height; 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 = ownerContext.peek('contentWidth'); // the padder element must have a height of at least 1px, or its width will be ignored by the container. // The extra height is adjusted for by adding a -1px top margin to the padder element padHeight = 1; if (scrollbars) { padding = 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'); } } }, completeLayout: function (ownerContext) { // Cache the scrollbar adjustment this.lastOverflowAdjust = ownerContext.state.overflowAdjust; }, /** * 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) { if (scrollRangeFlags & 5) { // if (loses parent bottom and/or right padding) out.push('<div id="',owner.id,'-overflowPadderEl" ', // the padder element must have a height of at least 1px, or its width will be ignored by the container. // The extra height is adjusted for by adding a -1px top margin to the padder element // A side effect of this approach is that click events will not register on bottom 1px of the container's // last child item, unless that item has a bottom margin. To fix this we make the padder element // relatively positioned and give it a large negative z-index. -99999 seems sufficient. 'style="font-size: 1px; height: 1px; margin-top: -1px; position: relative; z-index: -99999'); out.push('"></div>'); me.scrollRangeFlags = scrollRangeFlags; // remember for calculateOverflow } } }, /** * 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. * @param {Boolean} [ignoreOverflow=true] if true scrollbar size will not be * subtracted from container size. * @return {Object} The size * @return {Number} return.width The width * @return {Number} return.height The height * @protected */ getContainerSize : function(ownerContext, inDom, ignoreOverflow) { // 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(), // if we're managing padding the padding is on the innerCt instead of the targetEl padding = targetContext.getPaddingInfo(), got = 0, needed = 0, overflowAdjust = ignoreOverflow ? null : 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 the overflow-x style of the render target * @protected * @param {Ext.layout.ContextItem} ownerContext * @return {String} */ getOverflowXStyle: function(ownerContext) { var me = this; return me.overflowXStyle || (me.overflowXStyle = me.owner.scrollFlags.overflowX || ownerContext.targetContext.getStyle('overflow-x')); }, /** * returns the overflow-y style of the render target * @protected * @param {Ext.layout.ContextItem} ownerContext * @return {String} */ getOverflowYStyle: function(ownerContext) { var me = this; return me.overflowYStyle || (me.overflowYStyle = me.owner.scrollFlags.overflowY || ownerContext.targetContext.getStyle('overflow-y')); }, /** * 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; }; }()), 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.scrollFlags.y || me.reserveScrollbar) { me.lastOverflowAdjust = { width: scrollbarWidth, height: 0 }; } } }, setupRenderTpl: function (renderTpl) { this.callParent(arguments); renderTpl.renderPadder = this.doRenderPadder; } });