From 2d91494d96f8161f1bcce6589081880b72706154 Mon Sep 17 00:00:00 2001
From: fat <fat@folders.local>
Date: Mon, 11 May 2015 12:05:35 -0700
Subject: [PATCH] scrollspy es6

---
 Gruntfile.js                   |  15 +-
 js/dist/scrollspy.js           | 268 ++++++++++++++++++++++++++
 js/dist/scrollspy.js.map       | Bin 0 -> 13403 bytes
 js/modal.js                    | 337 ---------------------------------
 js/src/scrollspy.js            | 275 +++++++++++++++++++++++++++
 js/tests/index.html            |   2 +-
 js/tests/visual/scrollspy.html |  27 +--
 7 files changed, 561 insertions(+), 363 deletions(-)
 create mode 100644 js/dist/scrollspy.js
 create mode 100644 js/dist/scrollspy.js.map
 delete mode 100644 js/modal.js
 create mode 100644 js/src/scrollspy.js

diff --git a/Gruntfile.js b/Gruntfile.js
index 4182d9d3c3..b3c29c3533 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -63,13 +63,14 @@ module.exports = function (grunt) {
       },
       dist: {
         files: {
-          'js/dist/util.js'     : 'js/src/util.js',
-          'js/dist/alert.js'    : 'js/src/alert.js',
-          'js/dist/button.js'   : 'js/src/button.js',
-          'js/dist/carousel.js' : 'js/src/carousel.js',
-          'js/dist/collapse.js' : 'js/src/collapse.js',
-          'js/dist/dropdown.js' : 'js/src/dropdown.js',
-          'js/dist/modal.js'    : 'js/src/modal.js'
+          'js/dist/util.js'      : 'js/src/util.js',
+          'js/dist/alert.js'     : 'js/src/alert.js',
+          'js/dist/button.js'    : 'js/src/button.js',
+          'js/dist/carousel.js'  : 'js/src/carousel.js',
+          'js/dist/collapse.js'  : 'js/src/collapse.js',
+          'js/dist/dropdown.js'  : 'js/src/dropdown.js',
+          'js/dist/modal.js'     : 'js/src/modal.js',
+          'js/dist/scrollspy.js' : 'js/src/scrollspy.js'
         }
       }
     },
