|
1 YUI.add('widget-position-constrain', function (Y, NAME) { |
|
2 |
|
3 /** |
|
4 * Provides constrained xy positioning support for Widgets, through an extension. |
|
5 * |
|
6 * It builds on top of the widget-position module, to provide constrained positioning support. |
|
7 * |
|
8 * @module widget-position-constrain |
|
9 */ |
|
10 var CONSTRAIN = "constrain", |
|
11 CONSTRAIN_XYCHANGE = "constrain|xyChange", |
|
12 CONSTRAIN_CHANGE = "constrainChange", |
|
13 |
|
14 PREVENT_OVERLAP = "preventOverlap", |
|
15 ALIGN = "align", |
|
16 |
|
17 EMPTY_STR = "", |
|
18 |
|
19 BINDUI = "bindUI", |
|
20 |
|
21 XY = "xy", |
|
22 X_COORD = "x", |
|
23 Y_COORD = "y", |
|
24 |
|
25 Node = Y.Node, |
|
26 |
|
27 VIEWPORT_REGION = "viewportRegion", |
|
28 REGION = "region", |
|
29 |
|
30 PREVENT_OVERLAP_MAP; |
|
31 |
|
32 /** |
|
33 * A widget extension, which can be used to add constrained xy positioning support to the base Widget class, |
|
34 * through the <a href="Base.html#method_build">Base.build</a> method. This extension requires that |
|
35 * the WidgetPosition extension be added to the Widget (before WidgetPositionConstrain, if part of the same |
|
36 * extension list passed to Base.build). |
|
37 * |
|
38 * @class WidgetPositionConstrain |
|
39 * @param {Object} User configuration object |
|
40 */ |
|
41 function PositionConstrain(config) {} |
|
42 |
|
43 /** |
|
44 * Static property used to define the default attribute |
|
45 * configuration introduced by WidgetPositionConstrain. |
|
46 * |
|
47 * @property ATTRS |
|
48 * @type Object |
|
49 * @static |
|
50 */ |
|
51 PositionConstrain.ATTRS = { |
|
52 |
|
53 /** |
|
54 * @attribute constrain |
|
55 * @type boolean | Node |
|
56 * @default null |
|
57 * @description The node to constrain the widget's bounding box to, when setting xy. Can also be |
|
58 * set to true, to constrain to the viewport. |
|
59 */ |
|
60 constrain : { |
|
61 value: null, |
|
62 setter: "_setConstrain" |
|
63 }, |
|
64 |
|
65 /** |
|
66 * @attribute preventOverlap |
|
67 * @type boolean |
|
68 * @description If set to true, and WidgetPositionAlign is also added to the Widget, |
|
69 * constrained positioning will attempt to prevent the widget's bounding box from overlapping |
|
70 * the element to which it has been aligned, by flipping the orientation of the alignment |
|
71 * for corner based alignments |
|
72 */ |
|
73 preventOverlap : { |
|
74 value:false |
|
75 } |
|
76 }; |
|
77 |
|
78 /** |
|
79 * @property _PREVENT_OVERLAP |
|
80 * @static |
|
81 * @protected |
|
82 * @type Object |
|
83 * @description The set of positions for which to prevent |
|
84 * overlap. |
|
85 */ |
|
86 PREVENT_OVERLAP_MAP = PositionConstrain._PREVENT_OVERLAP = { |
|
87 x: { |
|
88 "tltr": 1, |
|
89 "blbr": 1, |
|
90 "brbl": 1, |
|
91 "trtl": 1 |
|
92 }, |
|
93 y : { |
|
94 "trbr": 1, |
|
95 "tlbl": 1, |
|
96 "bltl": 1, |
|
97 "brtr": 1 |
|
98 } |
|
99 }; |
|
100 |
|
101 PositionConstrain.prototype = { |
|
102 |
|
103 initializer : function() { |
|
104 if (!this._posNode) { |
|
105 Y.error("WidgetPosition needs to be added to the Widget, before WidgetPositionConstrain is added"); |
|
106 } |
|
107 Y.after(this._bindUIPosConstrained, this, BINDUI); |
|
108 }, |
|
109 |
|
110 /** |
|
111 * Calculates the constrained positions for the XY positions provided, using |
|
112 * the provided node argument is passed in. If no node value is passed in, the value of |
|
113 * the "constrain" attribute is used. |
|
114 * |
|
115 * @method getConstrainedXY |
|
116 * @param {Array} xy The xy values to constrain |
|
117 * @param {Node | boolean} node Optional. The node to constrain to, or true for the viewport |
|
118 * @return {Array} The constrained xy values |
|
119 */ |
|
120 getConstrainedXY : function(xy, node) { |
|
121 node = node || this.get(CONSTRAIN); |
|
122 |
|
123 var constrainingRegion = this._getRegion((node === true) ? null : node), |
|
124 nodeRegion = this._posNode.get(REGION); |
|
125 |
|
126 return [ |
|
127 this._constrain(xy[0], X_COORD, nodeRegion, constrainingRegion), |
|
128 this._constrain(xy[1], Y_COORD, nodeRegion, constrainingRegion) |
|
129 ]; |
|
130 }, |
|
131 |
|
132 /** |
|
133 * Constrains the widget's bounding box to a node (or the viewport). If xy or node are not |
|
134 * passed in, the current position and the value of "constrain" will be used respectively. |
|
135 * |
|
136 * The widget's position will be changed to the constrained position. |
|
137 * |
|
138 * @method constrain |
|
139 * @param {Array} xy Optional. The xy values to constrain |
|
140 * @param {Node | boolean} node Optional. The node to constrain to, or true for the viewport |
|
141 */ |
|
142 constrain : function(xy, node) { |
|
143 var currentXY, |
|
144 constrainedXY, |
|
145 constraint = node || this.get(CONSTRAIN); |
|
146 |
|
147 if (constraint) { |
|
148 currentXY = xy || this.get(XY); |
|
149 constrainedXY = this.getConstrainedXY(currentXY, constraint); |
|
150 |
|
151 if (constrainedXY[0] !== currentXY[0] || constrainedXY[1] !== currentXY[1]) { |
|
152 this.set(XY, constrainedXY, { constrained:true }); |
|
153 } |
|
154 } |
|
155 }, |
|
156 |
|
157 /** |
|
158 * The setter implementation for the "constrain" attribute. |
|
159 * |
|
160 * @method _setConstrain |
|
161 * @protected |
|
162 * @param {Node | boolean} val The attribute value |
|
163 */ |
|
164 _setConstrain : function(val) { |
|
165 return (val === true) ? val : Node.one(val); |
|
166 }, |
|
167 |
|
168 /** |
|
169 * The method which performs the actual constrain calculations for a given axis ("x" or "y") based |
|
170 * on the regions provided. |
|
171 * |
|
172 * @method _constrain |
|
173 * @protected |
|
174 * |
|
175 * @param {Number} val The value to constrain |
|
176 * @param {String} axis The axis to use for constrainment |
|
177 * @param {Region} nodeRegion The region of the node to constrain |
|
178 * @param {Region} constrainingRegion The region of the node (or viewport) to constrain to |
|
179 * |
|
180 * @return {Number} The constrained value |
|
181 */ |
|
182 _constrain: function(val, axis, nodeRegion, constrainingRegion) { |
|
183 if (constrainingRegion) { |
|
184 |
|
185 if (this.get(PREVENT_OVERLAP)) { |
|
186 val = this._preventOverlap(val, axis, nodeRegion, constrainingRegion); |
|
187 } |
|
188 |
|
189 var x = (axis == X_COORD), |
|
190 |
|
191 regionSize = (x) ? constrainingRegion.width : constrainingRegion.height, |
|
192 nodeSize = (x) ? nodeRegion.width : nodeRegion.height, |
|
193 minConstraint = (x) ? constrainingRegion.left : constrainingRegion.top, |
|
194 maxConstraint = (x) ? constrainingRegion.right - nodeSize : constrainingRegion.bottom - nodeSize; |
|
195 |
|
196 if (val < minConstraint || val > maxConstraint) { |
|
197 if (nodeSize < regionSize) { |
|
198 if (val < minConstraint) { |
|
199 val = minConstraint; |
|
200 } else if (val > maxConstraint) { |
|
201 val = maxConstraint; |
|
202 } |
|
203 } else { |
|
204 val = minConstraint; |
|
205 } |
|
206 } |
|
207 } |
|
208 |
|
209 return val; |
|
210 }, |
|
211 |
|
212 /** |
|
213 * The method which performs the preventOverlap calculations for a given axis ("x" or "y") based |
|
214 * on the value and regions provided. |
|
215 * |
|
216 * @method _preventOverlap |
|
217 * @protected |
|
218 * |
|
219 * @param {Number} val The value being constrain |
|
220 * @param {String} axis The axis to being constrained |
|
221 * @param {Region} nodeRegion The region of the node being constrained |
|
222 * @param {Region} constrainingRegion The region of the node (or viewport) we need to constrain to |
|
223 * |
|
224 * @return {Number} The constrained value |
|
225 */ |
|
226 _preventOverlap : function(val, axis, nodeRegion, constrainingRegion) { |
|
227 |
|
228 var align = this.get(ALIGN), |
|
229 x = (axis === X_COORD), |
|
230 nodeSize, |
|
231 alignRegion, |
|
232 nearEdge, |
|
233 farEdge, |
|
234 spaceOnNearSide, |
|
235 spaceOnFarSide; |
|
236 |
|
237 if (align && align.points && PREVENT_OVERLAP_MAP[axis][align.points.join(EMPTY_STR)]) { |
|
238 |
|
239 alignRegion = this._getRegion(align.node); |
|
240 |
|
241 if (alignRegion) { |
|
242 nodeSize = (x) ? nodeRegion.width : nodeRegion.height; |
|
243 nearEdge = (x) ? alignRegion.left : alignRegion.top; |
|
244 farEdge = (x) ? alignRegion.right : alignRegion.bottom; |
|
245 spaceOnNearSide = (x) ? alignRegion.left - constrainingRegion.left : alignRegion.top - constrainingRegion.top; |
|
246 spaceOnFarSide = (x) ? constrainingRegion.right - alignRegion.right : constrainingRegion.bottom - alignRegion.bottom; |
|
247 } |
|
248 |
|
249 if (val > nearEdge) { |
|
250 if (spaceOnFarSide < nodeSize && spaceOnNearSide > nodeSize) { |
|
251 val = nearEdge - nodeSize; |
|
252 } |
|
253 } else { |
|
254 if (spaceOnNearSide < nodeSize && spaceOnFarSide > nodeSize) { |
|
255 val = farEdge; |
|
256 } |
|
257 } |
|
258 } |
|
259 |
|
260 return val; |
|
261 }, |
|
262 |
|
263 /** |
|
264 * Binds event listeners responsible for updating the UI state in response to |
|
265 * Widget constrained positioning related state changes. |
|
266 * <p> |
|
267 * This method is invoked after bindUI is invoked for the Widget class |
|
268 * using YUI's aop infrastructure. |
|
269 * </p> |
|
270 * |
|
271 * @method _bindUIPosConstrained |
|
272 * @protected |
|
273 */ |
|
274 _bindUIPosConstrained : function() { |
|
275 this.after(CONSTRAIN_CHANGE, this._afterConstrainChange); |
|
276 this._enableConstraints(this.get(CONSTRAIN)); |
|
277 }, |
|
278 |
|
279 /** |
|
280 * After change listener for the "constrain" attribute, responsible |
|
281 * for updating the UI, in response to attribute changes. |
|
282 * |
|
283 * @method _afterConstrainChange |
|
284 * @protected |
|
285 * @param {EventFacade} e The event facade |
|
286 */ |
|
287 _afterConstrainChange : function(e) { |
|
288 this._enableConstraints(e.newVal); |
|
289 }, |
|
290 |
|
291 /** |
|
292 * Updates the UI if enabling constraints, and sets up the xyChange event listeners |
|
293 * to constrain whenever the widget is moved. Disabling constraints removes the listeners. |
|
294 * |
|
295 * @method _enableConstraints |
|
296 * @private |
|
297 * @param {boolean} enable Enable or disable constraints |
|
298 */ |
|
299 _enableConstraints : function(enable) { |
|
300 if (enable) { |
|
301 this.constrain(); |
|
302 this._cxyHandle = this._cxyHandle || this.on(CONSTRAIN_XYCHANGE, this._constrainOnXYChange); |
|
303 } else if (this._cxyHandle) { |
|
304 this._cxyHandle.detach(); |
|
305 this._cxyHandle = null; |
|
306 } |
|
307 }, |
|
308 |
|
309 /** |
|
310 * The on change listener for the "xy" attribute. Modifies the event facade's |
|
311 * newVal property with the constrained XY value. |
|
312 * |
|
313 * @method _constrainOnXYChange |
|
314 * @protected |
|
315 * @param {EventFacade} e The event facade for the attribute change |
|
316 */ |
|
317 _constrainOnXYChange : function(e) { |
|
318 if (!e.constrained) { |
|
319 e.newVal = this.getConstrainedXY(e.newVal); |
|
320 } |
|
321 }, |
|
322 |
|
323 /** |
|
324 * Utility method to normalize region retrieval from a node instance, |
|
325 * or the viewport, if no node is provided. |
|
326 * |
|
327 * @method _getRegion |
|
328 * @private |
|
329 * @param {Node} node Optional. |
|
330 */ |
|
331 _getRegion : function(node) { |
|
332 var region; |
|
333 if (!node) { |
|
334 region = this._posNode.get(VIEWPORT_REGION); |
|
335 } else { |
|
336 node = Node.one(node); |
|
337 if (node) { |
|
338 region = node.get(REGION); |
|
339 } |
|
340 } |
|
341 return region; |
|
342 } |
|
343 }; |
|
344 |
|
345 Y.WidgetPositionConstrain = PositionConstrain; |
|
346 |
|
347 |
|
348 }, '@VERSION@', {"requires": ["widget-position"]}); |