From 237a1711db5278a2ec74f6d6381bd9fe0c46216b Mon Sep 17 00:00:00 2001
From: Alex Kalderimis <alexkalderimis@gmail.com>
Date: Sat, 16 Mar 2013 20:50:05 +0000
Subject: [PATCH 1/2] Make title and content functions cachable.

This commit introduces an opt-in flag for tooltips and popovers which
allows the widgets to cache the return values from the `title` and
`content` functions, where they have been provided. This is useful for
when the title and content need computation, but are not going to
change. Obviously the functions themselves can be memoised, but this
approach also avoids the cost of repeated (and pointless) function
calls.
---
 js/bootstrap-popover.js            | 13 +++++++++++
 js/bootstrap-tooltip.js            |  8 +++++++
 js/tests/unit/bootstrap-popover.js | 37 +++++++++++++++++++++++++++++-
 js/tests/unit/bootstrap-tooltip.js | 36 +++++++++++++++++++++++++++++
 4 files changed, 93 insertions(+), 1 deletion(-)

diff --git a/js/bootstrap-popover.js b/js/bootstrap-popover.js
index 6ebbab1e61..e42375d918 100644
--- a/js/bootstrap-popover.js
+++ b/js/bootstrap-popover.js
@@ -38,6 +38,17 @@
 
     constructor: Popover
 
+  , getOptions: function(options) {
+      var cachable = (options && options.cachable)
+        , ret = $.fn.tooltip.Constructor.prototype.getOptions.call(this, options)
+
+      if (cachable !== null && typeof cachable === 'boolean') {
+        ret.cachable.content = cachable
+      }
+      
+      return ret
+    }
+
   , setContent: function () {
       var $tip = this.tip()
         , title = this.getTitle()
@@ -61,6 +72,8 @@
       content = (typeof o.content == 'function' ? o.content.call($e[0]) :  o.content)
         || $e.attr('data-content')
 
+      o.cachable.content && (o.content = content)
+
       return content
     }
 
diff --git a/js/bootstrap-tooltip.js b/js/bootstrap-tooltip.js
index 03a65e7e1d..205daefe59 100644
--- a/js/bootstrap-tooltip.js
+++ b/js/bootstrap-tooltip.js
@@ -75,6 +75,11 @@
         , hide: options.delay
         }
       }
+      if (typeof options.cachable === 'boolean') {
+        options.cachable = {
+          title: options.cachable
+        }
+      }
 
       return options
     }
@@ -277,6 +282,8 @@
       title = $e.attr('data-original-title')
         || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)
 
+      o.cachable.title && (o.title = title)
+
       return title
     }
 
@@ -347,6 +354,7 @@
   , delay: 0
   , html: false
   , container: false
+  , cachable: false
   }
 
 
diff --git a/js/tests/unit/bootstrap-popover.js b/js/tests/unit/bootstrap-popover.js
index 20234e1476..f3cf387c91 100644
--- a/js/tests/unit/bootstrap-popover.js
+++ b/js/tests/unit/bootstrap-popover.js
@@ -110,4 +110,39 @@ $(function () {
         ok(!$._data(popover[0], 'events').mouseover && !$._data(popover[0], 'events').mouseout, 'popover does not have any events')
       })
 
