/** * This class manages state information for a component or element during a layout. * * # Blocks * * A "block" is a required value that is preventing further calculation. When a layout has * encountered a situation where it cannot possibly calculate results, it can associate * itself with the context item and missing property so that it will not be rescheduled * until that property is set. * * Blocks are a one-shot registration. Once the property changes, the block is removed. * * Be careful with blocks. If *any* further calculations can be made, a block is not the * right choice. * * # Triggers * * Whenever any call to {@link #getProp}, {@link #getDomProp}, {@link #hasProp} or * {@link #hasDomProp} is made, the current layout is automatically registered as being * dependent on that property in the appropriate state. Any changes to the property will * trigger the layout and it will be queued in the {@link Ext.layout.Context}. * * Triggers, once added, remain for the entire layout. Any changes to the property will * reschedule all unfinished layouts in their trigger set. * @protected */ Ext.define('Ext.layout.ContextItem', { requires: ['Ext.layout.ClassList'], heightModel: null, widthModel: null, sizeModel: null, boxChildren: null, boxParent: null, children: [], dirty: null, // The number of dirty properties dirtyCount: 0, hasRawContent: true, isContextItem: true, isTopLevel: false, consumersContentHeight: 0, consumersContentWidth: 0, consumersContainerHeight: 0, consumersContainerWidth: 0, consumersHeight: 0, consumersWidth: 0, ownerCtContext: null, remainingChildLayouts: 0, remainingComponentChildLayouts: 0, remainingContainerChildLayouts: 0, /** * @property {Boolean} wrapsComponent * True if this item wraps a Component (rather than an Element). * @readonly */ wrapsComponent: false, constructor: function (config) { var me = this, target = config.target, // target must come in as a config el, sizeModel; Ext.apply(me, config); el = me.el; me.id = el.id; me.lastBox = el.lastBox; // These hold collections of layouts that are either blocked or triggered by sets // to our properties (either ASAP or after flushing to the DOM). All of them have // the same structure: // // me.blocks = { // width: { // 'layout-1001': layout1001 // } // } // // The property name is the primary key which yields an object keyed by layout id // with the layout instance as the value. This prevents duplicate entries for one // layout and gives O(1) access to the layout instance when we need to iterate and // process them. // me.blocks = {}; me.domBlocks = {}; me.triggers = {}; me.domTriggers = {}; me.flushedProps = {}; // the current set of property values: me.props = {}; /** * @property {Object} state * State variables that are cleared when invalidated. */ me.state = {}; // the set of cached styles for the element: me.styles = {}; if (target.isComponent) { me.wrapsComponent = true; me.sizeModel = sizeModel = target.getSizeModel(me.ownerCtContext && me.ownerCtContext.sizeModel); me.widthModel = sizeModel.width; me.heightModel = sizeModel.height; me.frameBodyContext = me.getEl('frameBody'); } }, /** * Initializes ContextItem. * @param ownerCtContext */ init: function (ownerCtContext) { var me = this, boxParent, widthModel; if (ownerCtContext) { me.ownerCtContext = ownerCtContext; me.isBoxParent = me.target.ownerLayout.isItemBoxParent(me); widthModel = me.widthModel; if (widthModel.shrinkWrap) { boxParent = ownerCtContext.isBoxParent ? ownerCtContext : ownerCtContext.boxParent; if (boxParent) { boxParent.addBoxChild(me); } } else if (widthModel.natural) { me.boxParent = ownerCtContext; } } else { me.isTopLevel = true; } }, initAnimatePolicy: function() { var me = this, target = me.target, ownerCtContext = me.ownerCtContext; if (ownerCtContext && ownerCtContext.isTopLevel) { // See which properties we are supposed to animate to their new state. // If there are any, queue ourself to be animated by the owning Context me.animatePolicy = target.ownerLayout.getAnimatePolicy(me); } // Collapsing/expnding a top level Panel with animation. // We need to fabricate an animatePolicy depending on which dimension the collapse is using, // isCollapsingOrExpanding is set during the collapse/expand process. else if (!ownerCtContext && target.isCollapsingOrExpanding && target.animCollapse) { me.animatePolicy = target.componentLayout.getAnimatePolicy(me); } if (me.animatePolicy) { me.context.queueAnimation(me); } }, noFraming: { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }, /** * Queue the addition of a class name (or array of class names) to this ContextItem's target when next flushed. */ addCls: function(newCls) { this.getClassList().addMany(newCls); }, /** * Queue the removal of a class name (or array of class names) from this ContextItem's target when next flushed. */ removeCls: function(removeCls) { this.getClassList().removeMany(removeCls); }, /** * Adds a block. * * @param {String} name The name of the block list ('blocks' or 'domBlocks'). * @param {Ext.layout.Layout} layout The layout that is blocked. * @param {String} propName The property name that blocked the layout (e.g., 'width'). * @private */ addBlock: function (name, layout, propName) { var me = this, collection = me[name], blockedLayouts = collection[propName] || (collection[propName] = {}); if (!blockedLayouts[layout.id]) { blockedLayouts[layout.id] = layout; ++layout.blockCount; ++me.context.blockCount; } }, addBoxChild: function (boxChildItem) { var me = this, children, widthModel = boxChildItem.widthModel; boxChildItem.boxParent = this; // Children that are widthModel.auto (regardless of heightModel) that measure the // DOM (by virtue of hasRawContent), need to wait for their "box parent" to be sized. // If they measure too early, they will be wrong results. In the widthModel.shrinkWrap // case, the boxParent "crushes" the child. In the case of widthModel.natural, the // boxParent's width is likely a key part of the child's width (e.g., "50%" or just // normal block-level behavior of 100% width) boxChildItem.measuresBox = widthModel.shrinkWrap ? boxChildItem.hasRawContent : widthModel.natural; if (boxChildItem.measuresBox) { children = me.boxChildren; if (children) { children.push(boxChildItem); } else { me.boxChildren = [ boxChildItem ]; } } }, /** * Adds a trigger. * * @param {String} propName The property name that triggers the layout (e.g., 'width'). * @param {Boolean} inDom True if the trigger list is `domTriggers`, false if `triggers`. * @private */ addTrigger: function (propName, inDom) { var me = this, collection = inDom ? me.domTriggers : me.triggers, context = me.context, layout = context.currentLayout, triggers = collection[propName] || (collection[propName] = {}); if (!triggers[layout.id]) { triggers[layout.id] = layout; ++layout.triggerCount; triggers = context.triggers[inDom ? 'dom' : 'data']; (triggers[layout.id] || (triggers[layout.id] = [])).push({ item: this, prop: propName }); if (me.props[propName] !== undefined) { if (!inDom || !(me.dirty && (propName in me.dirty))) { ++layout.firedTriggers; } } } }, boxChildMeasured: function () { var me = this, state = me.state, count = (state.boxesMeasured = (state.boxesMeasured || 0) + 1); if (count == me.boxChildren.length) { // all of our children have measured themselves, so we can clear the width // and resume layouts for this component... state.clearBoxWidth = 1; ++me.context.progressCount; me.markDirty(); } }, borderNames: [ 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'], marginNames: [ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ], paddingNames: [ 'padding-top', 'padding-right', 'padding-bottom', 'padding-left' ], trblNames: [ 'top', 'right', 'bottom', 'left' ], cacheMissHandlers: { borderInfo: function (me) { var info = me.getStyles(me.borderNames, me.trblNames); info.width = info.left + info.right; info.height = info.top + info.bottom; return info; }, marginInfo: function (me) { var info = me.getStyles(me.marginNames, me.trblNames); info.width = info.left + info.right; info.height = info.top + info.bottom; return info; }, paddingInfo: function (me) { // if this context item's target is a framed component the padding is on the frameBody, not on the main el var item = me.frameBodyContext || me, info = item.getStyles(me.paddingNames, me.trblNames); info.width = info.left + info.right; info.height = info.top + info.bottom; return info; } }, checkCache: function (entry) { return this.cacheMissHandlers[entry](this); }, clearAllBlocks: function (name) { var collection = this[name], propName; if (collection) { for (propName in collection) { this.clearBlocks(name, propName); } } }, /** * Removes any blocks on a property in the specified set. Any layouts that were blocked * by this property and are not still blocked (by other properties) will be rescheduled. * * @param {String} name The name of the block list ('blocks' or 'domBlocks'). * @param {String} propName The property name that blocked the layout (e.g., 'width'). * @private */ clearBlocks: function (name, propName) { var collection = this[name], blockedLayouts = collection && collection[propName], context, layout, layoutId; if (blockedLayouts) { delete collection[propName]; context = this.context; for (layoutId in blockedLayouts) { layout = blockedLayouts[layoutId]; --context.blockCount; if (! --layout.blockCount && !layout.pending && !layout.done) { context.queueLayout(layout); } } } }, /** * Registers a layout in the block list for the given property. Once the property is * set in the {@link Ext.layout.Context}, the layout is unblocked. * * @param {Ext.layout.Layout} layout * @param {String} propName The property name that blocked the layout (e.g., 'width'). */ block: function (layout, propName) { this.addBlock('blocks', layout, propName); }, /** * Registers a layout in the DOM block list for the given property. Once the property * flushed to the DOM by the {@link Ext.layout.Context}, the layout is unblocked. * * @param {Ext.layout.Layout} layout * @param {String} propName The property name that blocked the layout (e.g., 'width'). */ domBlock: function (layout, propName) { this.addBlock('domBlocks', layout, propName); }, /** * Clears all properties on this object except (perhaps) those not calculated by this * component. This is more complex than it would seem because a layout can decide to * invalidate its results and run the component's layouts again, but since some of the * values may be calculated by the container, care must be taken to preserve those * values. * * @param {Boolean} full True if all properties are to be invalidated, false to keep * those calculated by the ownerCt. */ doInvalidate: function (full) { var me = this, oldProps = me.props, oldDirty = me.dirty, ownerLayout = me.target.ownerLayout; me.dirty = me.invalid = false; me.props = {}; me.state = {}; me.clearAllBlocks('blocks'); me.clearAllBlocks('domBlocks'); // If we had dirty properties and we are not fully invalidating, we need to recover // some of thos values... if (me.wrapsComponent) { if (!full) { // these are almost always calculated by the ownerCt (we might need to track // this at some point more carefully): me.recoverProp('x', oldProps, oldDirty); me.recoverProp('y', oldProps, oldDirty); // if these are calculated by the ownerCt, don't trash them: if (me.widthModel.calculated) { me.recoverProp('width', oldProps, oldDirty); } if (me.heightModel.calculated) { me.recoverProp('height', oldProps, oldDirty); } } else { me.widthModel = me.sizeModel.width; me.heightModel = me.sizeModel.height; } } if (ownerLayout && ownerLayout.manageMargins) { me.recoverProp('margin-top', oldProps, oldDirty); me.recoverProp('margin-right', oldProps, oldDirty); me.recoverProp('margin-bottom', oldProps, oldDirty); me.recoverProp('margin-left', oldProps, oldDirty); } }, /** * Reschedules any layouts associated with a given trigger. * * @param {String} name The name of the trigger list ('triggers' or 'domTriggers'). * @param {String} propName The property name that triggers the layout (e.g., 'width'). * @private */ fireTriggers: function (name, propName) { var triggers = this[name][propName], context = this.context, layout, layoutId; if (triggers) { for (layoutId in triggers) { layout = triggers[layoutId]; ++layout.firedTriggers; if (!layout.done && !layout.blockCount && !layout.pending) { context.queueLayout(layout); } } } }, /** * Flushes any updates in the dirty collection to the DOM. This is only called if there * are dirty entries because this object is only added to the flushQueue of the * {@link Ext.layout.Context} when entries become dirty. */ flush: function () { var me = this, dirty = me.dirty, state = me.state, targetEl = me.el; me.dirtyCount = 0; // Flush added/removed classes if (me.classList && me.classList.dirty) { me.classList.flush(); } // Set any queued DOM attributes if ('attributes' in me) { targetEl.set(me.attributes); delete me.attributes; } // Set any queued DOM HTML content if ('innerHTML' in me) { targetEl.innerHTML = me.innerHTML; delete me.innerHTML; } if (state.clearBoxWidth) { state.clearBoxWidth = 0; me.el.setStyle('width', null); if (! --state.blocks) { me.context.queueItemLayouts(me); } } if (dirty) { delete me.dirty; me.writeProps(dirty, true); } }, flushAnimations: function() { var me = this, animateFrom = me.lastBox, target, targetAnim, duration, animateProps, anim, changeCount, j, propsLen, propName, oldValue, newValue; // Only animate if the Component has been previously layed out: first layout should not animate if (animateFrom) { target = me.target; targetAnim = target.layout && target.layout.animate; if (targetAnim) { duration = Ext.isNumber(targetAnim) ? targetAnim : targetAnim.duration; } animateProps = Ext.Object.getKeys(me.animatePolicy); // Create an animation block using the targetAnim configuration to provide defaults. // They may want custom duration, or easing, or listeners. anim = Ext.apply({}, { from: {}, to: {}, duration: duration || Ext.fx.Anim.prototype.duration }, targetAnim); for (changeCount = 0, j = 0, propsLen = animateProps.length; j < propsLen; j++) { propName = animateProps[j]; oldValue = animateFrom[propName]; newValue = me.peek(propName); if (oldValue != newValue) { propName = me.translateProps[propName]||propName; anim.from[propName] = oldValue; anim.to[propName] = newValue; ++changeCount; } } // If any values have changed, kick off animation from the cached old values to the new values if (changeCount) { // It'a Panel being collapsed. rollback, and then fix the class name string if (me.isCollapsingOrExpanding === 1) { target.componentLayout.undoLayout(me); } // Otherwise, undo just the animated properties so the animation can proceed from the old layout. else { me.writeProps(anim.from); } me.el.animate(anim); Ext.fx.Manager.getFxQueue(me.el.id)[0].on({ afteranimate: function() { if (me.isCollapsingOrExpanding === 1) { target.componentLayout.redoLayout(me); target.afterCollapse(true); } else if (me.isCollapsingOrExpanding === 2) { target.afterExpand(true); } } }); } } }, /** * Gets the border information for the element as an object with left, top, right and * bottom properties holding border size in pixels. This object is only read from the * DOM on first request and is cached. * @return {Object} */ getBorderInfo: function () { var me = this, info = me.borderInfo; if (!info) { me.borderInfo = info = me.checkCache('borderInfo'); } return info; }, /** * Returns a ClassList-like object to buffer access to this item's element's classes. */ getClassList: function () { return this.classList || (this.classList = new Ext.layout.ClassList(this)); }, /** * Returns the context item for an owned element. This should only be called on a * component's item. The list of child items is used to manage invalidating calculated * results. */ getEl: function (nameOrEl, owner) { var me = this, src, el, elContext; if (nameOrEl) { if (nameOrEl.dom) { el = nameOrEl; } else { src = me.target; if (owner) { src = owner; } el = src[nameOrEl]; if (typeof el == 'function') { // ex 'getTarget' el = el.call(src); if (el === me.el) { return this; // comp.getTarget() often returns comp.el } } } if (el) { elContext = me.context.getEl(me, el); } } return elContext || null; }, getFraming: function () { var me = this; if (!me.framingInfo) { me.framingInfo = me.target.frameSize || me.noFraming; } return me.framingInfo; }, /** * Gets the "frame" information for the element as an object with left, top, right and * bottom properties holding border+framing size in pixels. This object is calculated * on first request and is cached. * @return {Object} */ getFrameInfo: function () { var me = this, info = me.frameInfo, frame, border; if (!info) { frame = me.getFraming(); border = me.getBorderInfo(); me.frameInfo = info = { top : frame.top + border.top, right : frame.right + border.right, bottom: frame.bottom + border.bottom, left : frame.left + border.left, width : frame.width + border.width, height: frame.height + border.height }; } return info; }, /** * Gets the margin information for the element as an object with left, top, right and * bottom properties holding margin size in pixels. This object is only read from the * DOM on first request and is cached. * @return {Object} */ getMarginInfo: function () { var me = this, info = me.marginInfo, comp, manageMargins, margins, ownerLayout, ownerLayoutId; if (!info) { if (!me.wrapsComponent) { info = me.checkCache('marginInfo'); } else { comp = me.target; ownerLayout = comp.ownerLayout; ownerLayoutId = ownerLayout ? ownerLayout.id : null; manageMargins = ownerLayout && ownerLayout.manageMargins; // Option #1 for configuring margins on components is the "margin" config // property. When supplied, this config is written to the DOM during the // render process (see AbstractComponent#initStyles). // // Option #2 is available to some layouts (e.g., Box, Border, Fit) that // handle margin calculations themselves. These layouts support a "margins" // config property on their items and they have a "defaultMargins" config // property. These margin values are added to the "natural" margins read // from the DOM and 0's are written to the DOM after they are added. // To avoid having to do all this on every layout, we cache the results on // the component in the (private) "margin$" property. We identify the cache // results as belonging to the appropriate ownerLayout in case items are // moved around. info = comp.margin$; if (info && info.ownerId !== ownerLayoutId) { // got one but from the wrong owner info = null; // if (manageMargins) { // TODO: clear inline margins (the 0's we wrote last time)??? // } } if (!info) { // if (no cache) // CSS margins are only checked if there isn't a margin property on the component info = me.parseMargins(comp.margin) || me.checkCache('marginInfo'); // Some layouts also support margins and defaultMargins, e.g. Fit, Border, Box if (manageMargins) { margins = me.parseMargins(comp.margins, ownerLayout.defaultMargins); if (margins) { // if (using 'margins' and/or 'defaultMargins') // margin and margins can both be present at the same time and must be combined info = { top: info.top + margins.top, right: info.right + margins.right, bottom: info.bottom + margins.bottom, left: info.left + margins.left }; } me.setProp('margin-top', 0); me.setProp('margin-right', 0); me.setProp('margin-bottom', 0); me.setProp('margin-left', 0); } // cache the layout margins and tag them with the layout id: info.ownerId = ownerLayoutId; comp.margin$ = info; } info.width = info.left + info.right; info.height = info.top + info.bottom; } me.marginInfo = info; } return info; }, /** * clears the margin cache so that marginInfo get re-read from the dom on the next call to getMarginInfo() * This is needed in some special cases where the margins have changed since the last layout, making the cached * values invalid. For example collapsed window headers have different margin than expanded ones. */ clearMarginCache: function() { delete this.marginInfo; delete this.target.margin$; }, /** * Gets the padding information for the element as an object with left, top, right and * bottom properties holding padding size in pixels. This object is only read from the * DOM on first request and is cached. * @return {Object} */ getPaddingInfo: function () { var me = this, info = me.paddingInfo; if (!info) { me.paddingInfo = info = me.checkCache('paddingInfo'); } return info; }, /** * Gets a property of this object. Also tracks the current layout as dependent on this * property so that changes to it will trigger the layout to be recalculated. * @param {String} propName The property name that blocked the layout (e.g., 'width'). * @return {Object} The property value or undefined if not yet set. */ getProp: function (propName) { var me = this, result = me.props[propName]; me.addTrigger(propName); return result; }, /** * Gets a property of this object if it is correct in the DOM. Also tracks the current * layout as dependent on this property so that DOM writes of it will trigger the * layout to be recalculated. * @param {String} propName The property name (e.g., 'width'). * @return {Object} The property value or undefined if not yet set or is dirty. */ getDomProp: function (propName) { var me = this, result = (me.dirty && (propName in me.dirty)) ? undefined : me.props[propName]; me.addTrigger(propName, true); return result; }, /** * Returns a style for this item. Each style is read from the DOM only once on first * request and is then cached. If the value is an integer, it is parsed automatically * (so '5px' is not returned, but rather 5). * * @param {String} styleName The CSS style name. * @return {Object} The value of the DOM style (parsed as necessary). */ getStyle: function (styleName) { var me = this, styles = me.styles, info, value; if (styleName in styles) { value = styles[styleName]; } else { info = me.styleInfo[styleName]; value = me.el.getStyle(styleName); if (info && info.parseInt) { value = parseInt(value, 10) || 0; } styles[styleName] = value; } return value; }, /** * Returns styles for this item. Each style is read from the DOM only once on first * request and is then cached. If the value is an integer, it is parsed automatically * (so '5px' is not returned, but rather 5). * * @param {String[]} styleNames The CSS style names. * @param {String[]} [altNames] The alternate names for the returned styles. If given, * these names must correspond one-for-one to the {@link #styleNames}. * @return {Object} The values of the DOM styles (parsed as necessary). */ getStyles: function (styleNames, altNames) { var me = this, styleCache = me.styles, values = {}, hits = 0, n = styleNames.length, i, missing, missingAltNames, name, info, styleInfo, styles, value; altNames = altNames || styleNames; // We are optimizing this for all hits or all misses. If we hit on all styles, we // don't create a missing[]. If we miss on all styles, we also don't create one. for (i = 0; i < n; ++i) { name = styleNames[i]; if (name in styleCache) { values[altNames[i]] = styleCache[name]; ++hits; if (i && hits==1) { // if (first hit was after some misses) missing = styleNames.slice(0, i); missingAltNames = altNames.slice(0, i); } } else if (hits) { (missing || (missing = [])).push(name); (missingAltNames || (missingAltNames = [])).push(altNames[i]); } } if (hits < n) { missing = missing || styleNames; missingAltNames = missingAltNames || altNames; styleInfo = me.styleInfo; styles = me.el.getStyle(missing); for (i = missing.length; i--; ) { name = missing[i]; info = styleInfo[name]; value = styles[name]; if (info && info.parseInt) { value = parseInt(value, 10) || 0; } values[missingAltNames[i]] = value; styleCache[name] = value; } } return values; }, /** * Returns true if the given property has been set. This is equivalent to calling * {@link #getProp} and not getting an undefined result. In particular, this call * registers the current layout to be triggered by changes to this property. * * @param {String} propName The property name (e.g., 'width'). * @return {Boolean} */ hasProp: function (propName) { var value = this.getProp(propName); return typeof value != 'undefined'; }, /** * Returns true if the given property is correct in the DOM. This is equivalent to * calling {@link #getDomProp} and not getting an undefined result. In particular, * this call registers the current layout to be triggered by flushes of this property. * * @param {String} propName The property name (e.g., 'width'). * @return {Boolean} */ hasDomProp: function (propName) { var value = this.getDomProp(propName); return typeof value != 'undefined'; }, /** * Invalidates the component associated with this item. The layouts for this component * and all of its contained items will be re-run after first clearing any computed * values. * * If state needs to be carried forward beyond the invalidation, the `options` parameter * can be used. * * @param {Object} options An object describing how to handle the invalidation. * @param {Object} options.state An object to {@link Ext#apply} to the {@link #state} * of this item after invalidation clears all other properties. * @param {Function} options.before A function to call after the context data is cleared * and before the {@link Ext.layout.Layout#beginLayoutCycle} methods are called. * @param {Ext.layout.ContextItem} options.before.item This ContextItem. * @param {Object} options.before.options The options object passed to {@link #invalidate}. * @param {Function} options.after A function to call after the context data is cleared * and after the {@link Ext.layout.Layout#beginLayoutCycle} methods are called. * @param {Ext.layout.ContextItem} options.after.item This ContextItem. * @param {Object} options.after.options The options object passed to {@link #invalidate}. * @param {Object} options.scope The scope to use when calling the callback functions. */ invalidate: function (options) { this.invalid = true; this.context.queueInvalidate(this, options); }, markDirty: function () { if (++this.dirtyCount == 1) { // our first dirty property... queue us for flush this.context.queueFlush(this); } }, onBoxMeasured: function () { var boxParent = this.boxParent, state = this.state; if (boxParent && boxParent.widthModel.shrinkWrap && !state.boxMeasured && this.measuresBox) { // since an autoWidth boxParent is holding a width on itself to allow each // child to measure state.boxMeasured = 1; // best to only call once per child boxParent.boxChildMeasured(); } }, parseMargins: function (margins, defaultMargins) { if (margins === true) { margins = 5; } var type = typeof margins, ret; if (type == 'string' || type == 'number') { ret = Ext.util.Format.parseBox(margins); } else if (margins || defaultMargins) { ret = { top: 0, right: 0, bottom: 0, left: 0 }; // base defaults if (defaultMargins) { Ext.apply(ret, this.parseMargins(defaultMargins)); // + layout defaults } Ext.apply(ret, margins); // + config } return ret; }, peek: function (propName) { return this.props[propName]; }, /** * Recovers a property value from the last computation and restores its value and * dirty state. * * @param {String} propName The name of the property to recover. * @param {Object} oldProps The old "props" object from which to recover values. * @param {Object} oldDirty The old "dirty" object from which to recover state. */ recoverProp: function (propName, oldProps, oldDirty) { var me = this, props = me.props, dirty; if (propName in oldProps) { props[propName] = oldProps[propName]; if (oldDirty && propName in oldDirty) { dirty = me.dirty || (me.dirty = {}); dirty[propName] = oldDirty[propName]; } } }, redo: function(deep) { var me = this, items, len, i; me.revertProps(me.props); if (deep && me.wrapsComponent) { // Rollback the state of child Components if (me.childItems) { for (i = 0, items = me.childItems, len = items.length; i < len; i++) { items[i].redo(deep); } } // Rollback the state of child Elements for (i = 0, items = me.children, len = items.length; i < len; i++) { items[i].redo(); } } }, revertProps: function (props) { var name, flushed = this.flushedProps, reverted = {}; for (name in props) { if (flushed.hasOwnProperty(name)) { reverted[name] = props[name]; } } this.writeProps(reverted); }, /** * Queue the setting of a DOM attribute on this ContextItem's target when next flushed. */ setAttribute: function(name, value) { var me = this; if (!me.attributes) { me.attributes = {}; } me.attributes[name] = value; me.markDirty(); }, setBox: function (box) { var me = this; if ('left' in box) { me.setProp('x', box.left); } if ('top' in box) { me.setProp('y', box.top); } // if sizeModel says we should not be setting these, the appropriate calls will be // null operations... otherwise, we must set these values, so what we have in box // is what we go with (undefined, NaN and no change are handled at a lower level): me.setSize(box.width, box.height); }, /** * Sets the contentHeight property. If the component uses raw content, then only the * measured height is acceptable. * * Calculated values can sometimes be NaN or undefined, which generally mean the * calculation is not done. To indicate that such as value was passed, 0 is returned. * Otherwise, 1 is returned. * * If the caller is not measuring (i.e., they are calculating) and the component has raw * content, 1 is returned indicating that the caller is done. */ setContentHeight: function (height, measured) { if (!measured && this.hasRawContent) { return 1; } return this.setProp('contentHeight', height); }, /** * Sets the contentWidth property. If the component uses raw content, then only the * measured width is acceptable. * * Calculated values can sometimes be NaN or undefined, which generally means that the * calculation is not done. To indicate that such as value was passed, 0 is returned. * Otherwise, 1 is returned. * * If the caller is not measuring (i.e., they are calculating) and the component has raw * content, 1 is returned indicating that the caller is done. */ setContentWidth: function (width, measured) { if (!measured && this.hasRawContent) { return 1; } return this.setProp('contentWidth', width); }, /** * Sets the contentWidth and contentHeight properties. If the component uses raw content, * then only the measured values are acceptable. * * Calculated values can sometimes be NaN or undefined, which generally means that the * calculation is not done. To indicate that either passed value was such a value, false * returned. Otherwise, true is returned. * * If the caller is not measuring (i.e., they are calculating) and the component has raw * content, true is returned indicating that the caller is done. */ setContentSize: function (width, height, measured) { return this.setContentWidth(width, measured) + this.setContentHeight(height, measured) == 2; }, /** * Sets a property value. This will unblock and/or trigger dependent layouts if the * property value is being changed. Values of NaN and undefined are not accepted by * this method. * * @param {String} propName The property name (e.g., 'width'). * @param {Object} value The new value of the property. * @param {Boolean} dirty Optionally specifies if the value is currently in the DOM * (default is `true` which indicates the value is not in the DOM and must be flushed * at some point). * @return {Number} 1 if this call specified the property value, 0 if not. */ setProp: function (propName, value, dirty) { var me = this, valueType = typeof value, borderBox, info; if (valueType == 'undefined' || (valueType === 'number' && isNaN(value))) { return 0; } if (me.props[propName] === value) { return 1; } me.props[propName] = value; ++me.context.progressCount; if (dirty === false) { // if the prop is equivalent to what is in the DOM (we won't be writing it), // we need to clear hard blocks (domBlocks) on that property. me.fireTriggers('domTriggers', propName); me.clearBlocks('domBlocks', propName); } else { info = me.styleInfo[propName]; if (info) { if (!me.dirty) { me.dirty = {}; } if (propName == 'width' || propName == 'height') { borderBox = me.isBorderBoxValue; if (borderBox == null) { me.isBorderBoxValue = borderBox = !!me.el.isBorderBox(); } if (!borderBox) { me.borderInfo || me.getBorderInfo(); me.paddingInfo || me.getPaddingInfo(); } } me.dirty[propName] = value; me.markDirty(); } } // we always clear soft blocks on set me.fireTriggers('triggers', propName); me.clearBlocks('blocks', propName); return 1; }, /** * Sets the height and constrains the height to min/maxHeight range. * * @param {Number} height The height. * @param {Boolean} [dirty=true] Specifies if the value is currently in the DOM. A * value of `false` indicates that the value is already in the DOM. * @return {Number} The actual height after constraining. */ setHeight: function (height, dirty /*, private {Boolean} force */) { var me = this, comp = me.target, frameBody, frameInfo, padding; if (height < 0) { height = 0; } if (!me.wrapsComponent) { if (!me.setProp('height', height, dirty)) { return NaN; } } else { height = Ext.Number.constrain(height, comp.minHeight || 0, comp.maxHeight); if (!me.setProp('height', height, dirty)) { return NaN; } frameBody = me.frameBodyContext; if (frameBody){ frameInfo = me.getFrameInfo(); frameBody.setHeight(height - frameInfo.height, dirty); } } return height; }, /** * Sets the height and constrains the width to min/maxWidth range. * * @param {Number} width The width. * @param {Boolean} [dirty=true] Specifies if the value is currently in the DOM. A * value of `false` indicates that the value is already in the DOM. * @return {Number} The actual width after constraining. */ setWidth: function (width, dirty /*, private {Boolean} force */) { var me = this, comp = me.target, frameBody, frameInfo, padding; if (width < 0) { width = 0; } if (!me.wrapsComponent) { if (!me.setProp('width', width, dirty)) { return NaN; } } else { width = Ext.Number.constrain(width, comp.minWidth || 0, comp.maxWidth); if (!me.setProp('width', width, dirty)) { return NaN; } //if ((frameBody = me.target.frameBody) && (frameBody = me.getEl(frameBody))){ frameBody = me.frameBodyContext; if (frameBody) { frameInfo = me.getFrameInfo(); frameBody.setWidth(width - frameInfo.width, dirty); } /*if (owner.frameMC) { frameContext = ownerContext.frameContext || (ownerContext.frameContext = ownerContext.getEl('frameMC')); width += (frameContext.paddingInfo || frameContext.getPaddingInfo()).width; }*/ } return width; }, setSize: function (width, height, dirty) { this.setWidth(width, dirty); this.setHeight(height, dirty); }, translateProps: { x: 'left', y: 'top' }, undo: function(deep) { var me = this, items, len, i; me.revertProps(me.lastBox); if (deep && me.wrapsComponent) { // Rollback the state of child Components if (me.childItems) { for (i = 0, items = me.childItems, len = items.length; i < len; i++) { items[i].undo(deep); } } // Rollback the state of child Elements for (i = 0, items = me.children, len = items.length; i < len; i++) { items[i].undo(); } } }, unsetProp: function (propName) { var dirty = this.dirty; delete this.props[propName]; if (dirty) { delete dirty[propName]; } }, writeProps: function(dirtyProps, flushing) { if (!(dirtyProps && typeof dirtyProps == 'object')) { // Ext.Logger.warn('writeProps expected dirtyProps to be an object'); // return; } var me = this, el = me.el, styles = {}, styleCount = 0, // used as a boolean, the exact count doesn't matter styleInfo = me.styleInfo, info, propName, numericValue, dirtyX = 'x' in dirtyProps, dirtyY = 'y' in dirtyProps, x = dirtyProps.x, y = dirtyProps.y, width = dirtyProps.width, height = dirtyProps.height, isBorderBox = me.isBorderBoxValue, target = me.target, max = Math.max, paddingWidth = 0, paddingHeight = 0; // Process non-style properties: if ('displayed' in dirtyProps) { el.setDisplayed(dirtyProps.displayed); } // Unblock any hard blocks (domBlocks) and copy dom styles into 'styles' for (propName in dirtyProps) { if (flushing) { me.fireTriggers('domTriggers', propName); me.clearBlocks('domBlocks', propName); me.flushedProps[propName] = 1; } info = styleInfo[propName]; if (info && info.dom) { // Numeric dirty values should have their associated suffix added if (info.suffix && (numericValue = parseInt(dirtyProps[propName], 10))) { styles[propName] = numericValue + info.suffix; } // Non-numeric (eg "auto") go in unchanged. else { styles[propName] = dirtyProps[propName]; } ++styleCount; } } // convert x/y into setPosition (for a component) or left/top styles (for an el) if (dirtyX || dirtyY) { if (target.isComponent) { // Ensure we always pass the current coordinate in if one coordinate has not been dirtied by a calculation cycle. target.setPosition(x||me.props.x, y||me.props.y); } else { // we wrap an element, so convert x/y to styles: if (dirtyX) { styles.left = x + 'px'; ++styleCount; } if (dirtyY) { styles.top = y + 'px'; ++styleCount; } } } // Support for the content-box box model if (!isBorderBox && (width > 0 || height > 0)) { // no need to subtract from 0 // The width and height values assume the border-box box model, // so we must remove the padding & border to calculate the content-box. // if (!(me.borderInfo && me.paddingInfo)) { throw Error("Needed to have gotten the borderInfo and paddingInfo when the width or height was setProp'd"); } // if(!me.frameBodyContext) { // Padding needs to be removed only if the element is not framed. paddingWidth = me.paddingInfo.width; paddingHeight = me.paddingInfo.height; } if (width) { width = max(parseInt(width, 10) - (me.borderInfo.width + paddingWidth), 0); styles.width = width + 'px'; ++styleCount; } if (height) { height = max(parseInt(height, 10) - (me.borderInfo.height + paddingHeight), 0); styles.height = height + 'px'; ++styleCount; } } // we make only one call to setStyle to allow it to optimize itself: if (styleCount) { el.setStyle(styles); } } }, function () { var px = { dom: true, parseInt: true, suffix: 'px' }, isDom = { dom: true }, faux = { dom: false }; // If a property exists in styleInfo, it participates in some way with the DOM. It may // be virtualized (like 'x' and y') and be indirect, but still requires a flush cycle // to reach the DOM. Properties (like 'contentWidth' and 'contentHeight') have no real // presence in the DOM and hence have no flush intanglements. // // For simple styles, the object value on the right contains properties that help in // decoding values read by getStyle and preparing values to pass to setStyle. // this.prototype.styleInfo = { childrenDone: faux, componentChildrenDone: faux, containerChildrenDone: faux, containerLayoutDone: faux, displayed: faux, done: faux, x: faux, y: faux, left: px, top: px, right: px, bottom: px, width: px, height: px, 'border-top-width': px, 'border-right-width': px, 'border-bottom-width': px, 'border-left-width': px, 'margin-top': px, 'margin-right': px, 'margin-bottom': px, 'margin-left': px, 'padding-top': px, 'padding-right': px, 'padding-bottom': px, 'padding-left': px, 'line-height': isDom, display: isDom }; });