Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('gesture-simulate', function (Y, NAME) {
2
 
3
/**
4
 * Simulate high-level user gestures by generating a set of native DOM events.
5
 *
6
 * @module gesture-simulate
7
 * @requires event-simulate, async-queue, node-screen
8
 */
9
 
10
var NAME = "gesture-simulate",
11
 
12
    // phantomjs check may be temporary, until we determine if it really support touch all the way through, like it claims to (http://code.google.com/p/phantomjs/issues/detail?id=375)
13
    SUPPORTS_TOUCH = ((Y.config.win && ("ontouchstart" in Y.config.win)) && !(Y.UA.phantomjs) && !(Y.UA.chrome && Y.UA.chrome < 6)),
14
 
15
    gestureNames = {
16
        tap: 1,
17
        doubletap: 1,
18
        press: 1,
19
        move: 1,
20
        flick: 1,
21
        pinch: 1,
22
        rotate: 1
23
    },
24
 
25
    touchEvents = {
26
        touchstart: 1,
27
        touchmove: 1,
28
        touchend: 1,
29
        touchcancel: 1
30
    },
31
 
32
    document = Y.config.doc,
33
    emptyTouchList,
34
 
35
    EVENT_INTERVAL = 20,        // 20ms
36
    START_PAGEX,                // will be adjusted to the node element center
37
    START_PAGEY,                // will be adjusted to the node element center
38
 
39
    // defaults that user can override.
40
    DEFAULTS = {
41
        // tap gestures
42
        HOLD_TAP: 10,           // 10ms
43
        DELAY_TAP: 10,          // 10ms
44
 
45
        // press gesture
46
        HOLD_PRESS: 3000,       // 3sec
47
        MIN_HOLD_PRESS: 1000,   // 1sec
48
        MAX_HOLD_PRESS: 60000,  // 1min
49
 
50
        // move gesture
51
        DISTANCE_MOVE: 200,     // 200 pixels
52
        DURATION_MOVE: 1000,    // 1sec
53
        MAX_DURATION_MOVE: 5000,// 5sec
54
 
55
        // flick gesture
56
        MIN_VELOCITY_FLICK: 1.3,
57
        DISTANCE_FLICK: 200,     // 200 pixels
58
        DURATION_FLICK: 1000,    // 1sec
59
        MAX_DURATION_FLICK: 5000,// 5sec
60
 
61
        // pinch/rotation
62
        DURATION_PINCH: 1000     // 1sec
63
    },
64
 
65
    TOUCH_START = 'touchstart',
66
    TOUCH_MOVE = 'touchmove',
67
    TOUCH_END = 'touchend',
68
 
69
    GESTURE_START = 'gesturestart',
70
    GESTURE_CHANGE = 'gesturechange',
71
    GESTURE_END = 'gestureend',
72
 
73
    MOUSE_UP    = 'mouseup',
74
    MOUSE_MOVE  = 'mousemove',
75
    MOUSE_DOWN  = 'mousedown',
76
    MOUSE_CLICK = 'click',
77
    MOUSE_DBLCLICK = 'dblclick',
78
 
79
    X_AXIS = 'x',
80
    Y_AXIS = 'y';
81
 
82
 
83
function Simulations(node) {
84
    if(!node) {
85
        Y.error(NAME+': invalid target node');
86
    }
87
    this.node = node;
88
    this.target = Y.Node.getDOMNode(node);
89
 
90
    var startXY = this.node.getXY(),
91
        dims = this._getDims();
92
 
93
    START_PAGEX = startXY[0] + (dims[0])/2;
94
    START_PAGEY = startXY[1] + (dims[1])/2;
95
}
96
 
97
Simulations.prototype = {
98
 
99
    /**
100
     * Helper method to convert a degree to a radian.
101
     *
102
     * @method _toRadian
103
     * @private
104
     * @param {Number} deg A degree to be converted to a radian.
105
     * @return {Number} The degree in radian.
106
     */
107
    _toRadian: function(deg) {
108
        return deg * (Math.PI/180);
109
    },
110
 
111
    /**
112
     * Helper method to get height/width while accounting for
113
     * rotation/scale transforms where possible by using the
114
     * bounding client rectangle height/width instead of the
115
     * offsetWidth/Height which region uses.
116
     * @method _getDims
117
     * @private
118
     * @return {Array} Array with [height, width]
119
     */
120
    _getDims : function() {
121
        var region,
122
            width,
123
            height;
124
 
125
        // Ideally, this should be in DOM somewhere.
126
        if (this.target.getBoundingClientRect) {
127
            region = this.target.getBoundingClientRect();
128
 
129
            if ("height" in region) {
130
                height = region.height;
131
            } else {
132
                // IE7,8 has getBCR, but no height.
133
                height = Math.abs(region.bottom - region.top);
134
            }
135
 
136
            if ("width" in region) {
137
                width = region.width;
138
            } else {
139
                // IE7,8 has getBCR, but no width.
140
                width = Math.abs(region.right - region.left);
141
            }
142
        } else {
143
            region = this.node.get("region");
144
            width = region.width;
145
            height = region.height;
146
        }
147
 
148
        return [width, height];
149
    },
150
 
151
    /**
152
     * Helper method to convert a point relative to the node element into
153
     * the point in the page coordination.
154
     *
155
     * @method _calculateDefaultPoint
156
     * @private
157
     * @param {Array} point A point relative to the node element.
158
     * @return {Array} The same point in the page coordination.
159
     */
160
    _calculateDefaultPoint: function(point) {
161
 
162
        var height;
163
 
164
        if(!Y.Lang.isArray(point) || point.length === 0) {
165
            point = [START_PAGEX, START_PAGEY];
166
        } else {
167
            if(point.length == 1) {
168
                height = this._getDims[1];
169
                point[1] = height/2;
170
            }
171
            // convert to page(viewport) coordination
172
            point[0] = this.node.getX() + point[0];
173
            point[1] = this.node.getY() + point[1];
174
        }
175
 
176
        return point;
177
    },
178
 
179
    /**
180
     * The "rotate" and "pinch" methods are essencially same with the exact same
181
     * arguments. Only difference is the required parameters. The rotate method
182
     * requires "rotation" parameter while the pinch method requires "startRadius"
183
     * and "endRadius" parameters.
184
     *
185
     * @method rotate
186
     * @param {Function} cb The callback to execute when the gesture simulation
187
     *      is completed.
188
     * @param {Array} center A center point where the pinch gesture of two fingers
189
     *      should happen. It is relative to the top left corner of the target
190
     *      node element.
191
     * @param {Number} startRadius A radius of start circle where 2 fingers are
192
     *      on when the gesture starts. This is optional. The default is a fourth of
193
     *      either target node width or height whichever is smaller.
194
     * @param {Number} endRadius A radius of end circle where 2 fingers will be on when
195
     *      the pinch or spread gestures are completed. This is optional.
196
     *      The default is a fourth of either target node width or height whichever is less.
197
     * @param {Number} duration A duration of the gesture in millisecond.
198
     * @param {Number} start A start angle(0 degree at 12 o'clock) where the
199
     *      gesture should start. Default is 0.
200
     * @param {Number} rotation A rotation in degree. It is required.
201
     */
202
    rotate: function(cb, center, startRadius, endRadius, duration, start, rotation) {
203
        var radius,
204
            r1 = startRadius,   // optional
205
            r2 = endRadius;     // optional
206
 
207
        if(!Y.Lang.isNumber(r1) || !Y.Lang.isNumber(r2) || r1<0 || r2<0) {
208
            radius = (this.target.offsetWidth < this.target.offsetHeight)?
209
                this.target.offsetWidth/4 : this.target.offsetHeight/4;
210
            r1 = radius;
211
            r2 = radius;
212
        }
213
 
214
        // required
215
        if(!Y.Lang.isNumber(rotation)) {
216
            Y.error(NAME+'Invalid rotation detected.');
217
        }
218
 
219
        this.pinch(cb, center, r1, r2, duration, start, rotation);
220
    },
221
 
222
    /**
223
     * The "rotate" and "pinch" methods are essencially same with the exact same
224
     * arguments. Only difference is the required parameters. The rotate method
225
     * requires "rotation" parameter while the pinch method requires "startRadius"
226
     * and "endRadius" parameters.
227
     *
228
     * The "pinch" gesture can simulate various 2 finger gestures such as pinch,
229
     * spread and/or rotation. The "startRadius" and "endRadius" are required.
230
     * If endRadius is larger than startRadius, it becomes a spread gesture
231
     * otherwise a pinch gesture.
232
     *
233
     * @method pinch
234
     * @param {Function} cb The callback to execute when the gesture simulation
235
     *      is completed.
236
     * @param {Array} center A center point where the pinch gesture of two fingers
237
     *      should happen. It is relative to the top left corner of the target
238
     *      node element.
239
     * @param {Number} startRadius A radius of start circle where 2 fingers are
240
     *      on when the gesture starts. This paramenter is required.
241
     * @param {Number} endRadius A radius of end circle where 2 fingers will be on when
242
     *      the pinch or spread gestures are completed. This parameter is required.
243
     * @param {Number} duration A duration of the gesture in millisecond.
244
     * @param {Number} start A start angle(0 degree at 12 o'clock) where the
245
     *      gesture should start. Default is 0.
246
     * @param {Number} rotation If rotation is desired during the pinch or
247
     *      spread gestures, this parameter can be used. Default is 0 degree.
248
     */
249
    pinch: function(cb, center, startRadius, endRadius, duration, start, rotation) {
250
        var eventQueue,
251
            i,
252
            interval = EVENT_INTERVAL,
253
            touches,
254
            id = 0,
255
            r1 = startRadius,   // required
256
            r2 = endRadius,     // required
257
            radiusPerStep,
258
            centerX, centerY,
259
            startScale, endScale, scalePerStep,
260
            startRot, endRot, rotPerStep,
261
            path1 = {start: [], end: []}, // paths for 1st and 2nd fingers.
262
            path2 = {start: [], end: []},
263
            steps,
264
            touchMove;
265
 
266
        center = this._calculateDefaultPoint(center);
267
 
268
        if(!Y.Lang.isNumber(r1) || !Y.Lang.isNumber(r2) || r1<0 || r2<0) {
269
            Y.error(NAME+'Invalid startRadius and endRadius detected.');
270
        }
271
 
272
        if(!Y.Lang.isNumber(duration) || duration <= 0) {
273
            duration = DEFAULTS.DURATION_PINCH;
274
        }
275
 
276
        if(!Y.Lang.isNumber(start)) {
277
            start = 0.0;
278
        } else {
279
            start = start%360;
280
            while(start < 0) {
281
                start += 360;
282
            }
283
        }
284
 
285
        if(!Y.Lang.isNumber(rotation)) {
286
            rotation = 0.0;
287
        }
288
 
289
        Y.AsyncQueue.defaults.timeout = interval;
290
        eventQueue = new Y.AsyncQueue();
291
 
292
        // range determination
293
        centerX = center[0];
294
        centerY = center[1];
295
 
296
        startRot = start;
297
        endRot = start + rotation;
298
 
299
        // 1st finger path
300
        path1.start = [
301
            centerX + r1*Math.sin(this._toRadian(startRot)),
302
            centerY - r1*Math.cos(this._toRadian(startRot))
303
        ];
304
        path1.end   = [
305
            centerX + r2*Math.sin(this._toRadian(endRot)),
306
            centerY - r2*Math.cos(this._toRadian(endRot))
307
        ];
308
 
309
        // 2nd finger path
310
        path2.start = [
311
            centerX - r1*Math.sin(this._toRadian(startRot)),
312
            centerY + r1*Math.cos(this._toRadian(startRot))
313
        ];
314
        path2.end   = [
315
            centerX - r2*Math.sin(this._toRadian(endRot)),
316
            centerY + r2*Math.cos(this._toRadian(endRot))
317
        ];
318
 
319
        startScale = 1.0;
320
        endScale = endRadius/startRadius;
321
 
322
        // touch/gesture start
323
        eventQueue.add({
324
            fn: function() {
325
                var coord1, coord2, coord, touches;
326
 
327
                // coordinate for each touch object.
328
                coord1 = {
329
                    pageX: path1.start[0],
330
                    pageY: path1.start[1],
331
                    clientX: path1.start[0],
332
                    clientY: path1.start[1]
333
                };
334
                coord2 = {
335
                    pageX: path2.start[0],
336
                    pageY: path2.start[1],
337
                    clientX: path2.start[0],
338
                    clientY: path2.start[1]
339
                };
340
                touches = this._createTouchList([Y.merge({
341
                    identifier: (id++)
342
                }, coord1), Y.merge({
343
                    identifier: (id++)
344
                }, coord2)]);
345
 
346
                // coordinate for top level event
347
                coord = {
348
                    pageX: (path1.start[0] + path2.start[0])/2,
349
                    pageY: (path1.start[0] + path2.start[1])/2,
350
                    clientX: (path1.start[0] + path2.start[0])/2,
351
                    clientY: (path1.start[0] + path2.start[1])/2
352
                };
353
 
354
                this._simulateEvent(this.target, TOUCH_START, Y.merge({
355
                    touches: touches,
356
                    targetTouches: touches,
357
                    changedTouches: touches,
358
                    scale: startScale,
359
                    rotation: startRot
360
                }, coord));
361
 
362
                if(Y.UA.ios >= 2.0) {
363
                    /* gesture starts when the 2nd finger touch starts.
364
                    * The implementation will fire 1 touch start event for both fingers,
365
                    * simulating 2 fingers touched on the screen at the same time.
366
                    */
367
                    this._simulateEvent(this.target, GESTURE_START, Y.merge({
368
                        scale: startScale,
369
                        rotation: startRot
370
                    }, coord));
371
                }
372
            },
373
            timeout: 0,
374
            context: this
375
        });
376
 
377
        // gesture change
378
        steps = Math.floor(duration/interval);
379
        radiusPerStep = (r2 - r1)/steps;
380
        scalePerStep = (endScale - startScale)/steps;
381
        rotPerStep = (endRot - startRot)/steps;
382
 
383
        touchMove = function(step) {
384
            var radius = r1 + (radiusPerStep)*step,
385
                px1 = centerX + radius*Math.sin(this._toRadian(startRot + rotPerStep*step)),
386
                py1 = centerY - radius*Math.cos(this._toRadian(startRot + rotPerStep*step)),
387
                px2 = centerX - radius*Math.sin(this._toRadian(startRot + rotPerStep*step)),
388
                py2 = centerY + radius*Math.cos(this._toRadian(startRot + rotPerStep*step)),
389
                px = (px1+px2)/2,
390
                py = (py1+py2)/2,
391
                coord1, coord2, coord, touches;
392
 
393
            // coordinate for each touch object.
394
            coord1 = {
395
                pageX: px1,
396
                pageY: py1,
397
                clientX: px1,
398
                clientY: py1
399
            };
400
            coord2 = {
401
                pageX: px2,
402
                pageY: py2,
403
                clientX: px2,
404
                clientY: py2
405
            };
406
            touches = this._createTouchList([Y.merge({
407
                identifier: (id++)
408
            }, coord1), Y.merge({
409
                identifier: (id++)
410
            }, coord2)]);
411
 
412
            // coordinate for top level event
413
            coord = {
414
                pageX: px,
415
                pageY: py,
416
                clientX: px,
417
                clientY: py
418
            };
419
 
420
            this._simulateEvent(this.target, TOUCH_MOVE, Y.merge({
421
                touches: touches,
422
                targetTouches: touches,
423
                changedTouches: touches,
424
                scale: startScale + scalePerStep*step,
425
                rotation: startRot + rotPerStep*step
426
            }, coord));
427
 
428
            if(Y.UA.ios >= 2.0) {
429
                this._simulateEvent(this.target, GESTURE_CHANGE, Y.merge({
430
                    scale: startScale + scalePerStep*step,
431
                    rotation: startRot + rotPerStep*step
432
                }, coord));
433
            }
434
        };
435
 
436
        for (i=0; i < steps; i++) {
437
            eventQueue.add({
438
                fn: touchMove,
439
                args: [i],
440
                context: this
441
            });
442
        }
443
 
444
        // gesture end
445
        eventQueue.add({
446
            fn: function() {
447
                var emptyTouchList = this._getEmptyTouchList(),
448
                    coord1, coord2, coord, touches;
449
 
450
                // coordinate for each touch object.
451
                coord1 = {
452
                    pageX: path1.end[0],
453
                    pageY: path1.end[1],
454
                    clientX: path1.end[0],
455
                    clientY: path1.end[1]
456
                };
457
                coord2 = {
458
                    pageX: path2.end[0],
459
                    pageY: path2.end[1],
460
                    clientX: path2.end[0],
461
                    clientY: path2.end[1]
462
                };
463
                touches = this._createTouchList([Y.merge({
464
                    identifier: (id++)
465
                }, coord1), Y.merge({
466
                    identifier: (id++)
467
                }, coord2)]);
468
 
469
                // coordinate for top level event
470
                coord = {
471
                    pageX: (path1.end[0] + path2.end[0])/2,
472
                    pageY: (path1.end[0] + path2.end[1])/2,
473
                    clientX: (path1.end[0] + path2.end[0])/2,
474
                    clientY: (path1.end[0] + path2.end[1])/2
475
                };
476
 
477
                if(Y.UA.ios >= 2.0) {
478
                    this._simulateEvent(this.target, GESTURE_END, Y.merge({
479
                        scale: endScale,
480
                        rotation: endRot
481
                    }, coord));
482
                }
483
 
484
                this._simulateEvent(this.target, TOUCH_END, Y.merge({
485
                    touches: emptyTouchList,
486
                    targetTouches: emptyTouchList,
487
                    changedTouches: touches,
488
                    scale: endScale,
489
                    rotation: endRot
490
                }, coord));
491
            },
492
            context: this
493
        });
494
 
495
        if(cb && Y.Lang.isFunction(cb)) {
496
            eventQueue.add({
497
                fn: cb,
498
 
499
                // by default, the callback runs the node context where
500
                // simulateGesture method is called.
501
                context: this.node
502
 
503
                //TODO: Use args to pass error object as 1st param if there is an error.
504
                //args:
505
            });
506
        }
507
 
508
        eventQueue.run();
509
    },
510
 
511
    /**
512
     * The "tap" gesture can be used for various single touch point gestures
513
     * such as single tap, N number of taps, long press. The default is a single
514
     * tap.
515
     *
516
     * @method tap
517
     * @param {Function} cb The callback to execute when the gesture simulation
518
     *      is completed.
519
     * @param {Array} point A point(relative to the top left corner of the
520
     *      target node element) where the tap gesture should start. The default
521
     *      is the center of the taget node.
522
     * @param {Number} times The number of taps. Default is 1.
523
     * @param {Number} hold The hold time in milliseconds between "touchstart" and
524
     *      "touchend" event generation. Default is 10ms.
525
     * @param {Number} delay The time gap in millisecond between taps if this
526
     *      gesture has more than 1 tap. Default is 10ms.
527
     */
528
    tap: function(cb, point, times, hold, delay) {
529
        var eventQueue = new Y.AsyncQueue(),
530
            emptyTouchList = this._getEmptyTouchList(),
531
            touches,
532
            coord,
533
            i,
534
            touchStart,
535
            touchEnd;
536
 
537
        point = this._calculateDefaultPoint(point);
538
 
539
        if(!Y.Lang.isNumber(times) || times < 1) {
540
            times = 1;
541
        }
542
 
543
        if(!Y.Lang.isNumber(hold)) {
544
            hold = DEFAULTS.HOLD_TAP;
545
        }
546
 
547
        if(!Y.Lang.isNumber(delay)) {
548
            delay = DEFAULTS.DELAY_TAP;
549
        }
550
 
551
        coord = {
552
            pageX: point[0],
553
            pageY: point[1],
554
            clientX: point[0],
555
            clientY: point[1]
556
        };
557
 
558
        touches = this._createTouchList([Y.merge({identifier: 0}, coord)]);
559
 
560
        touchStart = function() {
561
            this._simulateEvent(this.target, TOUCH_START, Y.merge({
562
                touches: touches,
563
                targetTouches: touches,
564
                changedTouches: touches
565
            }, coord));
566
        };
567
 
568
        touchEnd = function() {
569
            this._simulateEvent(this.target, TOUCH_END, Y.merge({
570
                touches: emptyTouchList,
571
                targetTouches: emptyTouchList,
572
                changedTouches: touches
573
            }, coord));
574
        };
575
 
576
        for (i=0; i < times; i++) {
577
            eventQueue.add({
578
                fn: touchStart,
579
                context: this,
580
                timeout: (i === 0)? 0 : delay
581
            });
582
 
583
            eventQueue.add({
584
                fn: touchEnd,
585
                context: this,
586
                timeout: hold
587
            });
588
        }
589
 
590
        if(times > 1 && !SUPPORTS_TOUCH) {
591
            eventQueue.add({
592
                fn: function() {
593
                    this._simulateEvent(this.target, MOUSE_DBLCLICK, coord);
594
                },
595
                context: this
596
            });
597
        }
598
 
599
        if(cb && Y.Lang.isFunction(cb)) {
600
            eventQueue.add({
601
                fn: cb,
602
 
603
                // by default, the callback runs the node context where
604
                // simulateGesture method is called.
605
                context: this.node
606
 
607
                //TODO: Use args to pass error object as 1st param if there is an error.
608
                //args:
609
            });
610
        }
611
 
612
        eventQueue.run();
613
    },
614
 
615
    /**
616
     * The "flick" gesture is a specialized "move" that has some velocity
617
     * and the movement always runs either x or y axis. The velocity is calculated
618
     * with "distance" and "duration" arguments. If the calculated velocity is
619
     * below than the minimum velocity, the given duration will be ignored and
620
     * new duration will be created to make a valid flick gesture.
621
     *
622
     * @method flick
623
     * @param {Function} cb The callback to execute when the gesture simulation
624
     *      is completed.
625
     * @param {Array} point A point(relative to the top left corner of the
626
     *      target node element) where the flick gesture should start. The default
627
     *      is the center of the taget node.
628
     * @param {String} axis Either "x" or "y".
629
     * @param {Number} distance A distance in pixels to flick.
630
     * @param {Number} duration A duration of the gesture in millisecond.
631
     *
632
     */
633
    flick: function(cb, point, axis, distance, duration) {
634
        var path;
635
 
636
        point = this._calculateDefaultPoint(point);
637
 
638
        if(!Y.Lang.isString(axis)) {
639
            axis = X_AXIS;
640
        } else {
641
            axis = axis.toLowerCase();
642
            if(axis !== X_AXIS && axis !== Y_AXIS) {
643
                Y.error(NAME+'(flick): Only x or y axis allowed');
644
            }
645
        }
646
 
647
        if(!Y.Lang.isNumber(distance)) {
648
            distance = DEFAULTS.DISTANCE_FLICK;
649
        }
650
 
651
        if(!Y.Lang.isNumber(duration)){
652
            duration = DEFAULTS.DURATION_FLICK; // ms
653
        } else {
654
            if(duration > DEFAULTS.MAX_DURATION_FLICK) {
655
                duration = DEFAULTS.MAX_DURATION_FLICK;
656
            }
657
        }
658
 
659
        /*
660
         * Check if too slow for a flick.
661
         * Adjust duration if the calculated velocity is less than
662
         * the minimum velcocity to be claimed as a flick.
663
         */
664
        if(Math.abs(distance)/duration < DEFAULTS.MIN_VELOCITY_FLICK) {
665
            duration = Math.abs(distance)/DEFAULTS.MIN_VELOCITY_FLICK;
666
        }
667
 
668
        path = {
669
            start: Y.clone(point),
670
            end: [
671
                (axis === X_AXIS) ? point[0]+distance : point[0],
672
                (axis === Y_AXIS) ? point[1]+distance : point[1]
673
            ]
674
        };
675
 
676
        this._move(cb, path, duration);
677
    },
678
 
679
    /**
680
     * The "move" gesture simulate the movement of any direction between
681
     * the straight line of start and end point for the given duration.
682
     * The path argument is an object with "point", "xdist" and "ydist" properties.
683
     * The "point" property is an array with x and y coordinations(relative to the
684
     * top left corner of the target node element) while "xdist" and "ydist"
685
     * properties are used for the distance along the x and y axis. A negative
686
     * distance number can be used to drag either left or up direction.
687
     *
688
     * If no arguments are given, it will simulate the default move, which
689
     * is moving 200 pixels from the center of the element to the positive X-axis
690
     * direction for 1 sec.
691
     *
692
     * @method move
693
     * @param {Function} cb The callback to execute when the gesture simulation
694
     *      is completed.
695
     * @param {Object} path An object with "point", "xdist" and "ydist".
696
     * @param {Number} duration A duration of the gesture in millisecond.
697
     */
698
    move: function(cb, path, duration) {
699
        var convertedPath;
700
 
701
        if(!Y.Lang.isObject(path)) {
702
            path = {
703
                point: this._calculateDefaultPoint([]),
704
                xdist: DEFAULTS.DISTANCE_MOVE,
705
                ydist: 0
706
            };
707
        } else {
708
            // convert to the page coordination
709
            if(!Y.Lang.isArray(path.point)) {
710
                path.point = this._calculateDefaultPoint([]);
711
            } else {
712
                path.point = this._calculateDefaultPoint(path.point);
713
            }
714
 
715
            if(!Y.Lang.isNumber(path.xdist)) {
716
                path.xdist = DEFAULTS.DISTANCE_MOVE;
717
            }
718
 
719
            if(!Y.Lang.isNumber(path.ydist)) {
720
                path.ydist = 0;
721
            }
722
        }
723
 
724
        if(!Y.Lang.isNumber(duration)){
725
            duration = DEFAULTS.DURATION_MOVE; // ms
726
        } else {
727
            if(duration > DEFAULTS.MAX_DURATION_MOVE) {
728
                duration = DEFAULTS.MAX_DURATION_MOVE;
729
            }
730
        }
731
 
732
        convertedPath = {
733
            start: Y.clone(path.point),
734
            end: [path.point[0]+path.xdist, path.point[1]+path.ydist]
735
        };
736
 
737
        this._move(cb, convertedPath, duration);
738
    },
739
 
740
    /**
741
     * A base method on top of "move" and "flick" methods. The method takes
742
     * the path with start/end properties and duration to generate a set of
743
     * touch events for the movement gesture.
744
     *
745
     * @method _move
746
     * @private
747
     * @param {Function} cb The callback to execute when the gesture simulation
748
     *      is completed.
749
     * @param {Object} path An object with "start" and "end" properties. Each
750
     *      property should be an array with x and y coordination (e.g. start: [100, 50])
751
     * @param {Number} duration A duration of the gesture in millisecond.
752
     */
753
    _move: function(cb, path, duration) {
754
        var eventQueue,
755
            i,
756
            interval = EVENT_INTERVAL,
757
            steps, stepX, stepY,
758
            id = 0,
759
            touchMove;
760
 
761
        if(!Y.Lang.isNumber(duration)){
762
            duration = DEFAULTS.DURATION_MOVE; // ms
763
        } else {
764
            if(duration > DEFAULTS.MAX_DURATION_MOVE) {
765
                duration = DEFAULTS.MAX_DURATION_MOVE;
766
            }
767
        }
768
 
769
        if(!Y.Lang.isObject(path)) {
770
            path = {
771
                start: [
772
                    START_PAGEX,
773
                    START_PAGEY
774
                ],
775
                end: [
776
                    START_PAGEX + DEFAULTS.DISTANCE_MOVE,
777
                    START_PAGEY
778
                ]
779
            };
780
        } else {
781
            if(!Y.Lang.isArray(path.start)) {
782
                path.start = [
783
                    START_PAGEX,
784
                    START_PAGEY
785
                ];
786
            }
787
            if(!Y.Lang.isArray(path.end)) {
788
                path.end = [
789
                    START_PAGEX + DEFAULTS.DISTANCE_MOVE,
790
                    START_PAGEY
791
                ];
792
            }
793
        }
794
 
795
        Y.AsyncQueue.defaults.timeout = interval;
796
        eventQueue = new Y.AsyncQueue();
797
 
798
        // start
799
        eventQueue.add({
800
            fn: function() {
801
                var coord = {
802
                        pageX: path.start[0],
803
                        pageY: path.start[1],
804
                        clientX: path.start[0],
805
                        clientY: path.start[1]
806
                    },
807
                    touches = this._createTouchList([
808
                        Y.merge({identifier: (id++)}, coord)
809
                    ]);
810
 
811
                this._simulateEvent(this.target, TOUCH_START, Y.merge({
812
                    touches: touches,
813
                    targetTouches: touches,
814
                    changedTouches: touches
815
                }, coord));
816
            },
817
            timeout: 0,
818
            context: this
819
        });
820
 
821
        // move
822
        steps = Math.floor(duration/interval);
823
        stepX = (path.end[0] - path.start[0])/steps;
824
        stepY = (path.end[1] - path.start[1])/steps;
825
 
826
        touchMove = function(step) {
827
            var px = path.start[0]+(stepX * step),
828
                py = path.start[1]+(stepY * step),
829
                coord = {
830
                    pageX: px,
831
                    pageY: py,
832
                    clientX: px,
833
                    clientY: py
834
                },
835
                touches = this._createTouchList([
836
                    Y.merge({identifier: (id++)}, coord)
837
                ]);
838
 
839
            this._simulateEvent(this.target, TOUCH_MOVE, Y.merge({
840
                touches: touches,
841
                targetTouches: touches,
842
                changedTouches: touches
843
            }, coord));
844
        };
845
 
846
        for (i=0; i < steps; i++) {
847
            eventQueue.add({
848
                fn: touchMove,
849
                args: [i],
850
                context: this
851
            });
852
        }
853
 
854
        // last move
855
        eventQueue.add({
856
            fn: function() {
857
                var coord = {
858
                        pageX: path.end[0],
859
                        pageY: path.end[1],
860
                        clientX: path.end[0],
861
                        clientY: path.end[1]
862
                    },
863
                    touches = this._createTouchList([
864
                        Y.merge({identifier: id}, coord)
865
                    ]);
866
 
867
                this._simulateEvent(this.target, TOUCH_MOVE, Y.merge({
868
                    touches: touches,
869
                    targetTouches: touches,
870
                    changedTouches: touches
871
                }, coord));
872
            },
873
            timeout: 0,
874
            context: this
875
        });
876
 
877
        // end
878
        eventQueue.add({
879
            fn: function() {
880
                var coord = {
881
                    pageX: path.end[0],
882
                    pageY: path.end[1],
883
                    clientX: path.end[0],
884
                    clientY: path.end[1]
885
                },
886
                emptyTouchList = this._getEmptyTouchList(),
887
                touches = this._createTouchList([
888
                    Y.merge({identifier: id}, coord)
889
                ]);
890
 
891
                this._simulateEvent(this.target, TOUCH_END, Y.merge({
892
                    touches: emptyTouchList,
893
                    targetTouches: emptyTouchList,
894
                    changedTouches: touches
895
                }, coord));
896
            },
897
            context: this
898
        });
899
 
900
        if(cb && Y.Lang.isFunction(cb)) {
901
            eventQueue.add({
902
                fn: cb,
903
 
904
                // by default, the callback runs the node context where
905
                // simulateGesture method is called.
906
                context: this.node
907
 
908
                //TODO: Use args to pass error object as 1st param if there is an error.
909
                //args:
910
            });
911
        }
912
 
913
        eventQueue.run();
914
    },
915
 
916
    /**
917
     * Helper method to return a singleton instance of empty touch list.
918
     *
919
     * @method _getEmptyTouchList
920
     * @private
921
     * @return {TouchList | Array} An empty touch list object.
922
     */
923
    _getEmptyTouchList: function() {
924
        if(!emptyTouchList) {
925
            emptyTouchList = this._createTouchList([]);
926
        }
927
 
928
        return emptyTouchList;
929
    },
930
 
931
    /**
932
     * Helper method to convert an array with touch points to TouchList object as
933
     * defined in http://www.w3.org/TR/touch-events/
934
     *
935
     * @method _createTouchList
936
     * @private
937
     * @param {Array} touchPoints
938
     * @return {TouchList | Array} If underlaying platform support creating touch list
939
     *      a TouchList object will be returned otherwise a fake Array object
940
     *      will be returned.
941
     */
942
    _createTouchList: function(touchPoints) {
943
        /*
944
        * Android 4.0.3 emulator:
945
        * Native touch api supported starting in version 4.0 (Ice Cream Sandwich).
946
        * However the support seems limited. In Android 4.0.3 emulator, I got
947
        * "TouchList is not defined".
948
        */
949
        var touches = [],
950
            touchList,
951
            self = this;
952
 
953
        if(!!touchPoints && Y.Lang.isArray(touchPoints)) {
954
            if(Y.UA.android && Y.UA.android >= 4.0 || Y.UA.ios && Y.UA.ios >= 2.0) {
955
                Y.each(touchPoints, function(point) {
956
                    if(!point.identifier) {point.identifier = 0;}
957
                    if(!point.pageX) {point.pageX = 0;}
958
                    if(!point.pageY) {point.pageY = 0;}
959
                    if(!point.screenX) {point.screenX = 0;}
960
                    if(!point.screenY) {point.screenY = 0;}
961
 
962
                    touches.push(document.createTouch(Y.config.win,
963
                        self.target,
964
                        point.identifier,
965
                        point.pageX, point.pageY,
966
                        point.screenX, point.screenY));
967
                });
968
 
969
                touchList = document.createTouchList.apply(document, touches);
970
            } else if(Y.UA.ios && Y.UA.ios < 2.0) {
971
                Y.error(NAME+': No touch event simulation framework present.');
972
            } else {
973
                // this will inclide android(Y.UA.android && Y.UA.android < 4.0)
974
                // and desktops among all others.
975
 
976
                /*
977
                 * Touch APIs are broken in androids older than 4.0. We will use
978
                 * simulated touch apis for these versions.
979
                 */
980
                touchList = [];
981
                Y.each(touchPoints, function(point) {
982
                    if(!point.identifier) {point.identifier = 0;}
983
                    if(!point.clientX)  {point.clientX = 0;}
984
                    if(!point.clientY)  {point.clientY = 0;}
985
                    if(!point.pageX)    {point.pageX = 0;}
986
                    if(!point.pageY)    {point.pageY = 0;}
987
                    if(!point.screenX)  {point.screenX = 0;}
988
                    if(!point.screenY)  {point.screenY = 0;}
989
 
990
                    touchList.push({
991
                        target: self.target,
992
                        identifier: point.identifier,
993
                        clientX: point.clientX,
994
                        clientY: point.clientY,
995
                        pageX: point.pageX,
996
                        pageY: point.pageY,
997
                        screenX: point.screenX,
998
                        screenY: point.screenY
999
                    });
1000
                });
1001
 
1002
                touchList.item = function(i) {
1003
                    return touchList[i];
1004
                };
1005
            }
1006
        } else {
1007
            Y.error(NAME+': Invalid touchPoints passed');
1008
        }
1009
 
1010
        return touchList;
1011
    },
1012
 
1013
    /**
1014
     * @method _simulateEvent
1015
     * @private
1016
     * @param {HTMLElement} target The DOM element that's the target of the event.
1017
     * @param {String} type The type of event or name of the supported gesture to simulate
1018
     *      (i.e., "click", "doubletap", "flick").
1019
     * @param {Object} options (Optional) Extra options to copy onto the event object.
1020
     *      For gestures, options are used to refine the gesture behavior.
1021
     */
1022
    _simulateEvent: function(target, type, options) {
1023
        var touches;
1024
 
1025
        if (touchEvents[type]) {
1026
            if(SUPPORTS_TOUCH) {
1027
                Y.Event.simulate(target, type, options);
1028
            } else {
1029
                // simulate using mouse events if touch is not applicable on this platform.
1030
                // but only single touch event can be simulated.
1031
                if(this._isSingleTouch(options.touches, options.targetTouches, options.changedTouches)) {
1032
                    type = {
1033
                        touchstart: MOUSE_DOWN,
1034
                        touchmove: MOUSE_MOVE,
1035
                        touchend: MOUSE_UP
1036
                    }[type];
1037
 
1038
                    options.button = 0;
1039
                    options.relatedTarget = null; // since we are not using mouseover event.
1040
 
1041
                    // touchend has none in options.touches.
1042
                    touches = (type === MOUSE_UP)? options.changedTouches : options.touches;
1043
 
1044
                    options = Y.mix(options, {
1045
                        screenX: touches.item(0).screenX,
1046
                        screenY: touches.item(0).screenY,
1047
                        clientX: touches.item(0).clientX,
1048
                        clientY: touches.item(0).clientY
1049
                    }, true);
1050
 
1051
                    Y.Event.simulate(target, type, options);
1052
 
1053
                    if(type == MOUSE_UP) {
1054
                        Y.Event.simulate(target, MOUSE_CLICK, options);
1055
                    }
1056
                } else {
1057
                    Y.error("_simulateEvent(): Event '" + type + "' has multi touch objects that can't be simulated in your platform.");
1058
                }
1059
            }
1060
        } else {
1061
            // pass thru for all non touch events
1062
            Y.Event.simulate(target, type, options);
1063
        }
1064
    },
1065
 
1066
    /**
1067
     * Helper method to check the single touch.
1068
     * @method _isSingleTouch
1069
     * @private
1070
     * @param {TouchList} touches
1071
     * @param {TouchList} targetTouches
1072
     * @param {TouchList} changedTouches
1073
     */
1074
    _isSingleTouch: function(touches, targetTouches, changedTouches) {
1075
        return (touches && (touches.length <= 1)) &&
1076
            (targetTouches && (targetTouches.length <= 1)) &&
1077
            (changedTouches && (changedTouches.length <= 1));
1078
    }
1079
};
1080
 
1081
/*
1082
 * A gesture simulation class.
1083
 */
1084
Y.GestureSimulation = Simulations;
1085
 
1086
/*
1087
 * Various simulation default behavior properties. If user override
1088
 * Y.GestureSimulation.defaults, overriden values will be used and this
1089
 * should be done before the gesture simulation.
1090
 */
1091
Y.GestureSimulation.defaults = DEFAULTS;
1092
 
1093
/*
1094
 * The high level gesture names that YUI knows how to simulate.
1095
 */
1096
Y.GestureSimulation.GESTURES = gestureNames;
1097
 
1098
/**
1099
 * Simulates the higher user level gesture of the given name on a target.
1100
 * This method generates a set of low level touch events(Apple specific gesture
1101
 * events as well for the iOS platforms) asynchronously. Note that gesture
1102
 * simulation is relying on `Y.Event.simulate()` method to generate
1103
 * the touch events under the hood. The `Y.Event.simulate()` method
1104
 * itself is a synchronous method.
1105
 *
1106
 * Users are suggested to use `Node.simulateGesture()` method which
1107
 * basically calls this method internally. Supported gestures are `tap`,
1108
 * `doubletap`, `press`, `move`, `flick`, `pinch` and `rotate`.
1109
 *
1110
 * The `pinch` gesture is used to simulate the pinching and spreading of two
1111
 * fingers. During a pinch simulation, rotation is also possible. Essentially
1112
 * `pinch` and `rotate` simulations share the same base implementation to allow
1113
 * both pinching and rotation at the same time. The only difference is `pinch`
1114
 * requires `start` and `end` option properties while `rotate` requires `rotation`
1115
 * option property.
1116
 *
1117
 * The `pinch` and `rotate` gestures can be described as placing 2 fingers along a
1118
 * circle. Pinching and spreading can be described by start and end circles while
1119
 * rotation occurs on a single circle. If the radius of the start circle is greater
1120
 * than the end circle, the gesture becomes a pinch, otherwise it is a spread spread.
1121
 *
1122
 * @example
1123
 *
1124
 *     var node = Y.one("#target");
1125
 *
1126
 *     // double tap example
1127
 *     node.simulateGesture("doubletap", function() {
1128
 *         // my callback function
1129
 *     });
1130
 *
1131
 *     // flick example from the center of the node, move 50 pixels down for 50ms)
1132
 *     node.simulateGesture("flick", {
1133
 *         axis: y,
1134
 *         distance: -100
1135
 *         duration: 50
1136
 *     }, function() {
1137
 *         // my callback function
1138
 *     });
1139
 *
1140
 *     // simulate rotating a node 75 degrees counter-clockwise
1141
 *     node.simulateGesture("rotate", {
1142
 *         rotation: -75
1143
 *     });
1144
 *
1145
 *     // simulate a pinch and a rotation at the same time.
1146
 *     // fingers start on a circle of radius 100 px, placed at top/bottom
1147
 *     // fingers end on a circle of radius 50px, placed at right/left
1148
 *     node.simulateGesture("pinch", {
1149
 *         r1: 100,
1150
 *         r2: 50,
1151
 *         start: 0
1152
 *         rotation: 90
1153
 *     });
1154
 *
1155
 * @method simulateGesture
1156
 * @param {HTMLElement|Node} node The YUI node or HTML element that's the target
1157
 *      of the event.
1158
 * @param {String} name The name of the supported gesture to simulate. The
1159
 *      supported gesture name is one of "tap", "doubletap", "press", "move",
1160
 *      "flick", "pinch" and "rotate".
1161
 * @param {Object} [options] Extra options used to define the gesture behavior:
1162
 *
1163
 *      Valid options properties for the `tap` gesture:
1164
 *
1165
 *      @param {Array} [options.point] (Optional) Indicates the [x,y] coordinates
1166
 *        where the tap should be simulated. Default is the center of the node
1167
 *        element.
1168
 *      @param {Number} [options.hold=10] (Optional) The hold time in milliseconds.
1169
 *        This is the time between `touchstart` and `touchend` event generation.
1170
 *      @param {Number} [options.times=1] (Optional) Indicates the number of taps.
1171
 *      @param {Number} [options.delay=10] (Optional) The number of milliseconds
1172
 *        before the next tap simulation happens. This is valid only when `times`
1173
 *        is more than 1.
1174
 *
1175
 *      Valid options properties for the `doubletap` gesture:
1176
 *
1177
 *      @param {Array} [options.point] (Optional) Indicates the [x,y] coordinates
1178
 *        where the doubletap should be simulated. Default is the center of the
1179
 *        node element.
1180
 *
1181
 *      Valid options properties for the `press` gesture:
1182
 *
1183
 *      @param {Array} [options.point] (Optional) Indicates the [x,y] coordinates
1184
 *        where the press should be simulated. Default is the center of the node
1185
 *        element.
1186
 *      @param {Number} [options.hold=3000] (Optional) The hold time in milliseconds.
1187
 *        This is the time between `touchstart` and `touchend` event generation.
1188
 *        Default is 3000ms (3 seconds).
1189
 *
1190
 *      Valid options properties for the `move` gesture:
1191
 *
1192
 *      @param {Object} [options.path] (Optional) Indicates the path of the finger
1193
 *        movement. It's an object with three optional properties: `point`,
1194
 *        `xdist` and  `ydist`.
1195
 *        @param {Array} [options.path.point] A starting point of the gesture.
1196
 *          Default is the center of the node element.
1197
 *        @param {Number} [options.path.xdist=200] A distance to move in pixels
1198
 *          along the X axis. A negative distance value indicates moving left.
1199
 *        @param {Number} [options.path.ydist=0] A distance to move in pixels
1200
 *          along the Y axis. A negative distance value indicates moving up.
1201
 *      @param {Number} [options.duration=1000] (Optional) The duration of the
1202
 *        gesture in milliseconds.
1203
 *
1204
 *      Valid options properties for the `flick` gesture:
1205
 *
1206
 *      @param {Array} [options.point] (Optional) Indicates the [x, y] coordinates
1207
 *        where the flick should be simulated. Default is the center of the
1208
 *        node element.
1209
 *      @param {String} [options.axis='x'] (Optional) Valid values are either
1210
 *        "x" or "y". Indicates axis to move along. The flick can move to one of
1211
 *        4 directions(left, right, up and down).
1212
 *      @param {Number} [options.distance=200] (Optional) Distance to move in pixels
1213
 *      @param {Number} [options.duration=1000] (Optional) The duration of the
1214
 *        gesture in milliseconds. User given value could be automatically
1215
 *        adjusted by the framework if it is below the minimum velocity to be
1216
 *        a flick gesture.
1217
 *
1218
 *      Valid options properties for the `pinch` gesture:
1219
 *
1220
 *      @param {Array} [options.center] (Optional) The center of the circle where
1221
 *        two fingers are placed. Default is the center of the node element.
1222
 *      @param {Number} [options.r1] (Required) Pixel radius of the start circle
1223
 *        where 2 fingers will be on when the gesture starts. The circles are
1224
 *        centered at the center of the element.
1225
 *      @param {Number} [options.r2] (Required) Pixel radius of the end circle
1226
 *        when this gesture ends.
1227
 *      @param {Number} [options.duration=1000] (Optional) The duration of the
1228
 *        gesture in milliseconds.
1229
 *      @param {Number} [options.start=0] (Optional) Starting degree of the first
1230
 *        finger. The value is relative to the path of the north. Default is 0
1231
 *        (i.e., 12:00 on a clock).
1232
 *      @param {Number} [options.rotation=0] (Optional) Degrees to rotate from
1233
 *        the starting degree. A negative value means rotation to the
1234
 *        counter-clockwise direction.
1235
 *
1236
 *      Valid options properties for the `rotate` gesture:
1237
 *
1238
 *      @param {Array} [options.center] (Optional) The center of the circle where
1239
 *        two fingers are placed. Default is the center of the node element.
1240
 *      @param {Number} [options.r1] (Optional) Pixel radius of the start circle
1241
 *        where 2 fingers will be on when the gesture starts. The circles are
1242
 *        centered at the center of the element. Default is a fourth of the node
1243
 *        element width or height, whichever is smaller.
1244
 *      @param {Number} [options.r2] (Optional) Pixel radius of the end circle
1245
 *        when this gesture ends. Default is a fourth of the node element width or
1246
 *        height, whichever is smaller.
1247
 *      @param {Number} [options.duration=1000] (Optional) The duration of the
1248
 *        gesture in milliseconds.
1249
 *      @param {Number} [options.start=0] (Optional) Starting degree of the first
1250
 *        finger. The value is relative to the path of the north. Default is 0
1251
 *        (i.e., 12:00 on a clock).
1252
 *      @param {Number} [options.rotation] (Required) Degrees to rotate from
1253
 *        the starting degree. A negative value means rotation to the
1254
 *        counter-clockwise direction.
1255
 *
1256
 * @param {Function} [cb] The callback to execute when the asynchronouse gesture
1257
 *      simulation is completed.
1258
 *      @param {Error} cb.err An error object if the simulation is failed.
1259
 * @for Event
1260
 * @static
1261
 */
1262
Y.Event.simulateGesture = function(node, name, options, cb) {
1263
 
1264
    node = Y.one(node);
1265
 
1266
    var sim = new Y.GestureSimulation(node);
1267
    name = name.toLowerCase();
1268
 
1269
    if(!cb && Y.Lang.isFunction(options)) {
1270
        cb = options;
1271
        options = {};
1272
    }
1273
 
1274
    options = options || {};
1275
 
1276
    if (gestureNames[name]) {
1277
        switch(name) {
1278
            // single-touch: point gestures
1279
            case 'tap':
1280
                sim.tap(cb, options.point, options.times, options.hold, options.delay);
1281
                break;
1282
            case 'doubletap':
1283
                sim.tap(cb, options.point, 2);
1284
                break;
1285
            case 'press':
1286
                if(!Y.Lang.isNumber(options.hold)) {
1287
                    options.hold = DEFAULTS.HOLD_PRESS;
1288
                } else if(options.hold < DEFAULTS.MIN_HOLD_PRESS) {
1289
                    options.hold = DEFAULTS.MIN_HOLD_PRESS;
1290
                } else if(options.hold > DEFAULTS.MAX_HOLD_PRESS) {
1291
                    options.hold = DEFAULTS.MAX_HOLD_PRESS;
1292
                }
1293
                sim.tap(cb, options.point, 1, options.hold);
1294
                break;
1295
 
1296
            // single-touch: move gestures
1297
            case 'move':
1298
                sim.move(cb, options.path, options.duration);
1299
                break;
1300
            case 'flick':
1301
                sim.flick(cb, options.point, options.axis, options.distance,
1302
                    options.duration);
1303
                break;
1304
 
1305
            // multi-touch: pinch/rotation gestures
1306
            case 'pinch':
1307
                sim.pinch(cb, options.center, options.r1, options.r2,
1308
                    options.duration, options.start, options.rotation);
1309
                break;
1310
            case 'rotate':
1311
                sim.rotate(cb, options.center, options.r1, options.r2,
1312
                    options.duration, options.start, options.rotation);
1313
                break;
1314
        }
1315
    } else {
1316
        Y.error(NAME+': Not a supported gesture simulation: '+name);
1317
    }
1318
};
1319
 
1320
 
1321
}, '3.18.1', {"requires": ["async-queue", "event-simulate", "node-screen"]});