|
1 YUI.add('node-flick', function (Y, NAME) { |
|
2 |
|
3 /** |
|
4 * Provide a simple Flick plugin, which can be used along with the "flick" gesture event, to |
|
5 * animate the motion of the host node in response to a (mouse or touch) flick gesture. |
|
6 * |
|
7 * <p>The current implementation is designed to move the node, relative to the bounds of a parent node and is suitable |
|
8 * for scroll/carousel type implementations. Future versions will remove that constraint, to allow open ended movement within |
|
9 * the document.</p> |
|
10 * |
|
11 * @module node-flick |
|
12 */ |
|
13 |
|
14 var HOST = "host", |
|
15 PARENT_NODE = "parentNode", |
|
16 BOUNDING_BOX = "boundingBox", |
|
17 OFFSET_HEIGHT = "offsetHeight", |
|
18 OFFSET_WIDTH = "offsetWidth", |
|
19 SCROLL_HEIGHT = "scrollHeight", |
|
20 SCROLL_WIDTH = "scrollWidth", |
|
21 BOUNCE = "bounce", |
|
22 MIN_DISTANCE = "minDistance", |
|
23 MIN_VELOCITY = "minVelocity", |
|
24 BOUNCE_DISTANCE = "bounceDistance", |
|
25 DECELERATION = "deceleration", |
|
26 STEP = "step", |
|
27 DURATION = "duration", |
|
28 EASING = "easing", |
|
29 FLICK = "flick", |
|
30 |
|
31 getClassName = Y.ClassNameManager.getClassName; |
|
32 |
|
33 /** |
|
34 * A plugin class which can be used to animate the motion of a node, in response to a flick gesture. |
|
35 * |
|
36 * @class Flick |
|
37 * @namespace Plugin |
|
38 * @param {Object} config The initial attribute values for the plugin |
|
39 */ |
|
40 function Flick(config) { |
|
41 Flick.superclass.constructor.apply(this, arguments); |
|
42 } |
|
43 |
|
44 Flick.ATTRS = { |
|
45 |
|
46 /** |
|
47 * Drag coefficent for inertial scrolling. The closer to 1 this |
|
48 * value is, the less friction during scrolling. |
|
49 * |
|
50 * @attribute deceleration |
|
51 * @default 0.98 |
|
52 */ |
|
53 deceleration : { |
|
54 value: 0.98 |
|
55 }, |
|
56 |
|
57 /** |
|
58 * Drag coefficient for intertial scrolling at the upper |
|
59 * and lower boundaries of the scrollview. Set to 0 to |
|
60 * disable "rubber-banding". |
|
61 * |
|
62 * @attribute bounce |
|
63 * @type Number |
|
64 * @default 0.7 |
|
65 */ |
|
66 bounce : { |
|
67 value: 0.7 |
|
68 }, |
|
69 |
|
70 /** |
|
71 * The bounce distance in pixels |
|
72 * |
|
73 * @attribute bounceDistance |
|
74 * @type Number |
|
75 * @default 150 |
|
76 */ |
|
77 bounceDistance : { |
|
78 value: 150 |
|
79 }, |
|
80 |
|
81 /** |
|
82 * The minimum flick gesture velocity (px/ms) at which to trigger the flick response |
|
83 * |
|
84 * @attribute minVelocity |
|
85 * @type Number |
|
86 * @default 0 |
|
87 */ |
|
88 minVelocity : { |
|
89 value: 0 |
|
90 }, |
|
91 |
|
92 /** |
|
93 * The minimum flick gesture distance (px) for which to trigger the flick response |
|
94 * |
|
95 * @attribute minVelocity |
|
96 * @type Number |
|
97 * @default 10 |
|
98 */ |
|
99 minDistance : { |
|
100 value: 10 |
|
101 }, |
|
102 |
|
103 /** |
|
104 * The constraining box relative to which the flick animation and bounds should be calculated. |
|
105 * |
|
106 * @attribute boundingBox |
|
107 * @type Node |
|
108 * @default parentNode |
|
109 */ |
|
110 boundingBox : { |
|
111 valueFn : function() { |
|
112 return this.get(HOST).get(PARENT_NODE); |
|
113 } |
|
114 }, |
|
115 |
|
116 /** |
|
117 * Time between flick animation frames. |
|
118 * |
|
119 * @attribute step |
|
120 * @type Number |
|
121 * @default 10 |
|
122 */ |
|
123 step : { |
|
124 value:10 |
|
125 }, |
|
126 |
|
127 /** |
|
128 * The custom duration to apply to the flick animation. By default, |
|
129 * the animation duration is controlled by the deceleration factor. |
|
130 * |
|
131 * @attribute duration |
|
132 * @type Number |
|
133 * @default null |
|
134 */ |
|
135 duration : { |
|
136 value:null |
|
137 }, |
|
138 |
|
139 /** |
|
140 * The custom transition easing to use for the flick animation. If not |
|
141 * provided defaults to internally to Flick.EASING, or Flick.SNAP_EASING based |
|
142 * on whether or not we're animating the flick or bounce step. |
|
143 * |
|
144 * @attribute easing |
|
145 * @type String |
|
146 * @default null |
|
147 */ |
|
148 easing : { |
|
149 value:null |
|
150 } |
|
151 }; |
|
152 |
|
153 /** |
|
154 * The NAME of the Flick class. Used to prefix events generated |
|
155 * by the plugin. |
|
156 * |
|
157 * @property NAME |
|
158 * @static |
|
159 * @type String |
|
160 * @default "pluginFlick" |
|
161 */ |
|
162 Flick.NAME = "pluginFlick"; |
|
163 |
|
164 /** |
|
165 * The namespace for the plugin. This will be the property on the node, which will |
|
166 * reference the plugin instance, when it's plugged in. |
|
167 * |
|
168 * @property NS |
|
169 * @static |
|
170 * @type String |
|
171 * @default "flick" |
|
172 */ |
|
173 Flick.NS = "flick"; |
|
174 |
|
175 Y.extend(Flick, Y.Plugin.Base, { |
|
176 |
|
177 /** |
|
178 * The initializer lifecycle implementation. |
|
179 * |
|
180 * @method initializer |
|
181 * @param {Object} config The user configuration for the plugin |
|
182 */ |
|
183 initializer : function(config) { |
|
184 this._node = this.get(HOST); |
|
185 |
|
186 this._renderClasses(); |
|
187 this.setBounds(); |
|
188 |
|
189 this._node.on(FLICK, Y.bind(this._onFlick, this), { |
|
190 minDistance : this.get(MIN_DISTANCE), |
|
191 minVelocity : this.get(MIN_VELOCITY) |
|
192 }); |
|
193 }, |
|
194 |
|
195 /** |
|
196 * Sets the min/max boundaries for the flick animation, |
|
197 * based on the boundingBox dimensions. |
|
198 * |
|
199 * @method setBounds |
|
200 */ |
|
201 setBounds : function () { |
|
202 var box = this.get(BOUNDING_BOX), |
|
203 node = this._node, |
|
204 |
|
205 boxHeight = box.get(OFFSET_HEIGHT), |
|
206 boxWidth = box.get(OFFSET_WIDTH), |
|
207 |
|
208 contentHeight = node.get(SCROLL_HEIGHT), |
|
209 contentWidth = node.get(SCROLL_WIDTH); |
|
210 |
|
211 if (contentHeight > boxHeight) { |
|
212 this._maxY = contentHeight - boxHeight; |
|
213 this._minY = 0; |
|
214 this._scrollY = true; |
|
215 } |
|
216 |
|
217 if (contentWidth > boxWidth) { |
|
218 this._maxX = contentWidth - boxWidth; |
|
219 this._minX = 0; |
|
220 this._scrollX = true; |
|
221 } |
|
222 |
|
223 this._x = this._y = 0; |
|
224 |
|
225 node.set("top", this._y + "px"); |
|
226 node.set("left", this._x + "px"); |
|
227 }, |
|
228 |
|
229 /** |
|
230 * Adds the CSS classes, necessary to set up overflow/position properties on the |
|
231 * node and boundingBox. |
|
232 * |
|
233 * @method _renderClasses |
|
234 * @protected |
|
235 */ |
|
236 _renderClasses : function() { |
|
237 this.get(BOUNDING_BOX).addClass(Flick.CLASS_NAMES.box); |
|
238 this._node.addClass(Flick.CLASS_NAMES.content); |
|
239 }, |
|
240 |
|
241 /** |
|
242 * The flick event listener. Kicks off the flick animation. |
|
243 * |
|
244 * @method _onFlick |
|
245 * @param e {EventFacade} The flick event facade, containing e.flick.distance, e.flick.velocity etc. |
|
246 * @protected |
|
247 */ |
|
248 _onFlick: function(e) { |
|
249 this._v = e.flick.velocity; |
|
250 this._flick = true; |
|
251 this._flickAnim(); |
|
252 }, |
|
253 |
|
254 /** |
|
255 * Executes a single frame in the flick animation |
|
256 * |
|
257 * @method _flickFrame |
|
258 * @protected |
|
259 */ |
|
260 _flickAnim: function() { |
|
261 |
|
262 var y = this._y, |
|
263 x = this._x, |
|
264 |
|
265 maxY = this._maxY, |
|
266 minY = this._minY, |
|
267 maxX = this._maxX, |
|
268 minX = this._minX, |
|
269 velocity = this._v, |
|
270 |
|
271 step = this.get(STEP), |
|
272 deceleration = this.get(DECELERATION), |
|
273 bounce = this.get(BOUNCE); |
|
274 |
|
275 this._v = (velocity * deceleration); |
|
276 |
|
277 this._snapToEdge = false; |
|
278 |
|
279 if (this._scrollX) { |
|
280 x = x - (velocity * step); |
|
281 } |
|
282 |
|
283 if (this._scrollY) { |
|
284 y = y - (velocity * step); |
|
285 } |
|
286 |
|
287 if (Math.abs(velocity).toFixed(4) <= Flick.VELOCITY_THRESHOLD) { |
|
288 |
|
289 this._flick = false; |
|
290 |
|
291 this._killTimer(!(this._exceededYBoundary || this._exceededXBoundary)); |
|
292 |
|
293 if (this._scrollX) { |
|
294 if (x < minX) { |
|
295 this._snapToEdge = true; |
|
296 this._setX(minX); |
|
297 } else if (x > maxX) { |
|
298 this._snapToEdge = true; |
|
299 this._setX(maxX); |
|
300 } |
|
301 } |
|
302 |
|
303 if (this._scrollY) { |
|
304 if (y < minY) { |
|
305 this._snapToEdge = true; |
|
306 this._setY(minY); |
|
307 } else if (y > maxY) { |
|
308 this._snapToEdge = true; |
|
309 this._setY(maxY); |
|
310 } |
|
311 } |
|
312 |
|
313 } else { |
|
314 |
|
315 if (this._scrollX && (x < minX || x > maxX)) { |
|
316 this._exceededXBoundary = true; |
|
317 this._v *= bounce; |
|
318 } |
|
319 |
|
320 if (this._scrollY && (y < minY || y > maxY)) { |
|
321 this._exceededYBoundary = true; |
|
322 this._v *= bounce; |
|
323 } |
|
324 |
|
325 if (this._scrollX) { |
|
326 this._setX(x); |
|
327 } |
|
328 |
|
329 if (this._scrollY) { |
|
330 this._setY(y); |
|
331 } |
|
332 |
|
333 this._flickTimer = Y.later(step, this, this._flickAnim); |
|
334 } |
|
335 }, |
|
336 |
|
337 /** |
|
338 * Internal utility method to set the X offset position |
|
339 * |
|
340 * @method _setX |
|
341 * @param {Number} val |
|
342 * @private |
|
343 */ |
|
344 _setX : function(val) { |
|
345 this._move(val, null, this.get(DURATION), this.get(EASING)); |
|
346 }, |
|
347 |
|
348 /** |
|
349 * Internal utility method to set the Y offset position |
|
350 * |
|
351 * @method _setY |
|
352 * @param {Number} val |
|
353 * @private |
|
354 */ |
|
355 _setY : function(val) { |
|
356 this._move(null, val, this.get(DURATION), this.get(EASING)); |
|
357 }, |
|
358 |
|
359 /** |
|
360 * Internal utility method to move the node to a given XY position, |
|
361 * using transitions, if specified. |
|
362 * |
|
363 * @method _move |
|
364 * @param {Number} x The X offset position |
|
365 * @param {Number} y The Y offset position |
|
366 * @param {Number} duration The duration to use for the transition animation |
|
367 * @param {String} easing The easing to use for the transition animation. |
|
368 * |
|
369 * @private |
|
370 */ |
|
371 _move: function(x, y, duration, easing) { |
|
372 |
|
373 if (x !== null) { |
|
374 x = this._bounce(x); |
|
375 } else { |
|
376 x = this._x; |
|
377 } |
|
378 |
|
379 if (y !== null) { |
|
380 y = this._bounce(y); |
|
381 } else { |
|
382 y = this._y; |
|
383 } |
|
384 |
|
385 duration = duration || this._snapToEdge ? Flick.SNAP_DURATION : 0; |
|
386 easing = easing || this._snapToEdge ? Flick.SNAP_EASING : Flick.EASING; |
|
387 |
|
388 this._x = x; |
|
389 this._y = y; |
|
390 |
|
391 this._anim(x, y, duration, easing); |
|
392 }, |
|
393 |
|
394 /** |
|
395 * Internal utility method to perform the transition step |
|
396 * |
|
397 * @method _anim |
|
398 * @param {Number} x The X offset position |
|
399 * @param {Number} y The Y offset position |
|
400 * @param {Number} duration The duration to use for the transition animation |
|
401 * @param {String} easing The easing to use for the transition animation. |
|
402 * |
|
403 * @private |
|
404 */ |
|
405 _anim : function(x, y, duration, easing) { |
|
406 var xn = x * -1, |
|
407 yn = y * -1, |
|
408 |
|
409 transition = { |
|
410 duration : duration / 1000, |
|
411 easing : easing |
|
412 }; |
|
413 |
|
414 Y.log("Transition: duration, easing:" + transition.duration, transition.easing, "node-flick"); |
|
415 |
|
416 if (Y.Transition.useNative) { |
|
417 transition.transform = 'translate('+ (xn) + 'px,' + (yn) +'px)'; |
|
418 } else { |
|
419 transition.left = xn + 'px'; |
|
420 transition.top = yn + 'px'; |
|
421 } |
|
422 |
|
423 this._node.transition(transition); |
|
424 }, |
|
425 |
|
426 /** |
|
427 * Internal utility method to constrain the offset value |
|
428 * based on the bounce criteria. |
|
429 * |
|
430 * @method _bounce |
|
431 * @param {Number} x The offset value to constrain. |
|
432 * @param {Number} max The max offset value. |
|
433 * |
|
434 * @private |
|
435 */ |
|
436 _bounce : function(val, max) { |
|
437 var bounce = this.get(BOUNCE), |
|
438 dist = this.get(BOUNCE_DISTANCE), |
|
439 min = bounce ? -dist : 0; |
|
440 |
|
441 max = bounce ? max + dist : max; |
|
442 |
|
443 if(!bounce) { |
|
444 if(val < min) { |
|
445 val = min; |
|
446 } else if(val > max) { |
|
447 val = max; |
|
448 } |
|
449 } |
|
450 return val; |
|
451 }, |
|
452 |
|
453 /** |
|
454 * Stop the animation timer |
|
455 * |
|
456 * @method _killTimer |
|
457 * @private |
|
458 */ |
|
459 _killTimer: function() { |
|
460 if(this._flickTimer) { |
|
461 this._flickTimer.cancel(); |
|
462 } |
|
463 } |
|
464 |
|
465 }, { |
|
466 |
|
467 /** |
|
468 * The threshold used to determine when the decelerated velocity of the node |
|
469 * is practically 0. |
|
470 * |
|
471 * @property VELOCITY_THRESHOLD |
|
472 * @static |
|
473 * @type Number |
|
474 * @default 0.015 |
|
475 */ |
|
476 VELOCITY_THRESHOLD : 0.015, |
|
477 |
|
478 /** |
|
479 * The duration to use for the bounce snap-back transition |
|
480 * |
|
481 * @property SNAP_DURATION |
|
482 * @static |
|
483 * @type Number |
|
484 * @default 400 |
|
485 */ |
|
486 SNAP_DURATION : 400, |
|
487 |
|
488 /** |
|
489 * The default easing to use for the main flick movement transition |
|
490 * |
|
491 * @property EASING |
|
492 * @static |
|
493 * @type String |
|
494 * @default 'cubic-bezier(0, 0.1, 0, 1.0)' |
|
495 */ |
|
496 EASING : 'cubic-bezier(0, 0.1, 0, 1.0)', |
|
497 |
|
498 /** |
|
499 * The default easing to use for the bounce snap-back transition |
|
500 * |
|
501 * @property SNAP_EASING |
|
502 * @static |
|
503 * @type String |
|
504 * @default 'ease-out' |
|
505 */ |
|
506 SNAP_EASING : 'ease-out', |
|
507 |
|
508 /** |
|
509 * The default CSS class names used by the plugin |
|
510 * |
|
511 * @property CLASS_NAMES |
|
512 * @static |
|
513 * @type Object |
|
514 */ |
|
515 CLASS_NAMES : { |
|
516 box: getClassName(Flick.NS), |
|
517 content: getClassName(Flick.NS, "content") |
|
518 } |
|
519 }); |
|
520 |
|
521 Y.Plugin.Flick = Flick; |
|
522 |
|
523 |
|
524 }, '@VERSION@', {"requires": ["classnamemanager", "transition", "event-flick", "plugin"], "skinnable": true}); |