Draggable Bootstrap nav-tabs with jQuery UI

Horizontally draggable nav, also working on responsive websites

Updated
27/01/2016 - Added .draggable-center option.
14/12/2015 - Simplified code without display inline-block.
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" ...>

If you want to center the tabs when clicked, add the class .draggable-center to .draggable.

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

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 navs */

.draggable-container {
  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 {
  width: 9999999px;
  white-space: nowrap; /* all on same line */
  font-size: 0; /* fix inline block spacing */
}
.draggable.nav > li > a  {
  font-size: 14px; /* give font size to links */
}

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:

// calculate and set .draggable 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);
        }
      }
    }
  });
};

// check inside bounds

$.fn.draggable_nav_check = function() {
  return this.each( function(i) {
    var $element = $(this);
    // 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
    var left = parseInt($element[0].style.left);
    if (left > 0) {
      $element.css("left", 0);
    } else if (left < maxPosLeft) {
      $element.css("left", maxPosLeft);
    }
    // vertical
    var top = parseInt($element[0].style.top);
    if (top > 0) {
      $element.css("top", 0);
    } else if (top < maxPosTop) {
      $element.css("top", maxPosTop);
    }
  });
};

// init 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);
    // center clicked element
    if ($element.hasClass("draggable-center")) {
      $element.find('li a[data-toggle="tab"]').on("shown.bs.tab", function(e) {
        var $container = $(this).parents(".draggable-container");
        var $li = $(this).parents("li");
        if (options.axis === "x") {
            var left = - $li.position().left + $container.outerWidth() / 2 - $li.outerWidth() / 2;
            $element.css("left", left);
        } else if (options.axis === "y") {
            var top = - $li.position().top + $container.outerWidth() / 2 - $li.outerWidth() / 2;
            $element.css("top", top);
        }
        // put inside bounds
        $element.draggable_nav_check();
      });
    }
  });
};
$(".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);
}
function preventPageScroll(e) {
    e.preventDefault();
}
function initTouchHandler($element) {
  $element.on("touchstart touchmove touchend touchcancel", touchHandler);
  $element.on("touchmove", preventPageScroll);
}
initTouchHandler($(".draggable"));