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