-})
\ No newline at end of file
+      test("Should get content from function where supplied", function() {
+        var opts = {content: function () { return 'generated content' }}
+          , a = $('<a href="#"></a>').popover(opts)
+        
+        a.appendTo('#qunit-fixture').popover('show')
+
+        equal($('#qunit-fixture .popover-content').text(), 'generated content');
+      })
+
+      test("Should get new content each time shown", function() {
+        var n = 0
+          , opts = {content: function () { return 'shown ' + (++n) + ' times' }}
+          , a = $('<a href="#"></a>').popover(opts)
+        
+        a.appendTo('#qunit-fixture')
+         .popover('show').popover('hide').popover('show')
+
+        notEqual($('#qunit-fixture .popover-content').text(), 'shown 1 times');
+        equal($('#qunit-fixture .popover-content').text(), 'shown ' + n + ' times');
+      })
+
+      test("Should cache titles when asked", function() {
+        var n = 0
+          , opts = {
+            cachable: true
+            , content: function () { return 'shown ' + (++n) + ' times' }
+          }
+          , a = $('<a href="#"></a>').popover(opts)
+        
+        a.appendTo('#qunit-fixture')
+         .popover('show').popover('hide').popover('show')
+
+        equal($('#qunit-fixture .popover-content').text(), 'shown 1 times');
+      })
+
+})
diff --git a/js/tests/unit/bootstrap-tooltip.js b/js/tests/unit/bootstrap-tooltip.js
index 5b37b4e687..2d0d9fa78d 100644
--- a/js/tests/unit/bootstrap-tooltip.js
+++ b/js/tests/unit/bootstrap-tooltip.js
@@ -291,4 +291,40 @@ $(function () {
           container.remove()
         }, 100)
       })
+
+      test("Should get title from function where supplied", function() {
+        var opts = {title: function () { return 'generated title' }}
+          , tooltip = $('<a href="#" rel="tooltip" ></a>').tooltip(opts)
+        
+        tooltip.appendTo('#qunit-fixture').tooltip('show')
+
+        equal($('#qunit-fixture .tooltip-inner').text(), 'generated title');
+      })
+
+      test("Should get a new title each time shown", function() {
+        var n = 0
+          , opts = {title: function () { return 'shown ' + (++n) + ' times' }}
+          , tooltip = $('<a href="#" rel="tooltip" ></a>').tooltip(opts)
+        
+        tooltip.appendTo('#qunit-fixture')
+               .tooltip('show').tooltip('hide').tooltip('show')
+
+        notEqual($('#qunit-fixture .tooltip-inner').text(), 'shown 1 times');
+        equal($('#qunit-fixture .tooltip-inner').text(), 'shown ' + n + ' times');
+      })
+
+      test("Should cache titles when asked", function() {
+        var n = 0
+          , opts = {
+            cachable: true
+            , title: function () { return 'shown ' + (++n) + ' times' }
+          }
+          , tooltip = $('<a href="#" rel="tooltip" ></a>').tooltip(opts)
+        
+        tooltip.appendTo('#qunit-fixture')
+               .tooltip('show').tooltip('hide').tooltip('show')
+
+        equal($('#qunit-fixture .tooltip-inner').text(), 'shown 1 times');
+      })
+
 })
-- 
GitLab


From 10c3b15833bf4e2036616142ed37a7b3a8aba8d6 Mon Sep 17 00:00:00 2001
From: Alex Kalderimis <alexkalderimis@gmail.com>
Date: Sat, 16 Mar 2013 22:00:08 +0000
Subject: [PATCH 2/2] Added repositioning mechanism for dynamic content.

Tooltip and popover content may have cause to grow dynamically, and its
final form cannot always be determined synchronously. An important use
case for this is popover elements that fetch their content via ajax.
This commit adds a mechanism for such dynamic elements to be reflown
when their content changes. This must still be managed manually by the
caller, but at least it is now possible without external modification. A
typical system for doing this is expected to be calling the `reposition`
method on the tooltip/popover when the content is changed and its
position needs to be recalculated.
---
 js/bootstrap-tooltip.js            | 69 +++++++++++++++++-------------
 js/tests/unit/bootstrap-tooltip.js | 35 +++++++++++++++
 2 files changed, 75 insertions(+), 29 deletions(-)

diff --git a/js/bootstrap-tooltip.js b/js/bootstrap-tooltip.js
index 205daefe59..8a5705eccb 100644
--- a/js/bootstrap-tooltip.js
+++ b/js/bootstrap-tooltip.js
@@ -116,13 +116,46 @@
       }, self.options.delay.hide)
     }
 
