From 8534e12523c7156b53e76393cc8d016ffdcf5b4f Mon Sep 17 00:00:00 2001 From: Ben Ogle <ogle.ben@gmail.com> Date: Mon, 20 Jan 2014 11:54:22 -0800 Subject: [PATCH] Add tooltip `viewport` option, respect bounds of the viewport --- docs/_includes/js/tooltips.html | 8 +++ examples/tooltips/viewport.html | 103 ++++++++++++++++++++++++++++++++ js/tests/unit/tooltip.js | 81 ++++++++++++++++++++++++- js/tooltip.js | 102 +++++++++++++++++++------------ 4 files changed, 253 insertions(+), 41 deletions(-) create mode 100644 examples/tooltips/viewport.html diff --git a/docs/_includes/js/tooltips.html b/docs/_includes/js/tooltips.html index 2656556bed..b952e85040 100644 --- a/docs/_includes/js/tooltips.html +++ b/docs/_includes/js/tooltips.html @@ -134,6 +134,14 @@ $('#example').tooltip(options) <p>Appends the tooltip to a specific element. Example: <code>container: 'body'</code></p> </td> </tr> + <tr> + <td>viewport</td> + <td>string | object</td> + <td>{ selector: 'body', padding: 0 }</td> + <td> + <p>Keeps the tooltip within the bounds of this element. Example: <code>viewport: '#viewport'</code> or <code>{ selector: '#viewport', padding: 0 }</code></p> + </td> + </tr> </tbody> </table> </div><!-- /.table-responsive --> diff --git a/examples/tooltips/viewport.html b/examples/tooltips/viewport.html new file mode 100644 index 0000000000..6efcd0495c --- /dev/null +++ b/examples/tooltips/viewport.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="description" content=""> + <meta name="author" content=""> + <link rel="shortcut icon" href="../../docs-assets/ico/favicon.png"> + + <title>Tooltip Viewport Example for Bootstrap</title> + + <!-- Bootstrap core CSS --> + <link href="../../dist/css/bootstrap.css" rel="stylesheet"> + + <!-- Just for debugging purposes. Don't actually copy this line! --> + <!--[if lt IE 9]><script src="../../docs-assets/js/ie8-responsive-file-warning.js"></script><![endif]--> + + <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> + <![endif]--> + + <style> + body { + height: 1200px; + } + .tooltip { + min-width: 250px; + max-width: 500px; + } + .tooltip .tooltip-inner { + min-height: 200px; + min-width: 250px; + max-width: 500px; + } + .placeholder { + height: 900px; + } + .container-viewport { + position: absolute; + left: 200px; + top: 600px; + width: 600px; + height: 400px; + background: #c00; + } + .btn-bottom { + position: absolute; + left: 0; + bottom: 0; + } + </style> + </head> + + <body> + + <button class="btn pull-right tooltip-bottom", title="This should be shifted to the left">Shift Left</button> + <button class="btn tooltip-bottom", title="This should be shifted to the right">Shift Right</button> + <button class="btn tooltip-right", title="This should be shifted down">Shift Down</button> + + <div class="placeholder">There is a button down there ↓</div> + + <button class="btn tooltip-right", title="This should be shifted up">Shift Up</button> + + <div class="container-viewport"> + <button class="btn tooltip-viewport-bottom", title="This should be shifted Left">Shift Left</button> + <button class="btn tooltip-viewport-right", title="This should be shifted Down">Shift Down</button> + + <button class="btn pull-right tooltip-viewport-bottom", title="This should be shifted Right">Shift Right</button> + + <button class="btn tooltip-viewport-right btn-bottom", title="This should be shifted up">Shift Up</button> + </div> + + <!-- Bootstrap core JavaScript + ================================================== --> + <!-- Placed at the end of the document so the pages load faster --> + <script src="https://code.jquery.com/jquery-1.10.2.min.js"></script> + <script src="../../js/tooltip.js"></script> + + <script> + $(function(){ + $('.tooltip-right').tooltip({ + placement: 'right', + viewport: {selector: 'body', padding: 2} + }); + $('.tooltip-bottom').tooltip({ + placement: 'bottom', + viewport: {selector: 'body', padding: 2} + }); + $('.tooltip-viewport-right').tooltip({ + placement: 'right', + viewport: {selector: '.container-viewport', padding: 2} + }); + $('.tooltip-viewport-bottom').tooltip({ + placement: 'bottom', + viewport: {selector: '.container-viewport', padding: 2} + }); + }); + </script> + </body> +</html> diff --git a/js/tests/unit/tooltip.js b/js/tests/unit/tooltip.js index 9df234236c..e670883e35 100644 --- a/js/tests/unit/tooltip.js +++ b/js/tests/unit/tooltip.js @@ -337,12 +337,12 @@ $(function () { }) test('should add position class before positioning so that position-specific styles are taken into account', function () { - $('head').append('<style> .tooltip.right { white-space: nowrap; } .tooltip.right .tooltip-inner { max-width: none; } </style>') + $('head').append('<style id="test"> .tooltip.right { white-space: nowrap; } .tooltip.right .tooltip-inner { max-width: none; } </style>') var container = $('<div />').appendTo('body'), target = $('<a href="#" rel="tooltip" title="very very very very very very very very long tooltip in one line"></a>') .appendTo(container) - .tooltip({placement: 'right'}) + .tooltip({placement: 'right', viewport: null}) .tooltip('show'), tooltip = container.find('.tooltip') @@ -352,6 +352,7 @@ $(function () { var topDiff = top - top2 ok(topDiff <= 1 && topDiff >= -1) target.tooltip('hide') + $('head #test').remove() }) test('tooltip title test #1', function () { @@ -428,4 +429,80 @@ $(function () { ttContainer.remove() }) + test('should adjust the tip\'s top when up against the top of the viewport', function () { + $('head').append('<style id="test"> .tooltip .tooltip-inner { width: 200px; height: 200px; max-width: none; } </style>') + + var container = $('<div />').appendTo('body'), + target = $('<a href="#" rel="tooltip" title="tip" style="position: fixed; top: 0px; left: 0px;"></a>') + .appendTo(container) + .tooltip({placement: 'right', viewport: {selector: 'body', padding: 12}}) + .tooltip('show'), + tooltip = container.find('.tooltip') + + ok( Math.round(tooltip.offset().top) === 12 ) + target.tooltip('hide') + $('head #test').remove() + }) + + test('should adjust the tip\'s top when up against the bottom of the viewport', function () { + $('head').append('<style id="test"> .tooltip .tooltip-inner { width: 200px; height: 200px; max-width: none; } </style>') + + var container = $('<div />').appendTo('body'), + target = $('<a href="#" rel="tooltip" title="tip" style="position: fixed; bottom: 0px; left: 0px;"></a>') + .appendTo(container) + .tooltip({placement: 'right', viewport: {selector: 'body', padding: 12}}) + .tooltip('show'), + tooltip = container.find('.tooltip') + + ok( Math.round(tooltip.offset().top) === Math.round($(window).height() - 12 - tooltip[0].offsetHeight) ) + target.tooltip('hide') + $('head #test').remove() + }) + + test('should adjust the tip\'s left when up against the left of the viewport', function () { + $('head').append('<style id="test"> .tooltip .tooltip-inner { width: 200px; height: 200px; max-width: none; } </style>') + + var container = $('<div />').appendTo('body'), + target = $('<a href="#" rel="tooltip" title="tip" style="position: fixed; top: 0px; left: 0px;"></a>') + .appendTo(container) + .tooltip({placement: 'bottom', viewport: {selector: 'body', padding: 12}}) + .tooltip('show'), + tooltip = container.find('.tooltip') + + ok( Math.round(tooltip.offset().left) === 12 ) + target.tooltip('hide') + $('head #test').remove() + }) + + test('should adjust the tip\'s left when up against the right of the viewport', function () { + $('head').append('<style id="test"> .tooltip .tooltip-inner { width: 200px; height: 200px; max-width: none; } </style>') + + var container = $('<div />').appendTo('body'), + target = $('<a href="#" rel="tooltip" title="tip" style="position: fixed; top: 0px; right: 0px;"></a>') + .appendTo(container) + .tooltip({placement: 'bottom', viewport: {selector: 'body', padding: 12}}) + .tooltip('show'), + tooltip = container.find('.tooltip') + + ok( Math.round(tooltip.offset().left) === Math.round($(window).width() - 12 - tooltip[0].offsetWidth) ) + target.tooltip('hide') + $('head #test').remove() + }) + + test('should adjust the tip when up against the right of an arbitrary viewport', function () { + $('head').append('<style id="test"> .tooltip, .tooltip .tooltip-inner { width: 200px; height: 200px; max-width: none; } </style>') + $('head').append('<style id="viewport-style"> .container-viewport { position: absolute; top: 50px; left: 60px; width: 300px; height: 300px; } </style>') + + var container = $('<div />', {class: 'container-viewport'}).appendTo('body'), + target = $('<a href="#" rel="tooltip" title="tip" style="position: fixed; top: 50px; left: 350px;"></a>') + .appendTo(container) + .tooltip({placement: 'bottom', viewport: '.container-viewport'}) + .tooltip('show'), + tooltip = container.find('.tooltip') + + ok( Math.round(tooltip.offset().left) === Math.round(60 + container.width() - tooltip[0].offsetWidth) ) + target.tooltip('hide') + $('head #test').remove() + $('head #viewport-style').remove() + }) }) diff --git a/js/tooltip.js b/js/tooltip.js index eb7875c9fe..f27beacc6d 100644 --- a/js/tooltip.js +++ b/js/tooltip.js @@ -34,14 +34,19 @@ title: '', delay: 0, html: false, - container: false + container: false, + viewport: { + selector: 'body', + padding: 0 + } } Tooltip.prototype.init = function (type, element, options) { - this.enabled = true - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) + this.enabled = true + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport) var triggers = this.options.trigger.split(' ') @@ -157,18 +162,14 @@ var actualHeight = $tip[0].offsetHeight if (autoPlace) { - var $parent = this.$element.parent() - var orgPlacement = placement - var docScroll = document.documentElement.scrollTop - var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth() - var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight() - var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left - - placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' : - placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' : - placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' : - placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' : + var $parent = this.$element.parent() + var parentDim = this.getPosition($parent) + + placement = placement == 'bottom' && pos.top + pos.height + actualHeight - parentDim.scroll > parentDim.height ? 'top' : + placement == 'top' && pos.top - parentDim.scroll - actualHeight < 0 ? 'bottom' : + placement == 'right' && pos.right + actualWidth > parentDim.width ? 'left' : + placement == 'left' && pos.left - actualWidth < parentDim.left ? 'right' : placement $tip @@ -228,29 +229,20 @@ var actualHeight = $tip[0].offsetHeight if (placement == 'top' && actualHeight != height) { - replace = true offset.top = offset.top + height - actualHeight } - if (/bottom|top/.test(placement)) { - var delta = 0 + var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) - if (offset.left < 0) { - delta = offset.left * -2 - offset.left = 0 + if (delta.left) offset.left += delta.left + else offset.top += delta.top - $tip.offset(offset) - - actualWidth = $tip[0].offsetWidth - actualHeight = $tip[0].offsetHeight - } + var arrowDelta = delta.left ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight + var arrowPosition = delta.left ? 'left' : 'top' + var arrowOffsetPosition = delta.left ? 'offsetWidth' : 'offsetHeight' - this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') - } else { - this.replaceArrow(actualHeight - height, actualHeight, 'top') - } - - if (replace) $tip.offset(offset) + $tip.offset(offset) + this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], arrowPosition) } Tooltip.prototype.replaceArrow = function (delta, dimension, position) { @@ -303,12 +295,15 @@ return this.getTitle() } - Tooltip.prototype.getPosition = function () { - var el = this.$element[0] - return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { - width: el.offsetWidth, - height: el.offsetHeight - }, this.$element.offset()) + Tooltip.prototype.getPosition = function ($element) { + $element = $element || this.$element + var el = $element[0] + var isBody = el.tagName == 'BODY' + return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : null, { + scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop(), + width: isBody ? $(window).width() : $element.outerWidth(), + height: isBody ? $(window).height() : $element.outerHeight() + }, isBody ? {top: 0, left: 0} : $element.offset()) } Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { @@ -316,6 +311,35 @@ placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } + + } + + Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { + var delta = { top: 0, left: 0 } + if (!this.$viewport) return delta + + var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 + var viewportDimensions = this.getPosition(this.$viewport) + + if (/right|left/.test(placement)) { + var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll + var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight + if (topEdgeOffset < viewportDimensions.top) { // top overflow + delta.top = viewportDimensions.top - topEdgeOffset + } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow + delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset + } + } else { + var leftEdgeOffset = pos.left - viewportPadding + var rightEdgeOffset = pos.left + viewportPadding + actualWidth + if (leftEdgeOffset < viewportDimensions.left) { // left overflow + delta.left = viewportDimensions.left - leftEdgeOffset + } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow + delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset + } + } + + return delta } Tooltip.prototype.getTitle = function () { -- GitLab