diff --git a/js/dist/scrollspy.js b/js/dist/scrollspy.js
new file mode 100644
index 0000000000..a0d774f438
--- /dev/null
+++ b/js/dist/scrollspy.js
@@ -0,0 +1,268 @@
+'use strict';
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v4.0.0): scrollspy.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+var ScrollSpy = (function ($) {
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  var NAME = 'scrollspy';
+  var VERSION = '4.0.0';
+  var DATA_KEY = 'bs.scrollspy';
+  var JQUERY_NO_CONFLICT = $.fn[NAME];
+  var TRANSITION_DURATION = 150;
+
+  var Defaults = {
+    offset: 10
+  };
+
+  var Event = {
+    ACTIVATE: 'activate.bs.scrollspy',
+    SCROLL: 'scroll.bs.scrollspy',
+    LOAD: 'load.bs.scrollspy.data-api'
+  };
+
+  var ClassName = {
+    DROPDOWN_MENU: 'dropdown-menu',
+    ACTIVE: 'active'
+  };
+
+  var Selector = {
+    DATA_SPY: '[data-spy="scroll"]',
+    ACTIVE: '.active',
+    LI_DROPDOWN: 'li.dropdown',
+    LI: 'li'
+  };
+
+  /**
+   * ------------------------------------------------------------------------
+   * Class Definition
+   * ------------------------------------------------------------------------
+   */
+
+  var ScrollSpy = (function () {
+    function ScrollSpy(element, config) {
+      _classCallCheck(this, ScrollSpy);
+
+      this._scrollElement = element.tagName === 'BODY' ? window : element;
+      this._config = $.extend({}, Defaults, config);
+      this._selector = '' + (this._config.target || '') + ' .nav li > a';
+      this._offsets = [];
+      this._targets = [];
+      this._activeTarget = null;
+      this._scrollHeight = 0;
+
+      $(this._scrollElement).on(Event.SCROLL, this._process.bind(this));
+
+      this.refresh();
+      this._process();
+    }
+
+    _createClass(ScrollSpy, [{
+      key: 'refresh',
+
+      // public
+
+      value: function refresh() {
+        var _this = this;
+
+        var offsetMethod = 'offset';
+        var offsetBase = 0;
+
+        if (this._scrollElement !== this._scrollElement.window) {
+          offsetMethod = 'position';
+          offsetBase = this._getScrollTop();
+        }
+
+        this._offsets = [];
+        this._targets = [];
+
+        this._scrollHeight = this._getScrollHeight();
+
+        var targets = $.makeArray($(this._selector));
+
+        targets.map(function (element) {
+          var target = undefined;
+          var targetSelector = Util.getSelectorFromElement(element);
+
+          if (targetSelector) {
+            target = $(targetSelector)[0];
+          }
+
+          if (target && (target.offsetWidth || target.offsetHeight)) {
+            // todo (fat): remove sketch reliance on jQuery position/offset
+            return [$(target)[offsetMethod]().top + offsetBase, targetSelector];
+          }
+        }).filter(function (item) {
+          return item;
+        }).sort(function (a, b) {
+          return a[0] - b[0];
+        }).forEach(function (item) {
+          _this._offsets.push(item[0]);
+          _this._targets.push(item[1]);
+        });
+      }
+    }, {
+      key: '_getScrollTop',
+
+      // private
+
+      value: function _getScrollTop() {
+        return this._scrollElement === window ? this._scrollElement.scrollY : this._scrollElement.scrollTop;
+      }
+    }, {
+      key: '_getScrollHeight',
+      value: function _getScrollHeight() {
+        return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
+      }
+    }, {
+      key: '_process',
+      value: function _process() {
+        var scrollTop = this._getScrollTop() + this._config.offset;
+        var scrollHeight = this._getScrollHeight();
+        var maxScroll = this._config.offset + scrollHeight - this._scrollElement.offsetHeight;
+
+        if (this._scrollHeight !== scrollHeight) {
+          this.refresh();
+        }
+
+        if (scrollTop >= maxScroll) {
+          var target = this._targets[this._targets.length - 1];
+
+          if (this._activeTarget !== target) {
+            this._activate(target);
+          }
+        }
+
+        if (this._activeTarget && scrollTop < this._offsets[0]) {
+          this._activeTarget = null;
+          this._clear();
+          return;
+        }
+
+        for (var i = this._offsets.length; i--;) {
+          var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (this._offsets[i + 1] === undefined || scrollTop < this._offsets[i + 1]);
+
+          if (isActiveTarget) {
+            this._activate(this._targets[i]);
+          }
+        }
+      }
+    }, {
+      key: '_activate',
+      value: function _activate(target) {
+        this._activeTarget = target;
+
+        this._clear();
+
+        var selector = '' + this._selector + '[data-target="' + target + '"],' + ('' + this._selector + '[href="' + target + '"]');
+
+        // todo (fat): getting all the raw li's up the tree is not great.
+        var parentListItems = $(selector).parents(Selector.LI);
+
+        for (var i = parentListItems.length; i--;) {
+          $(parentListItems[i]).addClass(ClassName.ACTIVE);
+
+          var itemParent = parentListItems[i].parentNode;
+
+          if (itemParent && $(itemParent).hasClass(ClassName.DROPDOWN_MENU)) {
+            var closestDropdown = $(itemParent).closest(Selector.LI_DROPDOWN)[0];
+            $(closestDropdown).addClass(ClassName.ACTIVE);
+          }
+        }
+
+        $(this._scrollElement).trigger(Event.ACTIVATE, {
+          relatedTarget: target
+        });
+      }
+    }, {
+      key: '_clear',
+      value: function _clear() {
+        var activeParents = $(this._selector).parentsUntil(this._config.target, Selector.ACTIVE);
+
+        for (var i = activeParents.length; i--;) {
+          $(activeParents[i]).removeClass(ClassName.ACTIVE);
+        }
+      }
+    }], [{
+      key: 'VERSION',
+
+      // getters
+
+      get: function () {
+        return VERSION;
+      }
+    }, {
+      key: 'Default',
+      get: function () {
+        return Default;
+      }
+    }, {
+      key: '_jQueryInterface',
+
+      // static
+
+      value: function _jQueryInterface(config) {
+        return this.each(function () {
+          var data = $(this).data(DATA_KEY);
+          var _config = typeof config === 'object' && config || null;
+
+          if (!data) {
+            data = new ScrollSpy(this, _config);
+            $(this).data(DATA_KEY, data);
+          }
+
+          if (typeof config === 'string') {
+            data[config]();
+          }
+        });
+      }
+    }]);
+
+    return ScrollSpy;
+  })();
+
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+  $(window).on(Event.LOAD, function () {
+    var scrollSpys = $.makeArray($(Selector.DATA_SPY));
+
+    for (var i = scrollSpys.length; i--;) {
+      var $spy = $(scrollSpys[i]);
+      ScrollSpy._jQueryInterface.call($spy, $spy.data());
+    }
+  });
+
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME] = ScrollSpy._jQueryInterface;
+  $.fn[NAME].Constructor = ScrollSpy;
+  $.fn[NAME].noConflict = function () {
+    $.fn[NAME] = JQUERY_NO_CONFLICT;
+    return ScrollSpy._jQueryInterface;
+  };
+
+  return ScrollSpy;
+})(jQuery);
+//# sourceMappingURL=scrollspy.js.map
\ No newline at end of file
diff --git a/js/dist/scrollspy.js.map b/js/dist/scrollspy.js.map
new file mode 100644
index 0000000000000000000000000000000000000000..ba686262fc673f2f9f97afea8a501eb0480ca028
GIT binary patch
literal 13403
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

