Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Twipsy auto positioning #345

Closed
marcalj opened this issue Sep 30, 2011 · 13 comments
Closed

Twipsy auto positioning #345

marcalj opened this issue Sep 30, 2011 · 13 comments
Labels

Comments

@marcalj
Copy link

marcalj commented Sep 30, 2011

With Chrome if the Twipsy is shown upper an element without sufficient space on the left/right, a scroll is show in the window.
So could be helpful to detect if there's sufficient space for tipsy before is showed, and re-position the placement o have a fallback attribute for this.

Another example is a tipsy on a topbar element. Automatically will not show above the topbar.

Thanks.

@fat
Copy link
Member

fat commented Oct 3, 2011

Yep - i'll take a look at this

@fat
Copy link
Member

fat commented Oct 5, 2011

The twipsy placement options takes both a string and a function as an option. I don't feel comfortable including 'auto' as option here because i don't think it's very performant to do this sort of calculation all the time, and i'd rather encourage people to set a definite position.

That said - i did take the time to write out what you would need to do if you want the placement to be performed automatically. Below is a tested working example:

$("a[rel=twipsy]").twipsy({
  live: true
, placement: function (tip, element) {
    var $element = $(element)
      , pos = $.extend({}, $element.offset(), {
          width: element.offsetWidth
        , height: element.offsetHeight
        })
      , actualWidth = tip.offsetWidth
      , actualHeight = tip.offsetHeight
      , boundTop = $(document).scrollTop()
      , boundLeft = $(document).scrollLeft()
      , boundRight = boundLeft + $(window).width()
      , boundBottom = boundTop + $(window).height()
      , elementAbove = {
          top: pos.top - actualHeight - this.options.offset
        , left: pos.left + pos.width / 2 - actualWidth / 2
        }
      , elementBelow = {
          top: pos.top + pos.height + this.options.offset
        , left: pos.left + pos.width / 2 - actualWidth / 2
        }
      , elementLeft = {
          top: pos.top + pos.height / 2 - actualHeight / 2
        , left: pos.left - actualWidth - this.options.offset
        }
      , elementRight = {
          top: pos.top + pos.height / 2 - actualHeight / 2
        , left: pos.left + pos.width + this.options.offset
        }
      , above = isWithinBounds(elementAbove)
      , below = isWithinBounds(elementBelow)
      , left = isWithinBounds(elementLeft)
      , right = isWithinBounds(elementRight)

      function isWithinBounds (elementPosition) {
        return boundTop < elementPosition.top
          && boundLeft < elementPosition.left
          && boundRight > (elementPosition.left + actualWidth)
          && boundBottom > (elementPosition.top + actualHeight)
      }

      return above ? 'above' : below ? 'below' : left ? 'left' : right ? 'right' : 'above'
  }
})

cheers!

@fat fat closed this as completed Oct 5, 2011
@marcalj
Copy link
Author

marcalj commented Oct 5, 2011

Awesome! Thanks!

@fat
Copy link
Member

fat commented Oct 5, 2011

no problem :)

@marcalj
Copy link
Author

marcalj commented Oct 25, 2011

Hey I don't test it until now and it can't work. You need to add some fixes to twipsy code:

Line 93:

placement = _.maybeCall(this.options.placement, this.$element[0], this)

Line 198:

     maybeCall: function ( thing, ctx, twipsy ) {
       return (typeof thing == 'function') ? (thing.call(twipsy, ctx)) : thing
     }

