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

View file

@ -48,26 +48,9 @@ var DomEvent = {
target || DomEvent.getTarget(event))); 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) { stop: function(event) {
DomEvent.stopPropagation(event); event.stopPropagation();
DomEvent.preventDefault(event); event.preventDefault();
} }
}; };

View file

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

View file

@ -108,11 +108,12 @@ var CanvasView = View.extend(/** @lends CanvasView# */{
doubleClick, doubleClick,
clickTime; 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! // called or not!
function callEvent(type, event, point, target, lastPoint, bubble) { function callEvent(type, event, point, target, lastPoint) {
var item = target, var item = target,
mouseEvent; mouseEvent;
// Bubble up the DOM and find a parent that responds to this event.
while (item) { while (item) {
if (item.responds(type)) { if (item.responds(type)) {
// Create an reuse the event object if we're bubbling // 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, mouseEvent = new MouseEvent(type, event, point, target,
// Calculate delta if lastPoint was passed // Calculate delta if lastPoint was passed
lastPoint ? point.subtract(lastPoint) : null); lastPoint ? point.subtract(lastPoint) : null);
if (item.fire(type, mouseEvent) if (item.fire(type, mouseEvent) && mouseEvent.isStopped) {
&& (!bubble || mouseEvent._stopped)) // Call preventDefault() on native event if mouse event was
return false; // handled here.
event.preventDefault();
return true;
}
} }
item = item.getParent(); item = item.getParent();
} }
return true; return false;
} }
function handleEvent(view, type, event, point, lastPoint) { return {
if (view._eventCounters[type]) { /**
var project = view._project, * 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, { hit = project.hitTest(point, {
tolerance: project.options.hitTolerance || 0, tolerance: project.options.hitTolerance || 0,
fill: true, fill: true,
stroke: true stroke: true
}), }),
item = hit && hit.item; item = hit && hit.item,
if (item) { stopped = false;
// If this is a mousemove event and we change the overItem, // Now handle the mouse events
// reset lastPoint to point so delta is (0, 0) switch (type) {
if (type === 'mousemove' && item != overItem) case 'mousedown':
lastPoint = point; stopped = callEvent(type, event, point, item);
// If we have a downItem with a mousedrag event, do not send // See if we're clicking again on the same item, within the
// mousemove events to any item while we're dragging. // double-click time. Firefox uses 300ms as the max time
// TODO: Do we also need to lock mousenter / mouseleave in the // difference:
// same way? doubleClick = lastItem == item && (Date.now() - clickTime < 300);
if (type !== 'mousemove' || !hasDrag) downItem = lastItem = item;
callEvent(type, event, point, item, lastPoint); downPoint = lastPoint = overPoint = point;
return item; hasDrag = downItem && downItem.responds('mousedrag');
} break;
} case 'mouseup':
} // stopping mousup events does not prevent mousedrag / mousemove
// hanlding here, but it does click / doubleclick
return { stopped = callEvent(type, event, point, item, downPoint);
_onMouseDown: function(event, point) { if (hasDrag) {
var item = handleEvent(this, 'mousedown', event, point); // If the point has changed since the last mousedrag event,
// See if we're clicking again on the same item, within the // send another one
// double-click time. Firefox uses 300ms as the max time difference: if (lastPoint && !lastPoint.equals(point))
doubleClick = lastItem == item && (Date.now() - clickTime < 300); callEvent('mousedrag', event, point, downItem,
downItem = lastItem = item; lastPoint);
downPoint = lastPoint = overPoint = point; // If we end up over another item, send it a mousemove event
hasDrag = downItem && downItem.responds('mousedrag'); // now. Use point as overPoint, so delta is (0, 0) since
}, // this will be the first mousemove event for this item.
if (item !== downItem) {
_onMouseUp: function(event, point) { overPoint = point;
// TODO: Check callEvent('mousemove', event, point, item, overPoint);
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);
} }
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) { return stopped;
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);
}
} }
}; };
}); });

View file

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

View file

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