Implement propper support for pointer events and MSPointer events.

Closes #406 and #336.
This commit is contained in:
Jürg Lehni 2014-03-12 13:22:41 +01:00
parent 5f00c02e67
commit 9c552b1739
4 changed files with 132 additions and 74 deletions

View file

@ -46,6 +46,24 @@ var DomElement = new function() {
return res;
}
// Handles both getting and setting of vendor prefix values
function handlePrefix(el, name, set, value) {
var prefixes = ['webkit', 'moz', 'Moz', 'ms', 'o', ''],
suffix = name[0].toUpperCase() + name.substring(1);
for (var i = 0; i < 6; i++) {
var prefix = prefixes[i],
key = prefix ? prefix + suffix : name;
if (key in el) {
if (set) {
el[key] = value;
} else {
return el[key];
}
break;
}
}
}
return /** @lends DomElement */{
create: function(nodes, parent) {
var isArray = Array.isArray(nodes),
@ -203,13 +221,17 @@ var DomElement = new function() {
* Gets the given property from the element, trying out all browser
* prefix variants.
*/
getPrefixValue: function(el, name) {
var value = el[name],
prefixes = ['webkit', 'moz', 'ms', 'o'],
suffix = name[0].toUpperCase() + name.substring(1);
for (var i = 0; i < 4 && value == null; i++)
value = el[prefixes[i] + suffix];
return value;
getPrefixed: function(el, name) {
return handlePrefix(el, name);
},
setPrefixed: function(el, name, value) {
if (typeof name === 'object') {
for (var key in name)
handlePrefix(el, key, true, name[key]);
} else {
handlePrefix(el, name, true, value);
}
}
};
};

View file

@ -17,13 +17,21 @@
*/
var DomEvent = /** @lends DomEvent */{
add: function(el, events) {
for (var type in events)
el.addEventListener(type, events[type], false);
for (var type in events) {
var func = events[type],
parts = type.split(/[\s,]+/g);
for (var i = 0, l = parts.length; i < l; i++)
el.addEventListener(parts[i], func, false);
}
},
remove: function(el, events) {
for (var type in events)
el.removeEventListener(type, events[type], false);
for (var type in events) {
var func = events[type],
parts = type.split(/[\s,]+/g);
for (var i = 0, l = parts.length; i < l; i++)
el.removeEventListener(parts[i], func, false);
}
},
getPoint: function(event) {
@ -59,8 +67,7 @@ var DomEvent = /** @lends DomEvent */{
};
DomEvent.requestAnimationFrame = new function() {
var nativeRequest = DomElement.getPrefixValue(window,
'requestAnimationFrame'),
var nativeRequest = DomElement.getPrefixed(window, 'requestAnimationFrame'),
requested = false,
callbacks = [],
focused = true,

View file

@ -51,7 +51,7 @@ var CanvasView = View.extend(/** @lends CanvasView# */{
// Hi-DPI Canvas support based on:
// http://www.html5rocks.com/en/tutorials/canvas/hidpi/
var deviceRatio = window.devicePixelRatio || 1,
backingStoreRatio = DomElement.getPrefixValue(this._context,
backingStoreRatio = DomElement.getPrefixed(this._context,
'backingStorePixelRatio') || 1;
this._pixelRatio = deviceRatio / backingStoreRatio;
}

View file

@ -38,7 +38,20 @@ var View = Base.extend(Callback, /** @lends View# */{
if (this._id == null)
element.setAttribute('id', this._id = 'view-' + View._id++);
// Install event handlers
DomEvent.add(element, this._viewHandlers);
DomEvent.add(element, this._viewEvents);
// Borrowed from Hammer.js:
var none = 'none';
DomElement.setPrefixed(element.style, {
userSelect: none,
// This makes the element blocking in IE10+
// You could experiment with the value, see this issue:
// https://github.com/EightMedia/hammer.js/issues/241
touchAction: none,
touchCallout: none,
contentZooming: none,
userDrag: none,
tapHighlightColor: 'rgba(0,0,0,0)'
});
// If the element has the resize attribute, resize the it to fill the
// window and resize it again whenever the user resizes the window.
if (PaperScope.hasAttribute(element, 'resize')) {
@ -48,7 +61,7 @@ var View = Base.extend(Callback, /** @lends View# */{
that = this;
size = DomElement.getViewportBounds(element)
.getSize().subtract(offset);
this._windowHandlers = {
this._windowEvents = {
resize: function() {
// Only update element offset if it's not invisible, as
// otherwise the offset would be wrong.
@ -60,7 +73,7 @@ var View = Base.extend(Callback, /** @lends View# */{
.getSize().subtract(offset));
}
};
DomEvent.add(window, this._windowHandlers);
DomEvent.add(window, this._windowEvents);
} else {
// Try visible size first, since that will help handling previously
// scaled canvases (e.g. when dealing with pixel-ratio)
@ -130,8 +143,8 @@ var View = Base.extend(Callback, /** @lends View# */{
this._project.view = null;
/*#*/ if (__options.environment == 'browser') {
// Uninstall event handlers again for this view.
DomEvent.remove(this._element, this._viewHandlers);
DomEvent.remove(window, this._windowHandlers);
DomEvent.remove(this._element, this._viewEvents);
DomEvent.remove(window, this._windowEvents);
/*#*/ } // __options.environment == 'browser'
this._element = this._project = null;
// Remove all onFrame handlers.
@ -687,7 +700,68 @@ var View = Base.extend(Callback, /** @lends View# */{
}
}
function mousedown(event) {
function handleMouseMove(view, point, event) {
view._handleEvent('mousemove', point, event);
var tool = view._scope.tool;
if (tool) {
// If there's no onMouseDrag, fire onMouseMove while dragging.
tool._handleEvent(dragging && tool.responds('mousedrag')
? 'mousedrag' : 'mousemove', point, event);
}
view.update();
return tool;
}
// Touch handling inspired by Hammer.js
var navigator = window.navigator,
mousedown, mousemove, mouseup;
if (navigator.pointerEnabled || navigator.msPointerEnabled) {
// HTML5 / MS pointer events
mousedown = 'pointerdown MSPointerDown';
mousemove = 'pointermove MSPointerMove';
mouseup = 'pointerup pointercancel MSPointerUp MSPointerCancel';
} else {
mousedown = 'touchstart';
mousemove = 'touchmove';
mouseup = 'touchend touchcancel';
// Do not add mouse events on mobile and tablet devices
if (!('ontouchstart' in window && navigator.userAgent.match(
/mobile|tablet|ip(ad|hone|od)|android|silk/i))) {
// For non pointer events browsers and mixed browsers, like chrome
// on Windows8 touch laptop.
mousedown += ' mousedown';
mousemove += ' mousemove';
mouseup += ' mouseup';
}
}
var viewEvents = {
selectstart: function(event) {
// Only stop this even if we're dragging already, since otherwise no
// text whatsoever can be selected on the page.
if (dragging)
event.preventDefault();
}
};
var docEvents = {
mouseout: function(event) {
// When the moues leaves the document, fire one last mousemove
// event, to give items the change to receive a mouseleave, etc.
var view = View._focused,
target = DomEvent.getRelatedTarget(event);
if (view && (!target || target.nodeName === 'HTML'))
handleMouseMove(view, viewToProject(view, event), event);
},
scroll: updateFocus
};
// mousemove and mouseup events need to be installed on document, not the
// view element, since we want to catch the end of drag events even outside
// our view. Only the mousedown events are installed on the view, as defined
// by _viewEvents below.
viewEvents[mousedown] = function(event) {
// Get the view from the event, and store a reference to the view that
// should receive keyboard input.
var view = View._focused = getView(event),
@ -701,29 +775,17 @@ var View = Base.extend(Callback, /** @lends View# */{
// In the end we always call update(), which only updates the view if
// anything has changed in the above calls.
view.update();
}
};
function handleMouseMove(view, point, event) {
view._handleEvent('mousemove', point, event);
var tool = view._scope.tool;
if (tool) {
// If there's no onMouseDrag, fire onMouseMove while dragging.
tool._handleEvent(dragging && tool.responds('mousedrag')
? 'mousedrag' : 'mousemove', point, event);
}
view.update();
return tool;
}
function mousemove(event) {
docEvents[mousemove] = function(event) {
var view = View._focused;
if (!dragging) {
// See if we can get the view from the current event target, and
// handle the mouse move over it.
var target = getView(event);
if (target) {
// Temporarily focus this view without making it sticky, so
// Key events are handled too during the mouse over
// Temporarily focus this view without making it sticky, so Key
// events are handled too during the mouse over.
// If we switch view, fire one last mousemove in the old view,
// to give items the change to receive a mouseleave, etc.
if (view !== target)
@ -741,18 +803,9 @@ var View = Base.extend(Callback, /** @lends View# */{
if (dragging || view.getBounds().contains(point))
tool = handleMouseMove(view, point, event);
}
}
};
function mouseout(event) {
// When the moues leaves the document, fire one last mousemove event,
// to give items the change to receive a mouseleave, etc.
var view = View._focused,
target = DomEvent.getRelatedTarget(event);
if (view && (!target || target.nodeName === 'HTML'))
handleMouseMove(view, viewToProject(view, event), event);
}
function mouseup(event) {
docEvents[mouseup] = function(event) {
var view = View._focused;
if (!view || !dragging)
return;
@ -763,40 +816,16 @@ var View = Base.extend(Callback, /** @lends View# */{
if (tool)
tool._handleEvent('mouseup', point, event);
view.update();
}
};
function selectstart(event) {
// Only stop this even if we're dragging already, since otherwise no
// text whatsoever can be selected on the page.
if (dragging)
event.preventDefault();
}
// mousemove and mouseup events need to be installed on document, not the
// view element, since we want to catch the end of drag events even outside
// our view. Only the mousedown events are installed on the view, as handled
// by _createHandlers below.
DomEvent.add(document, {
mousemove: mousemove,
mouseout: mouseout,
mouseup: mouseup,
touchmove: mousemove,
touchend: mouseup,
selectstart: selectstart,
scroll: updateFocus
});
DomEvent.add(document, docEvents);
DomEvent.add(window, {
load: updateFocus
});
return {
_viewHandlers: {
mousedown: mousedown,
touchstart: mousedown,
selectstart: selectstart
},
_viewEvents: viewEvents,
// To be defined in subclasses
_handleEvent: function(/* type, point, event */) {},