Completely rework event handling on view and tools.

Fixes multiple issues on iOS:
- mousedown events were sometimes fired twice.,
- the presence of mousedown handlers broke scrolling.
Closes #266.
This commit is contained in:
Jürg Lehni 2013-12-06 21:49:44 +01:00
parent 0797202b22
commit 2cfa329fa6
6 changed files with 116 additions and 118 deletions

View file

@ -86,8 +86,10 @@ var Callback = {
// When the handler function returns false, prevent the default
// behaviour and stop propagation of the event by calling stop()
if (handlers[i].apply(that, args) === false
&& event && event.stop)
&& event && event.stop) {
event.stop();
break;
}
}
}
// See PaperScript.handleException for an explanation of the following.
@ -135,9 +137,7 @@ var Callback = {
types[type] = isString ? {} : entry;
// Create getters and setters for the property
// with the on*-name name:
// Use '__' as there are some _onMouse* functions
// already, e.g.g on View.
name = '__' + name;
name = '_' + name;
src['get' + part] = function() {
return this[name];
};

View file

@ -48,26 +48,9 @@ var DomEvent = {
target || DomEvent.getTarget(event)));
},
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
// IE
event.returnValue = false;
}
},
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},
stop: function(event) {
DomEvent.stopPropagation(event);
DomEvent.preventDefault(event);
event.stopPropagation();
event.preventDefault();
}
};

View file