So with this fixes your can use a function like this:

    function (element) {
        var tip = this.tip();
        var position;

        if (position = $(element).data('placement')) {
            return position;
        } else {
            var $element = $(element)
            , pos = $.extend({}, $element.offset(), {
                width: element.offsetWidth
                , height: element.offsetHeight
                })
            , actualWidth = tip.offsetWidth
            , actualHeight = tip.offsetHeight
            , boundTop = $(document).scrollTop()
            , boundLeft = $(document).scrollLeft()
            , boundRight = boundLeft + $(window).width()
            , boundBottom = boundTop + $(window).height()
            , elementAbove = {
                top: pos.top - actualHeight - this.options.offset
                , left: pos.left + pos.width / 2 - actualWidth / 2
                }
            , elementBelow = {
                top: pos.top + pos.height + this.options.offset
                , left: pos.left + pos.width / 2 - actualWidth / 2
                }
            , elementLeft = {
                top: pos.top + pos.height / 2 - actualHeight / 2
                , left: pos.left - actualWidth - this.options.offset
                }
            , elementRight = {
                top: pos.top + pos.height / 2 - actualHeight / 2
                , left: pos.left + pos.width + this.options.offset
                }
            , above = isWithinBounds(elementAbove)
            , below = isWithinBounds(elementBelow)
            , left = isWithinBounds(elementLeft)
            , right = isWithinBounds(elementRight)

            function isWithinBounds (elementPosition) {
                return boundTop < elementPosition.top
                && boundLeft < elementPosition.left
                && boundRight > (elementPosition.left + actualWidth)
                && boundBottom > (elementPosition.top + actualHeight)
            }

            return above ? 'above' : below ? 'below' : left ? 'left' : right ? 'right' : 'above';
        }
    }

Can you add the changes or fix the way you want?

Thanks.

@marcalj
Copy link
Author

marcalj commented Oct 26, 2011

Also your function it doesn't work preperly on Chrome 14, I'm not sure if it's related with my CSS modification on bootstrap CSS or not...

@fat
Copy link
Member

fat commented Oct 30, 2011

you're using an old version of of the plugin - in 1.3 maybeCall passes two arguments

@paulrusso
Copy link

Any chance someone can update this for 2.0?

@arakelian
Copy link

The function you provided does not work with 2.0.3. Broken when 'inside' feature added?

In bootstrap-tooltip.js, the placement function is called before this.$element[0] is added to the DOM. As a result, the width and height are zero (at least in Chrome).

    placement = typeof this.options.placement == 'function' ?
      this.options.placement.call(this, $tip[0], this.$element[0]) :
      this.options.placement

    inside = /in/.test(placement)

    $tip
      .remove()
      .css({ top: 0, left: 0, display: 'block' })
      .appendTo(inside ? this.$element : document.body)

It seems computing 'inside' should be independent of 'placement' because you have a chicken-and-egg problem.

@wleeper
Copy link

wleeper commented May 28, 2012

This is my stab at 2.0.3 support.

The @options.offset is gone and I just put in an average of the size of my popup, if I had a larger one it might not work though.

Also above is now top and below is now bottom.

  $("a[rel=popover]").popover placement: (tip, element) ->
    isWithinBounds = (elementPosition) ->
      boundTop < elementPosition.top and boundLeft < elementPosition.left and boundRight > (elementPosition.left + actualWidth) and 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 then "top" else (if below then "bottom" else (if left then "left" else (if right then "right" else "right"))))

@brianfeister
Copy link

Commenting so I can find this later - I have updated code for a similar use case that I'll post.

@archonic
Copy link

@fat

I don't feel comfortable including 'auto' as option here because i don't think it's very performant to do this sort of calculation all the time, and i'd rather encourage people to set a definite position.

There currently isn't a way to set a definite position, only to dynamically set a direction with the above code. Trying to override the popover div placement with margins just gets undone by the dynamic placement.

Anyone else in the same boat? Implementing offset again would fix my issue and I'm sure others`. I would just extend but I don't have a way to do that without keeping my gem update-able.

Update: This commit solves this issue and a few related to it #7173

@eagor
Copy link

eagor commented Apr 26, 2013

this doesn't work on SVG elements

DocX pushed a commit to DocX/bootstrap that referenced this issue Sep 16, 2014
Made it easier to get to the variables file so can customize the look of bootstrap easier
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants