diff --git a/src/event/MouseEvent.js b/src/event/MouseEvent.js index ef7b1742..5edf2bd7 100644 --- a/src/event/MouseEvent.js +++ b/src/event/MouseEvent.js @@ -30,7 +30,7 @@ var MouseEvent = Event.extend(/** @lends MouseEvent# */{ this.type = type; this.event = event; this.point = point; - this.target = target; + this._target = target; this.delta = delta; }, @@ -56,6 +56,12 @@ var MouseEvent = Event.extend(/** @lends MouseEvent# */{ * @name MouseEvent#target * @type Item */ + getTarget: function() { + var target = this._target; + if (typeof target === 'function') + target = this._target = target(); + return target; + }, // DOCS: document MouseEvent#delta /** @@ -69,7 +75,7 @@ var MouseEvent = Event.extend(/** @lends MouseEvent# */{ toString: function() { return "{ type: '" + this.type + "', point: " + this.point - + ', target: ' + this.target + + ', target: ' + this.getTarget() + (this.delta ? ', delta: ' + this.delta : '') + ', modifiers: ' + this.getModifiers() + ' }'; diff --git a/src/view/View.js b/src/view/View.js index cf7b6bec..c9fbdf69 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -1155,9 +1155,9 @@ new function() { // Injection scope for event handling on the browser }; // Returns true if event was prevented, false otherwise. - function emitMouseEvent(obj, type, event, point, prevPoint, stopItem) { - var target = obj, - stopped = false, + function emitMouseEvent(obj, target, type, event, point, prevPoint, + stopItem) { + var stopped = false, mouseEvent; // 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 // reuse it if we're bubbling. if (!mouseEvent) { - mouseEvent = new MouseEvent(type, event, point, target, + mouseEvent = new MouseEvent(type, event, point, + target || obj, // Calculate delta if prevPoint was passed 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. - 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. // NOTE: As soon as there is one event handler receiving mousedrag // 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() prevented = called = false; // First handle the drag-item and its parents, through bubbling. - return (dragItem && emitMouseEvent(dragItem, type, event, point, - prevPoint) - // Next handle the hit-test item, if it's different from the drag - // item and not a descendant of it (in which case it would already - // have received an event in the call above). Use fallbacks to - // translate mousedrag to mousemove, since drag is handled above. - || item && item !== dragItem && !item.isDescendant(dragItem) - && emitMouseEvent(item, fallbacks[type] || type, event, point, - prevPoint, dragItem) + return (dragItem && emitMouseEvent(dragItem, null, type, event, + point, prevPoint) + // Next handle the hit-item, if it's different from the drag-item + // and not a descendant of it (in which case it would already have + // received an event in the call above). Use fallbacks to translate + // mousedrag to mousemove, since drag is handled above. + || hitItem && hitItem !== dragItem + && !hitItem.isDescendant(dragItem) + && emitMouseEvent(hitItem, null, fallbacks[type] || type, event, + point, prevPoint, dragItem) // 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 // so for this given mouse event, see hitItems, #_countItemEvent(): var inView = this.getBounds().contains(point), - hit = hitItems && inView && this._project.hitTest(point, { - tolerance: 0, - fill: true, - stroke: true - }), - item = hit && hit.item || null, + hitItem = undefined, // Keep track if view event should be handled, so we can use it // to decide if tool._handleMouseEvent() shall be called after. handle = false, @@ -1307,34 +1308,56 @@ new function() { // Injection scope for event handling on the browser // mouse event types. mouse[type.substr(5)] = true; - // Always first call the view's mouse handlers, as required by - // CanvasView, and then handle the active tool after, if any. - if (hitItems && item !== overItem) { - // But first handle mouseenter / leave between items and also on - // the view, but only if hitItems is true, see above. + // Provide a hit-test function that makes sure to only perform the + // hit-test once, and only when it's actually required. This method + // is passed to emitMouseEvents() and as target to emitMouseEvent(), + // as the fall-back getter for MouseEvent#target. + 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) { - emitMouseEvent(overItem, 'mouseleave', event, point); + emitMouseEvent(overItem, null, 'mouseleave', event, point); } - if (item) { - emitMouseEvent(item, 'mouseenter', event, point); + if (hitItem) { + emitMouseEvent(hitItem, null, 'mouseenter', event, point); } - overItem = item; + overItem = hitItem; } // Handle mouseenter / leave on the view. if (wasInView ^ inView) { - emitMouseEvent(this, inView ? 'mouseenter' : 'mouseleave', + emitMouseEvent(this, null, inView ? 'mouseenter' : 'mouseleave', event, point); overView = inView ? this : null; 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, // but do not trigger two subsequent even with the same location. if ((inView || mouse.drag) && !point.equals(lastPoint)) { // Handle mousemove even if this is not actually a mousemove // event but the mouse has moved since the last event. - emitMouseEvents(this, item, nativeMove ? type : 'mousemove', - event, point, lastPoint); + emitMouseEvents(this, hitItem, hitTest, + nativeMove ? type : 'mousemove', event, + point, lastPoint); handle = true; } 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, // as long as the mousedown event was inside. 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) { // See if we're clicking again on the same item, within the // double-click time. Firefox uses 300ms as the max time // difference: - dblClick = item === clickItem + dblClick = hitItem === clickItem && (Date.now() - clickTime < 300); - downItem = clickItem = item; + downItem = clickItem = hitItem; // Only start dragging if the mousedown event has not // prevented the default. - dragItem = !prevented && item; + dragItem = !prevented && hitItem; downPoint = point; } else if (mouse.up) { - // Emulate click / doubleclick, but only on item, not view - if (!prevented && item === downItem) { + // Emulate click / doubleclick, but only on the hit-item, + // not the view. + if (!prevented && hitItem === downItem) { clickTime = Date.now(); - emitMouseEvents(this, item, - dblClick ? 'doubleclick' : 'click', - event, point, downPoint); + emitMouseEvents(this, hitItem, hitTest, + dblClick ? 'doubleclick' : 'click', event, + point, downPoint); dblClick = false; } downItem = dragItem = null;