1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /** @class SC.Touch
  9   Represents a touch. Single touch objects are passed to `touchStart`, `touchEnd` and `touchCancelled` event handlers;
 10   a specialized multitouch event object is sent to `touchesDragged`, which include access to all in-flight touches
 11   (see "The touchesDragged Multitouch Event Object" below).
 12 
 13   SC.Touch exposes a number of properties, including pageX/Y, clientX/Y, screenX/Y, and startX/Y (the values that
 14   pageX/Y had when the touch began, useful for calculating how far the touch has moved). It also exposes the touch's
 15   target element at `target`, its target SC.View at `targetView`, and the touch's unique identifier at `identifier`,
 16   which may be relied upon to identify a particular touch for the duration of its lifecycle.
 17 
 18   A touch object exists for the duration of the touch – literally for as long as your finger is on the screen – and
 19   is sent to a number of touch events on your views. Touch events are sent to the touch's current responder, set initially
 20   by checking the responder chain for views which implement `touchStart` (or `captureTouch`, see "Touch Events" below),
 21   and can be passed to other views as needed (see "Touch Responders" below).
 22 
 23   Touch Events
 24   -----
 25   You can use the following methods on your views to capture and handle touch events:
 26 
 27   - `captureTouch` -- Sometimes, a touch responder part way up the chain may need to capture the touch and prevent it
 28     from being made available to its childViews. The canonical use case for this behavior is SC.ScrollView, which by
 29     default captures touches and holds onto them for 150ms to see if the user is scrolling, only passing them on to
 30     children if not. (See SC.ScrollView#delaysContentTouches for more.) In order to support this use case, `captureTouch`
 31     bubbles the opposite way as usual: beginning with the target's pane and bubbling *down* towards the target itself.
 32     `captureTouch` is passed a single instance of `SC.Touch`, and must return YES if it wishes to capture the touch and
 33     become its responder. (If your view doesn't want to immediately capture the touch, but instead wants to suggest itself
 34     as a fallback handler in case the child view resigns respondership, it can do so by passing itself to the touch's
 35     `stackCandidateTouchResponder` method.)
 36   - `touchStart` -- When a touch begins, or when a new view responder is first given access to it (see "Touch Responders"
 37     below), the touch is passed to this method.
 38   - `touchesDragged` -- Whenever any touches move, the `touchesDragged` method is called on the current view responder
 39     for any touches that have changed. The method is provided two arguments: a special multitouch event object (see "The
 40     touchesDragged Multitouch Event Object" below), and an array containing all of the touches on that view. (This is
 41     the same as calling `touch.touchesForView(this)`.)
 42   - `touchEnd` -- When a touch is complete, its current responder's `touchEnd` handler is invoked, if present, and passed
 43     the touch object which is ending.
 44   - `touchCancelled` -- This method is generally only called if you have changed the touch's responder. See "Touch
 45     Responders" below; in brief, if you pass the touch to another responder via `makeTouchResponder`, fully resigning
 46     your touch respondership, you will receive a `touchCancelled` call for the next event; if you pass the touch to another
 47     responder via `stackNextTouchResponder`, and never receive it back, you will receive a `touchCancelled` call when the
 48     touch finishes. (Note that because RootResponder must call touchStart to determine if a view will accept respondership,
 49     touchStart is called on a new responder before touchCancelled is called on the outgoing one.)
 50 
 51   The touchesDragged Multitouch Event Object
 52   -----
 53   The specialized event object sent to `touchesDragged` includes access to all touches currently in flight. You can
 54   access the touches for a specific view from the `touchesForView` method, or get an average position of the touches
 55   on a view from the convenient `averagedTouchesForView` method. For your convenience when dealing with the common
 56   single-touch view, the `touchesDragged` event object also exposes the positional page, client, screen and start X/Y
 57   values from the *first touch*. If you are interested in handling more than one touch, or in handling an average of
 58   in-flight touches, you should ignore these values. (Note that this event object exposes an array of touch events at
 59   `touches`. These are the browser's raw touch events, and should be avoided or used with care.)
 60 
 61   Touch Responders: Passing Touches Around
 62   -----
 63   The touch responder is the view which is currently handling events for that touch. A touch may only have one responder
 64   at a time, though a view with `acceptsMultitouch: YES` may respond to more than one touch at a time.
 65 
 66   A view becomes a touch responder by implementing touchStart (and not returning NO). (Out-of-order views can capture
 67   touch responder status by implementing captureTouch and returning YES.) Once a view is a touch responder, only that
 68   view will receive subsequent `touchesDragged` and `touchEnd` events; these events do not bubble like mouse events, and
 69   they do *not* automatically switch to other views if the touch moves outside of its initial responder.
 70 
 71   In some situations, you will want to pass control on to another view during the course of a touch, for example if
 72   it goes over another view. To permanently pass respondership to another view:
 73 
 74       if (shouldPassTouch) {
 75         touch.makeTouchResponder(nextView);
 76       }
 77 
 78   This will trigger `touchStart` on the new responder, and `touchCancel` on the outgoing one. The new responder will begin
 79   receiving `touchesDragged` events in place of the outgoing one.
 80 
 81   If you want to pass respondership to another view, but are likely to want it back – for example, when a ScrollView
 82   passes respondership to a child view but expects that the child view will pass it back if it moves more than a certain
 83   amount:
 84 
 85     if (shouldTemporarlyPassTouch) {
 86       touch.stackNextTouchResponder(nextView);
 87     }
 88 
 89   This will trigger `touchStart` on the new responder, and it will start receiving `touchesDragged` and `touchEnd` events.
 90   Note that the previous responder will not receive `touchCancelled` immediately, since the touch may return to it before
 91   the end; instead, it will only receive `touchCancelled` when the touch is ended.
 92 
 93   (If you would like to add a view as a fallback responder without triggering unnecessary calls to its `touchStart` and
 94   `touchCancelled` events, for example as an alternative to returning YES from `captureTouch`, you can call
 95   `stackCandidateTouchResponder` instead.)
 96 
 97   When the child view decides that the touch has moved enough to be a scroll, it should pass touch respondership back
 98   to the scroll view with:
 99 
100     if (Math.abs(touch.pageX - touch.startX) > 4) {
101       touch.restoreLastTouchResponder();
102     }
103 
104   This will trigger `touchCancelled` on the second responder, and the first one will begin receiving `touchDragged` events
105   again.
106 */
107 SC.Touch = function(touch, touchContext) {
108   // get the raw target view (we'll refine later)
109   this.touchContext = touchContext;
110   // Get the touch's unique ID.
111   this.identifier = touch.identifier;
112 
113   var target = touch.target, targetView;
114 
115   // Special-case handling for TextFieldView's touch intercept overlays.
116   if (target && SC.$(target).hasClass("touch-intercept")) {
117     touch.target.style[SC.browser.experimentalStyleNameFor('transform')] = "translate3d(0px,-5000px,0px)";
118     target = document.elementFromPoint(touch.pageX, touch.pageY);
119     if (target) targetView = SC.viewFor(target);
120 
121     this.hidesTouchIntercept = NO;
122     if (target.tagName === "INPUT") {
123       this.hidesTouchIntercept = touch.target;
124     } else {
125       touch.target.style[SC.browser.experimentalStyleNameFor('transform')] = "translate3d(0px,0px,0px)";
126     }
127   } else {
128     targetView = touch.target ? SC.viewFor(touch.target) : null;
129   }
130 
131   this.targetView = targetView;
132   this.target = target;
133   this.type = touch.type;
134 
135   this.touchResponders = [];
136 
137   this.startX = this.pageX = touch.pageX;
138   this.startY = this.pageY = touch.pageY;
139   this.clientX = touch.clientX;
140   this.clientY = touch.clientY;
141   this.screenX = touch.screenX;
142   this.screenY = touch.screenY;
143 };
144 
145 SC.Touch.prototype = {
146   //@if(debug)
147   // Debug-mode only.
148   toString: function () {
149     return "SC.Touch (%@)".fmt(this.identifier);
150   },
151   //@endif
152 
153   /**@scope SC.Touch.prototype*/
154 
155   /** @private
156     The responder that's responsible for the creation and management of this touch. Usually this will be
157     your app's root responder. You must pass this on create, and should not change it afterwards.
158 
159     @type {SC.RootResponder}
160   */
161   touchContext: null,
162 
163   /**
164     This touch's unique identifier. Provided by the browser and used to track touches through their lifetime.
165     You will not usually need to use this, as SproutCore's touch objects themselves persist throughout the
166     lifetime of a touch.
167 
168     @type {Number}
169   */
170   identifier: null,
171 
172   /**
173     The touch's initial target element.
174 
175     @type: {Element}
176   */
177   target: null,
178 
179   /**
180     The view for the touch's initial target element.
181 
182     @type {SC.View}
183   */
184   targetView: null,
185 
186   /**
187     The touch's current view. (Usually this is the same as the current touchResponder.)
188 
189     @type {SC.View}
190   */
191   view: null,
192 
193   /**
194     The touch's current responder, i.e. the view that is currently receiving events for this touch.
195 
196     You can use the following methods to pass respondership for this touch between views as needed:
197     `makeTouchResponder`, `stackNextTouchResponder`, `restoreLastTouchResponder`, and `stackCandidateTouchResponder`.
198     See each method's documentation, and "Touch Responders: Passing Touches Around" above, for more.
199 
200     @type {SC.Responder}
201   */
202   touchResponder: null,
203 
204   /**
205     Whether the touch has ended yet. If you are caching touches outside of the RootResponder, it is your
206     responsibility to check this property and handle ended touches appropriately.
207 
208     @type {Boolean}
209   */
210   hasEnded: NO,
211 
212   /**
213     The touch's latest browser event's type, for example 'touchstart', 'touchmove', or 'touchend'.
214 
215     Note that SproutCore's event model differs from that of the browser, so it is not recommended that
216     you use this property unless you know what you're doing.
217 
218     @type {String}
219   */
220   type: null,
221 
222   /** @private
223     A faked mouse event property used to prevent unexpected behavior when proxying touch events to mouse
224     event handlers.
225   */
226   clickCount: 1,
227 
228   /**
229     The timestamp of the touch's most recent event. This is the time as of when all of the touch's
230     positional values are accurate.
231 
232     @type {Number}
233   */
234   timeStamp: null,
235 
236   /**
237     The touch's latest clientX position (relative to the viewport).
238 
239     @type {Number}
240   */
241   clientX: null,
242 
243   /**
244     The touch's latest clientY position (relative to the viewport).
245 
246     @type {Number}
247   */
248   clientY: null,
249 
250   /**
251     The touch's latest screenX position (relative to the screen).
252 
253     @type {Number}
254   */
255   screenX: null,
256 
257   /**
258     The touch's latest screenY position (relative to the screen).
259 
260     @type {Number}
261   */
262   screenY: null,
263 
264   /**
265     The touch's latest pageX position (relative to the document).
266 
267     @type {Number}
268   */
269   pageX: null,
270 
271   /**
272     The touch's latest pageY position (relative to the document).
273 
274     @type {Number}
275   */
276   pageY: null,
277 
278   /**
279     The touch's initial pageX value. Useful for tracking a touch's total relative movement.
280 
281     @type {Number}
282   */
283   startX: null,
284 
285   /**
286     The touch's initial pageY value.
287 
288     @type {Number}
289   */
290   startY: null,
291 
292   /**
293     The touch's horizontal velocity, in pixels per millisecond, at the time of its last event. (Positive
294     velocities indicate movement leftward, negative velocities indicate movement rightward.)
295 
296     @type {Number}
297   */
298   velocityX: 0,
299 
300   /**
301     The touch's vertical velocity, in pixels per millisecond, at the time of its last event. (Positive
302     velocities indicate movement downward, negative velocities indicate movement upward.)
303 
304     @type {Number}
305   */
306   velocityY: 0,
307 
308   /** @private */
309   unhideTouchIntercept: function() {
310     var intercept = this.hidesTouchIntercept;
311     if (intercept) {
312       setTimeout(function() { intercept.style[SC.browser.experimentalStyleNameFor('transform')] = "translate3d(0px,0px,0px)"; }, 500);
313     }
314   },
315 
316   /**
317     Indicates that you want to allow the normal default behavior.  Sets
318     the hasCustomEventHandling property to YES but does not cancel the event.
319   */
320   allowDefault: function() {
321     if (this.event) this.event.hasCustomEventHandling = YES ;
322   },
323 
324   /**
325     If the touch is associated with an event, prevents default action on the event. This is the
326     default behavior in SproutCore, which handles events through the RootResponder instead of
327     allowing native handling.
328   */
329   preventDefault: function() {
330     if (this.event) this.event.preventDefault();
331   },
332 
333   /**
334     Calls the native event's stopPropagation method, which prevents the method from continuing to
335     bubble. Usually, SproutCore will be handling the event via delegation at the `document` level,
336     so this method will have no effect.
337   */
338   stopPropagation: function() {
339     if (this.event) this.event.stopPropagation();
340   },
341 
342   stop: function() {
343     if (this.event) this.event.stop();
344   },
345 
346   /**
347     Removes from and calls touchEnd on the touch responder.
348   */
349   end: function() {
350     this.touchContext.endTouch(this);
351   },
352 
353   /** @private
354     This property, contrary to its name, stores the last touch responder for possible use later in the touch's
355     lifecycle. You will usually not use this property directly, instead calling `stackNextTouchResponder` to pass
356     the touch to a different view, and `restoreLastTouchResponder` to pass it back to the previous one.
357 
358     @type {SC.Responder}
359   */
360   nextTouchResponder: null,
361 
362   /** @private
363     An array of previous touch responders.
364 
365     @type {Array}
366   */
367   touchResponders: null,
368 
369   /** @private
370     A lazily-created array of candidate touch responders. Use `stackCandidateTouchResponder` to add candidates;
371     candidates are used as a fallback if the touch is out of previous touch responders.
372 
373     @type {Array}
374   */
375   candidateTouchResponders: null,
376 
377   /**
378     A convenience method for making the passed view the touch's new responder, retaining the
379     current responder for possible use later in the touch's lifecycle.
380 
381     For example, if the touch moves over a childView which implements its own touch handling,
382     you may pass the touch to it with:
383 
384       touchesDragged: function(evt, viewTouches) {
385         if ([touches should be passed to childView]) {
386           this.viewTouches.forEach(function(touch) {
387             touch.stackNextTouchResponder(this.someChildView);
388           }, this);
389         }
390       }
391 
392     The child view may easily pass the touch back to this view with `touch.restoreLastTouchResponder`. In the
393     mean time, this view will no longer receive `touchesDragged` events; if the touch is not returned to this
394     view before ending, it will receive a `touchCancelled` event rather than `touchEnd`.
395 
396     @param {SC.Responder} view The view which should become this touch's new responder.
397     @param {Boolean} upChain Whether or not a fallback responder should be sought up the responder chain if responder doesn't capture or handle the touch.
398   */
399   stackNextTouchResponder: function(view, upStack) {
400     this.makeTouchResponder(view, YES, upStack);
401   },
402 
403   /**
404     A convenience method for returning touch respondership to the previous touch responder.
405 
406     For example, if your view is in a ScrollView and has captured the touch from it, your view
407     will prevent scrolling until you return control of the touch to the ScrollView with:
408 
409         touchesDragged: function(evt, viewTouches) {
410           if (Math.abs(evt.pageY - evt.startY) > this.MAX_SWIPE) {
411             viewTouches.invoke('restoreLastTouchResponder');
412           }
413         }
414   */
415   restoreLastTouchResponder: function() {
416     // If we have a previous touch responder, go back to it.
417     if (this.nextTouchResponder) {
418       this.makeTouchResponder(this.nextTouchResponder);
419     }
420     // Otherwise, check if we have a candidate responder queued up.
421     else {
422       var candidates = this.candidateTouchResponders,
423           candidate = candidates ? candidates.pop() : null;
424       if (candidate) {
425         this.makeTouchResponder(candidate);
426       }
427     }
428   },
429 
430   /**
431     Changes the touch responder for the touch. If shouldStack is YES,
432     the current responder will be saved so that the next responder may
433     return to it.
434 
435     You will generally not call this method yourself, instead exposing on
436     your view either a `touchStart` event handler method, or a `captureTouch`
437     method which is passed a touch object and returns YES. This method
438     is used in situations where touches need to be juggled between views,
439     such as when being handled by a descendent of a ScrollView.
440 
441     When returning control of a touch to a previous handler, you should call
442     `restoreLastTouchResponder` instead.
443 
444     @param {SC.Responder} responder The view to assign to the touch. (It, or if bubbling then an ancestor,
445       must implement touchStart.)
446     @param {Boolean} shouldStack Whether the new responder should replace the old one, or stack with it.
447       Stacked responders are easy to revert via `SC.Touch#restoreLastTouchResponder`.
448     @param {Boolean|SC.Responder} bubblesTo If YES, will attempt to find a `touchStart` responder up the
449       responder chain. If NO or undefined, will only check the passed responder. If you pass a responder
450       for this argument, the attempt will bubble until it reaches the passed responder, allowing you to
451       restrict the bubbling to a portion of the responder chain. (Note that this responder will not be
452       given an opportunity to respond to the event.)
453     @returns {Boolean} Whether a valid touch responder was found and assigned.
454   */
455   makeTouchResponder: function(responder, shouldStack, bubblesTo) {
456     return this.touchContext.makeTouchResponder(this, responder, shouldStack, bubblesTo);
457   },
458 
459   /**
460     You may want your view to insert itself into the responder chain as a fallback, but without
461     having touchStart etc. called if it doesn't end up coming into play. For example, SC.ScrollView
462     adds itself as a candidate responder (when delaysTouchResponder is NO) so that views can easily
463     give it control, but without receiving unnecessary events if not.
464   */
465   stackCandidateTouchResponder: function(responder) {
466     // Fast path: if we're the first one it's easy.
467     if (!this.candidateTouchResponders) {
468       this.candidateTouchResponders = [responder];
469     }
470     // Just make sure it's not at the top of the stack. There may be a weird case where a
471     // view wants to be in a couple of spots in the stack, but it shouldn't want to be twice
472     // in a row.
473     else if (responder !== this.candidateTouchResponders[this.candidateTouchResponders.length - 1]) {
474       this.candidateTouchResponders.push(responder);
475     }
476   },
477 
478   /**
479     Captures, or recaptures, this touch. This works from the startingPoint's first child up to the
480     touch's target view to find a view which implements `captureTouch` and returns YES. If the touch
481     is captured, then this method will perform a standard `touchStart` event bubbling beginning with
482     the view which captured the touch. If no view captures the touch, then this method returns NO,
483     and you should call the `makeTouchResponder` method to trigger a standard `touchStart` bubbling
484     from the initial target on down.
485 
486     You will generally not call this method yourself, instead exposing on
487     your view either a `touchStart` event handler method, or a `captureTouch`
488     method which is passed a touch object and returns YES. This method
489     is used in situations where touches need to be juggled between views,
490     such as when being handled by a descendent of a ScrollView.
491 
492     @param {?SC.Responder} startingPoint The view whose children should be given an opportunity
493       to capture the event. (The starting point itself is not asked.)
494     @param {Boolean} shouldStack Whether any capturing responder should stack with existing responders.
495       Stacked responders are easy to revert via `SC.Touch#restoreLastTouchResponder`.
496 
497     @returns {Boolean} Whether the touch was captured. If it was not, you should pass it to
498       `makeTouchResponder` for standard event bubbling.
499   */
500   captureTouch: function(startingPoint, shouldStack) {
501     return this.touchContext.captureTouch(this, startingPoint, shouldStack);
502   },
503 
504   /**
505     Returns all touches for a specified view. Put as a convenience on the touch itself;
506     this method is also available on the event.
507 
508     For example, to retrieve the list of touches impacting the current event handler:
509 
510         touchesDragged: function(evt) {
511           var myTouches = evt.touchesForView(this);
512         }
513 
514     @param {SC.Responder} view
515   */
516   touchesForView: function(view) {
517     return this.touchContext.touchesForView(view);
518   },
519 
520   /**
521     A synonym for SC.Touch#touchesForView.
522   */
523   touchesForResponder: function(responder) {
524     return this.touchContext.touchesForView(responder);
525   },
526 
527   /**
528     Returns average data--x, y, and d (distance)--for the touches owned by the supplied view.
529 
530     See notes on the addSelf argument for an important consideration when calling from `touchStart`.
531 
532     @param {SC.Responder} view
533     @param {Boolean} addSelf Includes the receiver in calculations. Pass YES for this if calling
534         from touchStart, as the touch will not yet be included by default.
535   */
536   averagedTouchesForView: function(view, addSelf) {
537     return this.touchContext.averagedTouchesForView(view, (addSelf ? this : null));
538   }
539 };
540 
541 SC.mixin(SC.Touch, {
542 
543   create: function(touch, touchContext) {
544     return new SC.Touch(touch, touchContext);
545   },
546 
547   /**
548     Returns the averaged touch for an array of given touches. The averaged touch includes the
549     average position of all the touches (i.e. the center point), the averaged distance of all
550     the touches (i.e. the average of each touch's distance to the center) and the average velocity
551     of each touch.
552 
553     The returned Object has a signature like,
554 
555         {
556           x: ...,
557           y: ...,
558           velocityX: ...,
559           velocityY: ...,
560           d: ...
561         }
562 
563     @param {Array} touches An array of touches to average.
564     @param {Object} objectRef An Object to assign the values to rather than creating a new Object. If you pass an Object to this method, that same Object will be returned with the values assigned to it. This is useful in order to only alloc memory once and hold it in order to avoid garbage collection. The trade-off is that more memory remains in use indefinitely.
565     @returns {Object} The averaged touch.
566   */
567   averagedTouch: function (touches, objectRef) {
568     var len = touches.length,
569         ax = 0, ay = 0, avx = 0, avy = 0,
570         idx, touch;
571 
572     // If no holder object is given, create a new one.
573     if (objectRef === undefined) { objectRef = {}; }
574 
575     // Sum the positions and velocities of each touch.
576     for (idx = 0; idx < len; idx++) {
577       touch = touches[idx];
578       ax += touch.pageX;
579       ay += touch.pageY;
580       avx += touch.velocityX;
581       avy += touch.velocityY;
582     }
583 
584     // Average each value.
585     ax /= len;
586     ay /= len;
587     avx /= len;
588     avy /= len;
589 
590     // Calculate average distance.
591     var ad = 0;
592     for (idx = 0; idx < len; idx++) {
593       touch = touches[idx];
594 
595       // Get distance for each from average position.
596       var dx = Math.abs(touch.pageX - ax);
597       var dy = Math.abs(touch.pageY - ay);
598 
599       // Pythagoras was clever...
600       ad += Math.pow(dx * dx + dy * dy, 0.5);
601     }
602 
603     // Average value.
604     ad /= len;
605 
606     objectRef.x = ax;
607     objectRef.y = ay;
608     objectRef.velocityX = avx;
609     objectRef.velocityY = avy;
610     objectRef.d = ad;
611 
612     return objectRef;
613   }
614 });
615