From ef3c0576656e3d01c24df0b3f68fe41631147c98 Mon Sep 17 00:00:00 2001
From: Shane Tomlinson <stomlinson@mozilla.com>
Date: Mon, 6 Aug 2012 14:17:47 +0100
Subject: [PATCH] Make sure only one is ever shown at a time.  Some further
 cleanup because tooltip code was fugly.

---
 resources/static/common/js/tooltip.js         | 114 +++++++++---------
 .../static/test/cases/common/js/tooltip.js    |  40 +++---
 2 files changed, 75 insertions(+), 79 deletions(-)

diff --git a/resources/static/common/js/tooltip.js b/resources/static/common/js/tooltip.js
index e50d8bc89..673c64dc6 100644
--- a/resources/static/common/js/tooltip.js
+++ b/resources/static/common/js/tooltip.js
@@ -8,24 +8,23 @@ BrowserID.Tooltip = (function() {
   "use strict";
 
   var ANIMATION_TIME = 250,
-      TOOLTIP_DISPLAY = 2000,
+      TOOLTIP_MIN_DISPLAY = 2000,
       READ_WPM = 200,
       bid = BrowserID,
-      dom = bid.DOM,
       renderer = bid.Renderer,
-      hideTimer;
+      hideTimer,
+      tooltip;
 
   function createTooltip(el) {
-      var contents = el.html();
-
-      var tooltip = renderer.append("body", "tooltip", {
-        contents: contents
-      });
+    tooltip = renderer.append("body", "tooltip", {
+      contents: el.html()
+    });
 
-      return tooltip;
+    return tooltip;
   }
 
-  function positionTooltip(tooltip, target) {
+  function anchorTooltip(target) {
+    target = $(target);
     var targetOffset = target.offset();
     targetOffset.top -= (tooltip.outerHeight() + 5);
     targetOffset.left += 10;
@@ -33,56 +32,69 @@ BrowserID.Tooltip = (function() {
     tooltip.css(targetOffset);
   }
 
-  function animateTooltip(el, complete) {
-    var contents = el.text().replace(/\s+/, ' ').replace(/^\s+/, '').replace(/\s+$/, '');
-    var words = contents.split(' ').length;
+  function calculateDisplayTime(text) {
+    // Calculate the amount of time a tooltip should display based on the
+    // number of words in the content divided by the number of words an average
+    // person can read per minute.
+    var contents = text.replace(/\s+/, ' ').trim(),
+        words = contents.split(' ').length,
+        // The average person can read ± 250 wpm.
+        wordTimeMS = (words / READ_WPM) * 60 * 1000,
+        displayTimeMS = Math.max(wordTimeMS, TOOLTIP_MIN_DISPLAY);
+
+        return displayTimeMS;
+  }
 
-    // The average person can read ± 250 wpm.
-    var wordTimeMS = (words / READ_WPM) * 60 * 1000;
-    var displayTimeMS = Math.max(wordTimeMS, TOOLTIP_DISPLAY);
+  function animateTooltip(el, complete) {
+    var displayTimeMS = calculateDisplayTime(el.text());
 
     bid.Tooltip.shown = true;
     el.fadeIn(ANIMATION_TIME, function() {
       hideTimer = setTimeout(function() {
-        el.fadeOut(ANIMATION_TIME, function() {
-          bid.Tooltip.shown = false;
-          if(complete) complete();
-        });
+        el.fadeOut(ANIMATION_TIME, complete);
       }, displayTimeMS);
     });
-  }
 
-  function createAndShowRelatedTooltip(el, relatedTo, complete) {
-      // This means create a copy of the tooltip element and position it in
-      // relation to an element.  Right now we are putting the tooltip directly
-      // above the element.  Once the tooltip is no longer needed, remove it
-      // from the DOM.
-      var tooltip = createTooltip(el);
-
-      var target = $("#" + relatedTo);
-      positionTooltip(tooltip, target);
-
-      animateTooltip(tooltip, function() {
-        if (tooltip) {
-          tooltip.remove();
-          tooltip = null;
-        }
-        if (complete) complete();
-      });
+    return displayTimeMS;
   }
 
   function showTooltip(el, complete) {
-    el = $(el);
-    var messageFor = el.attr("for");
+    // Only one tooltip can be shown at a time, see issue #1615
+    removeTooltips();
+
+    // By default, the element passed in is the tooltip element.  If it has
+    // a "for" attribute, that means this tooltip should be anchored to the
+    // element listed in the "for" attribute. If that is the case, create a new
+    // tooltip and anchor it to the other element.
+    var tooltipEl = $(el),
+        tooltipAnchor = tooltipEl.attr("for");
+
+    if (tooltipAnchor) {
+      // The tooltip should be anchored to another element.  Place the tooltip
+      // directly above the element and remove it when it is no longer needed.
+      tooltipEl = createTooltip(tooltipEl);
+      anchorTooltip("#" + tooltipAnchor);
+    }
+
+    return animateTooltip(tooltipEl, function() {
+      removeTooltips();
+      complete && complete();
+    });
+  }
 
-    // First, see if we are "for" another element, if we are, create a copy of
-    // the tooltip to attach to the element.
-    if(messageFor) {
-      createAndShowRelatedTooltip(el, messageFor, complete);
+  function removeTooltips() {
+    if (tooltip) {
+      tooltip.remove();
+      tooltip = null;
     }
-    else {
-      animateTooltip(el, complete);
+
+    if (hideTimer) {
+      clearTimeout(hideTimer);
+      hideTimer = null;
     }
+
+    $('.tooltip').hide();
+    bid.Tooltip.shown = false;
   }
 
 
@@ -90,15 +102,7 @@ BrowserID.Tooltip = (function() {
    showTooltip: showTooltip
    // BEGIN TESTING API
    ,
-   reset: function() {
-     if(hideTimer) {
-       clearTimeout(hideTimer);
-       hideTimer = null;
-     }
-
-     $(".tooltip").hide();
-     bid.Tooltip.shown = false;
-   }
+   reset: removeTooltips
    // END TESTING API
  };
 
diff --git a/resources/static/test/cases/common/js/tooltip.js b/resources/static/test/cases/common/js/tooltip.js
index e084eb511..81df7ad02 100644
--- a/resources/static/test/cases/common/js/tooltip.js
+++ b/resources/static/test/cases/common/js/tooltip.js
@@ -7,42 +7,28 @@
   "use strict";
 
   var bid = BrowserID,
-      tooltip = bid.Tooltip
+      tooltip = bid.Tooltip,
+      testHelpers = bid.TestHelpers;
 
   module("common/js/tooltip", {
     setup: function() {
+      testHelpers.setup();
     },
     teardown: function() {
+      testHelpers.teardown();
     }
   });
 
 
-  asyncTest("show short tooltip, min of 2.5 seconds", function() {
-    var startTime = new Date().getTime();
-
-    tooltip.showTooltip("#shortTooltip", function() {
-      var endTime = new Date().getTime();
-      var diff = endTime - startTime;
-      ok(2000 <= diff && diff <= 3000, diff + " - minimum of 2 seconds, max of 3 seconds");
-
-      equal(tooltip.shown, false, "tooltip says it is no longer shown");
-
-      start();
-    });
-
+  test("show short tooltip - shows for about 2.5 seconds", function() {
+    var displayTime = tooltip.showTooltip("#shortTooltip");
+    ok(2000 <= displayTime && displayTime <= 3000, displayTime + " - minimum of 2 seconds, max of 3 seconds");
     equal(tooltip.shown, true, "tooltip says that it is shown");
   });
 
-  asyncTest("show long tooltip, takes about 5 seconds", function() {
-    var startTime = new Date().getTime();
-
-    tooltip.showTooltip("#longTooltip", function() {
-      var endTime = new Date().getTime();
-      var diff = endTime - startTime;
-      ok(diff >= 4500, diff + " - longer tooltip is on the screen for a bit longer");
-
-      start();
-    });
+  test("show long tooltip - shows for about 5 seconds", function() {
+    var displayTime = tooltip.showTooltip("#longTooltip");
+    ok(displayTime >= 4500, displayTime + " - longer tooltip is on the screen for a bit longer");
   });
 
   asyncTest("show tooltip, then reset - hides tooltip, resets shown status", function() {
@@ -56,4 +42,10 @@
     }, 100);
   });
 
+  test("only one tooltip shown at a time", function() {
+    tooltip.showTooltip("#shortTooltip");
+    tooltip.showTooltip("#shortTooltip");
+    equal($(".tooltip:visible").length, 1, "only one tooltip shown at a time");
+  });
+
 }());
-- 
GitLab