Skip to content

Commit

Permalink
Merge pull request #1309 from Polymer/0.8-gestures
Browse files Browse the repository at this point in the history
0.8 gestures
  • Loading branch information
frankiefu committed Mar 19, 2015
2 parents 319974d + 69fd257 commit b3be14e
Show file tree
Hide file tree
Showing 5 changed files with 457 additions and 13 deletions.
39 changes: 26 additions & 13 deletions src/features/standard/events.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,38 @@
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
-->

<link rel="import" href="../../lib/gestures.html">

<script>

/**
* Supports `listeners` and `keyPresses` objects.
*
*
* Example:
*
*
* using('Base', function(Base) {
*
*
* Polymer({
*
*
* listeners: {
* // `click` events on the host are delegated to `clickHandler`
* 'click': 'clickHandler'
* },
*
*
* keyPresses: {
* // 'ESC' key presses are delegated to `escHandler`
* Base.ESC_KEY: 'escHandler'
* },
*
*
* ...
*
*
* });
*
*
* });
*
*
* @class standard feature: events
*
*
*/

Polymer.Base.addFeature({
Expand Down Expand Up @@ -64,14 +67,24 @@

listen: function(node, eventName, methodName) {
var host = this;
node.addEventListener(eventName, function(e) {
var handler = function(e) {
if (host[methodName]) {
host[methodName](e, e.detail);
} else {
console.warn('[%s].[%s]: event handler [%s] is null in scope (%o)',
console.warn('[%s].[%s]: event handler [%s] is null in scope (%o)',
node.localName, eventName, methodName, host);
}
});
};
switch (eventName) {
case 'tap':
case 'track':
Polymer.Gestures.add(eventName, node, handler);
break;

default:
node.addEventListener(eventName, handler);
break;
}
},

keyPresses: {},
Expand Down
284 changes: 284 additions & 0 deletions src/lib/gestures.html
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>
1 change: 1 addition & 0 deletions test/runner.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
'unit/polymer-dom-shadow.html',
'unit/bind.html',
'unit/notify-path.html',
'unit/gestures.html',
'unit/utils.html',
'unit/utils-content.html',
'unit/resolveurl.html',
Expand Down
Loading

0 comments on commit b3be14e

Please sign in to comment.