26

I was wondering if any of you has what I am asking for ready, to save me from the trouble. What I am looking for is for a dropdown menu to have the dropup class automatically added according to its position on the screen - and also change automatically when the user scrolls or resizes window.

What I am looking for is already implemented in the bootstrap-select plugin, but the plugin is too 'heavy' to use.

Chris
  • 5,882
  • 2
  • 32
  • 57
scooterlord
  • 15,124
  • 11
  • 49
  • 68

7 Answers7

31

A bit late, but because is quite different from others, here is my solution for bootstrap 3. It applies globally to all dropdowns on the page, also those created or loaded dynamically.

Note that I had to use the shown.bs.dropdown event because the dimensions are ready only at that point.

$(document).on("shown.bs.dropdown", ".dropdown", function () {
    // calculate the required sizes, spaces
    var $ul = $(this).children(".dropdown-menu");
    var $button = $(this).children(".dropdown-toggle");
    var ulOffset = $ul.offset();
    // how much space would be left on the top if the dropdown opened that direction
    var spaceUp = (ulOffset.top - $button.height() - $ul.height()) - $(window).scrollTop();
    // how much space is left at the bottom
    var spaceDown = $(window).scrollTop() + $(window).height() - (ulOffset.top + $ul.height());
    // switch to dropup only if there is no space at the bottom AND there is space at the top, or there isn't either but it would be still better fit
    if (spaceDown < 0 && (spaceUp >= 0 || spaceUp > spaceDown))
      $(this).addClass("dropup");
}).on("hidden.bs.dropdown", ".dropdown", function() {
    // always reset after close
    $(this).removeClass("dropup");
});

One could easily improve it with @PeterWone's app height wizardry if needed, currently this was working well enough for me.

UPDATE

Here is a fiddle representing this solution: http://jsfiddle.net/3s2efe9u/

Community
  • 1
  • 1
Zoltán Tamási
  • 12,249
  • 8
  • 65
  • 93
12

I had performance issues with the provided answer on large pages.

I've "improved" it by using getBoundingClientRect(), and adding a new class to specific .btn-group's that contain dropdows., eg. btn-group-dropup.

Your mileage may vary.

(function() {
  // require menu height + margin, otherwise convert to drop-up
  var dropUpMarginBottom = 100;

  function dropUp() {
    var windowHeight = $(window).height();
    $(".btn-group-dropup").each(function() {
      var dropDownMenuHeight, 
          rect = this.getBoundingClientRect();
      // only toggle menu's that are visible on the current page
      if (rect.top > windowHeight) {
        return;
      }
      // if you know height of menu - set on parent, eg. `data-menu="100"`
      dropDownMenuHeight = $(this).data('menu');
      if (dropDownMenuHeight == null) {
        dropDownMenuHeight = $(this).children('.dropdown-menu').height();
      }
      $(this).toggleClass("dropup", ((windowHeight - rect.bottom) < (dropDownMenuHeight + dropUpMarginBottom)) && (rect.top > dropDownMenuHeight));
    });
  };

  // bind to load & scroll - but debounce scroll with `underscorejs`
  $(window).bind({
    "resize scroll touchstart touchmove mousewheel": _.debounce(dropUp, 100),
    "load": dropUp
  });

}).call(this);
nathan-m
  • 8,875
  • 2
  • 18
  • 29
7

Edit 07 Jan 2019: tidied up the variable names and optimized the code just a bit.

dropup = function() {
  $(".dropdown-toggle").each(function(){ 
    offsetTop=$(this).offset().top+$(this).height()-$(window).scrollTop();           
    offsetBottom=$(window).height()-$(this).height()-$(this).offset().top+$(window).scrollTop();
    ulHeight=$(this).parents('.btn-group').find('ul').height();

    if ((offsetBottom < ulHeight) && (offsetTop > ulHeight)) {
      parent.addClass('dropup');
    } else {
      parent.removeClass('dropup');
    }
  });
} 

$(window).on('load resize scroll', dropup);
scooterlord
  • 15,124
  • 11
  • 49
  • 68
  • 1
    I think you should do a complete edit on your first question, and integrate this ( your comment ) answer to your edited question. Maybe the downvote is because you just ask to find a tool without code or something else... – BENARD Patrick Jan 20 '14 at 14:14
  • Had to make some modification as per my requirement works like a charm! :) – Akshay Feb 12 '15 at 20:28
4

I know this is an old question, but it's high up on google results so here's another simple solution that worked for me. Adjust the 150 in the if statement for however much space your dropdown will need.

var dropUp = function() {
    var windowHeight = $(window).innerHeight();
    var pageScroll = $('body').scrollTop();

    $( ".your-dropdown" ).each( function() {
        var offset = $( this ).offset().top;
        var space = windowHeight - ( offset - pageScroll );

        if( space < 150 ) {
            $( this ).addClass( "dropup" );
        } else  {
            $( this ).removeClass( "dropup" );
        }
    });
}

$(window).load(dropUp);
$(window).bind('resize scroll mousewheel', dropUp);
user2407400
  • 213
  • 1
  • 5
  • 19