-  , show: function () {
-      var $tip
-        , pos
+  , getPlacement: function () {
+      var $tip = this.tip()
+
+      return typeof this.options.placement == 'function' ?
+          this.options.placement.call(this, $tip[0], this.$element[0]) :
+          this.options.placement
+    }
+
+  , reposition: function ($tip) {
+      var pos  = this.getPosition()
+        , placement = this.getPlacement()
         , actualWidth
         , actualHeight
-        , placement
-        , tp
+        , o
+
+      $tip || ($tip = this.tip())
+      actualWidth = $tip[0].offsetWidth
+      actualHeight = $tip[0].offsetHeight
+
+      switch (placement) {
+        case 'bottom':
+          o = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
+          break
+        case 'top':
+          o = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
+          break
+        case 'left':
+          o = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
+          break
+        case 'right':
+          o = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
+          break
+      }
+
+      this.applyPlacement(o, placement)
+      this.$element.trigger('positioned')
+    }
+
+  , show: function () {
+      var $tip
         , e = $.Event('show')
 
       if (this.hasContent() && this.enabled) {
@@ -135,37 +168,14 @@
           $tip.addClass('fade')
         }
 
-        placement = typeof this.options.placement == 'function' ?
-          this.options.placement.call(this, $tip[0], this.$element[0]) :
-          this.options.placement
-
         $tip
           .detach()
           .css({ top: 0, left: 0, display: 'block' })
 
         this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
 
-        pos = this.getPosition()
-
-        actualWidth = $tip[0].offsetWidth
-        actualHeight = $tip[0].offsetHeight
-
-        switch (placement) {
-          case 'bottom':
-            tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
-            break
-          case 'top':
-            tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
-            break
-          case 'left':
-            tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
-            break
-          case 'right':
-            tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
-            break
-        }
+        this.reposition($tip)
 
-        this.applyPlacement(tp, placement)
         this.$element.trigger('shown')
       }
     }
@@ -181,6 +191,7 @@
 
       $tip
         .offset(offset)
+        .removeClass('top right bottom left')
         .addClass(placement)
         .addClass('in')
 
diff --git a/js/tests/unit/bootstrap-tooltip.js b/js/tests/unit/bootstrap-tooltip.js
index 2d0d9fa78d..56ea02d728 100644
--- a/js/tests/unit/bootstrap-tooltip.js
+++ b/js/tests/unit/bootstrap-tooltip.js
@@ -327,4 +327,39 @@ $(function () {
         equal($('#qunit-fixture .tooltip-inner').text(), 'shown 1 times');
       })
 
+      test("Should expose a repositioning mechanism for dynamic content", function () {
+        var css = {position: "absolute", bottom: 0, left: 0, textAlign: "right", width: 300, height: 300}
+          , $container = $("<div />").appendTo("body").css(css)
+          , $p = $("<p style='margin-top:200px;margin-left:200px' />").appendTo($container)
+          , $title = $('<span>Some text</span>')
+          , opts = {
+            trigger: 'manual'
+            , html: true
+            , placement: 'left'
+            , animate: false
+            , title: $title
+            }
+          , $a = $('<a href="#" rel="tooltip">has tip</a>').appendTo($p).tooltip(opts).tooltip('show')
+
+        stop()
+
+        setTimeout(function() {
+          var tooltip = $container.find(".tooltip")
+            , offsetA = tooltip.offset()
+
+          $title.append('A whole lot more text than was there previously')
+          $a.tooltip('reposition')
+
+          setTimeout(function() {
+            var tooltip = $container.find(".tooltip")
+              , offsetB = tooltip.offset()
+
+            start()
+            notEqual(offsetA.top, offsetB.pop)
+            $container.remove()
+          }, 0)
+
+        }, 0)
+      })
+
 })
-- 
GitLab