From 00a7588a3afe6be494ec205f103ff9ae6b83cbff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=BCrg=20Lehni?= <juerg@scratchdisk.com>
Date: Wed, 13 Jan 2016 19:04:03 +0100
Subject: [PATCH] Allow any mouse handler to return true in order to enforce
 browser default.

Relates to #686
---
 src/core/Emitter.js | 16 ++++++++++++----
 src/dom/DomEvent.js | 16 ++++++++++++----
 src/event/Event.js  |  2 ++
 src/tool/Tool.js    | 25 +++++++++++++++++--------
 src/view/View.js    | 25 +++++++++++++++----------
 5 files changed, 58 insertions(+), 26 deletions(-)

diff --git a/src/core/Emitter.js b/src/core/Emitter.js
index ff8438cc..db4c53eb 100644
--- a/src/core/Emitter.js
+++ b/src/core/Emitter.js
@@ -74,8 +74,9 @@ var Emitter = {
         });
     },
 
+
     emit: function(type, event) {
-        // Returns true if fired, false otherwise
+        // Returns true if any events were emitted, false otherwise.
         var handlers = this._callbacks && this._callbacks[type];
         if (!handlers)
             return false;
@@ -84,12 +85,19 @@ var Emitter = {
         // won't throw us off track here:
         handlers = handlers.slice();
         for (var i = 0, l = handlers.length; i < l; i++) {
-            // When the handler function returns false, prevent the default
-            // behavior and stop propagation of the event by calling stop()
-            if (handlers[i].apply(this, args) === false) {
+            var res = handlers[i].apply(this, args);
+            // Look at the handler's return value to decide how to propagate:
+            if (res === false) {
+                // If it returns false, prevent the default behavior and stop
+                // propagation of the event by calling stop()
                 if (event && event.stop)
                     event.stop();
+                // Stop propagation right now!
                 break;
+            } else if (res === true) {
+                // If it return true, remember that one handler wants to enforce
+                // the browser's default behavior. This is handled later.
+                event._enforced = true;
             }
         }
         return true;
diff --git a/src/dom/DomEvent.js b/src/dom/DomEvent.js
index b83d3310..8352e721 100644
--- a/src/dom/DomEvent.js
+++ b/src/dom/DomEvent.js
@@ -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) {
diff --git a/src/event/Event.js b/src/event/Event.js
index d450221e..68b13f57 100644
--- a/src/event/Event.js
+++ b/src/event/Event.js
@@ -26,6 +26,8 @@ var Event = Base.extend(/** @lends Event# */{
 
     prevented: false,
     stopped: false,
+    // Internal flag indicating whether the default shall be enforced.
+    _enforced: false,
 
     /**
      * Cancels the event if it is cancelable, without stopping further
diff --git a/src/tool/Tool.js b/src/tool/Tool.js
index 8a2ed36c..6b79061b 100644
--- a/src/tool/Tool.js
+++ b/src/tool/Tool.js
@@ -282,7 +282,9 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{
     /**
      * Private method to handle tool-events.
      *
-     * @return true if at least one event handler was called, false otherwise.
+     * @return {@true if the default event should be prevented}. This is if at
+     *     least one event handler was called and none of the called handlers
+     *     wants to enforce the default.
      */
     _handleEvent: function(type, event, point) {
         // Update global reference to this scope.
@@ -299,12 +301,13 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{
             // case it is shorter than maxDistance, as this would produce weird
             // results. matchMaxDistance controls this.
             matchMaxDistance = false,
-            called = false,
+            called = false, // Has at least one handler been called?
+            enforced = false, // Does a handler want to enforce the default?
             tool = this,
             mouse = {};
-            // Create a simple lookup object to quickly check for different
-            // mouse event types.
-            mouse[type.substr(5)] = true;
+        // Create a simple lookup object to quickly check for different
+        // mouse event types.
+        mouse[type.substr(5)] = true;
 
         function update(start, minDistance, maxDistance) {
             var toolPoint = tool._point,
@@ -349,8 +352,14 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{
         }
 
         function emit() {
-            called = tool.responds(type) &&
-                    tool.emit(type, new ToolEvent(tool, type, event)) || called;
+            if (tool.responds(type)) {
+                var toolEvent = new ToolEvent(tool, type, event);
+                if (tool.emit(type, toolEvent)) {
+                    called = true;
+                    if (toolEvent._enforced)
+                        enforced = true;
+                }
+            }
         }
 
         if (mouse.down) {
@@ -378,7 +387,7 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{
                 }
             }
         }
-        return called;
+        return called && !enforced;
     }
     /**
      * {@grouptitle Event Handling}
diff --git a/src/view/View.js b/src/view/View.js
index cff92879..fa90de33 100644
--- a/src/view/View.js
+++ b/src/view/View.js
@@ -733,10 +733,10 @@ new function() { // Injection scope for mouse events on the browser
         var eventType = type === 'mousemove' && mouseDown ? 'mousedrag' : type,
             project = paper.project,
             tool = view._scope.tool,
-            called = false;
+            prevent = false;
 
         function handle(obj) {
-            called = obj._handleEvent(eventType, event, point) || called;
+            prevent = obj._handleEvent(eventType, event, point) || prevent;
         }
 
         if (!point)
@@ -752,7 +752,7 @@ new function() { // Injection scope for mouse events on the browser
             handle(tool);
         // Prevent default if at least one mouse event handler was called, to
         // prevent scrolling on touch devices.
-        if (called)
+        if (prevent)
             event.preventDefault();
         // In the end we always call update(), which only updates the view if
         // anything has changed in the above calls.
@@ -869,7 +869,8 @@ new function() { // Injection scope for mouse events on the browser
      * with support for bubbling (event-propagation).
      */
 
-    var called = false, // Keep track of whether at least one handler was called
+    var called = false, // Has at least one handler been called?
+        enforced = false, // Does a handler want to enforce the default?
         // Event fallbacks for "virutal" events, e.g. if an item doesn't respond
         // to doubleclick, fall back to click:
         fallbacks = {
@@ -905,6 +906,8 @@ new function() { // Injection scope for mouse events on the browser
                 }
                 if (obj.emit(type, mouseEvent)) {
                     called = true;
+                    if (mouseEvent._enforced)
+                        enforced = true;
                     // Bail out if propagation is stopped
                     if (mouseEvent.stopped)
                         return true;
@@ -928,9 +931,10 @@ new function() { // Injection scope for mouse events on the browser
     // Returns true if event was stopped, false otherwise, whether handler was
     // called or not!
     function emitEvents(view, item, type, event, point, prevPoint) {
-        // Set called to false, so it will reflect if the following calls to
-        // emitEvent() have at least called one handler.
-        called = false;
+        // Set enforced and called to false, so it will reflect if the following
+        // calls to emitEvent() have called a handler, and if  at least one of
+        // the handlers wants to enforce default.
+        called = enforced = false;
         // First handle the drag-item and its parents, through bubbling.
         return (dragItem && emitEvent(dragItem, type, event, point,
                     prevPoint)
@@ -977,8 +981,9 @@ new function() { // Injection scope for mouse events on the browser
         /**
          * Private method to handle view and item events.
          *
-         * @return true if at least one event handler was called, false
-         *     otherwise.
+         * @return {@true if the default event should be prevented}. This is if
+         *     at least one event handler was called and none of the called
+         *     handlers wants to enforce the default.
          */
         _handleEvent: function(type, event, point) {
             // Run the hit-test first
@@ -1047,7 +1052,7 @@ new function() { // Injection scope for mouse events on the browser
                 }
             }
             lastPoint = point;
-            return called;
+            return called && !enforced;
         },
 
         _installEvent: function(type) {