Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.8 gestures #1309

Merged
merged 3 commits into from
Mar 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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