Fix Issue 361: Moving the mouse over an iframe breaks range selector interaction
authorDan Vanderkam <danvk@google.com>
Thu, 16 Aug 2012 19:22:21 +0000 (15:22 -0400)
committerDan Vanderkam <danvk@google.com>
Thu, 16 Aug 2012 19:22:21 +0000 (15:22 -0400)
dygraph-range-selector.js
dygraph-utils.js
dygraph.js
tests/iframe.html [new file with mode: 0644]

index 0342ea7..e2973b0 100644 (file)
@@ -179,6 +179,10 @@ DygraphRangeSelector.prototype.initInteraction_ = function() {
   var isPanning = false;
   var dynamic = !this.isMobileDevice_ && !this.isUsingExcanvas_;
 
+  // We cover iframes during mouse interactions. See comments in
+  // dygraph-utils.js for more info on why this is a good idea.
+  var tarp = new Dygraph.IFrameTarp();
+
   // functions, defined below.  Defining them this way (rather than with
   // "function foo() {...}" makes JSHint happy.
   var toXDataWindow, onZoomStart, onZoom, onZoomEnd, doZoom, isMouseInPanZone,
@@ -212,6 +216,7 @@ DygraphRangeSelector.prototype.initInteraction_ = function() {
     self.dygraph_.addEvent(topElem, 'mousemove', onZoom);
     self.dygraph_.addEvent(topElem, 'mouseup', onZoomEnd);
     self.fgcanvas_.style.cursor = 'col-resize';
+    tarp.cover();
     return true;
   };
 
@@ -256,6 +261,7 @@ DygraphRangeSelector.prototype.initInteraction_ = function() {
       return false;
     }
     isZooming = false;
+    tarp.uncover();
     Dygraph.removeEvent(topElem, 'mousemove', onZoom);
     Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd);
     self.fgcanvas_.style.cursor = 'default';
index dd8c620..707bbca 100644 (file)
@@ -1015,3 +1015,68 @@ Dygraph.Circles = {
     ctx.stroke();
   }
 };
+
+/**
+ * To create a "drag" interaction, you typically register a mousedown event
+ * handler on the element where the drag begins. In that handler, you register a
+ * mouseup handler on the window to determine when the mouse is released,
+ * wherever that release happens. This works well, except when the user releases
+ * the mouse over an off-domain iframe. In that case, the mouseup event is
+ * handled by the iframe and never bubbles up to the window handler.
+ *
+ * To deal with this issue, we cover iframes with high z-index divs to make sure
+ * they don't capture mouseup.
+ *
+ * Usage:
+ * element.addEventListener('mousedown', function() {
+ *   var tarper = new Dygraph.IFrameTarp();
+ *   tarper.cover();
+ *   var mouseUpHandler = function() {
+ *     ...
+ *     window.removeEventListener(mouseUpHandler);
+ *     tarper.uncover();
+ *   };
+ *   window.addEventListener('mouseup', mouseUpHandler);
+ * };
+ * 
+ *
+ * @constructor
+ */
+Dygraph.IFrameTarp = function() {
+  this.tarps = [];
+};
+
+/**
+ * Find all the iframes in the document and cover them with high z-index
+ * transparent divs.
+ */
+Dygraph.IFrameTarp.prototype.cover = function() {
+  var iframes = document.getElementsByTagName("iframe");
+  for (var i = 0; i < iframes.length; i++) {
+    var iframe = iframes[i];
+    var x = Dygraph.findPosX(iframe),
+        y = Dygraph.findPosY(iframe),
+        width = iframe.offsetWidth,
+        height = iframe.offsetHeight;
+
+    var div = document.createElement("div");
+    div.style.position = "absolute";
+    div.style.left = x + 'px';
+    div.style.top = y + 'px';
+    div.style.width = width + 'px';
+    div.style.height = height + 'px';
+    div.style.zIndex = 999;
+    document.body.appendChild(div);
+    this.tarps.push(div);
+  }
+};
+
+/**
+ * Remove all the iframe covers. You should call this in a mouseup handler.
+ */
+Dygraph.IFrameTarp.prototype.uncover = function() {
+  for (var i = 0; i < this.tarps.length; i++) {
+    this.tarps[i].parentNode.removeChild(this.tarps[i]);
+  }
+  this.tarps = [];
+};
index 64ca4b6..c1764a1 100644 (file)
@@ -1247,6 +1247,10 @@ Dygraph.prototype.createDragInterface_ = function() {
     boundedDates: null, // [minDate, maxDate]
     boundedValues: null, // [[minValue, maxValue] ...]
 
+    // We cover iframes during mouse interactions. See comments in
+    // dygraph-utils.js for more info on why this is a good idea.
+    tarp: new Dygraph.IFrameTarp(),
+
     // contextB is the same thing as this context object but renamed.
     initializeMouseDown: function(event, g, contextB) {
       // prevents mouse drags from selecting page text.
@@ -1262,6 +1266,7 @@ Dygraph.prototype.createDragInterface_ = function() {
       contextB.dragStartX = g.dragGetX_(event, contextB);
       contextB.dragStartY = g.dragGetY_(event, contextB);
       contextB.cancelNextDblclick = false;
+      contextB.tarp.cover();
     }
   };
 
@@ -1301,6 +1306,8 @@ Dygraph.prototype.createDragInterface_ = function() {
         delete self.axes_[i].dragValueRange;
       }
     }
+
+    context.tarp.uncover();
   };
 
   this.addEvent(window, 'mouseup', this.mouseUpHandler_);
diff --git a/tests/iframe.html b/tests/iframe.html
new file mode 100644 (file)
index 0000000..7dbcf6f
--- /dev/null
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>demo</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../dygraph-dev.js"></script>
+
+    <style type="text/css">
+      .chart {
+        position: absolute;
+        left: 10px;
+        width: 500px;
+        height: 250px;
+        border: 1px solid black;
+      }
+      #chart1 {
+        top: 10px;
+      }
+      #chart2 {
+        top: 270px;
+      }
+      #iframe {
+        border: 1px solid black;
+        position: absolute;
+        left: 520px;
+      }
+      p {
+        position: absolute;
+        top: 550px;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="chart" id="chart1"></div>
+    <div class="chart" id="chart2"></div>
+    <iframe id="iframe" width=400 height=520 src="http://en.wikipedia.org/">iframe here</iframe>
+
+    <p>Click on various places in the chart, then drag the mouse over the iframe
+    and release it. When you mouse back over the charts, strange things will
+    happen.</p>
+
+    <script type="text/javascript">
+      var data = function() {
+        var zp = function(x) { if (x < 10) return "0"+x; else return x; };
+        var r = "date,parabola,line,another line,sine wave\n";
+        for (var i=1; i<=31; i++) {
+        r += "200610" + zp(i);
+        r += "," + 10*(i*(31-i));
+        r += "," + 10*(8*i);
+        r += "," + 10*(250 - 8*i);
+        r += "," + 10*(125 + 125 * Math.sin(0.3*i));
+        r += "\n";
+        }
+        return r;
+      };
+
+      g1 = new Dygraph(
+              document.getElementById("chart1"), data,
+              { }
+          );
+      g2 = new Dygraph(
+              document.getElementById("chart2"), data,
+              {
+                showRangeSelector: true
+              }
+          );
+    </script>
+</body>
+</html>