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;