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