/** * 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; });