You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
481 lines
18 KiB
481 lines
18 KiB
define(["./_base/kernel", "./aspect", "./dom", "./dom-class", "./_base/lang", "./on", "./has", "./mouse", "./domReady", "./_base/window"], |
|
function(dojo, aspect, dom, domClass, lang, on, has, mouse, domReady, win){ |
|
|
|
// module: |
|
// dojo/touch |
|
|
|
var ios4 = has("ios") < 5; |
|
|
|
// Detect if platform supports Pointer Events, and if so, the names of the events (pointerdown vs. MSPointerDown). |
|
var hasPointer = has("pointer-events") || has("MSPointer"), |
|
pointer = (function () { |
|
var pointer = {}; |
|
for (var type in { down: 1, move: 1, up: 1, cancel: 1, over: 1, out: 1 }) { |
|
pointer[type] = has("MSPointer") ? |
|
"MSPointer" + type.charAt(0).toUpperCase() + type.slice(1) : |
|
"pointer" + type; |
|
} |
|
return pointer; |
|
})(); |
|
|
|
// Detect if platform supports the webkit touchstart/touchend/... events |
|
var hasTouch = has("touch-events"); |
|
|
|
// Click generation variables |
|
var clicksInited, clickTracker, useTarget = false, clickTarget, clickX, clickY, clickDx, clickDy, clickTime; |
|
|
|
// Time of most recent touchstart, touchmove, or touchend event |
|
var lastTouch; |
|
|
|
function dualEvent(mouseType, touchType, pointerType){ |
|
// Returns synthetic event that listens for both the specified mouse event and specified touch event. |
|
// But ignore fake mouse events that were generated due to the user touching the screen. |
|
if(hasPointer && pointerType){ |
|
// IE10+: MSPointer* events are designed to handle both mouse and touch in a uniform way, |
|
// so just use that regardless of hasTouch. |
|
return function(node, listener){ |
|
return on(node, pointerType, listener); |
|
}; |
|
}else if(hasTouch){ |
|
return function(node, listener){ |
|
var handle1 = on(node, touchType, function(evt){ |
|
listener.call(this, evt); |
|
|
|
// On slow mobile browsers (see https://bugs.dojotoolkit.org/ticket/17634), |
|
// a handler for a touch event may take >1s to run. That time shouldn't |
|
// be included in the calculation for lastTouch. |
|
lastTouch = (new Date()).getTime(); |
|
}), |
|
handle2 = on(node, mouseType, function(evt){ |
|
if(!lastTouch || (new Date()).getTime() > lastTouch + 1000){ |
|
listener.call(this, evt); |
|
} |
|
}); |
|
return { |
|
remove: function(){ |
|
handle1.remove(); |
|
handle2.remove(); |
|
} |
|
}; |
|
}; |
|
}else{ |
|
// Avoid creating listeners for touch events on performance sensitive older browsers like IE6 |
|
return function(node, listener){ |
|
return on(node, mouseType, listener); |
|
}; |
|
} |
|
} |
|
|
|
function marked(/*DOMNode*/ node){ |
|
// Search for node ancestor has been marked with the dojoClick property to indicate special processing. |
|
// Returns marked ancestor. |
|
do{ |
|
if(node.dojoClick !== undefined){ return node; } |
|
}while(node = node.parentNode); |
|
} |
|
|
|
function doClicks(e, moveType, endType){ |
|
// summary: |
|
// Setup touch listeners to generate synthetic clicks immediately (rather than waiting for the browser |
|
// to generate clicks after the double-tap delay) and consistently (regardless of whether event.preventDefault() |
|
// was called in an event listener. Synthetic clicks are generated only if a node or one of its ancestors has |
|
// its dojoClick property set to truthy. If a node receives synthetic clicks because one of its ancestors has its |
|
// dojoClick property set to truthy, you can disable synthetic clicks on this node by setting its own dojoClick property |
|
// to falsy. |
|
|
|
if(mouse.isRight(e)){ |
|
return; // avoid spurious dojoclick event on IE10+; right click is just for context menu |
|
} |
|
|
|
var markedNode = marked(e.target); |
|
clickTracker = !e.target.disabled && markedNode && markedNode.dojoClick; // click threshold = true, number, x/y object, or "useTarget" |
|
if(clickTracker){ |
|
useTarget = (clickTracker == "useTarget"); |
|
clickTarget = (useTarget?markedNode:e.target); |
|
if(useTarget){ |
|
// We expect a click, so prevent any other |
|
// default action on "touchpress" |
|
e.preventDefault(); |
|
} |
|
clickX = e.changedTouches ? e.changedTouches[0].pageX - win.global.pageXOffset : e.clientX; |
|
clickY = e.changedTouches ? e.changedTouches[0].pageY - win.global.pageYOffset : e.clientY; |
|
clickDx = (typeof clickTracker == "object" ? clickTracker.x : (typeof clickTracker == "number" ? clickTracker : 0)) || 4; |
|
clickDy = (typeof clickTracker == "object" ? clickTracker.y : (typeof clickTracker == "number" ? clickTracker : 0)) || 4; |
|
|
|
// add move/end handlers only the first time a node with dojoClick is seen, |
|
// so we don't add too much overhead when dojoClick is never set. |
|
if(!clicksInited){ |
|
clicksInited = true; |
|
|
|
var updateClickTracker = function updateClickTracker(e){ |
|
if(useTarget){ |
|
clickTracker = dom.isDescendant( |
|
win.doc.elementFromPoint( |
|
(e.changedTouches ? e.changedTouches[0].pageX - win.global.pageXOffset : e.clientX), |
|
(e.changedTouches ? e.changedTouches[0].pageY - win.global.pageYOffset : e.clientY)), |
|
clickTarget); |
|
}else{ |
|
clickTracker = clickTracker && |
|
(e.changedTouches ? e.changedTouches[0].target : e.target) == clickTarget && |
|
Math.abs((e.changedTouches ? e.changedTouches[0].pageX - win.global.pageXOffset : e.clientX) - clickX) <= clickDx && |
|
Math.abs((e.changedTouches ? e.changedTouches[0].pageY - win.global.pageYOffset : e.clientY) - clickY) <= clickDy; |
|
} |
|
}; |
|
|
|
win.doc.addEventListener(moveType, function(e){ |
|
if(mouse.isRight(e)){ |
|
return; // avoid spurious dojoclick event on IE10+; right click is just for context menu |
|
} |
|
updateClickTracker(e); |
|
if(useTarget){ |
|
// prevent native scroll event and ensure touchend is |
|
// fire after touch moves between press and release. |
|
e.preventDefault(); |
|
} |
|
}, true); |
|
|
|
win.doc.addEventListener(endType, function(e){ |
|
if(mouse.isRight(e)){ |
|
return; // avoid spurious dojoclick event on IE10+; right click is just for context menu |
|
} |
|
updateClickTracker(e); |
|
if(clickTracker){ |
|
clickTime = (new Date()).getTime(); |
|
var target = (useTarget?clickTarget:e.target); |
|
if(target.tagName === "LABEL"){ |
|
// when clicking on a label, forward click to its associated input if any |
|
target = dom.byId(target.getAttribute("for")) || target; |
|
} |
|
//some attributes can be on the Touch object, not on the Event: |
|
//http://www.w3.org/TR/touch-events/#touch-interface |
|
var src = (e.changedTouches) ? e.changedTouches[0] : e; |
|
var createMouseEvent = function createMouseEvent(type){ |
|
//create the synthetic event. |
|
//http://www.w3.org/TR/DOM-Level-3-Events/#widl-MouseEvent-initMouseEvent |
|
var evt = document.createEvent("MouseEvents"); |
|
evt._dojo_click = true; |
|
evt.initMouseEvent(type, |
|
true, //bubbles |
|
true, //cancelable |
|
e.view, |
|
e.detail, |
|
src.screenX, |
|
src.screenY, |
|
src.clientX, |
|
src.clientY, |
|
e.ctrlKey, |
|
e.altKey, |
|
e.shiftKey, |
|
e.metaKey, |
|
0, //button |
|
null //related target |
|
); |
|
return evt; |
|
}; |
|
var mouseDownEvt = createMouseEvent("mousedown"); |
|
var mouseUpEvt = createMouseEvent("mouseup"); |
|
var clickEvt = createMouseEvent("click"); |
|
|
|
setTimeout(function(){ |
|
on.emit(target, "mousedown", mouseDownEvt); |
|
on.emit(target, "mouseup", mouseUpEvt); |
|
on.emit(target, "click", clickEvt); |
|
|
|
// refresh clickTime in case app-defined click handler took a long time to run |
|
clickTime = (new Date()).getTime(); |
|
}, 0); |
|
} |
|
}, true); |
|
|
|
var stopNativeEvents = function stopNativeEvents(type){ |
|
win.doc.addEventListener(type, function(e){ |
|
// Stop native events when we emitted our own click event. Note that the native click may occur |
|
// on a different node than the synthetic click event was generated on. For example, |
|
// click on a menu item, causing the menu to disappear, and then (~300ms later) the browser |
|
// sends a click event to the node that was *underneath* the menu. So stop all native events |
|
// sent shortly after ours, similar to what is done in dualEvent. |
|
// The INPUT.dijitOffScreen test is for offscreen inputs used in dijit/form/Button, on which |
|
// we call click() explicitly, we don't want to stop this event. |
|
var target = e.target; |
|
if(clickTracker && !e._dojo_click && |
|
(new Date()).getTime() <= clickTime + 1000 && |
|
!(target.tagName == "INPUT" && domClass.contains(target, "dijitOffScreen"))){ |
|
e.stopPropagation(); |
|
e.stopImmediatePropagation && e.stopImmediatePropagation(); |
|
if(type == "click" && |
|
(target.tagName != "INPUT" || |
|
(target.type == "radio" && |
|
// #18352 Do not preventDefault for radios that are not dijit or |
|
// dojox/mobile widgets. |
|
// (The CSS class dijitCheckBoxInput holds for both checkboxes and radio buttons.) |
|
(domClass.contains(target, "dijitCheckBoxInput") || |
|
domClass.contains(target, "mblRadioButton"))) || |
|
(target.type == "checkbox" && |
|
// #18352 Do not preventDefault for checkboxes that are not dijit or |
|
// dojox/mobile widgets. |
|
(domClass.contains(target, "dijitCheckBoxInput") || |
|
domClass.contains(target, "mblCheckBox")))) && |
|
target.tagName != "TEXTAREA" && target.tagName != "AUDIO" && target.tagName != "VIDEO"){ |
|
// preventDefault() breaks textual <input>s on android, keyboard doesn't popup, |
|
// but it is still needed for checkboxes and radio buttons, otherwise in some cases |
|
// the checked state becomes inconsistent with the widget's state |
|
e.preventDefault(); |
|
} |
|
} |
|
}, true); |
|
}; |
|
|
|
stopNativeEvents("click"); |
|
|
|
// We also stop mousedown/up since these would be sent well after with our "fast" click (300ms), |
|
// which can confuse some dijit widgets. |
|
stopNativeEvents("mousedown"); |
|
stopNativeEvents("mouseup"); |
|
} |
|
} |
|
} |
|
|
|
var hoveredNode; |
|
|
|
if(has("touch")){ |
|
if(hasPointer){ |
|
// MSPointer (IE10+) already has support for over and out, so we just need to init click support |
|
domReady(function(){ |
|
win.doc.addEventListener(pointer.down, function(evt){ |
|
doClicks(evt, pointer.move, pointer.up); |
|
}, true); |
|
}); |
|
}else{ |
|
domReady(function(){ |
|
// Keep track of currently hovered node |
|
hoveredNode = win.body(); // currently hovered node |
|
|
|
win.doc.addEventListener("touchstart", function(evt){ |
|
lastTouch = (new Date()).getTime(); |
|
|
|
// Precede touchstart event with touch.over event. DnD depends on this. |
|
// Use addEventListener(cb, true) to run cb before any touchstart handlers on node run, |
|
// and to ensure this code runs even if the listener on the node does event.stop(). |
|
var oldNode = hoveredNode; |
|
hoveredNode = evt.target; |
|
on.emit(oldNode, "dojotouchout", { |
|
relatedTarget: hoveredNode, |
|
bubbles: true |
|
}); |
|
on.emit(hoveredNode, "dojotouchover", { |
|
relatedTarget: oldNode, |
|
bubbles: true |
|
}); |
|
|
|
doClicks(evt, "touchmove", "touchend"); // init click generation |
|
}, true); |
|
|
|
function copyEventProps(evt){ |
|
// Make copy of event object and also set bubbles:true. Used when calling on.emit(). |
|
var props = lang.delegate(evt, { |
|
bubbles: true |
|
}); |
|
|
|
if(has("ios") >= 6){ |
|
// On iOS6 "touches" became a non-enumerable property, which |
|
// is not hit by for...in. Ditto for the other properties below. |
|
props.touches = evt.touches; |
|
props.altKey = evt.altKey; |
|
props.changedTouches = evt.changedTouches; |
|
props.ctrlKey = evt.ctrlKey; |
|
props.metaKey = evt.metaKey; |
|
props.shiftKey = evt.shiftKey; |
|
props.targetTouches = evt.targetTouches; |
|
} |
|
|
|
return props; |
|
} |
|
|
|
on(win.doc, "touchmove", function(evt){ |
|
lastTouch = (new Date()).getTime(); |
|
|
|
var newNode = win.doc.elementFromPoint( |
|
evt.pageX - (ios4 ? 0 : win.global.pageXOffset), // iOS 4 expects page coords |
|
evt.pageY - (ios4 ? 0 : win.global.pageYOffset) |
|
); |
|
|
|
if(newNode){ |
|
// Fire synthetic touchover and touchout events on nodes since the browser won't do it natively. |
|
if(hoveredNode !== newNode){ |
|
// touch out on the old node |
|
on.emit(hoveredNode, "dojotouchout", { |
|
relatedTarget: newNode, |
|
bubbles: true |
|
}); |
|
|
|
// touchover on the new node |
|
on.emit(newNode, "dojotouchover", { |
|
relatedTarget: hoveredNode, |
|
bubbles: true |
|
}); |
|
|
|
hoveredNode = newNode; |
|
} |
|
|
|
// Unlike a listener on "touchmove", on(node, "dojotouchmove", listener) fires when the finger |
|
// drags over the specified node, regardless of which node the touch started on. |
|
if(!on.emit(newNode, "dojotouchmove", copyEventProps(evt))){ |
|
// emit returns false when synthetic event "dojotouchmove" is cancelled, so we prevent the |
|
// default behavior of the underlying native event "touchmove". |
|
evt.preventDefault(); |
|
} |
|
} |
|
}); |
|
|
|
// Fire a dojotouchend event on the node where the finger was before it was removed from the screen. |
|
// This is different than the native touchend, which fires on the node where the drag started. |
|
on(win.doc, "touchend", function(evt){ |
|
lastTouch = (new Date()).getTime(); |
|
var node = win.doc.elementFromPoint( |
|
evt.pageX - (ios4 ? 0 : win.global.pageXOffset), // iOS 4 expects page coords |
|
evt.pageY - (ios4 ? 0 : win.global.pageYOffset) |
|
) || win.body(); // if out of the screen |
|
|
|
on.emit(node, "dojotouchend", copyEventProps(evt)); |
|
}); |
|
}); |
|
} |
|
} |
|
|
|
//device neutral events - touch.press|move|release|cancel/over/out |
|
var touch = { |
|
press: dualEvent("mousedown", "touchstart", pointer.down), |
|
move: dualEvent("mousemove", "dojotouchmove", pointer.move), |
|
release: dualEvent("mouseup", "dojotouchend", pointer.up), |
|
cancel: dualEvent(mouse.leave, "touchcancel", hasPointer ? pointer.cancel : null), |
|
over: dualEvent("mouseover", "dojotouchover", pointer.over), |
|
out: dualEvent("mouseout", "dojotouchout", pointer.out), |
|
enter: mouse._eventHandler(dualEvent("mouseover","dojotouchover", pointer.over)), |
|
leave: mouse._eventHandler(dualEvent("mouseout", "dojotouchout", pointer.out)) |
|
}; |
|
|
|
/*===== |
|
touch = { |
|
// summary: |
|
// This module provides unified touch event handlers by exporting |
|
// press, move, release and cancel which can also run well on desktop. |
|
// Based on http://dvcs.w3.org/hg/webevents/raw-file/tip/touchevents.html |
|
// Also, if the dojoClick property is set to truthy on a DOM node, dojo/touch generates |
|
// click events immediately for this node and its descendants (except for descendants that |
|
// have a dojoClick property set to falsy), to avoid the delay before native browser click events, |
|
// and regardless of whether evt.preventDefault() was called in a touch.press event listener. |
|
// |
|
// example: |
|
// Used with dojo/on |
|
// | define(["dojo/on", "dojo/touch"], function(on, touch){ |
|
// | on(node, touch.press, function(e){}); |
|
// | on(node, touch.move, function(e){}); |
|
// | on(node, touch.release, function(e){}); |
|
// | on(node, touch.cancel, function(e){}); |
|
// example: |
|
// Used with touch.* directly |
|
// | touch.press(node, function(e){}); |
|
// | touch.move(node, function(e){}); |
|
// | touch.release(node, function(e){}); |
|
// | touch.cancel(node, function(e){}); |
|
// example: |
|
// Have dojo/touch generate clicks without delay, with a default move threshold of 4 pixels |
|
// | node.dojoClick = true; |
|
// example: |
|
// Have dojo/touch generate clicks without delay, with a move threshold of 10 pixels horizontally and vertically |
|
// | node.dojoClick = 10; |
|
// example: |
|
// Have dojo/touch generate clicks without delay, with a move threshold of 50 pixels horizontally and 10 pixels vertically |
|
// | node.dojoClick = {x:50, y:5}; |
|
// example: |
|
// Disable clicks without delay generated by dojo/touch on a node that has an ancestor with property dojoClick set to truthy |
|
// | node.dojoClick = false; |
|
|
|
press: function(node, listener){ |
|
// summary: |
|
// Register a listener to 'touchstart'|'mousedown' for the given node |
|
// node: Dom |
|
// Target node to listen to |
|
// listener: Function |
|
// Callback function |
|
// returns: |
|
// A handle which will be used to remove the listener by handle.remove() |
|
}, |
|
move: function(node, listener){ |
|
// summary: |
|
// Register a listener that fires when the mouse cursor or a finger is dragged over the given node. |
|
// node: Dom |
|
// Target node to listen to |
|
// listener: Function |
|
// Callback function |
|
// returns: |
|
// A handle which will be used to remove the listener by handle.remove() |
|
}, |
|
release: function(node, listener){ |
|
// summary: |
|
// Register a listener to releasing the mouse button while the cursor is over the given node |
|
// (i.e. "mouseup") or for removing the finger from the screen while touching the given node. |
|
// node: Dom |
|
// Target node to listen to |
|
// listener: Function |
|
// Callback function |
|
// returns: |
|
// A handle which will be used to remove the listener by handle.remove() |
|
}, |
|
cancel: function(node, listener){ |
|
// summary: |
|
// Register a listener to 'touchcancel'|'mouseleave' for the given node |
|
// node: Dom |
|
// Target node to listen to |
|
// listener: Function |
|
// Callback function |
|
// returns: |
|
// A handle which will be used to remove the listener by handle.remove() |
|
}, |
|
over: function(node, listener){ |
|
// summary: |
|
// Register a listener to 'mouseover' or touch equivalent for the given node |
|
// node: Dom |
|
// Target node to listen to |
|
// listener: Function |
|
// Callback function |
|
// returns: |
|
// A handle which will be used to remove the listener by handle.remove() |
|
}, |
|
out: function(node, listener){ |
|
// summary: |
|
// Register a listener to 'mouseout' or touch equivalent for the given node |
|
// node: Dom |
|
// Target node to listen to |
|
// listener: Function |
|
// Callback function |
|
// returns: |
|
// A handle which will be used to remove the listener by handle.remove() |
|
}, |
|
enter: function(node, listener){ |
|
// summary: |
|
// Register a listener to mouse.enter or touch equivalent for the given node |
|
// node: Dom |
|
// Target node to listen to |
|
// listener: Function |
|
// Callback function |
|
// returns: |
|
// A handle which will be used to remove the listener by handle.remove() |
|
}, |
|
leave: function(node, listener){ |
|
// summary: |
|
// Register a listener to mouse.leave or touch equivalent for the given node |
|
// node: Dom |
|
// Target node to listen to |
|
// listener: Function |
|
// Callback function |
|
// returns: |
|
// A handle which will be used to remove the listener by handle.remove() |
|
} |
|
}; |
|
=====*/ |
|
|
|
has("extend-dojo") && (dojo.touch = touch); |
|
|
|
return touch; |
|
});
|
|
|