mirror of
https://github.com/Hopiu/jquery-mobile.git
synced 2026-03-23 09:20:28 +00:00
457 lines
No EOL
13 KiB
JavaScript
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); |