-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1309 from Polymer/0.8-gestures
0.8 gestures
- Loading branch information
Showing
5 changed files
with
457 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
<!-- | ||
@license | ||
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | ||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | ||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | ||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | ||
Code distributed by Google as part of the polymer project is also | ||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | ||
--> | ||
<script> | ||
|
||
(function(scope) { | ||
|
||
'use strict'; | ||
|
||
var async = scope.Base.async; | ||
|
||
var Gestures = { | ||
gestures: {}, | ||
|
||
// automate the event listeners for the native events | ||
// TODO(dfreedm): add a way to remove handlers. | ||
add: function(evType, node, handler) { | ||
// listen for events in order to "recognize" this event | ||
var g = this.gestures[evType]; | ||
var gn = '_' + evType; | ||
var info = {started: false, abortTrack: false, oneshot: false}; | ||
if (g && !node[gn]) { | ||
if (g.touchaction) { | ||
this._setupTouchAction(node, g.touchaction, info); | ||
} | ||
for (var i = 0, n, sn, fn; i < g.deps.length; i++) { | ||
n = g.deps[i]; | ||
fn = g[n].bind(g, info); | ||
sn = '_' + evType + '-' + n; | ||
// store the handler on the node for future removal | ||
node[sn] = fn; | ||
node.addEventListener(n, fn); | ||
} | ||
node[gn] = 0; | ||
} | ||
// listen for the gesture event | ||
node[gn]++; | ||
node.addEventListener(evType, handler); | ||
}, | ||
|
||
remove: function(evType, node, handler) { | ||
var g = this.gestures[evType]; | ||
var gn = '_' + evType; | ||
if (g && node[gn]) { | ||
for (var i = 0, n, sn, fn; i < g.deps.length; i++) { | ||
n = g.deps[i]; | ||
sn = '_' + evType + '-' + n; | ||
fn = node[sn]; | ||
if (fn){ | ||
node.removeEventListener(n, fn); | ||
// remove stored handler to allow GC | ||
node[sn] = undefined; | ||
} | ||
} | ||
node[gn] = node[gn] ? (node[gn] - 1) : 0; | ||
node.removeEventListener(evType, handler); | ||
} | ||
}, | ||
|
||
register: function(recog) { | ||
this.gestures[recog.name] = recog; | ||
}, | ||
|
||
// touch will make synthetic mouse events | ||
// preventDefault on touchend will cancel them, | ||
// but this breaks <input> focus and link clicks | ||
// Disabling "mouse" handlers for 500ms is enough | ||
|
||
_cancelFunction: null, | ||
|
||
cancelNextClick: function(timeout) { | ||
if (!this._cancelFunction) { | ||
timeout = timeout || 500; | ||
var self = this; | ||
var reset = function() { | ||
var cfn = self._cancelFunction; | ||
if (cfn) { | ||
clearTimeout(cfn.id); | ||
document.removeEventListener('click', cfn, true); | ||
self._cancelFunction = null; | ||
} | ||
}; | ||
var canceller = function(e) { | ||
e.tapPrevented = true; | ||
reset(); | ||
}; | ||
canceller.id = setTimeout(reset, timeout); | ||
this._cancelFunction = canceller; | ||
document.addEventListener('click', canceller, true); | ||
} | ||
}, | ||
|
||
// try to use the native touch-action, if it exists | ||
_hasNativeTA: typeof document.head.style.touchAction === 'string', | ||
|
||
// set scrolling direction on node to check later on first move | ||
// must call this before adding event listeners! | ||
setTouchAction: function(node, value) { | ||
if (this._hasNativeTA) { | ||
node.style.touchAction = value; | ||
} | ||
node.touchAction = value; | ||
}, | ||
|
||
_setupTouchAction: function(node, value, info) { | ||
// reuse custom value on node if set | ||
var ta = node.touchAction; | ||
value = ta || value; | ||
// set an anchor point to see how far first move is | ||
node.addEventListener('touchstart', function(e) { | ||
var t = e.changedTouches[0]; | ||
info.initialTouch = {x: t.clientX, y: t.clientY}; | ||
info.abortTrack = false; | ||
info.oneshot = false; | ||
}); | ||
node.addEventListener('touchmove', function(e) { | ||
// only run this once | ||
if (info.oneshot) { | ||
return; | ||
} | ||
info.oneshot = true; | ||
// "none" means always track | ||
if (value === 'none') { | ||
return; | ||
} | ||
// "auto" is default, always scroll | ||
// bail-out if touch-action did its job | ||
// the touchevent is non-cancelable if the page/area is scrolling | ||
if (value === 'auto' || !value || (ta && !e.cancelable)) { | ||
info.abortTrack = true; | ||
return; | ||
} | ||
// check first move direction | ||
// unfortunately, we can only make the decision in the first move, | ||
// so we have to use whatever values are available. | ||
// Typically, this can be a really small amount, :( | ||
var t = e.changedTouches[0]; | ||
var x = t.clientX, y = t.clientY; | ||
var dx = Math.abs(info.initialTouch.x - x); | ||
var dy = Math.abs(info.initialTouch.y - y); | ||
// scroll in x axis, abort track if we move more in x direction | ||
if (value === 'pan-x') { | ||
info.abortTrack = dx >= dy; | ||
// scroll in y axis, abort track if we move more in y direction | ||
} else if (value === 'pan-y') { | ||
info.abortTrack = dy >= dx; | ||
} | ||
}); | ||
}, | ||
|
||
fire: function(target, type, detail, bubbles, cancelable) { | ||
return target.dispatchEvent( | ||
new CustomEvent(type, { | ||
detail: detail, | ||
bubbles: bubbles, | ||
cancelable: cancelable | ||
}) | ||
); | ||
} | ||
|
||
}; | ||
|
||
Gestures.register({ | ||
name: 'track', | ||
touchaction: 'none', | ||
deps: ['mousedown', 'touchmove', 'touchend'], | ||
|
||
mousedown: function(info, e) { | ||
var t = e.currentTarget; | ||
var self = this; | ||
var movefn = function movefn(e, up) { | ||
if (!info.tracking && !up) { | ||
// set up tap prevention | ||
Gestures.cancelNextClick(); | ||
} | ||
// first move is 'start', subsequent moves are 'move', mouseup is 'end' | ||
var state = up ? 'end' : (!info.started ? 'start' : 'move'); | ||
info.started = true; | ||
self.fire(t, e, state); | ||
e.preventDefault(); | ||
}; | ||
var upfn = function upfn(e) { | ||
// call mousemove function with 'end' state | ||
movefn(e, true); | ||
info.started = false; | ||
// remove the temporary listeners | ||
document.removeEventListener('mousemove', movefn); | ||
document.removeEventListener('mouseup', upfn); | ||
}; | ||
// add temporary document listeners as mouse retargets | ||
document.addEventListener('mousemove', movefn); | ||
document.addEventListener('mouseup', upfn); | ||
}, | ||
|
||
touchmove: function(info, e) { | ||
var t = e.currentTarget; | ||
var ct = e.changedTouches[0]; | ||
// if track was aborted, stop tracking | ||
if (info.abortTrack) { | ||
return; | ||
} | ||
e.preventDefault(); | ||
// the first track event is sent after some hysteresis with touchmove. | ||
// Use `started` state variable to differentiate the "first" move from | ||
// the rest to make track.state == 'start' | ||
// first move is 'start', subsequent moves are 'move' | ||
var state = !info.started ? 'start' : 'move'; | ||
info.started = true; | ||
this.fire(t, ct, state); | ||
}, | ||
|
||
touchend: function(info, e) { | ||
var t = e.currentTarget; | ||
var ct = e.changedTouches[0]; | ||
// only trackend if track was started and not aborted | ||
if (info.started && !info.abortTrack) { | ||
// reset started state on up | ||
info.started = false; | ||
var ne = this.fire(t, ct, 'end'); | ||
// iff tracking, always prevent tap | ||
e.tapPrevented = true; | ||
} | ||
}, | ||
|
||
fire: function(target, touch, state) { | ||
return Gestures.fire(target, 'track', { | ||
state: state, | ||
x: touch.clientX, | ||
y: touch.clientY | ||
}); | ||
} | ||
|
||
}); | ||
|
||
// dispatch a *bubbling* "tap" only at the node that is the target of the | ||
// generating event. | ||
// dispatch *synchronously* so that we can implement prevention of native | ||
// actions like links being followed. | ||
// | ||
// TODO(dfreedm): a tap should not occur when there's too much movement. | ||
// Right now, a tap can occur when a touchend happens very far from the | ||
// generating touch. | ||
// This *should* obviate the need for tapPrevented via track. | ||
Gestures.register({ | ||
name: 'tap', | ||
deps: ['click', 'touchend'], | ||
|
||
click: function(info, e) { | ||
this.forward(e); | ||
}, | ||
|
||
touchend: function(info, e) { | ||
Gestures.cancelNextClick(); | ||
this.forward(e); | ||
}, | ||
|
||
forward: function(e) { | ||
// prevent taps from being generated from events that have been | ||
// canceled (e.g. via cancelNextClick) or already handled via | ||
// a listener lower in the tree. | ||
if (!e.tapPrevented) { | ||
e.tapPrevented = true; | ||
this.fire(e.target); | ||
} | ||
}, | ||
|
||
// fire a bubbling event from the generating target. | ||
fire: function(target) { | ||
Gestures.fire(target, 'tap', {}, true); | ||
} | ||
|
||
}); | ||
|
||
scope.Gestures = Gestures; | ||
|
||
})(Polymer); | ||
|
||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.