Adding a pinch handler for multi-touch devices. Great patch from Bruno. p=bbinet, r=me (closes #3077)
git-svn-id: http://svn.openlayers.org/trunk/openlayers@11389 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
@@ -170,6 +170,7 @@
|
|||||||
"OpenLayers/Handler/Polygon.js",
|
"OpenLayers/Handler/Polygon.js",
|
||||||
"OpenLayers/Handler/Feature.js",
|
"OpenLayers/Handler/Feature.js",
|
||||||
"OpenLayers/Handler/Drag.js",
|
"OpenLayers/Handler/Drag.js",
|
||||||
|
"OpenLayers/Handler/Pinch.js",
|
||||||
"OpenLayers/Handler/RegularPolygon.js",
|
"OpenLayers/Handler/RegularPolygon.js",
|
||||||
"OpenLayers/Handler/Box.js",
|
"OpenLayers/Handler/Box.js",
|
||||||
"OpenLayers/Handler/MouseWheel.js",
|
"OpenLayers/Handler/MouseWheel.js",
|
||||||
|
|||||||
@@ -104,6 +104,20 @@ OpenLayers.Event = {
|
|||||||
return event.touches && event.touches.length == 1;
|
return event.touches && event.touches.length == 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method: isMultiTouch
|
||||||
|
* Determine whether event was caused by a multi touch
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* event - {Event}
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* {Boolean}
|
||||||
|
*/
|
||||||
|
isMultiTouch: function(event) {
|
||||||
|
return event.touches && event.touches.length > 1;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method: isLeftClick
|
* Method: isLeftClick
|
||||||
* Determine whether event was caused by a left click.
|
* Determine whether event was caused by a left click.
|
||||||
|
|||||||
227
lib/OpenLayers/Handler/Pinch.js
Normal file
227
lib/OpenLayers/Handler/Pinch.js
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
/* Copyright (c) 2006-2011 by OpenLayers Contributors (see authors.txt for
|
||||||
|
* full list of contributors). Published under the Clear BSD license.
|
||||||
|
* See http://svn.openlayers.org/trunk/openlayers/license.txt for the
|
||||||
|
* full text of the license. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires OpenLayers/Handler.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class: OpenLayers.Handler.Pinch
|
||||||
|
* The pinch handler is used to deal with sequences of browser events related
|
||||||
|
* to pinch gestures. The handler is used by controls that want to know
|
||||||
|
* when a pinch sequence begins, when a pinch is happening, and when it has
|
||||||
|
* finished.
|
||||||
|
*
|
||||||
|
* Controls that use the pinch handler typically construct it with callbacks
|
||||||
|
* for 'start', 'move', and 'done'. Callbacks for these keys are
|
||||||
|
* called when the pinch begins, with each change, and when the pinch is
|
||||||
|
* done.
|
||||||
|
*
|
||||||
|
* Create a new pinch handler with the <OpenLayers.Handler.Pinch> constructor.
|
||||||
|
*
|
||||||
|
* Inherits from:
|
||||||
|
* - <OpenLayers.Handler>
|
||||||
|
*/
|
||||||
|
OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property: started
|
||||||
|
* {Boolean} When a touchstart event is received, we want to record it,
|
||||||
|
* but not set 'pinching' until the touchmove get started after
|
||||||
|
* starting.
|
||||||
|
*/
|
||||||
|
started: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property: stopDown
|
||||||
|
* {Boolean} Stop propagation of touchstart events from getting to
|
||||||
|
* listeners on the same element. Default is true.
|
||||||
|
*/
|
||||||
|
stopDown: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property: pinching
|
||||||
|
* {Boolean}
|
||||||
|
*/
|
||||||
|
pinching: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property: last
|
||||||
|
* {Object} Object that store informations related to pinch last touch.
|
||||||
|
*/
|
||||||
|
last: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property: start
|
||||||
|
* {Object} Object that store informations related to pinch touchstart.
|
||||||
|
*/
|
||||||
|
start: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor: OpenLayers.Handler.Pinch
|
||||||
|
* Returns OpenLayers.Handler.Pinch
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* control - {<OpenLayers.Control>} The control that is making use of
|
||||||
|
* this handler. If a handler is being used without a control, the
|
||||||
|
* handlers setMap method must be overridden to deal properly with
|
||||||
|
* the map.
|
||||||
|
* callbacks - {Object} An object containing functions to be called when
|
||||||
|
* the pinch operation start, change, or is finished. The callbacks
|
||||||
|
* should expect to receive an object argument, which contains
|
||||||
|
* information about scale, distance, and position of touch points.
|
||||||
|
* options - {Object}
|
||||||
|
*/
|
||||||
|
initialize: function(control, callbacks, options) {
|
||||||
|
OpenLayers.Handler.prototype.initialize.apply(this, arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method: touchstart
|
||||||
|
* Handle touchstart events
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* evt - {Event}
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* {Boolean} Let the event propagate.
|
||||||
|
*/
|
||||||
|
touchstart: function(evt) {
|
||||||
|
var propagate = true;
|
||||||
|
this.pinching = false;
|
||||||
|
if (OpenLayers.Event.isMultiTouch(evt)) {
|
||||||
|
this.started = true;
|
||||||
|
this.last = this.start = {
|
||||||
|
distance: this.getDistance(evt.touches),
|
||||||
|
delta: 0,
|
||||||
|
scale: 1
|
||||||
|
};
|
||||||
|
this.callback("start", [evt, this.start]);
|
||||||
|
propagate = !this.stopDown;
|
||||||
|
} else {
|
||||||
|
this.started = false;
|
||||||
|
this.start = null;
|
||||||
|
this.last = null;
|
||||||
|
}
|
||||||
|
OpenLayers.Event.stop(evt);
|
||||||
|
return propagate;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method: touchmove
|
||||||
|
* Handle touchmove events
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* evt - {Event}
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* {Boolean} Let the event propagate.
|
||||||
|
*/
|
||||||
|
touchmove: function(evt) {
|
||||||
|
if (this.started && OpenLayers.Event.isMultiTouch(evt)) {
|
||||||
|
this.pinching = true;
|
||||||
|
var current = this.getPinchData(evt);
|
||||||
|
this.callback("move", [evt, current]);
|
||||||
|
this.last = current;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method: touchend
|
||||||
|
* Handle touchend events
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* evt - {Event}
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* {Boolean} Let the event propagate.
|
||||||
|
*/
|
||||||
|
touchend: function(evt) {
|
||||||
|
if (this.started) {
|
||||||
|
this.started = false;
|
||||||
|
this.pinching = false;
|
||||||
|
this.callback("done", [evt, this.start, this.last]);
|
||||||
|
this.start = null;
|
||||||
|
this.last = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method: activate
|
||||||
|
* Activate the handler.
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* {Boolean} The handler was successfully activated.
|
||||||
|
*/
|
||||||
|
activate: function() {
|
||||||
|
var activated = false;
|
||||||
|
if (OpenLayers.Handler.prototype.activate.apply(this, arguments)) {
|
||||||
|
this.pinching = false;
|
||||||
|
activated = true;
|
||||||
|
}
|
||||||
|
return activated;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method: deactivate
|
||||||
|
* Deactivate the handler.
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* {Boolean} The handler was successfully deactivated.
|
||||||
|
*/
|
||||||
|
deactivate: function() {
|
||||||
|
var deactivated = false;
|
||||||
|
if (OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) {
|
||||||
|
this.started = false;
|
||||||
|
this.pinching = false;
|
||||||
|
this.start = null;
|
||||||
|
this.last = null;
|
||||||
|
deactivated = true;
|
||||||
|
}
|
||||||
|
return deactivated;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method: getDistance
|
||||||
|
* Get the distance in pixels between two touches.
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* touches - {Array(Object)}
|
||||||
|
*/
|
||||||
|
getDistance: function(touches) {
|
||||||
|
var t0 = touches[0];
|
||||||
|
var t1 = touches[1];
|
||||||
|
return Math.sqrt(
|
||||||
|
Math.pow(t0.clientX - t1.clientX, 2) +
|
||||||
|
Math.pow(t0.clientY - t1.clientY, 2)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method: getPinchData
|
||||||
|
* Get informations about the pinch event.
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* evt - {Event}
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* {Object} Object that contains data about the current pinch.
|
||||||
|
*/
|
||||||
|
getPinchData: function(evt) {
|
||||||
|
var distance = this.getDistance(evt.touches);
|
||||||
|
var scale = distance / this.start.distance;
|
||||||
|
return {
|
||||||
|
distance: distance,
|
||||||
|
delta: this.last.distance - distance,
|
||||||
|
scale: scale
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
CLASS_NAME: "OpenLayers.Handler.Pinch"
|
||||||
|
});
|
||||||
|
|
||||||
264
tests/Handler/Pinch.html
Normal file
264
tests/Handler/Pinch.html
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="../OLLoader.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function test_constructor(t) {
|
||||||
|
t.plan(3);
|
||||||
|
var control = new OpenLayers.Control();
|
||||||
|
control.id = Math.random();
|
||||||
|
var callbacks = {foo: "bar"};
|
||||||
|
var options = {bar: "foo"};
|
||||||
|
|
||||||
|
var oldInit = OpenLayers.Handler.prototype.initialize;
|
||||||
|
|
||||||
|
OpenLayers.Handler.prototype.initialize = function(con, call, opt) {
|
||||||
|
t.eq(con.id, control.id,
|
||||||
|
"constructor calls parent with the correct control");
|
||||||
|
t.eq(call, callbacks,
|
||||||
|
"constructor calls parent with the correct callbacks");
|
||||||
|
t.eq(opt, options,
|
||||||
|
"constructor calls parent with the correct options");
|
||||||
|
};
|
||||||
|
var handler = new OpenLayers.Handler.Pinch(control, callbacks, options);
|
||||||
|
|
||||||
|
OpenLayers.Handler.prototype.initialize = oldInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_activate(t) {
|
||||||
|
t.plan(3);
|
||||||
|
var map = new OpenLayers.Map('map');
|
||||||
|
var control = new OpenLayers.Control();
|
||||||
|
map.addControl(control);
|
||||||
|
var handler = new OpenLayers.Handler.Pinch(control);
|
||||||
|
handler.active = true;
|
||||||
|
var activated = handler.activate();
|
||||||
|
t.ok(!activated,
|
||||||
|
"activate returns false if the handler was already active");
|
||||||
|
handler.active = false;
|
||||||
|
handler.pinching = true;
|
||||||
|
activated = handler.activate();
|
||||||
|
t.ok(activated,
|
||||||
|
"activate returns true if the handler was not already active");
|
||||||
|
t.ok(!handler.pinching,
|
||||||
|
"activate sets pinching to false");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_events(t) {
|
||||||
|
// each handled event should be activated twice when handler is
|
||||||
|
// activated, so:
|
||||||
|
// 27 = 4tests * 2*3events + 1tests * 3events
|
||||||
|
t.plan(27);
|
||||||
|
|
||||||
|
var map = new OpenLayers.Map('map');
|
||||||
|
var control = new OpenLayers.Control();
|
||||||
|
map.addControl(control);
|
||||||
|
var handler = new OpenLayers.Handler.Pinch(control);
|
||||||
|
|
||||||
|
// list below events that should be handled (events) and those
|
||||||
|
// that should not be handled (nonevents) by the handler
|
||||||
|
var events = ["touchend", "touchmove", "touchstart"];
|
||||||
|
var nonevents = ["mousedown", "mouseup", "mousemove", "mouseout",
|
||||||
|
"click", "dblclick", "resize", "focus", "blur"];
|
||||||
|
map.events.registerPriority = function(type, obj, func) {
|
||||||
|
// this is one of the mock handler methods
|
||||||
|
t.eq(OpenLayers.Util.indexOf(nonevents, type), -1,
|
||||||
|
"registered method is not one of the events " +
|
||||||
|
"that should not be handled: " + type);
|
||||||
|
t.ok(OpenLayers.Util.indexOf(events, type) > -1,
|
||||||
|
"activate calls registerPriority with browser event: " + type);
|
||||||
|
t.eq(typeof func, "function",
|
||||||
|
"activate calls registerPriority with a function");
|
||||||
|
t.eq(obj["CLASS_NAME"], "OpenLayers.Handler.Pinch",
|
||||||
|
"activate calls registerPriority with the handler");
|
||||||
|
};
|
||||||
|
handler.activate();
|
||||||
|
handler.deactivate();
|
||||||
|
|
||||||
|
// set browser event like properties on the handler
|
||||||
|
for(var i=0; i<events.length; ++i) {
|
||||||
|
setMethod(events[i]);
|
||||||
|
}
|
||||||
|
function setMethod(key) {
|
||||||
|
handler[key] = function() {return key;};
|
||||||
|
}
|
||||||
|
|
||||||
|
map.events.registerPriority = function(type, obj, func) {
|
||||||
|
var r = func();
|
||||||
|
if(typeof r == "string") {
|
||||||
|
t.eq(r, type,
|
||||||
|
"activate calls registerPriority with the correct method");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.activate();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_callbacks(t) {
|
||||||
|
t.plan(23);
|
||||||
|
|
||||||
|
var map = new OpenLayers.Map('map', {controls: []});
|
||||||
|
|
||||||
|
var control = new OpenLayers.Control();
|
||||||
|
map.addControl(control);
|
||||||
|
|
||||||
|
// set fake values for touches
|
||||||
|
var testEvents = {
|
||||||
|
start: {
|
||||||
|
type: 'start',
|
||||||
|
touches: [{
|
||||||
|
clientX: 100,
|
||||||
|
clientY: 0
|
||||||
|
}, {
|
||||||
|
clientX: 0,
|
||||||
|
clientY: 0
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
move: {
|
||||||
|
type: 'move',
|
||||||
|
touches: [{
|
||||||
|
clientX: 100,
|
||||||
|
clientY: 0
|
||||||
|
}, {
|
||||||
|
clientX: 20,
|
||||||
|
clientY: 0
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
done: {
|
||||||
|
type: 'done',
|
||||||
|
touches: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// set callback methods
|
||||||
|
var customCb = OpenLayers.Function.False;
|
||||||
|
var cb = function(evt) {
|
||||||
|
var tch = testEvents[evt.type].touches;
|
||||||
|
t.ok(evt.touches[0].clientX == tch[0].clientX &&
|
||||||
|
evt.touches[0].clientY == tch[0].clientY,
|
||||||
|
"touchstart sets first touch position correctly in evt");
|
||||||
|
t.ok(evt.touches[1].clientX == tch[1].clientX &&
|
||||||
|
evt.touches[1].clientY == tch[1].clientY,
|
||||||
|
"touchstart sets second touch position correctly in evt");
|
||||||
|
t.eq(handler.start.distance, 100, "start distance is " +
|
||||||
|
"always the same");
|
||||||
|
customCb.apply(this, arguments);
|
||||||
|
}
|
||||||
|
var callbacks = {
|
||||||
|
start: cb,
|
||||||
|
move: cb,
|
||||||
|
done: customCb
|
||||||
|
};
|
||||||
|
|
||||||
|
var handler = new OpenLayers.Handler.Pinch(control, callbacks);
|
||||||
|
handler.activate();
|
||||||
|
|
||||||
|
var old_isMultiTouch = OpenLayers.Event.isMultiTouch;
|
||||||
|
var old_stop = OpenLayers.Event.stop;
|
||||||
|
|
||||||
|
// test single touch
|
||||||
|
OpenLayers.Event.isMultiTouch = function() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
handler.started = true;
|
||||||
|
handler.start = {
|
||||||
|
distance: 100,
|
||||||
|
delta: 0,
|
||||||
|
scale: 1
|
||||||
|
};
|
||||||
|
handler.last = {
|
||||||
|
distance: 150,
|
||||||
|
delta: 10,
|
||||||
|
scale: 1.5
|
||||||
|
};
|
||||||
|
map.events.triggerEvent("touchstart", testEvents.start);
|
||||||
|
t.ok(!handler.started, "1) touchstart (singletouch) sets started to false");
|
||||||
|
t.eq(handler.start, null, "1) touchstart (singletouch) sets start to null");
|
||||||
|
t.eq(handler.last, null, "1) touchstart (singletouch) sets last to null");
|
||||||
|
|
||||||
|
OpenLayers.Event.stop = function(evt, allowDefault) {
|
||||||
|
if(allowDefault) {
|
||||||
|
t.fail(
|
||||||
|
"touchstart is prevented from falling to other elements");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenLayers.Event.isMultiTouch = function(evt) {
|
||||||
|
var res = old_isMultiTouch(evt);
|
||||||
|
t.ok(res, "fake event is a mutitouch touch event");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
customCb = function(evt, pinchdata) {
|
||||||
|
t.eq(pinchdata.distance, 100, "2) calculated distance is correct");
|
||||||
|
t.eq(pinchdata.delta, 0, "2) calculated delta is correct");
|
||||||
|
t.eq(pinchdata.scale, 1, "2) calculated scale is correct");
|
||||||
|
}
|
||||||
|
map.events.triggerEvent("touchstart", testEvents.start);
|
||||||
|
t.ok(handler.started, "2) touchstart sets the started flag to true");
|
||||||
|
t.ok(!handler.pinching, "2) touchstart sets the pinching flag to false");
|
||||||
|
|
||||||
|
customCb = function(evt, pinchdata) {
|
||||||
|
t.eq(pinchdata.distance, 80, "3) calculated distance is correct");
|
||||||
|
t.eq(pinchdata.delta, 20, "3) calculated delta is correct");
|
||||||
|
t.eq(pinchdata.scale, 0.8, "3) calculated scale is correct");
|
||||||
|
}
|
||||||
|
map.events.triggerEvent("touchmove", testEvents.move);
|
||||||
|
t.ok(handler.started, "3) started flag still set to true");
|
||||||
|
t.ok(handler.pinching, "3) touchmove sets the pinching flag to true");
|
||||||
|
|
||||||
|
|
||||||
|
customCb = function(evt, first, last) {
|
||||||
|
t.eq(first.distance, 100, "4) calculated distance is correct");
|
||||||
|
t.eq(first.delta, 0, "4) calculated delta is correct");
|
||||||
|
t.eq(first.scale, 1, "4) calculated scale is correct");
|
||||||
|
t.eq(last.distance, 80, "4) calculated distance is correct");
|
||||||
|
t.eq(last.delta, 20, "4) calculated delta is correct");
|
||||||
|
t.eq(last.scale, 0.8, "4) calculated scale is correct");
|
||||||
|
}
|
||||||
|
map.events.triggerEvent("touchend", testEvents.done);
|
||||||
|
t.ok(!handler.started, "4) started flag is set to false");
|
||||||
|
t.ok(!handler.pinching, "4) touchdone sets the pinching flag to false");
|
||||||
|
|
||||||
|
OpenLayers.Event.stop = old_stop;
|
||||||
|
OpenLayers.Event.isMultiTouch = old_isMultiTouch;
|
||||||
|
|
||||||
|
// test move or done before start
|
||||||
|
customCb = function(evt) {
|
||||||
|
t.fail("should not pass here")
|
||||||
|
}
|
||||||
|
map.events.triggerEvent("touchmove", testEvents.move);
|
||||||
|
map.events.triggerEvent("touchend", testEvents.end);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_deactivate(t) {
|
||||||
|
t.plan(6);
|
||||||
|
var map = new OpenLayers.Map('map');
|
||||||
|
var control = new OpenLayers.Control();
|
||||||
|
map.addControl(control);
|
||||||
|
var handler = new OpenLayers.Handler.Pinch(control);
|
||||||
|
handler.active = false;
|
||||||
|
var deactivated = handler.deactivate();
|
||||||
|
t.ok(!deactivated,
|
||||||
|
"deactivate returns false if the handler was not already active");
|
||||||
|
handler.active = true;
|
||||||
|
handler.pinching = true;
|
||||||
|
deactivated = handler.deactivate();
|
||||||
|
t.ok(deactivated,
|
||||||
|
"deactivate returns true if the handler was active already");
|
||||||
|
t.ok(!handler.started,
|
||||||
|
"deactivate sets started to false");
|
||||||
|
t.ok(!handler.pinching,
|
||||||
|
"deactivate sets pinching to false");
|
||||||
|
t.ok(handler.start == null,
|
||||||
|
"deactivate sets start to null");
|
||||||
|
t.ok(handler.last == null,
|
||||||
|
"deactivate sets start to null");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map" style="width: 300px; height: 150px;"/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -118,6 +118,7 @@
|
|||||||
<li>Handler/Box.html</li>
|
<li>Handler/Box.html</li>
|
||||||
<li>Handler/Click.html</li>
|
<li>Handler/Click.html</li>
|
||||||
<li>Handler/Drag.html</li>
|
<li>Handler/Drag.html</li>
|
||||||
|
<li>Handler/Pinch.html</li>
|
||||||
<li>Handler/Feature.html</li>
|
<li>Handler/Feature.html</li>
|
||||||
<li>Handler/Hover.html</li>
|
<li>Handler/Hover.html</li>
|
||||||
<li>Handler/Keyboard.html</li>
|
<li>Handler/Keyboard.html</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user