@ -350,7 +350,7 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{
&& this.fire(type, new ToolEvent(this, type, event));
},
_onHandleEvent: function(type, point, event) {
_handleEvent: function(type, point, event) {
// Update global reference to this scope.
paper = this._scope;
// Now handle event callbacks
@ -401,7 +401,9 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{
}
break;
}
// Return if a callback was called or not.
// Prevent default if mouse event was handled.
if (called)
event.preventDefault();
return called;
}
/**

View file

@ -108,11 +108,12 @@ var CanvasView = View.extend(/** @lends CanvasView# */{
doubleClick,
clickTime;
// Returns false if event was stopped, true otherwise, whether handler was
// Returns true if event was stopped, false otherwise, whether handler was
// called or not!
function callEvent(type, event, point, target, lastPoint, bubble) {
function callEvent(type, event, point, target, lastPoint) {
var item = target,
mouseEvent;
// Bubble up the DOM and find a parent that responds to this event.
while (item) {
if (item.responds(type)) {
// Create an reuse the event object if we're bubbling
@ -120,91 +121,102 @@ var CanvasView = View.extend(/** @lends CanvasView# */{
mouseEvent = new MouseEvent(type, event, point, target,
// Calculate delta if lastPoint was passed
lastPoint ? point.subtract(lastPoint) : null);
if (item.fire(type, mouseEvent)
&& (!bubble || mouseEvent._stopped))
return false;
if (item.fire(type, mouseEvent) && mouseEvent.isStopped) {
// Call preventDefault() on native event if mouse event was
// handled here.
event.preventDefault();
return true;
}
}
item = item.getParent();
}
return true;
return false;
}
function handleEvent(view, type, event, point, lastPoint) {
if (view._eventCounters[type]) {
var project = view._project,
return {
/**
* Returns true if event was stopped, false otherwise, whether handler
* was called or not!
*/
_handleEvent: function(type, point, event) {
// Drop out if we don't have any event handlers for this type
if (!this._eventCounters[type])
return;
// Run the hit-test first
var project = this._project,
hit = project.hitTest(point, {
tolerance: project.options.hitTolerance || 0,
fill: true,
stroke: true
}),
item = hit && hit.item;
if (item) {
// If this is a mousemove event and we change the overItem,
// reset lastPoint to point so delta is (0, 0)
if (type === 'mousemove' && item != overItem)
lastPoint = point;
// If we have a downItem with a mousedrag event, do not send
// mousemove events to any item while we're dragging.
// TODO: Do we also need to lock mousenter / mouseleave in the
// same way?
if (type !== 'mousemove' || !hasDrag)
callEvent(type, event, point, item, lastPoint);
return item;
}
}
}
return {
_onMouseDown: function(event, point) {
var item = handleEvent(this, 'mousedown', event, point);
// See if we're clicking again on the same item, within the
// double-click time. Firefox uses 300ms as the max time difference:
doubleClick = lastItem == item && (Date.now() - clickTime < 300);
downItem = lastItem = item;
downPoint = lastPoint = overPoint = point;
hasDrag = downItem && downItem.responds('mousedrag');
},
_onMouseUp: function(event, point) {
// TODO: Check
var item = handleEvent(this, 'mouseup', event, point);
if (hasDrag) {
// If the point has changed since the last mousedrag event, send
// another one
if (lastPoint && !lastPoint.equals(point))
callEvent('mousedrag', event, point, downItem, lastPoint);
// If we had a mousedrag event locking mousemove events and are
// over another item, send it a mousemove event now.
// Use point as overPoint, so delta is (0, 0) since this will
// be the first mousemove event for this item.
if (item != downItem) {
overPoint = point;
callEvent('mousemove', event, point, item, overPoint);
item = hit && hit.item,
stopped = false;
// Now handle the mouse events
switch (type) {
case 'mousedown':
stopped = callEvent(type, event, point, item);
// See if we're clicking again on the same item, within the
// double-click time. Firefox uses 300ms as the max time
// difference:
doubleClick = lastItem == item && (Date.now() - clickTime < 300);
downItem = lastItem = item;
downPoint = lastPoint = overPoint = point;
hasDrag = downItem && downItem.responds('mousedrag');
break;
case 'mouseup':
// stopping mousup events does not prevent mousedrag / mousemove
// hanlding here, but it does click / doubleclick
stopped = callEvent(type, event, point, item, downPoint);
if (hasDrag) {
// If the point has changed since the last mousedrag event,
// send another one
if (lastPoint && !lastPoint.equals(point))
callEvent('mousedrag', event, point, downItem,
lastPoint);
// If we end up over another item, send it a mousemove event
// now. Use point as overPoint, so delta is (0, 0) since
// this will be the first mousemove event for this item.
if (item !== downItem) {
overPoint = point;
callEvent('mousemove', event, point, item, overPoint);
}
}
if (!stopped && item === downItem) {
clickTime = Date.now();
callEvent(doubleClick && downItem.responds('doubleclick')
? 'doubleclick' : 'click', event, downPoint, item);
doubleClick = false;
}
downItem = null;
hasDrag = false;
break;
case 'mousemove':
// Allow both mousedrag and mousemove events to stop mousemove
// events from reaching tools.
if (hasDrag)
stopped = callEvent('mousedrag', event, point, downItem,
lastPoint);
// TODO: Consider implementing this again? "If we have a
// mousedrag event, do not send mousemove events to any
// item while we're dragging."
// For now, we let other items receive mousemove events even
// during a drag event.
// If we change the overItem, reset overPoint to point so
// delta is (0, 0)
if (!stopped) {
if (item !== overItem)
overPoint = point;
stopped = callEvent(type, event, point, item, overPoint);
}
lastPoint = overPoint = point;
if (item !== overItem) {
callEvent('mouseleave', event, point, overItem);
overItem = item;
callEvent('mouseenter', event, point, item);
}
break;
}
if (item === downItem) {
clickTime = Date.now();
if (!doubleClick
// callEvent returns false if event is stopped.
|| callEvent('doubleclick', event, downPoint, item))
callEvent('click', event, downPoint, item);
doubleClick = false;
}
downItem = null;
hasDrag = false;
},
_onMouseMove: function(event, point) {
// Call the mousedrag event first if an item was clicked earlier
if (downItem)
callEvent('mousedrag', event, point, downItem, lastPoint);
var item = handleEvent(this, 'mousemove', event, point, overPoint);
lastPoint = overPoint = point;
if (item !== overItem) {
callEvent('mouseleave', event, point, overItem);
overItem = item;
callEvent('mouseenter', event, point, item);
}
return stopped;
}
};
});

View file

@ -21,14 +21,17 @@ var Event = Base.extend(/** @lends Event# */{
this.event = event;
},
isPrevented: false,
isStopped: false,
preventDefault: function() {
this._prevented = true;
DomEvent.preventDefault(this.event);
this.isPrevented = true;
this.event.preventDefault();
},
stopPropagation: function() {
this._stopped = true;
DomEvent.stopPropagation(this.event);
this.isStopped = true;
this.event.stopPropagation();
},
stop: function() {

View file

@ -637,10 +637,9 @@ var View = Base.extend(Callback, /** @lends View# */{
dragging = true;
// Always first call the view's mouse handlers, as required by
// CanvasView, and then handle the active tool, if any.
if (view._onMouseDown)
view._onMouseDown(event, point);
view._handleEvent('mousedown', point, event);
if (tool = view._scope._tool)
tool._onHandleEvent('mousedown', point, event);
tool._handleEvent('mousedown', point, event);
// In the end we always call draw(), but pass checkRedraw = true, so we
// only redraw the view if anything has changed in the above calls.
view.draw(true);
@ -668,13 +667,11 @@ var View = Base.extend(Callback, /** @lends View# */{
var point = event && viewToProject(view, event);
if (dragging || new Rectangle(new Point(),
view.getViewSize()).contains(point)) {
if (view._onMouseMove)
view._onMouseMove(event, point);
view._handleEvent('mousemove', point, event);
if (tool = view._scope._tool) {
// If there's no onMouseDrag, fire onMouseMove while dragging.
if (tool._onHandleEvent(dragging && tool.responds('mousedrag')
? 'mousedrag' : 'mousemove', point, event))
DomEvent.stop(event);
tool._handleEvent(dragging && tool.responds('mousedrag')
? 'mousedrag' : 'mousemove', point, event);
}
view.draw(true);
}
@ -687,11 +684,9 @@ var View = Base.extend(Callback, /** @lends View# */{
var point = viewToProject(view, event);
curPoint = null;
dragging = false;
if (view._onMouseUp)
view._onMouseUp(event, point);
// Cancel DOM-event if it was handled by our tool
if (tool && tool._onHandleEvent('mouseup', point, event))
DomEvent.stop(event);
view._handleEvent('mouseup', point, event);
if (tool)
tool._handleEvent('mouseup', point, event);
view.draw(true);
}
@ -699,7 +694,7 @@ var View = Base.extend(Callback, /** @lends View# */{
// Only stop this even if we're dragging already, since otherwise no
// text whatsoever can be selected on the page.
if (dragging)
DomEvent.stop(event);
event.preventDefault();
}
// mousemove and mouseup events need to be installed on document, not the
@ -727,6 +722,9 @@ var View = Base.extend(Callback, /** @lends View# */{
selectstart: selectstart
},
// To be defined in subclasses
_handleEvent: function(/* type, point, event */) {},
statics: {
/**
* Loops through all views and sets the focus on the first