/**
* 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
};
});