var Lang = Y.Lang,
isBoolean = Lang.isBoolean,
isNumber = Lang.isNumber,
isString = Lang.isString,
capitalize = Y.Resize.capitalize,
isNode = function(v) {
return (v instanceof Y.Node);
},
toNumber = function(num) {
return parseFloat(num) || 0;
},
BORDER_BOTTOM_WIDTH = 'borderBottomWidth',
BORDER_LEFT_WIDTH = 'borderLeftWidth',
BORDER_RIGHT_WIDTH = 'borderRightWidth',
BORDER_TOP_WIDTH = 'borderTopWidth',
BORDER = 'border',
BOTTOM = 'bottom',
CON = 'con',
CONSTRAIN = 'constrain',
HOST = 'host',
LEFT = 'left',
MAX_HEIGHT = 'maxHeight',
MAX_WIDTH = 'maxWidth',
MIN_HEIGHT = 'minHeight',
MIN_WIDTH = 'minWidth',
NODE = 'node',
OFFSET_HEIGHT = 'offsetHeight',
OFFSET_WIDTH = 'offsetWidth',
PRESEVE_RATIO = 'preserveRatio',
REGION = 'region',
RESIZE_CONTRAINED = 'resizeConstrained',
RIGHT = 'right',
TICK_X = 'tickX',
TICK_Y = 'tickY',
TOP = 'top',
WIDTH = 'width',
VIEW = 'view',
VIEWPORT_REGION = 'viewportRegion';
/**
A Resize plugin that will attempt to constrain the resize node to the boundaries.
@module resize
@submodule resize-contrain
@class ResizeConstrained
@param config {Object} Object literal specifying widget configuration properties.
@constructor
@extends Plugin.Base
@namespace Plugin
*/
function ResizeConstrained() {
ResizeConstrained.superclass.constructor.apply(this, arguments);
}
Y.mix(ResizeConstrained, {
NAME: RESIZE_CONTRAINED,
NS: CON,
ATTRS: {
/**
* Will attempt to constrain the resize node to the boundaries. Arguments:<br>
* 'view': Contrain to Viewport<br>
* '#selector_string': Constrain to this node<br>
* '{Region Object}': An Object Literal containing a valid region (top, right, bottom, left) of page positions
*
* @attribute constrain
* @type {String|Object|Node}
*/
constrain: {
setter: function(v) {
if (v && (isNode(v) || isString(v) || v.nodeType)) {
v = Y.one(v);
}
return v;
}
},
/**
* The minimum height of the element
*
* @attribute minHeight
* @default 15
* @type Number
*/
minHeight: {
value: 15,
validator: isNumber
},
/**
* The minimum width of the element
*
* @attribute minWidth
* @default 15
* @type Number
*/
minWidth: {
value: 15,
validator: isNumber
},
/**
* The maximum height of the element
*
* @attribute maxHeight
* @default Infinity
* @type Number
*/
maxHeight: {
value: Infinity,
validator: isNumber
},
/**
* The maximum width of the element
*
* @attribute maxWidth
* @default Infinity
* @type Number
*/
maxWidth: {
value: Infinity,
validator: isNumber
},
/**
* Maintain the element's ratio when resizing.
*
* @attribute preserveRatio
* @default false
* @type boolean
*/
preserveRatio: {
value: false,
validator: isBoolean
},
/**
* The number of x ticks to span the resize to.
*
* @attribute tickX
* @default false
* @type Number | false
*/
tickX: {
value: false
},
/**
* The number of y ticks to span the resize to.
*
* @attribute tickY
* @default false
* @type Number | false
*/
tickY: {
value: false
}
}
});
Y.extend(ResizeConstrained, Y.Plugin.Base, {
/**
* Stores the <code>constrain</code>
* surrounding information retrieved from
* <a href="Resize.html#method__getBoxSurroundingInfo">_getBoxSurroundingInfo</a>.
*
* @property constrainSurrounding
* @type Object
* @default null
*/
constrainSurrounding: null,
initializer: function() {
var instance = this,
host = instance.get(HOST);
host.delegate.dd.plug(
Y.Plugin.DDConstrained,
{
tickX: instance.get(TICK_X),
tickY: instance.get(TICK_Y)
}
);
host.after('resize:align', Y.bind(instance._handleResizeAlignEvent, instance));
host.on('resize:start', Y.bind(instance._handleResizeStartEvent, instance));
},
/**
* Helper method to update the current values on
* <a href="Resize.html#property_info">info</a> to respect the
* constrain node.
*
* @method _checkConstrain
* @param {String} axis 'top' or 'left'
* @param {String} axisConstrain 'bottom' or 'right'
* @param {String} offset 'offsetHeight' or 'offsetWidth'
* @protected
*/
_checkConstrain: function(axis, axisConstrain, offset) {
var instance = this,
point1,
point1Constrain,
point2,
point2Constrain,
host = instance.get(HOST),
info = host.info,
constrainBorders = instance.constrainSurrounding.border,
region = instance._getConstrainRegion();
if (region) {
point1 = info[axis] + info[offset];
point1Constrain = region[axisConstrain] - toNumber(constrainBorders[capitalize(BORDER, axisConstrain, WIDTH)]);
if (point1 >= point1Constrain) {
info[offset] -= (point1 - point1Constrain);
}
point2 = info[axis];
point2Constrain = region[axis] + toNumber(constrainBorders[capitalize(BORDER, axis, WIDTH)]);
if (point2 <= point2Constrain) {
info[axis] += (point2Constrain - point2);
info[offset] -= (point2Constrain - point2);
}
}
},
/**
* Update the current values on <a href="Resize.html#property_info">info</a>
* to respect the maxHeight and minHeight.
*
* @method _checkHeight
* @protected
*/
_checkHeight: function() {
var instance = this,
host = instance.get(HOST),
info = host.info,
maxHeight = (instance.get(MAX_HEIGHT) + host.totalVSurrounding),
minHeight = (instance.get(MIN_HEIGHT) + host.totalVSurrounding);
instance._checkConstrain(TOP, BOTTOM, OFFSET_HEIGHT);
if (info.offsetHeight > maxHeight) {
host._checkSize(OFFSET_HEIGHT, maxHeight);
}
if (info.offsetHeight < minHeight) {
host._checkSize(OFFSET_HEIGHT, minHeight);
}
},
/**
* Update the current values on <a href="Resize.html#property_info">info</a>
* calculating the correct ratio for the other values.
*
* @method _checkRatio
* @protected
*/
_checkRatio: function() {
var instance = this,
host = instance.get(HOST),
info = host.info,
originalInfo = host.originalInfo,
oWidth = originalInfo.offsetWidth,
oHeight = originalInfo.offsetHeight,
oTop = originalInfo.top,
oLeft = originalInfo.left,
oBottom = originalInfo.bottom,
oRight = originalInfo.right,
// wRatio/hRatio functions keep the ratio information always synced with the current info information
// RETURN: percentage how much width/height has changed from the original width/height
wRatio = function() {
return (info.offsetWidth/oWidth);
},
hRatio = function() {
return (info.offsetHeight/oHeight);
},
isClosestToHeight = host.changeHeightHandles,
bottomDiff,
constrainBorders,
constrainRegion,
leftDiff,
rightDiff,
topDiff;
// check whether the resizable node is closest to height or not
if (instance.get(CONSTRAIN) && host.changeHeightHandles && host.changeWidthHandles) {
constrainRegion = instance._getConstrainRegion();
constrainBorders = instance.constrainSurrounding.border;
bottomDiff = (constrainRegion.bottom - toNumber(constrainBorders[BORDER_BOTTOM_WIDTH])) - oBottom;
leftDiff = oLeft - (constrainRegion.left + toNumber(constrainBorders[BORDER_LEFT_WIDTH]));
rightDiff = (constrainRegion.right - toNumber(constrainBorders[BORDER_RIGHT_WIDTH])) - oRight;
topDiff = oTop - (constrainRegion.top + toNumber(constrainBorders[BORDER_TOP_WIDTH]));
if (host.changeLeftHandles && host.changeTopHandles) {
isClosestToHeight = (topDiff < leftDiff);
}
else if (host.changeLeftHandles) {
isClosestToHeight = (bottomDiff < leftDiff);
}
else if (host.changeTopHandles) {
isClosestToHeight = (topDiff < rightDiff);
}
else {
isClosestToHeight = (bottomDiff < rightDiff);
}
}
// when the height of the resizable element touch the border of the constrain first
// force the offsetWidth to be calculated based on the height ratio
if (isClosestToHeight) {
info.offsetWidth = oWidth*hRatio();
instance._checkWidth();
info.offsetHeight = oHeight*wRatio();
}
else {
info.offsetHeight = oHeight*wRatio();
instance._checkHeight();
info.offsetWidth = oWidth*hRatio();
}
// fixing the top on handles which are able to change top
// the idea here is change the top based on how much the height has changed instead of follow the dy
if (host.changeTopHandles) {
info.top = oTop + (oHeight - info.offsetHeight);
}
// fixing the left on handles which are able to change left
// the idea here is change the left based on how much the width has changed instead of follow the dx
if (host.changeLeftHandles) {
info.left = oLeft + (oWidth - info.offsetWidth);
}
// rounding values to avoid pixel jumpings
Y.each(info, function(value, key) {
if (isNumber(value)) {
info[key] = Math.round(value);
}
});
},
/**
* Check whether the resizable node is inside the constrain region.
*
* @method _checkRegion
* @protected
* @return {boolean}
*/
_checkRegion: function() {
var instance = this,
host = instance.get(HOST),
region = instance._getConstrainRegion();
return Y.DOM.inRegion(null, region, true, host.info);
},
/**
* Update the current values on <a href="Resize.html#property_info">info</a>
* to respect the maxWidth and minWidth.
*
* @method _checkWidth
* @protected
*/
_checkWidth: function() {
var instance = this,
host = instance.get(HOST),
info = host.info,
maxWidth = (instance.get(MAX_WIDTH) + host.totalHSurrounding),
minWidth = (instance.get(MIN_WIDTH) + host.totalHSurrounding);
instance._checkConstrain(LEFT, RIGHT, OFFSET_WIDTH);
if (info.offsetWidth < minWidth) {
host._checkSize(OFFSET_WIDTH, minWidth);
}
if (info.offsetWidth > maxWidth) {
host._checkSize(OFFSET_WIDTH, maxWidth);
}
},
/**
* Get the constrain region based on the <code>constrain</code>
* attribute.
*
* @method _getConstrainRegion
* @protected
* @return {Object Region}
*/
_getConstrainRegion: function() {
var instance = this,
host = instance.get(HOST),
node = host.get(NODE),
constrain = instance.get(CONSTRAIN),
region = null;
if (constrain) {
if (constrain === VIEW) {
region = node.get(VIEWPORT_REGION);
}
else if (isNode(constrain)) {
region = constrain.get(REGION);
}
else {
region = constrain;
}
}
return region;
},
_handleResizeAlignEvent: function() {
var instance = this,
host = instance.get(HOST);
// check the max/min height and locking top when these values are reach
instance._checkHeight();
// check the max/min width and locking left when these values are reach
instance._checkWidth();
// calculating the ratio, for proportionally resizing
if (instance.get(PRESEVE_RATIO)) {
instance._checkRatio();
}
if (instance.get(CONSTRAIN) && !instance._checkRegion()) {
host.info = host.lastInfo;
}
},
_handleResizeStartEvent: function() {
var instance = this,
constrain = instance.get(CONSTRAIN),
host = instance.get(HOST);
instance.constrainSurrounding = host._getBoxSurroundingInfo(constrain);
}
});
Y.namespace('Plugin');
Y.Plugin.ResizeConstrained = ResizeConstrained;