Make sure mouse events propagate to the view while their targets remain consistent.

Closes #995
This commit is contained in:
Jürg Lehni 2016-06-14 16:52:31 +02:00
parent 93e4d81645
commit 724bcb2e35
2 changed files with 76 additions and 45 deletions

View file

@ -30,7 +30,7 @@ var MouseEvent = Event.extend(/** @lends MouseEvent# */{
this.type = type; this.type = type;
this.event = event; this.event = event;
this.point = point; this.point = point;
this.target = target; this._target = target;
this.delta = delta; this.delta = delta;
}, },
@ -56,6 +56,12 @@ var MouseEvent = Event.extend(/** @lends MouseEvent# */{
* @name MouseEvent#target * @name MouseEvent#target
* @type Item * @type Item
*/ */
getTarget: function() {
var target = this._target;
if (typeof target === 'function')
target = this._target = target();
return target;
},
// DOCS: document MouseEvent#delta // DOCS: document MouseEvent#delta
/** /**
@ -69,7 +75,7 @@ var MouseEvent = Event.extend(/** @lends MouseEvent# */{
toString: function() { toString: function() {
return "{ type: '" + this.type return "{ type: '" + this.type
+ "', point: " + this.point + "', point: " + this.point
+ ', target: ' + this.target + ', target: ' + this.getTarget()
+ (this.delta ? ', delta: ' + this.delta : '') + (this.delta ? ', delta: ' + this.delta : '')
+ ', modifiers: ' + this.getModifiers() + ', modifiers: ' + this.getModifiers()
+ ' }'; + ' }';

View file

@ -1155,9 +1155,9 @@ new function() { // Injection scope for event handling on the browser
}; };
// Returns true if event was prevented, false otherwise. // Returns true if event was prevented, false otherwise.
function emitMouseEvent(obj, type, event, point, prevPoint, stopItem) { function emitMouseEvent(obj, target, type, event, point, prevPoint,
var target = obj, stopItem) {
stopped = false, var stopped = false,
mouseEvent; mouseEvent;
// Returns true if the event was stopped, false otherwise. // Returns true if the event was stopped, false otherwise.
@ -1166,7 +1166,8 @@ new function() { // Injection scope for event handling on the browser
// Only produce the event object if we really need it, and then // Only produce the event object if we really need it, and then
// reuse it if we're bubbling. // reuse it if we're bubbling.
if (!mouseEvent) { if (!mouseEvent) {
mouseEvent = new MouseEvent(type, event, point, target, mouseEvent = new MouseEvent(type, event, point,
target || obj,
// Calculate delta if prevPoint was passed // Calculate delta if prevPoint was passed
prevPoint ? point.subtract(prevPoint) : null); prevPoint ? point.subtract(prevPoint) : null);
} }
@ -1195,7 +1196,8 @@ new function() { // Injection scope for event handling on the browser
} }
// Returns true if event was stopped, false otherwise. // Returns true if event was stopped, false otherwise.
function emitMouseEvents(view, item, type, event, point, prevPoint) { function emitMouseEvents(view, hitItem, hitTest, type, event, point,
prevPoint) {
// Before handling events, process removeOn() calls for cleanup. // Before handling events, process removeOn() calls for cleanup.
// NOTE: As soon as there is one event handler receiving mousedrag // NOTE: As soon as there is one event handler receiving mousedrag
// events, non of the removeOnMove() items will be removed while the // events, non of the removeOnMove() items will be removed while the
@ -1206,17 +1208,21 @@ new function() { // Injection scope for event handling on the browser
// and of the handlers called event.preventDefault() // and of the handlers called event.preventDefault()
prevented = called = false; prevented = called = false;
// First handle the drag-item and its parents, through bubbling. // First handle the drag-item and its parents, through bubbling.
return (dragItem && emitMouseEvent(dragItem, type, event, point, return (dragItem && emitMouseEvent(dragItem, null, type, event,
prevPoint) point, prevPoint)
// Next handle the hit-test item, if it's different from the drag // Next handle the hit-item, if it's different from the drag-item
// item and not a descendant of it (in which case it would already // and not a descendant of it (in which case it would already have
// have received an event in the call above). Use fallbacks to // received an event in the call above). Use fallbacks to translate
// translate mousedrag to mousemove, since drag is handled above. // mousedrag to mousemove, since drag is handled above.
|| item && item !== dragItem && !item.isDescendant(dragItem) || hitItem && hitItem !== dragItem
&& emitMouseEvent(item, fallbacks[type] || type, event, point, && !hitItem.isDescendant(dragItem)
prevPoint, dragItem) && emitMouseEvent(hitItem, null, fallbacks[type] || type, event,
point, prevPoint, dragItem)
// Lastly handle the mouse events on the view, if we're still here. // Lastly handle the mouse events on the view, if we're still here.
|| emitMouseEvent(view, type, event, point, prevPoint)); // Choose from the potential targets in the right sequence, with the
// hitTest() function as the fall-back getter for MouseEvent#target.
|| emitMouseEvent(view, dragItem || hitItem || hitTest, type, event,
point, prevPoint));
} }
/** /**
@ -1293,12 +1299,7 @@ new function() { // Injection scope for event handling on the browser
// Run the hit-test on items first, but only if we're required to do // Run the hit-test on items first, but only if we're required to do
// so for this given mouse event, see hitItems, #_countItemEvent(): // so for this given mouse event, see hitItems, #_countItemEvent():
var inView = this.getBounds().contains(point), var inView = this.getBounds().contains(point),
hit = hitItems && inView && this._project.hitTest(point, { hitItem = undefined,
tolerance: 0,
fill: true,
stroke: true
}),
item = hit && hit.item || null,
// Keep track if view event should be handled, so we can use it // Keep track if view event should be handled, so we can use it
// to decide if tool._handleMouseEvent() shall be called after. // to decide if tool._handleMouseEvent() shall be called after.
handle = false, handle = false,
@ -1307,34 +1308,56 @@ new function() { // Injection scope for event handling on the browser
// mouse event types. // mouse event types.
mouse[type.substr(5)] = true; mouse[type.substr(5)] = true;
// Always first call the view's mouse handlers, as required by // Provide a hit-test function that makes sure to only perform the
// CanvasView, and then handle the active tool after, if any. // hit-test once, and only when it's actually required. This method
if (hitItems && item !== overItem) { // is passed to emitMouseEvents() and as target to emitMouseEvent(),
// But first handle mouseenter / leave between items and also on // as the fall-back getter for MouseEvent#target.
// the view, but only if hitItems is true, see above. function hitTest() {
//
if (hitItem === undefined) {
var hit = inView && view._project.hitTest(point, {
tolerance: 0,
fill: true,
stroke: true
});
hitItem = hit && hit.item || null;
}
// Return the target with view as the fall-back, as expected by
// MouseEvent#target.
return hitItem || view;
}
// Execute hitTest right away if we have events relying on hitItem.
if (hitItems)
hitTest();
// Handle mouseenter / leave between items and views first.
if (hitItems && hitItem !== overItem) {
if (overItem) { if (overItem) {
emitMouseEvent(overItem, 'mouseleave', event, point); emitMouseEvent(overItem, null, 'mouseleave', event, point);
} }
if (item) { if (hitItem) {
emitMouseEvent(item, 'mouseenter', event, point); emitMouseEvent(hitItem, null, 'mouseenter', event, point);
} }
overItem = item; overItem = hitItem;
} }
// Handle mouseenter / leave on the view. // Handle mouseenter / leave on the view.
if (wasInView ^ inView) { if (wasInView ^ inView) {
emitMouseEvent(this, inView ? 'mouseenter' : 'mouseleave', emitMouseEvent(this, null, inView ? 'mouseenter' : 'mouseleave',
event, point); event, point);
overView = inView ? this : null; overView = inView ? this : null;
handle = true; // To include the leaving move. handle = true; // To include the leaving move.
} }
// Now finally handle the mousemove / mousedrag event. // Now handle the mousemove / mousedrag event.
// Always call the view's mouse handlers first, as required by
// CanvasView, and then handle the active tool after, if any.
// mousedrag is allowed to leave the view and still triggers events, // mousedrag is allowed to leave the view and still triggers events,
// but do not trigger two subsequent even with the same location. // but do not trigger two subsequent even with the same location.
if ((inView || mouse.drag) && !point.equals(lastPoint)) { if ((inView || mouse.drag) && !point.equals(lastPoint)) {
// Handle mousemove even if this is not actually a mousemove // Handle mousemove even if this is not actually a mousemove
// event but the mouse has moved since the last event. // event but the mouse has moved since the last event.
emitMouseEvents(this, item, nativeMove ? type : 'mousemove', emitMouseEvents(this, hitItem, hitTest,
event, point, lastPoint); nativeMove ? type : 'mousemove', event,
point, lastPoint);
handle = true; handle = true;
} }
wasInView = inView; wasInView = inView;
@ -1342,25 +1365,27 @@ new function() { // Injection scope for event handling on the browser
// We emit mousedown only when in the view, and mouseup regardless, // We emit mousedown only when in the view, and mouseup regardless,
// as long as the mousedown event was inside. // as long as the mousedown event was inside.
if (mouse.down && inView || mouse.up && downPoint) { if (mouse.down && inView || mouse.up && downPoint) {
emitMouseEvents(this, item, type, event, point, downPoint); emitMouseEvents(this, hitItem, hitTest, type, event,
point, downPoint);
if (mouse.down) { if (mouse.down) {
// See if we're clicking again on the same item, within the // See if we're clicking again on the same item, within the
// double-click time. Firefox uses 300ms as the max time // double-click time. Firefox uses 300ms as the max time
// difference: // difference:
dblClick = item === clickItem dblClick = hitItem === clickItem
&& (Date.now() - clickTime < 300); && (Date.now() - clickTime < 300);
downItem = clickItem = item; downItem = clickItem = hitItem;
// Only start dragging if the mousedown event has not // Only start dragging if the mousedown event has not
// prevented the default. // prevented the default.
dragItem = !prevented && item; dragItem = !prevented && hitItem;
downPoint = point; downPoint = point;
} else if (mouse.up) { } else if (mouse.up) {
// Emulate click / doubleclick, but only on item, not view // Emulate click / doubleclick, but only on the hit-item,
if (!prevented && item === downItem) { // not the view.
if (!prevented && hitItem === downItem) {
clickTime = Date.now(); clickTime = Date.now();
emitMouseEvents(this, item, emitMouseEvents(this, hitItem, hitTest,
dblClick ? 'doubleclick' : 'click', dblClick ? 'doubleclick' : 'click', event,
event, point, downPoint); point, downPoint);
dblClick = false; dblClick = false;
} }
downItem = dragItem = null; downItem = dragItem = null;