2

Rather than diddling every menu every time something changes clipping bounds, why not take a JIT approach?

<body>
  <div id="viewport-proxy" style="visibility:hidden; 
    position:absolute; height:100%; width:100%; margin:0; padding:0; 
    top:0; left: 0; right:0; bottom:0;"></div>
...

var w = window, d = document, e = d.documentElement;
var g = d.getElementsByTagName('body')[0];
function appHeight() { 
  var a = w.innerHeight || e.clientHeight || g.clientHeight;
  var b = $("#viewport-proxy").height();
  return a < b ? a : b;
};

$(".dropdown").on('show.bs.dropdown', function () {
    var windowHeight = appHeight();
    var rect = this.getBoundingClientRect();
    if (rect.top > windowHeight) return;
    var menuHeight = $(this).children('.dropdown-menu').height();
    $(this).toggleClass("dropup", 
      ((windowHeight - rect.bottom) < menuHeight) && 
      (rect.top > menuHeight));
  });

Obviously you'll have to manage event bindings as menus come and go. In the Durandal app for which Nathan's answer morphed into the above, there is a context menu for each row representing a datum. I create the bindings on data load, keep them in the view-model and destroy them on deactivate. I don't need to recreate them on activate because that calls the data load routine, which in turn creates the bindings.

I also added this:

$(window).on("scroll resize", function () { $(".dropdown.open").dropdown("toggle"); });

You put this in shell.js so it's loaded early and affects all views. If it's not already obvious, what this does is close any open dropup when the window scrolls or resizes. This conforms with Windows convention and makes it unnecessary to recompute positioning when resize or scroll events occur.

Note the appHeight function. Even jQuery fails to accurately report window height on mobile phones, but appHeight works correctly on every platform and browser that I've tested so far (IE,Ch,FF desktop, IE,Ch,Sa mobile).

In its current incarnation it measures an absolutely positioned hidden div set to fill the viewport. This works on Android Chrome but not IE, whereas the other method shown in the code works with IE but not Android Chrome, so I use both and use the lower number.

In my Durandal app the function is implemented as a height method on the app object but for brevity and clarity I flattened the example code.

Peter Wone
  • 17,965
  • 12
  • 82
  • 134
0

I found the answer from Zoltán Tamási to be a good starting point but not enough when the dropdown should appear within a div with a vertical scrollbar. I needed the height of that div (the scrollHeight variable in the code below) to figure out if the dropdown will take to much space at the bottom.

jquery(document).on("show.bs.dropdown", ".dropdown", function () {
  var $ul = jquery(this).children(".dropdown-menu");
  var menuHeight = $ul.height()
  var buttonHeight = jquery(this).children(".dropdown-toggle").height();
  var scrollHeight = $ul.parent().offsetParent().height();
  var menuAnchorTop = $ul.parent().position().top + buttonHeight;

  // how much space would be left at the top
  var spaceUp = (menuAnchorTop - buttonHeight - menuHeight);
  // how much space would be left at the bottom
  var spaceDown = scrollHeight - (menuAnchorTop + menuHeight);
  // Switch to dropup if more space there
  if (spaceDown < 0 && (spaceUp >= 0 || spaceUp > spaceDown))
    jquery(this).addClass("dropup");
}).on("hidden.bs.dropdown", ".dropdown", function() {
  // always reset after close
  jquery(this).removeClass("dropup");
});
0

I have checked the performance and browser support (chrome,Firefox,Edge,IE 10 and above).

I have covered both vertical and horizontal auto positioning.

Code:

(function () {
  var dd_obj = {
    jq_win :jQuery(window),
    jq_doc :jQuery(document),
  }
  function selfCalc(ele) {
    var $this = jQuery(ele),
        $dd_menu = $this.children(".dropdown-menu"), 
        ddOffset = $this.offset(),
        ddMenu_posTop = dd_obj.jq_win.outerHeight() - (($this.outerHeight() + ddOffset.top + $dd_menu.outerHeight()) - dd_obj.jq_doc.scrollTop());
        ddMenu_posRight = dd_obj.jq_win.outerWidth() - (($this.outerWidth() + ddOffset.left + $dd_menu.outerWidth()) - dd_obj.jq_doc.scrollLeft());

    (ddMenu_posTop <= 0) ? $this.addClass("dropup") : $this.removeClass("dropup"); 
    (ddMenu_posRight <= 0) ? $this.find('.dropdown-menu').addClass("dropdown-menu-right") : $this.find('.dropdown-menu').removeClass("dropdown-menu-right");
  }
  jQuery('body').on("shown.bs.dropdown", ".dropdown", function () {
    var self = this;
    selfCalc(self)
    dd_obj.jq_win.on('resize.custDd scroll.custDd mousewheel.custDd',function() {
      selfCalc(self)
    })
  }).on("hidden.bs.dropdown", ".dropdown", function() {
      // there is no need to keep the window event after dropdown has closed, 
      // still if we keep the event then there is no use
      dd_obj.jq_win.off('resize.custDd scroll.custDd mousewheel.custDd')
  });
})();