/**
* This is a layout that enables anchoring of contained elements relative to the container's
* dimensions. If the container is resized, all anchored items are automatically rerendered
* according to their `{@link #anchor}` rules.
*
* This class is intended to be extended or created via the
* {@link Ext.container.Container#layout layout}: 'anchor' config, and should generally not need
* to be created directly via the new keyword.
*
* AnchorLayout does not have any direct config options (other than inherited ones). By default,
* AnchorLayout will calculate anchor measurements based on the size of the container itself.
* However, the container using the AnchorLayout can supply an anchoring-specific config property
* of `anchorSize`.
*
* If anchorSize is specifed, the layout will use it as a virtual container for the purposes
* of calculating anchor measurements based on it instead, allowing the container to be sized
* independently of the anchoring logic if necessary.
*
* @example
* Ext.create('Ext.Panel', {
* width: 500,
* height: 400,
* title: "AnchorLayout Panel",
* layout: 'anchor',
* renderTo: Ext.getBody(),
* items: [
* {
* xtype: 'panel',
* title: '75% Width and 20% Height',
* anchor: '75% 20%'
* },
* {
* xtype: 'panel',
* title: 'Offset -300 Width & -200 Height',
* anchor: '-300 -200'
* },
* {
* xtype: 'panel',
* title: 'Mixed Offset and Percent',
* anchor: '-250 20%'
* }
* ]
* });
*/
Ext.define('Ext.layout.container.Anchor', {
extend: 'Ext.layout.container.Auto',
alternateClassName: 'Ext.layout.AnchorLayout',
alias: 'layout.anchor',
type: 'anchor',
/**
* @cfg {String} anchor
*
* This configuration option is to be applied to **child `items`** of a container managed
* by an {@link Ext.layout.container.Anchor Anchor Layout}.
*
* This value is what tells the layout how an item should be anchored to the container. `items`
* added to an AnchorLayout accept an anchoring-specific config property of **anchor**
* which is a string containing two values: the horizontal anchor value and the vertical anchor
* value (for example, '100% 50%'). The following types of anchor values are supported:
*
* - **Percentage** : Any value between 1 and 100, expressed as a percentage.
*
* The first anchor is the percentage width that the item should take up within the container,
* and the second is the percentage height. For example:
*
* // two values specified
* anchor: '100% 50%' // render item complete width of the container and
* // 1/2 height of the container
* // one value specified
* anchor: '100%' // the width value; the height will default to auto
*
* - **Offsets** : Any positive or negative integer value.
*
* This is a raw adjustment where the first anchor is the offset from the right edge
* of the container, and the second is the offset from the bottom edge. For example:
*
* // two values specified
* anchor: '-50 -100' // render item the complete width of the container
* // minus 50 pixels and
* // the complete height minus 100 pixels.
* // one value specified
* anchor: '-50' // anchor value is assumed to be the right offset value
* // bottom offset will default to 0
*
* - **Sides** : Valid values are `right` (or `r`) and `bottom` (or `b`).
*
* Either the container must have a fixed size or an anchorSize config value defined
* at render time in order for these to have any effect.
*
* - **Mixed** :
*
* Anchor values can also be mixed as needed. For example, to render the width offset
* from the container right edge by 50 pixels and 75% of the container's height use:
*
* anchor: '-50 75%'
*/
/**
* @cfg {String} defaultAnchor
* Default anchor for all child **container** items applied if no anchor or specific width
* is set on the child item.
*/
defaultAnchor: '100%',
parseAnchorRE: /^(r|right|b|bottom)$/i,
manageOverflow: true,
// Anchor layout does not read the size of individual items in the shrink-wrapping
// dimension(s) because, as a subclass of autocontainer, it measures them as a whole
// using an outer element. However, anchor layout may set the size of its items in
// non-shrink-wrapping dimension(s).
setsItemSize: true,
beginLayoutCycle: function(ownerContext) {
var me = this,
dimensions = 0,
anchorSpec, childContext, childItems, i, length;
me.callParent(arguments);
childItems = ownerContext.childItems; // populated by callParent
length = childItems.length;
for (i = 0; i < length; ++i) {
childContext = childItems[i];
anchorSpec = childContext.target.anchorSpec;
if (anchorSpec) {
if (childContext.widthModel.calculated && anchorSpec.right) {
dimensions |= 1;
}
if (childContext.heightModel.calculated && anchorSpec.bottom) {
dimensions |= 2;
}
if (dimensions === 3) { // if (both dimensions in play)
break;
}
}
}
ownerContext.anchorDimensions = dimensions;
//<debug>
me.sanityCheck(ownerContext);
//</debug>
},
calculateItems: function(ownerContext, containerSize) {
var me = this,
childItems = ownerContext.childItems,
length = childItems.length,
gotHeight = containerSize.gotHeight,
gotWidth = containerSize.gotWidth,
ownerHeight = containerSize.height,
ownerWidth = containerSize.width,
knownDimensions = (gotWidth ? 1 : 0) | (gotHeight ? 2 : 0),
anchorDimensions = ownerContext.anchorDimensions,
anchorSpec, childContext, childMargins, height, i, width;
if (!anchorDimensions) {
return true;
}
for (i = 0; i < length; i++) {
childContext = childItems[i];
childMargins = childContext.getMarginInfo();
anchorSpec = childContext.target.anchorSpec;
// Check widthModel in case "defaults" has applied an anchor to a component
// that also has width (which must win). If we did not make this check in this
// way, we would attempt to calculate a width where it had been configured.
//
if (gotWidth && childContext.widthModel.calculated) {
width = anchorSpec.right(ownerWidth) - childMargins.width;
width = me.adjustWidthAnchor(width, childContext);
childContext.setWidth(width);
}
// Repeat for height
if (gotHeight && childContext.heightModel.calculated) {
height = anchorSpec.bottom(ownerHeight) - childMargins.height;
height = me.adjustHeightAnchor(height, childContext);
childContext.setHeight(height);
}
}
// If all required dimensions are known, we're done
return (knownDimensions & anchorDimensions) === anchorDimensions;
},
//<debug>
sanityCheck: function(ownerContext) {
var shrinkWrapWidth = ownerContext.widthModel.shrinkWrap,
shrinkWrapHeight = ownerContext.heightModel.shrinkWrap,
children = ownerContext.childItems,
anchorSpec, comp, childContext,
i, length;
for (i = 0, length = children.length; i < length; ++i) {
childContext = children[i];
comp = childContext.target;
anchorSpec = comp.anchorSpec;
if (anchorSpec) {
if (childContext.widthModel.calculated && anchorSpec.right) {
if (shrinkWrapWidth) {
Ext.log({
level: 'warn',
msg: 'Right anchor on ' + comp.id + ' in shrinkWrap width container'
});
}
}
if (childContext.heightModel.calculated && anchorSpec.bottom) {
if (shrinkWrapHeight) {
Ext.log({
level: 'warn',
msg: 'Bottom anchor on ' + comp.id + ' in shrinkWrap height container'
});
}
}
}
}
},
//</debug>
/**
* @private
*/
anchorFactory: {
offset: function(delta) {
return function(v) {
return v + delta;
};
},
ratio: function(ratio) {
return function(v) {
return Math.floor(v * ratio);
};
},
standard: function(diff) {
return function(v) {
return v - diff;
};
}
},
parseAnchor: function(a, start, cstart) {
var factory = this.anchorFactory,
delta;
if (a && a !== 'none') {
if (this.parseAnchorRE.test(a)) {
return factory.standard(cstart - start);
}
if (a.indexOf('%') !== -1) {
return factory.ratio(parseFloat(a.replace('%', '')) * 0.01);
}
delta = parseInt(a, 10);
if (!isNaN(delta)) {
return factory.offset(delta);
}
}
return null;
},
/**
* @private
*/
adjustWidthAnchor: function(value, childContext) {
return value;
},
/**
* @private
*/
adjustHeightAnchor: function(value, childContext) {
return value;
},
configureItem: function(item) {
var me = this,
owner = me.owner,
anchor = item.anchor,
anchorsArray,
anchorWidth,
anchorHeight;
me.callParent(arguments);
if (!item.anchor && item.items && !Ext.isNumber(item.width)) {
item.anchor = anchor = me.defaultAnchor;
}
/**
* @cfg {Number/Object} anchorSize
* Defines the anchoring size of container.
* Either a number to define the width of the container or an object
* with `width` and `height` fields.
* @member Ext.container.Container
*/
if (owner.anchorSize) {
if (typeof owner.anchorSize === 'number') {
anchorWidth = owner.anchorSize;
}
else {
anchorWidth = owner.anchorSize.width;
anchorHeight = owner.anchorSize.height;
}
}
else {
anchorWidth = owner.initialConfig.width;
anchorHeight = owner.initialConfig.height;
}
if (anchor) {
// cache all anchor values
anchorsArray = anchor.split(' ');
item.anchorSpec = {
right: me.parseAnchor(anchorsArray[0], item.initialConfig.width, anchorWidth),
bottom: me.parseAnchor(anchorsArray[1], item.initialConfig.height, anchorHeight)
};
}
},
sizePolicy: {
$: {
readsWidth: 1,
readsHeight: 1,
setsWidth: 0,
setsHeight: 0
},
b: {
readsWidth: 1,
readsHeight: 0,
setsWidth: 0,
setsHeight: 1
},
r: {
$: {
readsWidth: 0,
readsHeight: 1,
setsWidth: 1,
setsHeight: 0
},
b: {
readsWidth: 0,
readsHeight: 0,
setsWidth: 1,
setsHeight: 1
}
}
},
getItemSizePolicy: function(item) {
var anchorSpec = item.anchorSpec,
key = '$',
policy = this.sizePolicy,
sizeModel;
if (anchorSpec) {
sizeModel = this.owner.getSizeModel();
if (anchorSpec.right && !sizeModel.width.shrinkWrap) {
policy = policy.r;
}
if (anchorSpec.bottom && !sizeModel.height.shrinkWrap) {
key = 'b';
}
}
return policy[key];
}
});