/**
* Handles MultiTouch event, creating appropritate Touch and MultiTouch
* objects and firing events on the DOM.
*
* @name Touches
* @namespace
* @private
*/
define(["lib/jquery", "lib/underscore", "common/util/log",
"common/network/dispatcher", "common/models/touch", "common/models/multi_touch",
"common/util/dom_utils", "common/util/multi_map", "common/network/tracer",
"ARCore/coordinate", "ARCore/embed"],
function($, _, Log,
Dispatcher, Touch, MultiTouch,
DOMUtils, MultiMap, Tracer,
Coordinate, Embed) {
"use strict";
var logger = Log("Touches");
var touches = {
// touch id -> array of touches (one for each element the touch affects)
// the touches are in order of specificity; e.g. the first item is
// the element touched and the last item is the document.
byId: MultiMap(),
// element -> array of touches that started on that element
byEl: MultiMap(),
// touch id -> whether it's eligible to become a click
clicks: {}
};
// eventName -> MultiMap{element -> [handler...]}
var capturePhaseHandlers = {
"lens:touchstart": MultiMap(),
"lens:touchmove": MultiMap(),
"lens:touchend": MultiMap()
};
// multitouch id -> MultiTouch
var multiTouches = {};
// given a element -> [Touch...] MultiMap, dispatches the given event
// on each element, using the Touch array as the changedTouches array
//
// Returns a deferred that is resolved with true if event propagation is
// not stopped and false if it is.
var dispatchEvents = function(map, eventName, bridgeProtocolName, invalidateClicks) {
var deferred = $.Deferred();
if(map.size() === 0) {
deferred.resolve(true);
return deferred;
}
var allTouches = MultiTouch(touches.byId.firsts(), document.body);
// builds the properties for a touch event on el
var evProps = function(changedTouches, el) {
var targetTouches = MultiTouch(touches.byEl.get(el), el);
changedTouches = MultiTouch(changedTouches, el);
return {
touch: changedTouches[0],
touches: allTouches,
targetTouches: targetTouches,
changedTouches: changedTouches
};
};
// stops the given touches from becoming clicks if invalidateClicks is true
var stopClicks = function(touchesToStop) {
if(invalidateClicks) {
_.each(touchesToStop, function(touch) {
touches.clicks[touch.id] = false;
});
}
};
var fireCapturing = function() {
return map.everyReverse(function(changedTouches, el) {
var handlers = capturePhaseHandlers[eventName].get(el);
if(handlers.length > 0) {
var props = evProps(changedTouches, el);
var evt = DOMUtils.buildEvent(eventName, false, props);
_.each(handlers, function(handler) {
handler.call(el, evt);
});
if(evt._propStopped) {
stopClicks(props.targetTouches);
return false;
}
else {
return true;
}
}
return true;
});
};
var fireBubbling = function() {
return map.every(function(changedTouches, el) {
var props = evProps(changedTouches, el);
var shouldContinue = DOMUtils.fire(eventName, el, props);
if(!shouldContinue) {
stopClicks(props.targetTouches);
}
return shouldContinue;
});
};
if(fireCapturing()) {
// made it through the capturing phase
var lowestPair = map.pairs()[0];
var lowestElement = lowestPair[0];
var touchesOnLowestElement = lowestPair[1];
var embed = $(lowestElement).data("lens-embed");
if(embed) {
// it's an embedded lens; propagate the event across the iframe
// boundary
var offset = $(lowestElement).offset();
var data = {};
data[bridgeProtocolName] = touchesOnLowestElement.map(function(touch) {
return {
x: touch.pageX - offset.left,
y: touch.pageY - offset.top,
w: touch.w,
h: touch.h,
id: touch.id
};
});
embed.dispatcher.callRPC("lens:touchevent", data, function(shouldContinue) {
if(shouldContinue) {
deferred.resolve(fireBubbling());
}
else {
stopClicks();
deferred.resolve(false);
}
});
}
else {
deferred.resolve(fireBubbling());
}
}
else {
// didn't make it though capturing phase
deferred.resolve(false);
}
return deferred;
};
// Handles incoming touch data.
//
// Returns a deferred that is resolved with true if event propagation is
// not stopped and false if it is.
var handleIncomingMessage = function(data, traceId) {
var deferred = $.Deferred();
// element -> fingers down on that element
var touchstartEvents = MultiMap();
// handle data.down: register new touches and populate touchstartEvents
_.each(data.down, function(touchData) {
logger.info("New touch:", touchData.id,
"at (" + touchData.x + "," + touchData.y + ")");
var els = DOMUtils.elementsAt(touchData.x, touchData.y);
MultiMap.updateOrder(els);
// register a Touch for each element
_.each(els, function(el) {
var touch = Touch(touchData.id, touchData.x, touchData.y,
touchData.w, touchData.h, el, els[0]);
touches.byId.put(touchData.id, touch);
touches.byEl.put(el, touch);
touchstartEvents.put(el, touch);
});
// add it to the map of touches eligable to be clicks
touches.clicks[touchData.id] = true;
});
// element -> fingers up on that element
var touchendEvents = MultiMap();
// handle data.up: deregister touches and populate touchendEvents
_.each(data.up, function(touchData) {
logger.info("Touch ended:", touchData.id);
_.each(touches.byId.get(touchData.id), function(touch) {
// add to touchendEvents
touchendEvents.put(touch.el, touch);
// remove from touches.byEl and touches.byId
touches.byEl.removeVal(touch.el, touch);
// inform end subscribers for the touch
touch._markDone();
});
// the the touch is eligible to become a click, fire a click event
if(touches.clicks[touchData.id]) {
// find the lowest element the touch is still in
var clickTarget = _.find(touches.byId.get(touchData.id), function() {
return true; //touch.isInEl(); TODO: check if the touch is in the element
});
logger.info("clicking ", clickTarget);
if(clickTarget) {
$(clickTarget.el).click();
}
}
delete touches.clicks[touchData.id];
touches.byId.delete(touchData.id);
});
// element -> fingers moved on that element
var touchmoveEvents = MultiMap();
// handled data.move: update touches and populate touchmoveEvents
_.each(data.move, function(touchData) {
_.each(touches.byId.get(touchData.id), function(touch) {
// add to touchmoveEvents
touchmoveEvents.put(touch.el, touch);
// update touch, informing move subscribers
touch._update(touchData.x, touchData.y);
});
});
// handle multitouches
var endedTouches = _.pluck(data.up, "id");
var movedTouches = _.pluck(data.move, "id");
_.each(multiTouches, function(multiTouch) {
// check if any of the touches have ended; if so, the multitouch has
// ended.
var hasEnded = _.any(multiTouch, function(touch) {
return (endedTouches.indexOf(touch.id) !== -1);
});
if(hasEnded) {
logger.info("MultiTouch Ended:", multiTouch);
multiTouch._markDone();
}
else {
// check if any of the touches have moved; if so, the multitouch
// has moved.
var hasMoved = _.any(multiTouch, function(touch) {
return (movedTouches.indexOf(touch.id) !== -1);
});
if(hasMoved) {
multiTouch._update();
}
}
});
Tracer.post("Constructed Touch objects", traceId);
// fire events
$.when(dispatchEvents(touchstartEvents, "lens:touchstart", "down", true),
dispatchEvents(touchendEvents, "lens:touchend", "up"),
dispatchEvents(touchmoveEvents, "lens:touchmove", "move"))
.then(function(down, up, move) {
deferred.resolve(down && up && move);
});
Tracer.post("Fired events", traceId);
return deferred;
};
if(Embed.parentDispatcher) {
// in a child lens, we listen to the parent for touch events
Embed.parentDispatcher.registerAsyncRPC("lens:touchevent", function(data, callback) {
handleIncomingMessage(data).done(function(result) {
callback(result);
});
});
}
else {
// in a top-level lens, we listen to the bridge for touch events
Dispatcher._bridge.subscribe("touch", function(data, traceId) {
Tracer.post("Touches got event", traceId);
handleIncomingMessage(data);
});
}
var Touches = /** @lends Touches */ {
/**
* Registers a MultiTouch for updates. By default, MultiTouches aren't
* updated. However, once they have subscribers, the should call this
* method to start receiving updates.
*/
registerMultiTouch: function(multiTouch) {
logger.info("Tracking MultiTouch:", multiTouch);
multiTouches[multiTouch.id] = multiTouch;
},
/**
* Stops a multitouch from getting updates.
*/
deregisterMultiTouch: function(multiTouch) {
delete multiTouches[multiTouch.id];
}
};
// override Element#addEventListener to properly handling capture phase
// handlers
Element.prototype.addEventListener = _.wrap(Element.prototype.addEventListener, function(_super, name, handler, useCapture) {
if(useCapture && (capturePhaseHandlers.hasOwnProperty(name))) {
capturePhaseHandlers[name].put(this, handler);
}
else {
_super.call(this, name, handler, useCapture);
}
});
return Touches;
});