Draggable Bootstrap nav-tabs with jQuery UI

Horizontally draggable nav, also working on responsive websites

Alert
Be sure to include also jQuery UI Draggable component besides jQuery and Bootstrap.

The Code

Demo
Demo
Demo and source code

Inspired by how Google handles long nav menus on mobile, I've found a method to add horizontal scrolling on Bootstrap navs.

The first thing to do is to add a direct ancestor to the Bootstrap .nav, we call it .draggable-container in the demo, and then we add the .draggable class to the .nav:

<div class="draggable-container">
  <ul class="nav nav-tabs draggable" ...>

We add the styles that keeps the .nav all on one line, the .draggable-container has width 100% so it inherit the width of the parents (columns or others):

/* draggable */

.draggable-container {
  position: relative;
  float: left;
  width: 100%;
  overflow: hidden;
  /* no selection */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
.draggable {
  white-space: nowrap; /* all on same line */
  font-size: 0; /* fix inline block spacing */
}
.draggable.nav > li {
  display: inline-block;
  float: none;
}
.draggable.nav > li > a  {
  margin: 0;
  font-size: 14px;
}
.draggable.nav > li + li {
  margin-left: 8px;
}
.draggable.btn-toolbar {
  margin-bottom: 0 !important;
  margin-left: 0 !important;
}
.draggable .btn-group {
  margin-bottom: 0 !important;
}
.draggable .btn-group:first-child {
  margin-left: 0 !important;
}

Be sure to set the desired font-size inside .draggable.nav > li > a and the margin-left of the .draggable.nav > li + li.

As last we use this javascript to do all the calculations on load and window resize, and to make .draggable draggable with jQuery UI:

// function to calculate and set nav width

$.fn.draggable_nav_calc = function(options) {
  return this.each( function(i) {
    var $element = $(this);
    if ($element.is(":visible")) {
      // x or y
      if (options.axis === "x") {
        // calculate
        var navWidth = 1;
        $element.find("> *").each( function(i) {
          navWidth += $(this).outerWidth(true);
        });
        // set width
        var parentWidth = $element.parent().width();
        if (navWidth > parentWidth) {
          $element.css("width", navWidth);
        } else {
          $element.css("width", parentWidth);
        }
      } else if (options.axis === "y") {
        // calculate
        var navHeight = 1;
        $element.find("> *").each( function(i) {
          navHeight += $(this).outerHeight(true);
        });
        // set height
        var parentHeight = $element.parent().width();
        if (navHeight > parentHeight) {
          $element.css("height", navHeight);
        } else {
          $element.css("height", parentHeight);
        }
      }
    }
  });
};

// function to calculate and set nav width

$.fn.draggable_nav_check = function() {
  return this.each( function(i) {
    var $element = $(this);
    var position = $element.position();
    // calculate
    var w = $element.width();
    var pw = $element.parent().width();
    var maxPosLeft = 0;
    if (w > pw) {
      maxPosLeft = - (w - pw);
    }
    var h = $element.height();
    var ph = $element.parent().height();
    var maxPosTop = 0;
    if (h > ph) {
      maxPosTop = h - ph;
    }
    // horizontal
    if (position.left > 0) {
      $element.css("left", 0);
    } else if (position.left < maxPosLeft) {
      $element.css("left", maxPosLeft);
    }
    // vertical
    if (position.top > 0) {
      $element.css("top", 0);
    } else if (position.top < maxPosTop) {
      $element.css("top", maxPosTop);
    }
  });
};

// function to activate draggable nav

$.fn.draggable_nav = function(options) {
  return this.each( function(i) {
    var $element = $(this);
    // calculate first time, after delay to fix resize bugs
    window.setTimeout( function(e) {
      $element.draggable_nav_calc(options);
    }, 100);
    // on shown tabs recalculate
    $element.find('[data-toggle="tab"]').on('shown.bs.tab', function(e) {
      $element.draggable_nav_calc(options);
    });
    // on resize recalculate
    function draggable_nav_resize_after() {
      clearTimeout($element.data("draggable_nav_timeout"));
      var timeout = window.setTimeout( function(e) {
        $element.draggable_nav_calc(options);
        $element.draggable_nav_check();
      }, 0);
      $element.data("draggable_nav_timeout", timeout);
    }
    $(window).on('resize', draggable_nav_resize_after);
    $(window).on('scroll', draggable_nav_resize_after);
  });
};

// init draggable_nav

$(".draggable").draggable_nav({
  axis: 'x' // only horizontally
});

// jquery ui draggable

$(".draggable").draggable({
  axis: 'x', // only horizontally
  drag: function(e, ui) {
    var $element = ui.helper;
    // calculate
    var w = $element.width();
    var pw = $element.parent().width();
    var maxPosLeft = 0;
    if (w > pw) {
      maxPosLeft = - (w - pw);
    }
    var h = $element.height();
    var ph = $element.parent().height();
    var maxPosTop = 0;
    if (h > ph) {
      maxPosTop = h - ph;
    }
    // horizontal
    if (ui.position.left > 0) {
      ui.position.left = 0;
    } else if (ui.position.left < maxPosLeft) {
      ui.position.left = maxPosLeft;
    }
    // vertical
    if (ui.position.top > 0) {
      ui.position.top = 0;
    } else if (ui.position.top < maxPosTop) {
      ui.position.top = maxPosTop;
    }
  }
});

// jquey draggable also on touch devices
// http://stackoverflow.com/questions/5186441/javascript-drag-and-drop-for-touch-devices

function touchHandler(e) {
  var touch = e.originalEvent.changedTouches[0];
  var simulatedEvent = document.createEvent("MouseEvent");
    simulatedEvent.initMouseEvent({
    touchstart: "mousedown",
    touchmove: "mousemove",
    touchend: "mouseup"
  }[e.type], true, true, window, 1,
    touch.screenX, touch.screenY,
    touch.clientX, touch.clientY, false,
    false, false, false, 0, null);
  touch.target.dispatchEvent(simulatedEvent);
  // without prevent default or links inside stop working
  // event.preventDefault();
}
function initTouchHandler($element) {
  $element.on("touchstart touchmove touchend touchcancel", touchHandler);
}
initTouchHandler($(".draggable"));