jquery-mobile/js/jquery.mobile.vmouse.js

457 lines
No EOL
13 KiB
JavaScript

/*
* 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 simply adds mBind and mUnbind to the $.fn space,
// but we're considering other methods for making this easier. One alternative
// would be to allow users to use virtual mouse event names, such as
// "vmousedown", "vmouseup", etc, to triggerVirtualEvent the traditional jQuery special/custom
// event api, which would then triggerVirtualEvent this same code.
(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,
startScrollX = 0,
startScrollY = 0,
didScroll = false,
clickBlockList = [],
blockMouseTriggers = false,
scrollTopSupported = $.support.scrollTop,
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);
if (typeof ne.touches !== "undefined" && ne.touches[0]){
var touch = ne.touches[0];
for (var i = 0; i < touchEventProps.length; i++){
var prop = touchEventProps[i];
event[prop] = touch[prop];
}
}
}
return event;
}
function getVirtualBindingFlags(element)
{
var flags = {};
var $ele = $(element);
while ($ele && $ele.length){
var b = $ele.data(dataPropertyName);
for (var k in b) {
if (b[k]){
flags[k] = flags.hasVirtualBinding = true;
}
}
$ele = $ele.parent();
}
return flags;
}
function getClosestElementWithVirtualBinding(element, eventType)
{
var $ele = $(element);
while ($ele && $ele.length){
var b = $ele.data(dataPropertyName);
if (b && (!eventType || b[eventType])) {
return $ele;
}
$ele = $ele.parent();
}
return null;
}
function enableTouchBindings()
{
if (!activeDocHandlers["touchbindings"]){
$document.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);
activeDocHandlers["touchbindings"] = 1;
}
}
function disableTouchBindings()
{
if (activeDocHandlers["touchbindings"]){
$document.unbind("touchmove", handleTouchMove)
.unbind("touchend", handleTouchEnd)
.unbind("scroll", handleScroll);
activeDocHandlers["touchbindings"] = 0;
}
}
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 = $(event.target).data(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++;
$(target).data(touchTargetPropertyName, lastTouchID);
clearResetTimer();
disableMouseBindings();
didScroll = false;
var t = getNativeEvent(event).touches[0];
startX = t.pageX;
startY = t.pageY;
if (scrollTopSupported){
startScrollX = window.pageXOffset;
startScrollY = window.pageYOffset;
}
triggerVirtualEvent("vmouseover", event, flags);
triggerVirtualEvent("vmousedown", event, flags);
}
}
}
function handleScroll(event)
{
if (!didScroll){
triggerVirtualEvent("vmousecancel", event, getVirtualBindingFlags(event.target));
}
didScroll = true;
startResetTimer();
}
function handleTouchMove(event)
{
var t = getNativeEvent(event).touches[0];
var didCancel = didScroll,
moveThreshold = $.vmouse.moveDistanceThreshold;
didScroll = didScroll
|| (scrollTopSupported && (startScrollX !== window.pageXOffset || startScrollY !== window.pageYOffset))
|| (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)
{
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 = $ele.data(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.
var $this = $(this);
if (!hasVirtualBindings($this)){
$this.data(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 = $this.data(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);
}
}
},
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);
}
}
var $this = $(this),
bindings = $this.data(dataPropertyName);
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) || $(ele).data(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);