/* * jQuery Mobile Framework : "mouse" plugin * Copyright (c) jQuery Project * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license */ // This plugin is an experiment for abstracting away the touch and mouse // events so that developers don't have to worry about which method of input // the device their document is loaded on supports. // // The idea here is to allow the developer to register listeners for the // basic mouse events, such as mousedown, mousemove, mouseup, and click, // and the plugin will take care of registering the correct listeners // behind the scenes to invoke the listener at the fastest possible time // for that device, while still retaining the order of event firing in // the traditional mouse environment, should multiple handlers be registered // on the same element for different events. // // The current version exposes the following virtual events to jQuery bind methods: // "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel" (function($, window, document, undefined) { var dataPropertyName = "virtualMouseBindings", touchTargetPropertyName = "virtualTouchID", virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split(" "), touchEventProps = "clientX clientY pageX pageY screenX screenY".split(" "), activeDocHandlers = {}, resetTimerID = 0, startX = 0, startY = 0, didScroll = false, clickBlockList = [], blockMouseTriggers = false, blockTouchTriggers = false, eventCaptureSupported = $.support.eventCapture, $document = $(document), nextTouchID = 1, lastTouchID = 0; $.vmouse = { moveDistanceThreshold: 10, clickDistanceThreshold: 10, resetTimerDuration: 1500 }; function getNativeEvent(event) { while (event && typeof event.originalEvent !== "undefined") { event = event.originalEvent; } return event; } function createVirtualEvent(event, eventType) { var t = event.type; event = $.Event(event); event.type = eventType; var oe = event.originalEvent; var props = $.event.props; // copy original event properties over to the new event // this would happen if we could call $.event.fix instead of $.Event // but we don't have a way to force an event to be fixed multiple times if (oe) { for ( var i = props.length, prop; i; ) { prop = props[ --i ]; event[prop] = oe[prop]; } } if (t.search(/^touch/) !== -1){ var ne = getNativeEvent(oe), t = ne.touches, ct = ne.changedTouches, touch = (t && t.length) ? t[0] : ((ct && ct.length) ? ct[0] : undefined); if (touch){ for (var i = 0, len = touchEventProps.length; i < len; i++){ var prop = touchEventProps[i]; event[prop] = touch[prop]; } } } return event; } function getVirtualBindingFlags(element) { var flags = {}; while (element){ var b = $.data(element, dataPropertyName); for (var k in b) { if (b[k]){ flags[k] = flags.hasVirtualBinding = true; } } element = element.parentNode; } return flags; } function getClosestElementWithVirtualBinding(element, eventType) { while (element){ var b = $.data(element, dataPropertyName); if (b && (!eventType || b[eventType])) { return element; } element = element.parentNode; } return null; } function enableTouchBindings() { blockTouchTriggers = false; } function disableTouchBindings() { blockTouchTriggers = true; } function enableMouseBindings() { lastTouchID = 0; clickBlockList.length = 0; blockMouseTriggers = false; // When mouse bindings are enabled, our // touch bindings are disabled. disableTouchBindings(); } function disableMouseBindings() { // When mouse bindings are disabled, our // touch bindings are enabled. enableTouchBindings(); } function startResetTimer() { clearResetTimer(); resetTimerID = setTimeout(function(){ resetTimerID = 0; enableMouseBindings(); }, $.vmouse.resetTimerDuration); } function clearResetTimer() { if (resetTimerID){ clearTimeout(resetTimerID); resetTimerID = 0; } } function triggerVirtualEvent(eventType, event, flags) { var defaultPrevented = false; if ((flags && flags[eventType]) || (!flags && getClosestElementWithVirtualBinding(event.target, eventType))) { var ve = createVirtualEvent(event, eventType); $(event.target).trigger(ve); defaultPrevented = ve.isDefaultPrevented(); } return defaultPrevented; } function mouseEventCallback(event) { var touchID = $.data(event.target, touchTargetPropertyName); if (!blockMouseTriggers && (!lastTouchID || lastTouchID !== touchID)){ triggerVirtualEvent("v" + event.type, event); } } function handleTouchStart(event) { var touches = getNativeEvent(event).touches; if (touches && touches.length === 1){ var target = event.target, flags = getVirtualBindingFlags(target); if (flags.hasVirtualBinding){ lastTouchID = nextTouchID++; $.data(target, touchTargetPropertyName, lastTouchID); clearResetTimer(); disableMouseBindings(); didScroll = false; var t = getNativeEvent(event).touches[0]; startX = t.pageX; startY = t.pageY; triggerVirtualEvent("vmouseover", event, flags); triggerVirtualEvent("vmousedown", event, flags); } } } function handleScroll(event) { if (blockTouchTriggers){ return; } if (!didScroll){ triggerVirtualEvent("vmousecancel", event, getVirtualBindingFlags(event.target)); } didScroll = true; startResetTimer(); } function handleTouchMove(event) { if (blockTouchTriggers){ return; } var t = getNativeEvent(event).touches[0]; var didCancel = didScroll, moveThreshold = $.vmouse.moveDistanceThreshold; didScroll = didScroll || (Math.abs(t.pageX - startX) > moveThreshold || Math.abs(t.pageY - startY) > moveThreshold); var flags = getVirtualBindingFlags(event.target); if (didScroll && !didCancel){ triggerVirtualEvent("vmousecancel", event, flags); } triggerVirtualEvent("vmousemove", event, flags); startResetTimer(); } function handleTouchEnd(event) { if (blockTouchTriggers){ return; } disableTouchBindings(); var flags = getVirtualBindingFlags(event.target); triggerVirtualEvent("vmouseup", event, flags); if (!didScroll){ if (triggerVirtualEvent("vclick", event, flags)){ // The target of the mouse events that follow the touchend // event don't necessarily match the target used during the // touch. This means we need to rely on coordinates for blocking // any click that is generated. var t = getNativeEvent(event).changedTouches[0]; clickBlockList.push({ touchID: lastTouchID, x: t.clientX, y: t.clientY }); // Prevent any mouse events that follow from triggering // virtual event notifications. blockMouseTriggers = true; } } triggerVirtualEvent("vmouseout", event, flags); didScroll = false; startResetTimer(); } function hasVirtualBindings(ele) { var bindings = $.data(ele, dataPropertyName), k; if (bindings){ for (k in bindings){ if (bindings[k]){ return true; } } } return false; } function dummyMouseHandler(){} function getSpecialEventObject(eventType) { var realType = eventType.substr(1); return { setup: function(data, namespace) { // If this is the first virtual mouse binding for this element, // add a bindings object to its data. if (!hasVirtualBindings(this)){ $.data(this, dataPropertyName, {}); } // If setup is called, we know it is the first binding for this // eventType, so initialize the count for the eventType to zero. var bindings = $.data(this, dataPropertyName); bindings[eventType] = true; // If this is the first virtual mouse event for this type, // register a global handler on the document. activeDocHandlers[eventType] = (activeDocHandlers[eventType] || 0) + 1; if (activeDocHandlers[eventType] === 1){ $document.bind(realType, mouseEventCallback); } // Some browsers, like Opera Mini, won't dispatch mouse/click events // for elements unless they actually have handlers registered on them. // To get around this, we register dummy handlers on the elements. $(this).bind(realType, dummyMouseHandler); // For now, if event capture is not supported, we rely on mouse handlers. if (eventCaptureSupported){ // If this is the first virtual mouse binding for the document, // register our touchstart handler on the document. activeDocHandlers["touchstart"] = (activeDocHandlers["touchstart"] || 0) + 1; if (activeDocHandlers["touchstart"] === 1) { $document.bind("touchstart", handleTouchStart) .bind("touchend", handleTouchEnd) // On touch platforms, touching the screen and then dragging your finger // causes the window content to scroll after some distance threshold is // exceeded. On these platforms, a scroll prevents a click event from being // dispatched, and on some platforms, even the touchend is suppressed. To // mimic the suppression of the click event, we need to watch for a scroll // event. Unfortunately, some platforms like iOS don't dispatch scroll // events until *AFTER* the user lifts their finger (touchend). This means // we need to watch both scroll and touchmove events to figure out whether // or not a scroll happenens before the touchend event is fired. .bind("touchmove", handleTouchMove) .bind("scroll", handleScroll); } } }, teardown: function(data, namespace) { // If this is the last virtual binding for this eventType, // remove its global handler from the document. --activeDocHandlers[eventType]; if (!activeDocHandlers[eventType]){ $document.unbind(realType, mouseEventCallback); } if (eventCaptureSupported){ // If this is the last virtual mouse binding in existence, // remove our document touchstart listener. --activeDocHandlers["touchstart"]; if (!activeDocHandlers["touchstart"]) { $document.unbind("touchstart", handleTouchStart) .unbind("touchmove", handleTouchMove) .unbind("touchend", handleTouchEnd) .unbind("scroll", handleScroll); } } var $this = $(this), bindings = $.data(this, dataPropertyName); // teardown may be called when an element was // removed from the DOM. If this is the case, // jQuery core may have already stripped the element // of any data bindings so we need to check it before // using it. if (bindings){ bindings[eventType] = false; } // Unregister the dummy event handler. $this.unbind(realType, dummyMouseHandler); // If this is the last virtual mouse binding on the // element, remove the binding data from the element. if (!hasVirtualBindings(this)){ $this.removeData(dataPropertyName); } } }; } // Expose our custom events to the jQuery bind/unbind mechanism. for (var i = 0; i < virtualEventNames.length; i++){ $.event.special[virtualEventNames[i]] = getSpecialEventObject(virtualEventNames[i]); } // Add a capture click handler to block clicks. // Note that we require event capture support for this so if the device // doesn't support it, we punt for now and rely solely on mouse events. if (eventCaptureSupported){ document.addEventListener("click", function(e){ var cnt = clickBlockList.length; var target = e.target; if (cnt) { var x = e.clientX, y = e.clientY, threshold = $.vmouse.clickDistanceThreshold; // The idea here is to run through the clickBlockList to see if // the current click event is in the proximity of one of our // vclick events that had preventDefault() called on it. If we find // one, then we block the click. // // Why do we have to rely on proximity? // // Because the target of the touch event that triggered the vclick // can be different from the target of the click event synthesized // by the browser. The target of a mouse/click event that is syntehsized // from a touch event seems to be implementation specific. For example, // some browsers will fire mouse/click events for a link that is near // a touch event, even though the target of the touchstart/touchend event // says the user touched outside the link. Also, it seems that with most // browsers, the target of the mouse/click event is not calculated until the // time it is dispatched, so if you replace an element that you touched // with another element, the target of the mouse/click will be the new // element underneath that point. // // Aside from proximity, we also check to see if the target and any // of its ancestors were the ones that blocked a click. This is necessary // because of the strange mouse/click target calculation done in the // Android 2.1 browser, where if you click on an element, and there is a // mouse/click handler on one of its ancestors, the target will be the // innermost child of the touched element, even if that child is no where // near the point of touch. var ele = target; while (ele) { for (var i = 0; i < cnt; i++) { var o = clickBlockList[i], touchID = 0; if ((ele === target && Math.abs(o.x - x) < threshold && Math.abs(o.y - y) < threshold) || $.data(ele, touchTargetPropertyName) === o.touchID){ // XXX: We may want to consider removing matches from the block list // instead of waiting for the reset timer to fire. e.preventDefault(); e.stopPropagation(); return; } } ele = ele.parentNode; } } }, true); } })(jQuery, window, document);