literal 0
HcmV?d00001

diff --git a/js/modal.js b/js/modal.js
deleted file mode 100644
index 0426561af6..0000000000
--- a/js/modal.js
+++ /dev/null
@@ -1,337 +0,0 @@
-/* ========================================================================
- * Bootstrap: modal.js v3.3.4
- * http://getbootstrap.com/javascript/#modals
- * ========================================================================
- * Copyright 2011-2015 Twitter, Inc.
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
- * ======================================================================== */
-
-
-+function ($) {
-  'use strict';
-
-  // MODAL CLASS DEFINITION
-  // ======================
-
-  var Modal = function (element, options) {
-    this.options             = options
-    this.$body               = $(document.body)
-    this.$element            = $(element)
-    this.$dialog             = this.$element.find('.modal-dialog')
-    this.$backdrop           = null
-    this.isShown             = null
-    this.originalBodyPad     = null
-    this.scrollbarWidth      = 0
-    this.ignoreBackdropClick = false
-
-    if (this.options.remote) {
-      this.$element
-        .find('.modal-content')
-        .load(this.options.remote, $.proxy(function () {
-          this.$element.trigger('loaded.bs.modal')
-        }, this))
-    }
-  }
-
-  Modal.VERSION  = '3.3.4'
-
-  Modal.TRANSITION_DURATION = 300
-  Modal.BACKDROP_TRANSITION_DURATION = 150
-
-  Modal.DEFAULTS = {
-    backdrop: true,
-    keyboard: true,
-    show: true
-  }
-
-  Modal.prototype.toggle = function (_relatedTarget) {
-    return this.isShown ? this.hide() : this.show(_relatedTarget)
-  }
-
-  Modal.prototype.show = function (_relatedTarget) {
-    var that = this
-    var e    = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
-
-    this.$element.trigger(e)
-
-    if (this.isShown || e.isDefaultPrevented()) return
-
-    this.isShown = true
-
-    this.checkScrollbar()
-    this.setScrollbar()
-    this.$body.addClass('modal-open')
-
-    this.escape()
-    this.resize()
-
-    this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
-
-    this.$dialog.on('mousedown.dismiss.bs.modal', function () {
-      that.$element.one('mouseup.dismiss.bs.modal', function (e) {
-        if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true
-      })
-    })
-
-    this.backdrop(function () {
-      var transition = $.support.transition && that.$element.hasClass('fade')
-
-      if (!that.$element.parent().length) {
-        that.$element.appendTo(that.$body) // don't move modals dom position
-      }
-
-      that.$element
-        .show()
-        .scrollTop(0)
-
-      that.adjustDialog()
-
-      if (transition) {
-        that.$element[0].offsetWidth // force reflow
-      }
-
-      that.$element.addClass('in')
-
-      that.enforceFocus()
-
-      var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
-
-      transition ?
-        that.$dialog // wait for modal to slide in
-          .one('bsTransitionEnd', function () {
-            that.$element.trigger('focus').trigger(e)
-          })
-          .emulateTransitionEnd(Modal.TRANSITION_DURATION) :
-        that.$element.trigger('focus').trigger(e)
-    })
-  }
-
-  Modal.prototype.hide = function (e) {
-    if (e) e.preventDefault()
-
-    e = $.Event('hide.bs.modal')
-
-    this.$element.trigger(e)
-
-    if (!this.isShown || e.isDefaultPrevented()) return
-
-    this.isShown = false
-
-    this.escape()
-    this.resize()
-
-    $(document).off('focusin.bs.modal')
-
-    this.$element
-      .removeClass('in')
-      .off('click.dismiss.bs.modal')
-      .off('mouseup.dismiss.bs.modal')
-
-    this.$dialog.off('mousedown.dismiss.bs.modal')
-
-    $.support.transition && this.$element.hasClass('fade') ?
-      this.$element
-        .one('bsTransitionEnd', $.proxy(this.hideModal, this))
-        .emulateTransitionEnd(Modal.TRANSITION_DURATION) :
-      this.hideModal()
-  }
-
-  Modal.prototype.enforceFocus = function () {
-    $(document)
-      .off('focusin.bs.modal') // guard against infinite focus loop
-      .on('focusin.bs.modal', $.proxy(function (e) {
-        if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
-          this.$element.trigger('focus')
-        }
-      }, this))
-  }
-
-  Modal.prototype.escape = function () {
-    if (this.isShown && this.options.keyboard) {
-      this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) {
-        e.which == 27 && this.hide()
-      }, this))
-    } else if (!this.isShown) {
-      this.$element.off('keydown.dismiss.bs.modal')
-    }
-  }
-
-  Modal.prototype.resize = function () {
-    if (this.isShown) {
-      $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this))
-    } else {
-      $(window).off('resize.bs.modal')
-    }
-  }
-
-  Modal.prototype.hideModal = function () {
-    var that = this
-    this.$element.hide()
-    this.backdrop(function () {
-      that.$body.removeClass('modal-open')
-      that.resetAdjustments()
-      that.resetScrollbar()
-      that.$element.trigger('hidden.bs.modal')
-    })
-  }
-
-  Modal.prototype.removeBackdrop = function () {
-    this.$backdrop && this.$backdrop.remove()
-    this.$backdrop = null
-  }
-
-  Modal.prototype.backdrop = function (callback) {
-    var that = this
-    var animate = this.$element.hasClass('fade') ? 'fade' : ''
-
-    if (this.isShown && this.options.backdrop) {
-      var doAnimate = $.support.transition && animate
-
-      this.$backdrop = $(document.createElement('div'))
-        .addClass('modal-backdrop ' + animate)
-        .appendTo(this.$body)
-
-      this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) {
-        if (this.ignoreBackdropClick) {
-          this.ignoreBackdropClick = false
-          return
-        }
-        if (e.target !== e.currentTarget) return
-        this.options.backdrop == 'static'
-          ? this.$element[0].focus()
-          : this.hide()
-      }, this))
-
-      if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
-
-      this.$backdrop.addClass('in')
-
-      if (!callback) return
-
-      doAnimate ?
-        this.$backdrop
-          .one('bsTransitionEnd', callback)
-          .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
-        callback()
-
-    } else if (!this.isShown && this.$backdrop) {
-      this.$backdrop.removeClass('in')
-
-      var callbackRemove = function () {
-        that.removeBackdrop()
-        callback && callback()
-      }
-      $.support.transition && this.$element.hasClass('fade') ?
-        this.$backdrop
-          .one('bsTransitionEnd', callbackRemove)
-          .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
-        callbackRemove()
-
-    } else if (callback) {
-      callback()
-    }
-  }
-
-  // these following methods are used to handle overflowing modals
-
-  Modal.prototype.handleUpdate = function () {
-    this.adjustDialog()
-  }
-
-  Modal.prototype.adjustDialog = function () {
-    var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight
-
-    this.$element.css({
-      paddingLeft:  !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',
-      paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : ''
-    })
-  }
-
-  Modal.prototype.resetAdjustments = function () {
-    this.$element.css({
-      paddingLeft: '',
-      paddingRight: ''
-    })
-  }
-
-  Modal.prototype.checkScrollbar = function () {
-    var fullWindowWidth = window.innerWidth
-    if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8
-      var documentElementRect = document.documentElement.getBoundingClientRect()
-      fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left)
-    }
-    this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth
-    this.scrollbarWidth = this.measureScrollbar()
-  }
-
-  Modal.prototype.setScrollbar = function () {
-    var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10)
-    this.originalBodyPad = document.body.style.paddingRight || ''
-    if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth)
-  }
-
-  Modal.prototype.resetScrollbar = function () {
-    this.$body.css('padding-right', this.originalBodyPad)
-  }
-
-  Modal.prototype.measureScrollbar = function () { // thx walsh
-    var scrollDiv = document.createElement('div')
-    scrollDiv.className = 'modal-scrollbar-measure'
-    this.$body.append(scrollDiv)
-    var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
-    this.$body[0].removeChild(scrollDiv)
-    return scrollbarWidth
-  }
-
-
-  // MODAL PLUGIN DEFINITION
-  // =======================
-
-  function Plugin(option, _relatedTarget) {
-    return this.each(function () {
-      var $this   = $(this)
-      var data    = $this.data('bs.modal')
-      var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
-
-      if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
-      if (typeof option == 'string') data[option](_relatedTarget)
-      else if (options.show) data.show(_relatedTarget)
-    })
-  }
-
-  var old = $.fn.modal
-
-  $.fn.modal             = Plugin
-  $.fn.modal.Constructor = Modal
-
-
-  // MODAL NO CONFLICT
-  // =================
-
-  $.fn.modal.noConflict = function () {
-    $.fn.modal = old
-    return this
-  }
-
-
-  // MODAL DATA-API
-  // ==============
-
-  $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
-    var $this   = $(this)
-    var href    = $this.attr('href')
-    var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7
-    var option  = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
-
-    if ($this.is('a')) e.preventDefault()
-
-    $target.one('show.bs.modal', function (showEvent) {
-      if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown
-      $target.one('hidden.bs.modal', function () {
-        $this.is(':visible') && $this.trigger('focus')
-      })
-    })
-    Plugin.call($target, option, this)
-  })
-
-}(jQuery);
diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js
new file mode 100644
index 0000000000..985da708d0
--- /dev/null
+++ b/js/src/scrollspy.js
@@ -0,0 +1,275 @@
+import Util from './util'
+
+
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v4.0.0): scrollspy.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+const ScrollSpy = (($) => {
+
+
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  const NAME                = 'scrollspy'
+  const VERSION             = '4.0.0'
+  const DATA_KEY            = 'bs.scrollspy'
+  const JQUERY_NO_CONFLICT  = $.fn[NAME]
+  const TRANSITION_DURATION = 150
+
+  const Defaults = {
+    offset : 10
+  }
+
+  const Event = {
+    ACTIVATE : 'activate.bs.scrollspy',
+    SCROLL   : 'scroll.bs.scrollspy',
+    LOAD     : 'load.bs.scrollspy.data-api'
+  }
+
+  const ClassName = {
+    DROPDOWN_MENU : 'dropdown-menu',
+    ACTIVE        : 'active'
+  }
+
+  const Selector = {
+    DATA_SPY    : '[data-spy="scroll"]',
+    ACTIVE      : '.active',
+    LI_DROPDOWN : 'li.dropdown',
+    LI          : 'li'
+  }
+
+
+  /**
+   * ------------------------------------------------------------------------
+   * Class Definition
+   * ------------------------------------------------------------------------
+   */
+
+  class ScrollSpy {
+
+    constructor(element, config) {
+      this._scrollElement = element.tagName === 'BODY' ? window : element
+      this._config        = $.extend({}, Defaults, config)
+      this._selector      = `${this._config.target || ''} .nav li > a`
+      this._offsets       = []
+      this._targets       = []
+      this._activeTarget  = null
+      this._scrollHeight  = 0
+
+      $(this._scrollElement).on(Event.SCROLL, this._process.bind(this))
+
+      this.refresh()
+      this._process()
+    }
+
+
+    // getters
+
+    static get VERSION() {
+      return VERSION
+    }
+
+    static get Default() {
+      return Default
+    }
+
+
+    // public
+
+    refresh() {
+      let offsetMethod = 'offset'
+      let offsetBase   = 0
+
+      if (this._scrollElement !== this._scrollElement.window) {
+        offsetMethod = 'position'
+        offsetBase   = this._getScrollTop()
+      }
+
+      this._offsets = []
+      this._targets = []
+
+      this._scrollHeight = this._getScrollHeight()
+
+      let targets = $.makeArray($(this._selector))
+
+      targets
+        .map((element) => {
+          let target
+          let targetSelector = Util.getSelectorFromElement(element)
+
+          if (targetSelector) {
+            target = $(targetSelector)[0]
+          }
+
+          if (target && (target.offsetWidth || target.offsetHeight)) {
+            // todo (fat): remove sketch reliance on jQuery position/offset
+            return [
+              $(target)[offsetMethod]().top + offsetBase,
+              targetSelector
+            ]
+          }
+        })
+        .filter((item)  => item)
+        .sort((a, b)    => a[0] - b[0])
+        .forEach((item) => {
+          this._offsets.push(item[0])
+          this._targets.push(item[1])
+        })
+    }
+
+
+    // private
+
+    _getScrollTop() {
+      return this._scrollElement === window ?
+          this._scrollElement.scrollY : this._scrollElement.scrollTop
+    }
+
+    _getScrollHeight() {
+      return this._scrollElement.scrollHeight || Math.max(
+        document.body.scrollHeight,
+        document.documentElement.scrollHeight
+      )
+    }
+
+    _process() {
+      let scrollTop    = this._getScrollTop() + this._config.offset
+      let scrollHeight = this._getScrollHeight()
+      let maxScroll    = this._config.offset
+        + scrollHeight
+        - this._scrollElement.offsetHeight
+
+      if (this._scrollHeight !== scrollHeight) {
+        this.refresh()
+      }
+
+      if (scrollTop >= maxScroll) {
+        let target = this._targets[this._targets.length - 1]
+
+        if (this._activeTarget !== target) {
+          this._activate(target)
+        }
+      }
+
+      if (this._activeTarget && scrollTop < this._offsets[0]) {
+        this._activeTarget = null
+        this._clear()
+        return
+      }
+
+      for (let i = this._offsets.length; i--;) {
+        let isActiveTarget = this._activeTarget !== this._targets[i]
+            && scrollTop >= this._offsets[i]
+            && (this._offsets[i + 1] === undefined ||
+                scrollTop < this._offsets[i + 1])
+
+        if (isActiveTarget) {
+          this._activate(this._targets[i])
+        }
+      }
+    }
+
+    _activate(target) {
+      this._activeTarget = target
+
+      this._clear()
+
+      let selector =
+        `${this._selector}[data-target="${target}"],` +
+        `${this._selector}[href="${target}"]`
+
+      // todo (fat): getting all the raw li's up the tree is not great.
+      let parentListItems = $(selector).parents(Selector.LI)
+
+      for (let i = parentListItems.length; i--;) {
+        $(parentListItems[i]).addClass(ClassName.ACTIVE)
+
+        let itemParent = parentListItems[i].parentNode
+
+        if (itemParent && $(itemParent).hasClass(ClassName.DROPDOWN_MENU)) {
+          let closestDropdown = $(itemParent)
+            .closest(Selector.LI_DROPDOWN)[0]
+          $(closestDropdown).addClass(ClassName.ACTIVE)
+        }
+      }
+
+      $(this._scrollElement).trigger(Event.ACTIVATE, {
+        relatedTarget: target
+      })
+    }
+
+    _clear() {
+      let activeParents = $(this._selector).parentsUntil(
+        this._config.target,
+        Selector.ACTIVE
+      )
+
+      for (let i = activeParents.length; i--;) {
+        $(activeParents[i]).removeClass(ClassName.ACTIVE)
+      }
+    }
+
+
+    // static
+
+    static _jQueryInterface(config) {
+      return this.each(function () {
+        let data    = $(this).data(DATA_KEY)
+        let _config = typeof config === 'object' && config || null
+
+        if (!data) {
+          data = new ScrollSpy(this, _config)
+          $(this).data(DATA_KEY, data)
+        }
+
+        if (typeof config === 'string') {
+          data[config]()
+        }
+      })
+    }
+
+
+  }
+
+
+  /**
+   * ------------------------------------------------------------------------
+   * Data Api implementation
+   * ------------------------------------------------------------------------
+   */
+
+  $(window).on(Event.LOAD, function () {
+    let scrollSpys = $.makeArray($(Selector.DATA_SPY))
+
+    for (let i = scrollSpys.length; i--;) {
+      let $spy = $(scrollSpys[i])
+      ScrollSpy._jQueryInterface.call($spy, $spy.data())
+    }
+  })
+
+
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME]             = ScrollSpy._jQueryInterface
+  $.fn[NAME].Constructor = ScrollSpy
+  $.fn[NAME].noConflict  = function () {
+    $.fn[NAME] = JQUERY_NO_CONFLICT
+    return ScrollSpy._jQueryInterface
+  }
+
+  return ScrollSpy
+
+})(jQuery)
+
+export default ScrollSpy
diff --git a/js/tests/index.html b/js/tests/index.html
index 65c75acfee..0262eec2a9 100644
--- a/js/tests/index.html
+++ b/js/tests/index.html
@@ -137,9 +137,9 @@
     <script src="../../js/dist/collapse.js"></script>
     <script src="../../js/dist/dropdown.js"></script>
     <script src="../../js/dist/modal.js"></script>
