|
1 YUI.add('dd-constrain', function (Y, NAME) { |
|
2 |
|
3 |
|
4 /** |
|
5 * The Drag & Drop Utility allows you to create a draggable interface efficiently, |
|
6 * buffering you from browser-level abnormalities and enabling you to focus on the interesting |
|
7 * logic surrounding your particular implementation. This component enables you to create a |
|
8 * variety of standard draggable objects with just a few lines of code and then, |
|
9 * using its extensive API, add your own specific implementation logic. |
|
10 * @module dd |
|
11 * @main dd |
|
12 * @submodule dd-constrain |
|
13 */ |
|
14 /** |
|
15 * Plugin for the dd-drag module to add the constraining methods to it. |
|
16 * It supports constraining to a node or viewport. It supports tick based moves and XY axis constraints. |
|
17 * @class DDConstrained |
|
18 * @extends Base |
|
19 * @constructor |
|
20 * @namespace Plugin |
|
21 */ |
|
22 |
|
23 var DRAG_NODE = 'dragNode', |
|
24 OFFSET_HEIGHT = 'offsetHeight', |
|
25 OFFSET_WIDTH = 'offsetWidth', |
|
26 HOST = 'host', |
|
27 TICK_X_ARRAY = 'tickXArray', |
|
28 TICK_Y_ARRAY = 'tickYArray', |
|
29 DDM = Y.DD.DDM, |
|
30 TOP = 'top', |
|
31 RIGHT = 'right', |
|
32 BOTTOM = 'bottom', |
|
33 LEFT = 'left', |
|
34 VIEW = 'view', |
|
35 proto = null, |
|
36 |
|
37 /** |
|
38 * Fires when this node is aligned with the tickX value. |
|
39 * @event drag:tickAlignX |
|
40 * @param {EventFacade} event An Event Facade object |
|
41 * @type {CustomEvent} |
|
42 */ |
|
43 EV_TICK_ALIGN_X = 'drag:tickAlignX', |
|
44 |
|
45 /** |
|
46 * Fires when this node is aligned with the tickY value. |
|
47 * @event drag:tickAlignY |
|
48 * @param {EventFacade} event An Event Facade object |
|
49 * @type {CustomEvent} |
|
50 */ |
|
51 EV_TICK_ALIGN_Y = 'drag:tickAlignY', |
|
52 |
|
53 C = function() { |
|
54 this._lazyAddAttrs = false; |
|
55 C.superclass.constructor.apply(this, arguments); |
|
56 }; |
|
57 |
|
58 C.NAME = 'ddConstrained'; |
|
59 /** |
|
60 * The Constrained instance will be placed on the Drag instance under the con namespace. |
|
61 * @property NS |
|
62 * @default con |
|
63 * @readonly |
|
64 * @protected |
|
65 * @static |
|
66 * @type {String} |
|
67 */ |
|
68 C.NS = 'con'; |
|
69 |
|
70 C.ATTRS = { |
|
71 host: { |
|
72 }, |
|
73 /** |
|
74 * Stick the drag movement to the X-Axis. Default: false |
|
75 * @attribute stickX |
|
76 * @type Boolean |
|
77 */ |
|
78 stickX: { |
|
79 value: false |
|
80 }, |
|
81 /** |
|
82 * Stick the drag movement to the Y-Axis |
|
83 * @type Boolean |
|
84 * @attribute stickY |
|
85 */ |
|
86 stickY: { |
|
87 value: false |
|
88 }, |
|
89 /** |
|
90 * The X tick offset the drag node should snap to on each drag move. False for no ticks. Default: false |
|
91 * @type Number/false |
|
92 * @attribute tickX |
|
93 */ |
|
94 tickX: { |
|
95 value: false |
|
96 }, |
|
97 /** |
|
98 * The Y tick offset the drag node should snap to on each drag move. False for no ticks. Default: false |
|
99 * @type Number/false |
|
100 * @attribute tickY |
|
101 */ |
|
102 tickY: { |
|
103 value: false |
|
104 }, |
|
105 /** |
|
106 * An array of page coordinates to use as X ticks for drag movement. |
|
107 * @type Array |
|
108 * @attribute tickXArray |
|
109 */ |
|
110 tickXArray: { |
|
111 value: false |
|
112 }, |
|
113 /** |
|
114 * An array of page coordinates to use as Y ticks for drag movement. |
|
115 * @type Array |
|
116 * @attribute tickYArray |
|
117 */ |
|
118 tickYArray: { |
|
119 value: false |
|
120 }, |
|
121 /** |
|
122 * CSS style string for the gutter of a region (supports negative values): '5 0' |
|
123 * (sets top and bottom to 5px, left and right to 0px), '1 2 3 4' (top 1px, right 2px, bottom 3px, left 4px) |
|
124 * @attribute gutter |
|
125 * @type String |
|
126 */ |
|
127 gutter: { |
|
128 value: '0', |
|
129 setter: function(gutter) { |
|
130 return Y.DD.DDM.cssSizestoObject(gutter); |
|
131 } |
|
132 }, |
|
133 /** |
|
134 * Will attempt to constrain the drag node to the boundaries. Arguments:<br> |
|
135 * 'view': Contrain to Viewport<br> |
|
136 * '#selector_string': Constrain to this node<br> |
|
137 * '{Region Object}': An Object Literal containing a valid region (top, right, bottom, left) of page positions |
|
138 * @attribute constrain |
|
139 * @type {String/Object/Node} |
|
140 */ |
|
141 constrain: { |
|
142 value: VIEW, |
|
143 setter: function(con) { |
|
144 var node = Y.one(con); |
|
145 if (node) { |
|
146 con = node; |
|
147 } |
|
148 return con; |
|
149 } |
|
150 }, |
|
151 /** |
|
152 * An Object Literal containing a valid region (top, right, bottom, left) of page positions to constrain the drag node to. |
|
153 * @deprecated |
|
154 * @attribute constrain2region |
|
155 * @type Object |
|
156 */ |
|
157 constrain2region: { |
|
158 setter: function(r) { |
|
159 return this.set('constrain', r); |
|
160 } |
|
161 }, |
|
162 /** |
|
163 * Will attempt to constrain the drag node to the boundaries of this node. |
|
164 * @deprecated |
|
165 * @attribute constrain2node |
|
166 * @type Object |
|
167 */ |
|
168 constrain2node: { |
|
169 setter: function(n) { |
|
170 return this.set('constrain', Y.one(n)); |
|
171 } |
|
172 }, |
|
173 /** |
|
174 * Will attempt to constrain the drag node to the boundaries of the viewport region. |
|
175 * @deprecated |
|
176 * @attribute constrain2view |
|
177 * @type Object |
|
178 */ |
|
179 constrain2view: { |
|
180 setter: function() { |
|
181 return this.set('constrain', VIEW); |
|
182 } |
|
183 }, |
|
184 /** |
|
185 * Should the region be cached for performace. Default: true |
|
186 * @attribute cacheRegion |
|
187 * @type Boolean |
|
188 */ |
|
189 cacheRegion: { |
|
190 value: true |
|
191 } |
|
192 }; |
|
193 |
|
194 proto = { |
|
195 _lastTickXFired: null, |
|
196 _lastTickYFired: null, |
|
197 |
|
198 initializer: function() { |
|
199 this._createEvents(); |
|
200 |
|
201 this._eventHandles = [ |
|
202 this.get(HOST).on('drag:end', Y.bind(this._handleEnd, this)), |
|
203 this.get(HOST).on('drag:start', Y.bind(this._handleStart, this)), |
|
204 this.get(HOST).after('drag:align', Y.bind(this.align, this)), |
|
205 this.get(HOST).after('drag:drag', Y.bind(this.drag, this)) |
|
206 ]; |
|
207 }, |
|
208 destructor: function() { |
|
209 Y.Array.each( |
|
210 this._eventHandles, |
|
211 function(handle) { |
|
212 handle.detach(); |
|
213 } |
|
214 ); |
|
215 |
|
216 this._eventHandles.length = 0; |
|
217 }, |
|
218 /** |
|
219 * This method creates all the events for this Event Target and publishes them so we get Event Bubbling. |
|
220 * @private |
|
221 * @method _createEvents |
|
222 */ |
|
223 _createEvents: function() { |
|
224 var ev = [ |
|
225 EV_TICK_ALIGN_X, |
|
226 EV_TICK_ALIGN_Y |
|
227 ]; |
|
228 |
|
229 Y.Array.each(ev, function(v) { |
|
230 this.publish(v, { |
|
231 type: v, |
|
232 emitFacade: true, |
|
233 bubbles: true, |
|
234 queuable: false, |
|
235 prefix: 'drag' |
|
236 }); |
|
237 }, this); |
|
238 }, |
|
239 /** |
|
240 * Fires on drag:end |
|
241 * @private |
|
242 * @method _handleEnd |
|
243 */ |
|
244 _handleEnd: function() { |
|
245 this._lastTickYFired = null; |
|
246 this._lastTickXFired = null; |
|
247 }, |
|
248 /** |
|
249 * Fires on drag:start and clears the _regionCache |
|
250 * @private |
|
251 * @method _handleStart |
|
252 */ |
|
253 _handleStart: function() { |
|
254 this.resetCache(); |
|
255 }, |
|
256 /** |
|
257 * Store a cache of the region that we are constraining to |
|
258 * @private |
|
259 * @property _regionCache |
|
260 * @type Object |
|
261 */ |
|
262 _regionCache: null, |
|
263 /** |
|
264 * Get's the region and caches it, called from window.resize and when the cache is null |
|
265 * @private |
|
266 * @method _cacheRegion |
|
267 */ |
|
268 _cacheRegion: function() { |
|
269 this._regionCache = this.get('constrain').get('region'); |
|
270 }, |
|
271 /** |
|
272 * Reset the internal region cache. |
|
273 * @method resetCache |
|
274 */ |
|
275 resetCache: function() { |
|
276 this._regionCache = null; |
|
277 }, |
|
278 /** |
|
279 * Standardizes the 'constraint' attribute |
|
280 * @private |
|
281 * @method _getConstraint |
|
282 */ |
|
283 _getConstraint: function() { |
|
284 var con = this.get('constrain'), |
|
285 g = this.get('gutter'), |
|
286 region; |
|
287 |
|
288 if (con) { |
|
289 if (con instanceof Y.Node) { |
|
290 if (!this._regionCache) { |
|
291 this._eventHandles.push(Y.on('resize', Y.bind(this._cacheRegion, this), Y.config.win)); |
|
292 this._cacheRegion(); |
|
293 } |
|
294 region = Y.clone(this._regionCache); |
|
295 if (!this.get('cacheRegion')) { |
|
296 this.resetCache(); |
|
297 } |
|
298 } else if (Y.Lang.isObject(con)) { |
|
299 region = Y.clone(con); |
|
300 } |
|
301 } |
|
302 if (!con || !region) { |
|
303 con = VIEW; |
|
304 } |
|
305 if (con === VIEW) { |
|
306 region = this.get(HOST).get(DRAG_NODE).get('viewportRegion'); |
|
307 } |
|
308 |
|
309 Y.Object.each(g, function(i, n) { |
|
310 if ((n === RIGHT) || (n === BOTTOM)) { |
|
311 region[n] -= i; |
|
312 } else { |
|
313 region[n] += i; |
|
314 } |
|
315 }); |
|
316 return region; |
|
317 }, |
|
318 |
|
319 /** |
|
320 * Get the active region: viewport, node, custom region |
|
321 * @method getRegion |
|
322 * @param {Boolean} inc Include the node's height and width |
|
323 * @return {Object} The active region. |
|
324 */ |
|
325 getRegion: function(inc) { |
|
326 var r = {}, oh = null, ow = null, |
|
327 host = this.get(HOST); |
|
328 |
|
329 r = this._getConstraint(); |
|
330 |
|
331 if (inc) { |
|
332 oh = host.get(DRAG_NODE).get(OFFSET_HEIGHT); |
|
333 ow = host.get(DRAG_NODE).get(OFFSET_WIDTH); |
|
334 r[RIGHT] = r[RIGHT] - ow; |
|
335 r[BOTTOM] = r[BOTTOM] - oh; |
|
336 } |
|
337 return r; |
|
338 }, |
|
339 /** |
|
340 * Check if xy is inside a given region, if not change to it be inside. |
|
341 * @private |
|
342 * @method _checkRegion |
|
343 * @param {Array} _xy The XY to check if it's in the current region, if it isn't |
|
344 * inside the region, it will reset the xy array to be inside the region. |
|
345 * @return {Array} The new XY that is inside the region |
|
346 */ |
|
347 _checkRegion: function(_xy) { |
|
348 var oxy = _xy, |
|
349 r = this.getRegion(), |
|
350 host = this.get(HOST), |
|
351 oh = host.get(DRAG_NODE).get(OFFSET_HEIGHT), |
|
352 ow = host.get(DRAG_NODE).get(OFFSET_WIDTH); |
|
353 |
|
354 if (oxy[1] > (r[BOTTOM] - oh)) { |
|
355 _xy[1] = (r[BOTTOM] - oh); |
|
356 } |
|
357 if (r[TOP] > oxy[1]) { |
|
358 _xy[1] = r[TOP]; |
|
359 |
|
360 } |
|
361 if (oxy[0] > (r[RIGHT] - ow)) { |
|
362 _xy[0] = (r[RIGHT] - ow); |
|
363 } |
|
364 if (r[LEFT] > oxy[0]) { |
|
365 _xy[0] = r[LEFT]; |
|
366 } |
|
367 |
|
368 return _xy; |
|
369 }, |
|
370 /** |
|
371 * Checks if the XY passed or the dragNode is inside the active region. |
|
372 * @method inRegion |
|
373 * @param {Array} xy Optional XY to check, if not supplied this.get('dragNode').getXY() is used. |
|
374 * @return {Boolean} True if the XY is inside the region, false otherwise. |
|
375 */ |
|
376 inRegion: function(xy) { |
|
377 xy = xy || this.get(HOST).get(DRAG_NODE).getXY(); |
|
378 |
|
379 var _xy = this._checkRegion([xy[0], xy[1]]), |
|
380 inside = false; |
|
381 if ((xy[0] === _xy[0]) && (xy[1] === _xy[1])) { |
|
382 inside = true; |
|
383 } |
|
384 return inside; |
|
385 }, |
|
386 /** |
|
387 * Modifies the Drag.actXY method from the after drag:align event. This is where the constraining happens. |
|
388 * @method align |
|
389 */ |
|
390 align: function() { |
|
391 var host = this.get(HOST), |
|
392 _xy = [host.actXY[0], host.actXY[1]], |
|
393 r = this.getRegion(true); |
|
394 |
|
395 if (this.get('stickX')) { |
|
396 _xy[1] = (host.startXY[1] - host.deltaXY[1]); |
|
397 } |
|
398 if (this.get('stickY')) { |
|
399 _xy[0] = (host.startXY[0] - host.deltaXY[0]); |
|
400 } |
|
401 |
|
402 if (r) { |
|
403 _xy = this._checkRegion(_xy); |
|
404 } |
|
405 |
|
406 _xy = this._checkTicks(_xy, r); |
|
407 |
|
408 host.actXY = _xy; |
|
409 }, |
|
410 /** |
|
411 * Fires after drag:drag. Handle the tickX and tickX align events. |
|
412 * @method drag |
|
413 */ |
|
414 drag: function() { |
|
415 var host = this.get(HOST), |
|
416 xt = this.get('tickX'), |
|
417 yt = this.get('tickY'), |
|
418 _xy = [host.actXY[0], host.actXY[1]]; |
|
419 |
|
420 if ((Y.Lang.isNumber(xt) || this.get(TICK_X_ARRAY)) && (this._lastTickXFired !== _xy[0])) { |
|
421 this._tickAlignX(); |
|
422 this._lastTickXFired = _xy[0]; |
|
423 } |
|
424 |
|
425 if ((Y.Lang.isNumber(yt) || this.get(TICK_Y_ARRAY)) && (this._lastTickYFired !== _xy[1])) { |
|
426 this._tickAlignY(); |
|
427 this._lastTickYFired = _xy[1]; |
|
428 } |
|
429 }, |
|
430 /** |
|
431 * This method delegates the proper helper method for tick calculations |
|
432 * @private |
|
433 * @method _checkTicks |
|
434 * @param {Array} xy The XY coords for the Drag |
|
435 * @param {Object} r The optional region that we are bound to. |
|
436 * @return {Array} The calced XY coords |
|
437 */ |
|
438 _checkTicks: function(xy, r) { |
|
439 var host = this.get(HOST), |
|
440 lx = (host.startXY[0] - host.deltaXY[0]), |
|
441 ly = (host.startXY[1] - host.deltaXY[1]), |
|
442 xt = this.get('tickX'), |
|
443 yt = this.get('tickY'); |
|
444 if (xt && !this.get(TICK_X_ARRAY)) { |
|
445 xy[0] = DDM._calcTicks(xy[0], lx, xt, r[LEFT], r[RIGHT]); |
|
446 } |
|
447 if (yt && !this.get(TICK_Y_ARRAY)) { |
|
448 xy[1] = DDM._calcTicks(xy[1], ly, yt, r[TOP], r[BOTTOM]); |
|
449 } |
|
450 if (this.get(TICK_X_ARRAY)) { |
|
451 xy[0] = DDM._calcTickArray(xy[0], this.get(TICK_X_ARRAY), r[LEFT], r[RIGHT]); |
|
452 } |
|
453 if (this.get(TICK_Y_ARRAY)) { |
|
454 xy[1] = DDM._calcTickArray(xy[1], this.get(TICK_Y_ARRAY), r[TOP], r[BOTTOM]); |
|
455 } |
|
456 |
|
457 return xy; |
|
458 }, |
|
459 /** |
|
460 * Fires when the actXY[0] reach a new value respecting the tickX gap. |
|
461 * @private |
|
462 * @method _tickAlignX |
|
463 */ |
|
464 _tickAlignX: function() { |
|
465 this.fire(EV_TICK_ALIGN_X); |
|
466 }, |
|
467 /** |
|
468 * Fires when the actXY[1] reach a new value respecting the tickY gap. |
|
469 * @private |
|
470 * @method _tickAlignY |
|
471 */ |
|
472 _tickAlignY: function() { |
|
473 this.fire(EV_TICK_ALIGN_Y); |
|
474 } |
|
475 }; |
|
476 |
|
477 Y.namespace('Plugin'); |
|
478 Y.extend(C, Y.Base, proto); |
|
479 Y.Plugin.DDConstrained = C; |
|
480 |
|
481 Y.mix(DDM, { |
|
482 /** |
|
483 * Helper method to calculate the tick offsets for a given position |
|
484 * @for DDM |
|
485 * @namespace DD |
|
486 * @private |
|
487 * @method _calcTicks |
|
488 * @param {Number} pos The current X or Y position |
|
489 * @param {Number} start The start X or Y position |
|
490 * @param {Number} tick The X or Y tick increment |
|
491 * @param {Number} off1 The min offset that we can't pass (region) |
|
492 * @param {Number} off2 The max offset that we can't pass (region) |
|
493 * @return {Number} The new position based on the tick calculation |
|
494 */ |
|
495 _calcTicks: function(pos, start, tick, off1, off2) { |
|
496 var ix = ((pos - start) / tick), |
|
497 min = Math.floor(ix), |
|
498 max = Math.ceil(ix); |
|
499 if ((min !== 0) || (max !== 0)) { |
|
500 if ((ix >= min) && (ix <= max)) { |
|
501 pos = (start + (tick * min)); |
|
502 if (off1 && off2) { |
|
503 if (pos < off1) { |
|
504 pos = (start + (tick * (min + 1))); |
|
505 } |
|
506 if (pos > off2) { |
|
507 pos = (start + (tick * (min - 1))); |
|
508 } |
|
509 } |
|
510 } |
|
511 } |
|
512 return pos; |
|
513 }, |
|
514 /** |
|
515 * This method is used with the tickXArray and tickYArray config options |
|
516 * @for DDM |
|
517 * @namespace DD |
|
518 * @private |
|
519 * @method _calcTickArray |
|
520 * @param {Number} pos The current X or Y position |
|
521 * @param {Number} ticks The array containing our custom tick positions. |
|
522 * @param {Number} off1 The min offset that we can't pass (region) |
|
523 * @param {Number} off2 The max offset that we can't pass (region) |
|
524 * @return The tick position |
|
525 */ |
|
526 _calcTickArray: function(pos, ticks, off1, off2) { |
|
527 var i = 0, len = ticks.length, next = 0, |
|
528 diff1, diff2, ret; |
|
529 |
|
530 if (!ticks || (ticks.length === 0)) { |
|
531 return pos; |
|
532 } |
|
533 if (ticks[0] >= pos) { |
|
534 return ticks[0]; |
|
535 } |
|
536 |
|
537 for (i = 0; i < len; i++) { |
|
538 next = (i + 1); |
|
539 if (ticks[next] && ticks[next] >= pos) { |
|
540 diff1 = pos - ticks[i]; |
|
541 diff2 = ticks[next] - pos; |
|
542 ret = (diff2 > diff1) ? ticks[i] : ticks[next]; |
|
543 if (off1 && off2) { |
|
544 if (ret > off2) { |
|
545 if (ticks[i]) { |
|
546 ret = ticks[i]; |
|
547 } else { |
|
548 ret = ticks[len - 1]; |
|
549 } |
|
550 } |
|
551 } |
|
552 return ret; |
|
553 } |
|
554 |
|
555 } |
|
556 return ticks[ticks.length - 1]; |
|
557 } |
|
558 }); |
|
559 |
|
560 |
|
561 |
|
562 }, '@VERSION@', {"requires": ["dd-drag"]}); |