How can you ensure twitter bootstrap popover windows are visible?

11,795

Solution 1

The placement can be a function instead of a string. An example of auto placement written by fat and then ported to the most recent version of bootstrap by wleeper is in one of the github issues on the project here: https://github.com/twitter/bootstrap/issues/345

Here is the result of compiling the CoffeeScript to JavaScript:

$("a[rel=popover]").popover({
  placement: function(tip, element) {
    var $element, above, actualHeight, actualWidth, below, boundBottom, boundLeft, boundRight, boundTop, elementAbove, elementBelow, elementLeft, elementRight, isWithinBounds, left, pos, right;
    isWithinBounds = function(elementPosition) {
      return boundTop < elementPosition.top && boundLeft < elementPosition.left && boundRight > (elementPosition.left + actualWidth) && boundBottom > (elementPosition.top + actualHeight);
    };
    $element = $(element);
    pos = $.extend({}, $element.offset(), {
      width: element.offsetWidth,
      height: element.offsetHeight
    });
    actualWidth = 283;
    actualHeight = 117;
    boundTop = $(document).scrollTop();
    boundLeft = $(document).scrollLeft();
    boundRight = boundLeft + $(window).width();
    boundBottom = boundTop + $(window).height();
    elementAbove = {
      top: pos.top - actualHeight,
      left: pos.left + pos.width / 2 - actualWidth / 2
    };
    elementBelow = {
      top: pos.top + pos.height,
      left: pos.left + pos.width / 2 - actualWidth / 2
    };
    elementLeft = {
      top: pos.top + pos.height / 2 - actualHeight / 2,
      left: pos.left - actualWidth
    };
    elementRight = {
      top: pos.top + pos.height / 2 - actualHeight / 2,
      left: pos.left + pos.width
    };
    above = isWithinBounds(elementAbove);
    below = isWithinBounds(elementBelow);
    left = isWithinBounds(elementLeft);
    right = isWithinBounds(elementRight);
    if (above) {
      return "top";
    } else {
      if (below) {
        return "bottom";
      } else {
        if (left) {
          return "left";
        } else {
          if (right) {
            return "right";
          } else {
            return "right";
          }
        }
      }
    }
  }
});

It is working well for me except for one case: if the item is in the upper right corner there is no good spot for the popover to appear that is one of the options and it appears partially off the screen.

Solution 2

For those interested in a solution that will take a default placement (using the data-placement attribute on the element), I have adapted the great answer from Cymen.

I've also ensured that no boundaries are calculated unnecessarily, so it should be slightly more performant.

$('[data-toggle="popover"]').each(function() {
    var trigger = $(this);
    trigger.popover({
        animation: true,
        delay: { show: 0, hide: 0 },
        html: true,
        trigger: 'hover focus',
        placement: getPlacementFunction(trigger.attr("data-placement"), 283, 117)
    });
});

var getPlacementFunction = function (defaultPosition, width, height) {
    return function (tip, element) {
        var position, top, bottom, left, right;

        var $element = $(element);
        var boundTop = $(document).scrollTop();
        var boundLeft = $(document).scrollLeft();
        var boundRight = boundLeft + $(window).width();
        var boundBottom = boundTop + $(window).height();

        var pos = $.extend({}, $element.offset(), {
            width: element.offsetWidth,
            height: element.offsetHeight
        });

        var isWithinBounds = function (elPos) {
            return boundTop < elPos.top && boundLeft < elPos.left && boundRight > (elPos.left + width) && boundBottom > (elPos.top + height);
        };

        var testTop = function () {
            if (top === false) return false;
            top = isWithinBounds({
                top: pos.top - height,
                left: pos.left + pos.width / 2 - width / 2
            });
            return top ? "top" : false;
        };

        var testBottom = function () {
            if (bottom === false) return false;
            bottom = isWithinBounds({
                top: pos.top + pos.height,
                left: pos.left + pos.width / 2 - width / 2
            });
            return bottom ? "bottom" : false;
        };

        var testLeft = function () {
            if (left === false) return false;
            left = isWithinBounds({
                top: pos.top + pos.height / 2 - height / 2,
                left: pos.left - width
            });
            return left ? "left" : false;
        };

        var testRight = function () {
            if (right === false) return false;
            right = isWithinBounds({
                top: pos.top + pos.height / 2 - height / 2,
                left: pos.left + pos.width
            });
            return right ? "right" : false;
        };

        switch (defaultPosition) {
            case "top":
                if (position = testTop()) return position;
            case "bottom":
                if (position = testBottom()) return position;
            case "left":
                if (position = testLeft()) return position;
            case "right":
                if (position = testRight()) return position;
            default:
                if (position = testTop()) return position;
                if (position = testBottom()) return position;
                if (position = testLeft()) return position;
                if (position = testRight()) return position;
                return defaultPosition;
        }
    }
};
Share:
11,795

Related videos on Youtube

Steve Mitcham
Author by

Steve Mitcham

Long time developer looking to be useful in the community.

Updated on June 04, 2022

Comments

  • Steve Mitcham
    Steve Mitcham almost 2 years

    Does anyone know of an extension to the popover component of twitter bootstrap that dynamically changes the placement option to ensure that the popover displays on the screen?

  • Björn Lindqvist
    Björn Lindqvist about 11 years
    This is a good answer but doesn't solve the problem entirely. The width and height of the tooltip is hardcoded in actualWidth and actualHeight and there is no way to dynamically get the size of the tooltip before it is rendered.
  • Cymen
    Cymen about 11 years
    @BjörnLindqvist That is a good point. I've been thinking about it for a while but it seems like a difficult problem to solve besides adjusting the values to match your popup (assuming it is a common size). One could render it in a hidden iframe or do some other hijinks to determine the size but I can't think of a clean way of doing that.
  • Ryan Charmley
    Ryan Charmley about 11 years
    This is a great solution if you need to flip / re-orientate a twitter boostrap popover to ensure it is visible.
  • kplus
    kplus about 11 years
    @BjörnLindqvist This is the problem I am currently having. It would be nice to be able to pass in the actualWidth and actualHeight of a popover dynamically.
  • Chris Haines
    Chris Haines over 10 years
    This is good, but it will override the default placement. With a bit of a tweak you can pick up the data-placement from the element and favour that first.
  • JazzyWhit
    JazzyWhit about 10 years
    This was excellent, and it worked quite well with minimal updates. Thank you!
  • Cymen
    Cymen over 9 years
    It is worth mentioning again that the people who originally wrote this did so as an example. It is a difficult problem to solve generically in a way that would satisfy everyone. There are tradeoffs involved. Thankfully, you can write the function to determine placement based on your needs.
  • Chris Haines
    Chris Haines almost 9 years
    couldn't say, you'd have to try it out