+    <script src="../../js/dist/scrollspy.js"></script>
 
     <!-- Old Plugin sources -->
-    <script src="../../js/scrollspy.js"></script>
     <script src="../../js/tab.js"></script>
     <script src="../../js/tooltip.js"></script>
     <script src="../../js/popover.js"></script>
diff --git a/js/tests/visual/scrollspy.html b/js/tests/visual/scrollspy.html
index 5952b7e593..9958f90c08 100644
--- a/js/tests/visual/scrollspy.html
+++ b/js/tests/visual/scrollspy.html
@@ -24,21 +24,12 @@
 
   <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
     <div class="container-fluid">
-      <div class="navbar-header">
-        <button class="navbar-toggle collapsed" type="button" data-toggle="collapse" data-target=".js-navbar-scrollspy">
-          <span class="sr-only">Toggle navigation</span>
-          <span class="icon-bar"></span>
-          <span class="icon-bar"></span>
-          <span class="icon-bar"></span>
-        </button>
-        <a class="navbar-brand" href="#">Project Name</a>
-      </div>
-      <div class="collapse navbar-collapse js-navbar-scrollspy">
+      <div class="js-navbar-scrollspy">
         <ul class="nav navbar-nav">
-          <li class=""><a href="#fat">@fat</a></li>
-          <li class=""><a href="#mdo">@mdo</a></li>
-          <li class="dropdown">
-            <a href="#" class="dropdown-toggle" data-toggle="dropdown">Dropdown <b class="caret"></b></a>
+          <li class="nav-item active"><a class="nav-link" href="#fat">@fat</a></li>
+          <li class="nav-item"><a class="nav-link" href="#mdo">@mdo</a></li>
+          <li class="dropdown nav-item">
+            <a href="#" class="dropdown-toggle nav-link" data-toggle="dropdown">Dropdown <b class="caret"></b></a>
             <ul class="dropdown-menu" role="menu">
               <li class=""><a href="#one" tabindex="-1">one</a></li>
               <li><a href="#two" tabindex="-1">two</a></li>
@@ -100,10 +91,10 @@
 
 <!-- JavaScript Includes -->
 <script src="../vendor/jquery.min.js"></script>
-<script src="../../transition.js"></script>
-<script src="../../scrollspy.js"></script>
-<script src="../../dropdown.js"></script>
-<script src="../../collapse.js"></script>
+<script src="../../dist/util.js"></script>
+<script src="../../dist/scrollspy.js"></script>
+<script src="../../dist/dropdown.js"></script>
+<script src="../../dist/collapse.js"></script>
 
 </body>
 </html>
-- 
GitLab