/* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html version: 3.3.0 build: 3167 */ YUI.add('widget-position-constrain', function(Y) { /** * Provides constrained xy positioning support for Widgets, through an extension. * * It builds on top of the widget-position module, to provide constrained positioning support. * * @module widget-position-constrain */ var CONSTRAIN = "constrain", CONSTRAIN_XYCHANGE = "constrain|xyChange", CONSTRAIN_CHANGE = "constrainChange", PREVENT_OVERLAP = "preventOverlap", ALIGN = "align", EMPTY_STR = "", BINDUI = "bindUI", XY = "xy", X_COORD = "x", Y_COORD = "y", Node = Y.Node, VIEWPORT_REGION = "viewportRegion", REGION = "region", PREVENT_OVERLAP_MAP; /** * A widget extension, which can be used to add constrained xy positioning support to the base Widget class, * through the Base.build method. This extension requires that * the WidgetPosition extension be added to the Widget (before WidgetPositionConstrain, if part of the same * extension list passed to Base.build). * * @class WidgetPositionConstrain * @param {Object} User configuration object */ function PositionConstrain(config) { if (!this._posNode) { Y.error("WidgetPosition needs to be added to the Widget, before WidgetPositionConstrain is added"); } Y.after(this._bindUIPosConstrained, this, BINDUI); } /** * Static property used to define the default attribute * configuration introduced by WidgetPositionConstrain. * * @property WidgetPositionConstrain.ATTRS * @type Object * @static */ PositionConstrain.ATTRS = { /** * @attribute constrain * @type boolean | Node * @default null * @description The node to constrain the widget's bounding box to, when setting xy. Can also be * set to true, to constrain to the viewport. */ constrain : { value: null, setter: "_setConstrain" }, /** * @attribute preventOverlap * @type boolean * @description If set to true, and WidgetPositionAlign is also added to the Widget, * constrained positioning will attempt to prevent the widget's bounding box from overlapping * the element to which it has been aligned, by flipping the orientation of the alignment * for corner based alignments */ preventOverlap : { value:false } }; /** * @property WidgetPositionConstrain._PREVENT_OVERLAP * @static * @protected * @type Object * @description The set of positions for which to prevent * overlap. */ PREVENT_OVERLAP_MAP = PositionConstrain._PREVENT_OVERLAP = { x: { "tltr": 1, "blbr": 1, "brbl": 1, "trtl": 1 }, y : { "trbr": 1, "tlbl": 1, "bltl": 1, "brtr": 1 } }; PositionConstrain.prototype = { /** * Calculates the constrained positions for the XY positions provided, using * the provided node argument is passed in. If no node value is passed in, the value of * the "constrain" attribute is used. * * @method getConstrainedXY * @param {Array} xy The xy values to constrain * @param {Node | boolean} node Optional. The node to constrain to, or true for the viewport * @return {Array} The constrained xy values */ getConstrainedXY : function(xy, node) { node = node || this.get(CONSTRAIN); var constrainingRegion = this._getRegion((node === true) ? null : node), nodeRegion = this._posNode.get(REGION); return [ this._constrain(xy[0], X_COORD, nodeRegion, constrainingRegion), this._constrain(xy[1], Y_COORD, nodeRegion, constrainingRegion) ]; }, /** * Constrains the widget's bounding box to a node (or the viewport). If xy or node are not * passed in, the current position and the value of "constrain" will be used respectively. * * The widget's position will be changed to the constrained position. * * @param {Array} xy Optional. The xy values to constrain * @param {Node | boolean} node Optional. The node to constrain to, or true for the viewport */ constrain : function(xy, node) { var currentXY, constrainedXY, constraint = node || this.get(CONSTRAIN); if (constraint) { currentXY = xy || this.get(XY); constrainedXY = this.getConstrainedXY(currentXY, constraint); if (constrainedXY[0] !== currentXY[0] || constrainedXY[1] !== currentXY[1]) { this.set(XY, constrainedXY, { constrained:true }); } } }, /** * The setter implementation for the "constrain" attribute. * * @method _setConstrain * @protected * @param {Node | boolean} val The attribute value */ _setConstrain : function(val) { return (val === true) ? val : Node.one(val); }, /** * The method which performs the actual constrain calculations for a given axis ("x" or "y") based * on the regions provided. * * @method _constrain * @protected * * @param {Number} val The value to constrain * @param {String} axis The axis to use for constrainment * @param {Region} nodeRegion The region of the node to constrain * @param {Region} constrainingRegion The region of the node (or viewport) to constrain to * * @return {Number} The constrained value */ _constrain: function(val, axis, nodeRegion, constrainingRegion) { if (constrainingRegion) { if (this.get(PREVENT_OVERLAP)) { val = this._preventOverlap(val, axis, nodeRegion, constrainingRegion); } var x = (axis == X_COORD), regionSize = (x) ? constrainingRegion.width : constrainingRegion.height, nodeSize = (x) ? nodeRegion.width : nodeRegion.height, minConstraint = (x) ? constrainingRegion.left : constrainingRegion.top, maxConstraint = (x) ? constrainingRegion.right - nodeSize : constrainingRegion.bottom - nodeSize; if (val < minConstraint || val > maxConstraint) { if (nodeSize < regionSize) { if (val < minConstraint) { val = minConstraint; } else if (val > maxConstraint) { val = maxConstraint; } } else { val = minConstraint; } } } return val; }, /** * The method which performs the preventOverlap calculations for a given axis ("x" or "y") based * on the value and regions provided. * * @method _preventOverlap * @protected * * @param {Number} val The value being constrain * @param {String} axis The axis to being constrained * @param {Region} nodeRegion The region of the node being constrained * @param {Region} constrainingRegion The region of the node (or viewport) we need to constrain to * * @return {Number} The constrained value */ _preventOverlap : function(val, axis, nodeRegion, constrainingRegion) { var align = this.get(ALIGN), x = (axis === X_COORD), nodeSize, alignRegion, nearEdge, farEdge, spaceOnNearSide, spaceOnFarSide; if (align && align.points && PREVENT_OVERLAP_MAP[axis][align.points.join(EMPTY_STR)]) { alignRegion = this._getRegion(align.node); if (alignRegion) { nodeSize = (x) ? nodeRegion.width : nodeRegion.height; nearEdge = (x) ? alignRegion.left : alignRegion.top; farEdge = (x) ? alignRegion.right : alignRegion.bottom; spaceOnNearSide = (x) ? alignRegion.left - constrainingRegion.left : alignRegion.top - constrainingRegion.top; spaceOnFarSide = (x) ? constrainingRegion.right - alignRegion.right : constrainingRegion.bottom - alignRegion.bottom; } if (val > nearEdge) { if (spaceOnFarSide < nodeSize && spaceOnNearSide > nodeSize) { val = nearEdge - nodeSize; } } else { if (spaceOnNearSide < nodeSize && spaceOnFarSide > nodeSize) { val = farEdge; } } } return val; }, /** * Binds event listeners responsible for updating the UI state in response to * Widget constrained positioning related state changes. *

* This method is invoked after bindUI is invoked for the Widget class * using YUI's aop infrastructure. *

* * @method _bindUIPosConstrained * @protected */ _bindUIPosConstrained : function() { this.after(CONSTRAIN_CHANGE, this._afterConstrainChange); this._enableConstraints(this.get(CONSTRAIN)); }, /** * After change listener for the "constrain" attribute, responsible * for updating the UI, in response to attribute changes. * * @method _afterConstrainChange * @protected * @param {EventFacade} e The event facade */ _afterConstrainChange : function(e) { this._enableConstraints(e.newVal); }, /** * Updates the UI if enabling constraints, and sets up the xyChange event listeners * to constrain whenever the widget is moved. Disabling constraints removes the listeners. * * @method enable or disable constraints listeners * @private * @param {boolean} enable Enable or disable constraints */ _enableConstraints : function(enable) { if (enable) { this.constrain(); this._cxyHandle = this._cxyHandle || this.on(CONSTRAIN_XYCHANGE, this._constrainOnXYChange); } else if (this._cxyHandle) { this._cxyHandle.detach(); this._cxyHandle = null; } }, /** * The on change listener for the "xy" attribute. Modifies the event facade's * newVal property with the constrained XY value. * * @method _constrainOnXYChange * @protected * @param {EventFacade} e The event facade for the attribute change */ _constrainOnXYChange : function(e) { if (!e.constrained) { e.newVal = this.getConstrainedXY(e.newVal); } }, /** * Utility method to normalize region retrieval from a node instance, * or the viewport, if no node is provided. * * @method _getRegion * @private * @param {Node} node Optional. */ _getRegion : function(node) { var region; if (!node) { region = this._posNode.get(VIEWPORT_REGION); } else { node = Node.one(node); if (node) { region = node.get(REGION); } } return region; } }; Y.WidgetPositionConstrain = PositionConstrain; }, '3.3.0' ,{requires:['widget-position']});