|
1 /* |
|
2 Copyright (c) 2009, Yahoo! Inc. All rights reserved. |
|
3 Code licensed under the BSD License: |
|
4 http://developer.yahoo.net/yui/license.txt |
|
5 version: 3.0.0b1 |
|
6 build: 1163 |
|
7 */ |
|
8 YUI.add('dd-constrain', function(Y) { |
|
9 |
|
10 |
|
11 /** |
|
12 * The Drag & Drop Utility allows you to create a draggable interface efficiently, buffering you from browser-level abnormalities and enabling you to focus on the interesting logic surrounding your particular implementation. This component enables you to create a variety of standard draggable objects with just a few lines of code and then, using its extensive API, add your own specific implementation logic. |
|
13 * @module dd |
|
14 * @submodule dd-constrain |
|
15 */ |
|
16 /** |
|
17 * This is a plugin for the dd-drag module to add the constraining methods to it. It supports constraining to a renodenode or viewport. It anode* supports tick based moves and XY axis constraints. |
|
18 * @class DragConstrained |
|
19 * @extends Base |
|
20 * @constructor |
|
21 * @namespace Plugin |
|
22 */ |
|
23 |
|
24 var DRAG_NODE = 'dragNode', |
|
25 OFFSET_HEIGHT = 'offsetHeight', |
|
26 OFFSET_WIDTH = 'offsetWidth', |
|
27 HOST = 'host', |
|
28 CON_2_REGION = 'constrain2region', |
|
29 CON_2_NODE = 'constrain2node', |
|
30 TICK_X_ARRAY = 'tickXArray', |
|
31 TICK_Y_ARRAY = 'tickYArray', |
|
32 DDM = Y.DD.DDM, |
|
33 TOP = 'top', |
|
34 RIGHT = 'right', |
|
35 BOTTOM = 'bottom', |
|
36 LEFT = 'left', |
|
37 proto = null; |
|
38 |
|
39 var C = function(config) { |
|
40 C.superclass.constructor.apply(this, arguments); |
|
41 }; |
|
42 |
|
43 C.NAME = 'DragConstrained'; |
|
44 /** |
|
45 * @property con |
|
46 * @description The Constrained instance will be placed on the Drag instance under the con namespace. |
|
47 * @type {String} |
|
48 */ |
|
49 C.NS = 'con'; |
|
50 |
|
51 C.ATTRS = { |
|
52 host: { |
|
53 }, |
|
54 /** |
|
55 * @attribute stickX |
|
56 * @description Stick the drag movement to the X-Axis. Default: false |
|
57 * @type Boolean |
|
58 */ |
|
59 stickX: { |
|
60 value: false |
|
61 }, |
|
62 /** |
|
63 * @attribute stickY |
|
64 * @description Stick the drag movement to the Y-Axis |
|
65 * @type Boolean |
|
66 */ |
|
67 stickY: { |
|
68 value: false |
|
69 }, |
|
70 /** |
|
71 * @attribute tickX |
|
72 * @description The X tick offset the drag node should snap to on each drag move. False for no ticks. Default: false |
|
73 * @type Number/false |
|
74 */ |
|
75 tickX: { |
|
76 value: false |
|
77 }, |
|
78 /** |
|
79 * @attribute tickY |
|
80 * @description The Y tick offset the drag node should snap to on each drag move. False for no ticks. Default: false |
|
81 * @type Number/false |
|
82 */ |
|
83 tickY: { |
|
84 value: false |
|
85 }, |
|
86 /** |
|
87 * @attribute tickXArray |
|
88 * @description An array of page coordinates to use as X ticks for drag movement. |
|
89 * @type Array |
|
90 */ |
|
91 tickXArray: { |
|
92 value: false |
|
93 }, |
|
94 /** |
|
95 * @attribute tickYArray |
|
96 * @description An array of page coordinates to use as Y ticks for drag movement. |
|
97 * @type Array |
|
98 */ |
|
99 tickYArray: { |
|
100 value: false |
|
101 }, |
|
102 /** |
|
103 * @attribute constrain2region |
|
104 * @description An Object Literal containing a valid region (top, right, bottom, left) of page positions to constrain the drag node to. |
|
105 * @type Object |
|
106 */ |
|
107 constrain2region: { |
|
108 value: false, |
|
109 getter: function(r) { |
|
110 if (Y.Lang.isObject(r)) { |
|
111 var o = {}; |
|
112 Y.mix(o, r); |
|
113 return o; |
|
114 } else { |
|
115 return false; |
|
116 } |
|
117 }, |
|
118 setter: function (r) { |
|
119 if (Y.Lang.isObject(r)) { |
|
120 if (Y.Lang.isNumber(r[TOP]) && Y.Lang.isNumber(r[RIGHT]) && Y.Lang.isNumber(r[LEFT]) && Y.Lang.isNumber(r[BOTTOM])) { |
|
121 var o = {}; |
|
122 Y.mix(o, r); |
|
123 return o; |
|
124 } else { |
|
125 return false; |
|
126 } |
|
127 } else if (r !== false) { |
|
128 return false; |
|
129 } |
|
130 return r; |
|
131 } |
|
132 }, |
|
133 /** |
|
134 * @attribute gutter |
|
135 * @description CSS style string for the gutter of a region (supports negative values): '5 0' (sets top and bottom to 5px, left and right to 0px), '1 2 3 4' (top 1px, right 2px, bottom 3px, left 4px) |
|
136 * @type String |
|
137 */ |
|
138 gutter: { |
|
139 value: '0', |
|
140 setter: function(gutter) { |
|
141 return Y.DD.DDM.cssSizestoObject(gutter); |
|
142 } |
|
143 }, |
|
144 /** |
|
145 * @attribute constrain2node |
|
146 * @description Will attempt to constrain the drag node to the bounderies of this node. |
|
147 * @type Object |
|
148 */ |
|
149 constrain2node: { |
|
150 value: false, |
|
151 setter: function(n) { |
|
152 if (!this.get(CON_2_REGION)) { |
|
153 var node = Y.Node.get(n); |
|
154 if (node) { |
|
155 return node; |
|
156 } |
|
157 } else if (this.get(CON_2_REGION) !== false) { |
|
158 } |
|
159 return false; |
|
160 } |
|
161 }, |
|
162 /** |
|
163 * @attribute constrain2view |
|
164 * @description Will attempt to constrain the drag node to the bounderies of the viewport region. |
|
165 * @type Object |
|
166 */ |
|
167 constrain2view: { |
|
168 value: false |
|
169 } |
|
170 }; |
|
171 |
|
172 proto = { |
|
173 initializer: function() { |
|
174 this.get(HOST).on('drag:start', Y.bind(this._handleStart, this)); |
|
175 this.get(HOST).after('drag:align', Y.bind(this.align, this)); |
|
176 }, |
|
177 /** |
|
178 * @private |
|
179 * @method _handleStart |
|
180 * @description Fires on drag:start and clears the _regionCache |
|
181 */ |
|
182 _handleStart: function() { |
|
183 this._regionCache = null; |
|
184 }, |
|
185 /** |
|
186 * @private |
|
187 * @property _regionCache |
|
188 * @description Store a cache of the region that we are constraining to |
|
189 * @type Object |
|
190 */ |
|
191 _regionCache: null, |
|
192 /** |
|
193 * @private |
|
194 * @method _cacheRegion |
|
195 * @description Get's the region and caches it, called from window.resize and when the cache is null |
|
196 */ |
|
197 _cacheRegion: function() { |
|
198 this._regionCache = this.get(CON_2_NODE).get('region'); |
|
199 }, |
|
200 /** |
|
201 * @method getRegion |
|
202 * @description Get the active region: viewport, node, custom region |
|
203 * @param {Boolean} inc Include the node's height and width |
|
204 * @return {Object} |
|
205 */ |
|
206 getRegion: function(inc) { |
|
207 var r = {}, oh = null, ow = null, |
|
208 g = this.get('gutter'), |
|
209 host = this.get(HOST); |
|
210 |
|
211 if (this.get(CON_2_NODE)) { |
|
212 if (!this._regionCache) { |
|
213 Y.on('resize', Y.bind(this._cacheRegion, this), window); |
|
214 this._cacheRegion(); |
|
215 } |
|
216 r = Y.clone(this._regionCache); |
|
217 } else if (this.get(CON_2_REGION)) { |
|
218 r = this.get(CON_2_REGION); |
|
219 } else if (this.get('constrain2view')) { |
|
220 r = host.get(DRAG_NODE).get('viewportRegion'); |
|
221 } else { |
|
222 return false; |
|
223 } |
|
224 |
|
225 Y.each(g, function(i, n) { |
|
226 if ((n == RIGHT) || (n == BOTTOM)) { |
|
227 r[n] -= i; |
|
228 } else { |
|
229 r[n] += i; |
|
230 } |
|
231 }); |
|
232 if (inc) { |
|
233 oh = host.get(DRAG_NODE).get(OFFSET_HEIGHT); |
|
234 ow = host.get(DRAG_NODE).get(OFFSET_WIDTH); |
|
235 r[RIGHT] = r[RIGHT] - ow; |
|
236 r[BOTTOM] = r[BOTTOM] - oh; |
|
237 } |
|
238 return r; |
|
239 }, |
|
240 /** |
|
241 * @private |
|
242 * @method _checkRegion |
|
243 * @description Check if xy is inside a given region, if not change to it be inside. |
|
244 * @param {Array} _xy The XY to check if it's in the current region, if it isn't inside the region, it will reset the xy array to be inside the region. |
|
245 * @return {Array} The new XY that is inside the region |
|
246 */ |
|
247 _checkRegion: function(_xy) { |
|
248 var oxy = _xy, |
|
249 r = this.getRegion(), |
|
250 host = this.get(HOST), |
|
251 oh = host.get(DRAG_NODE).get(OFFSET_HEIGHT), |
|
252 ow = host.get(DRAG_NODE).get(OFFSET_WIDTH); |
|
253 |
|
254 if (oxy[1] > (r[BOTTOM] - oh)) { |
|
255 _xy[1] = (r[BOTTOM] - oh); |
|
256 } |
|
257 if (r[TOP] > oxy[1]) { |
|
258 _xy[1] = r[TOP]; |
|
259 |
|
260 } |
|
261 if (oxy[0] > (r[RIGHT] - ow)) { |
|
262 _xy[0] = (r[RIGHT] - ow); |
|
263 } |
|
264 if (r[LEFT] > oxy[0]) { |
|
265 _xy[0] = r[LEFT]; |
|
266 } |
|
267 |
|
268 return _xy; |
|
269 }, |
|
270 /** |
|
271 * @method inRegion |
|
272 * @description Checks if the XY passed or the dragNode is inside the active region. |
|
273 * @param {Array} xy Optional XY to check, if not supplied this.get('dragNode').getXY() is used. |
|
274 * @return {Boolean} True if the XY is inside the region, false otherwise. |
|
275 */ |
|
276 inRegion: function(xy) { |
|
277 xy = xy || this.get(HOST).get(DRAG_NODE).getXY(); |
|
278 |
|
279 var _xy = this._checkRegion([xy[0], xy[1]]), |
|
280 inside = false; |
|
281 if ((xy[0] === _xy[0]) && (xy[1] === _xy[1])) { |
|
282 inside = true; |
|
283 } |
|
284 return inside; |
|
285 }, |
|
286 /** |
|
287 * @method align |
|
288 * @description Modifies the Drag.actXY method from the after drag:align event. This is where the constraining happens. |
|
289 */ |
|
290 align: function() { |
|
291 var host = this.get(HOST), |
|
292 _xy = host.actXY, |
|
293 r = this.getRegion(true); |
|
294 |
|
295 if (this.get('stickX')) { |
|
296 _xy[1] = (host.startXY[1] - host.deltaXY[1]); |
|
297 } |
|
298 if (this.get('stickY')) { |
|
299 _xy[0] = (host.startXY[0] - host.deltaXY[0]); |
|
300 } |
|
301 |
|
302 if (r) { |
|
303 _xy = this._checkRegion(_xy); |
|
304 } |
|
305 |
|
306 _xy = this._checkTicks(_xy, r); |
|
307 host.actXY = _xy; |
|
308 }, |
|
309 /** |
|
310 * @private |
|
311 * @method _checkTicks |
|
312 * @description This method delegates the proper helper method for tick calculations |
|
313 * @param {Array} xy The XY coords for the Drag |
|
314 * @param {Object} r The optional region that we are bound to. |
|
315 * @return {Array} The calced XY coords |
|
316 */ |
|
317 _checkTicks: function(xy, r) { |
|
318 var host = this.get(HOST), |
|
319 lx = (host.startXY[0] - host.deltaXY[0]), |
|
320 ly = (host.startXY[1] - host.deltaXY[1]), |
|
321 xt = this.get('tickX'), |
|
322 yt = this.get('tickY'); |
|
323 if (xt && !this.get(TICK_X_ARRAY)) { |
|
324 xy[0] = DDM._calcTicks(xy[0], lx, xt, r[LEFT], r[RIGHT]); |
|
325 } |
|
326 if (yt && !this.get(TICK_Y_ARRAY)) { |
|
327 xy[1] = DDM._calcTicks(xy[1], ly, yt, r[TOP], r[BOTTOM]); |
|
328 } |
|
329 if (this.get(TICK_X_ARRAY)) { |
|
330 xy[0] = DDM._calcTickArray(xy[0], this.get(TICK_X_ARRAY), r[LEFT], r[RIGHT]); |
|
331 } |
|
332 if (this.get(TICK_Y_ARRAY)) { |
|
333 xy[1] = DDM._calcTickArray(xy[1], this.get(TICK_Y_ARRAY), r[TOP], r[BOTTOM]); |
|
334 } |
|
335 |
|
336 return xy; |
|
337 } |
|
338 }; |
|
339 |
|
340 Y.namespace('Plugin'); |
|
341 Y.extend(C, Y.Base, proto); |
|
342 Y.Plugin.DDConstrained = C; |
|
343 |
|
344 Y.mix(DDM, { |
|
345 /** |
|
346 * @for DDM |
|
347 * @namespace DD |
|
348 * @private |
|
349 * @method _calcTicks |
|
350 * @description Helper method to calculate the tick offsets for a given position |
|
351 * @param {Number} pos The current X or Y position |
|
352 * @param {Number} start The start X or Y position |
|
353 * @param {Number} tick The X or Y tick increment |
|
354 * @param {Number} off1 The min offset that we can't pass (region) |
|
355 * @param {Number} off2 The max offset that we can't pass (region) |
|
356 * @return {Number} The new position based on the tick calculation |
|
357 */ |
|
358 _calcTicks: function(pos, start, tick, off1, off2) { |
|
359 var ix = ((pos - start) / tick), |
|
360 min = Math.floor(ix), |
|
361 max = Math.ceil(ix); |
|
362 if ((min !== 0) || (max !== 0)) { |
|
363 if ((ix >= min) && (ix <= max)) { |
|
364 pos = (start + (tick * min)); |
|
365 if (off1 && off2) { |
|
366 if (pos < off1) { |
|
367 pos = (start + (tick * (min + 1))); |
|
368 } |
|
369 if (pos > off2) { |
|
370 pos = (start + (tick * (min - 1))); |
|
371 } |
|
372 } |
|
373 } |
|
374 } |
|
375 return pos; |
|
376 }, |
|
377 /** |
|
378 * @for DDM |
|
379 * @namespace DD |
|
380 * @private |
|
381 * @method _calcTickArray |
|
382 * @description This method is used with the tickXArray and tickYArray config options |
|
383 * @param {Number} pos The current X or Y position |
|
384 * @param {Number} ticks The array containing our custom tick positions. |
|
385 * @param {Number} off1 The min offset that we can't pass (region) |
|
386 * @param {Number} off2 The max offset that we can't pass (region) |
|
387 * @return The tick position |
|
388 */ |
|
389 _calcTickArray: function(pos, ticks, off1, off2) { |
|
390 var i = 0, len = ticks.length, next = 0, |
|
391 diff1, diff2, ret; |
|
392 |
|
393 if (!ticks || (ticks.length === 0)) { |
|
394 return pos; |
|
395 } else if (ticks[0] >= pos) { |
|
396 return ticks[0]; |
|
397 } else { |
|
398 for (i = 0; i < len; i++) { |
|
399 next = (i + 1); |
|
400 if (ticks[next] && ticks[next] >= pos) { |
|
401 diff1 = pos - ticks[i]; |
|
402 diff2 = ticks[next] - pos; |
|
403 ret = (diff2 > diff1) ? ticks[i] : ticks[next]; |
|
404 if (off1 && off2) { |
|
405 if (ret > off2) { |
|
406 if (ticks[i]) { |
|
407 ret = ticks[i]; |
|
408 } else { |
|
409 ret = ticks[len - 1]; |
|
410 } |
|
411 } |
|
412 } |
|
413 return ret; |
|
414 } |
|
415 |
|
416 } |
|
417 return ticks[ticks.length - 1]; |
|
418 } |
|
419 } |
|
420 }); |
|
421 |
|
422 |
|
423 |
|
424 |
|
425 }, '3.0.0b1' ,{requires:['dd-drag'], skinnable:false}); |