From 0263d1742ce8ad25f0f2de30beebae69b2f55f10 Mon Sep 17 00:00:00 2001
From: Alessandro Chitolina <alekitto@gmail.com>
Date: Mon, 25 Sep 2017 09:09:01 +0200
Subject: [PATCH] rewritten scrollspy without jquery

---
 js/src/dom/manipulator.js      | 16 ++++++
 js/src/dom/selectorEngine.js   | 42 ++++++++++++++--
 js/src/scrollspy.js            | 91 ++++++++++++++++++++--------------
 js/tests/unit/scrollspy.js     | 14 +++---
 js/tests/visual/scrollspy.html |  3 ++
 5 files changed, 117 insertions(+), 49 deletions(-)

diff --git a/js/src/dom/manipulator.js b/js/src/dom/manipulator.js
index 215837bf67..201902b77e 100644
--- a/js/src/dom/manipulator.js
+++ b/js/src/dom/manipulator.js
@@ -40,6 +40,22 @@ const Manipulator = {
     element.removeAttribute(`data-${key.replace(/[A-Z]/g, (chr) => `-${chr.toLowerCase()}`)}`)
   },
 
+  offset(element) {
+    const rect = element.getBoundingClientRect()
+
+    return {
+      top: rect.top + document.body.scrollTop,
+      left: rect.left + document.body.scrollLeft
+    }
+  },
+
+  position(element) {
+    return {
+      top: element.offsetTop,
+      left: element.offsetLeft
+    }
+  },
+
   toggleClass(element, className) {
     if (typeof element === 'undefined' || element === null) {
       return
diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js
index a3e88ad6eb..e515164458 100644
--- a/js/src/dom/selectorEngine.js
+++ b/js/src/dom/selectorEngine.js
@@ -111,10 +111,6 @@ const SelectorEngine = (() => {
         return null
       }
 
-      if (selector.indexOf('#') === 0) {
-        return SelectorEngine.findOne(selector, element)
-      }
-
       return findFn.call(element, selector)
     },
 
@@ -135,8 +131,46 @@ const SelectorEngine = (() => {
       return children.filter((child) => this.matches(child, selector))
     },
 
+    parents(element, selector) {
+      if (typeof selector !== 'string') {
+        return null
+      }
+
+      const parents = []
+
+      let ancestor = element.parentNode
+      while (ancestor && ancestor.nodeType === Node.ELEMENT_NODE) {
+        if (fnMatches.call(ancestor, selector)) {
+          parents.push(ancestor)
+        }
+
+        ancestor = ancestor.parentNode
+      }
+
+      return parents
+    },
+
     closest(element, selector) {
       return fnClosest(element, selector)
+    },
+
+    prev(element, selector) {
+      if (typeof selector !== 'string') {
+        return null
+      }
+
+      const siblings = []
+
+      let previous = element.previousSibling
+      while (previous) {
+        if (fnMatches.call(previous, selector)) {
+          siblings.push(previous)
+        }
+
+        previous = previous.previousSibling
+      }
+
+      return siblings
     }
   }
 })()
diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js
index e8cd6bf98c..ea6d528157 100644
--- a/js/src/scrollspy.js
+++ b/js/src/scrollspy.js
@@ -5,7 +5,10 @@
  * --------------------------------------------------------------------------
  */
 
-import $ from 'jquery'
+import Data from './dom/data'
+import EventHandler from './dom/eventHandler'
+import Manipulator from './dom/manipulator'
+import SelectorEngine from './dom/selectorEngine'
 import Util from './util'
 
 /**
@@ -19,7 +22,6 @@ const VERSION            = '4.3.1'
 const DATA_KEY           = 'bs.scrollspy'
 const EVENT_KEY          = `.${DATA_KEY}`
 const DATA_API_KEY       = '.data-api'
-const JQUERY_NO_CONFLICT = $.fn[NAME]
 
 const Default = {
   offset : 10,
@@ -81,10 +83,12 @@ class ScrollSpy {
     this._activeTarget  = null
     this._scrollHeight  = 0
 
-    $(this._scrollElement).on(Event.SCROLL, (event) => this._process(event))
+    EventHandler.on(this._scrollElement, Event.SCROLL, (event) => this._process(event))
 
     this.refresh()
     this._process()
+
+    Data.setData(element, DATA_KEY, this)
   }
 
   // Getters
@@ -114,7 +118,7 @@ class ScrollSpy {
 
     this._scrollHeight = this._getScrollHeight()
 
-    const targets = [].slice.call(document.querySelectorAll(this._selector))
+    const targets = Util.makeArray(document.querySelectorAll(this._selector))
 
     targets
       .map((element) => {
@@ -130,7 +134,7 @@ class ScrollSpy {
           if (targetBCR.width || targetBCR.height) {
             // TODO (fat): remove sketch reliance on jQuery position/offset
             return [
-              $(target)[offsetMethod]().top + offsetBase,
+              Manipulator[offsetMethod](target).top + offsetBase,
               targetSelector
             ]
           }
@@ -146,8 +150,8 @@ class ScrollSpy {
   }
 
   dispose() {
-    $.removeData(this._element, DATA_KEY)
-    $(this._scrollElement).off(EVENT_KEY)
+    Data.removeData(this._element, DATA_KEY)
+    EventHandler.off(this._scrollElement, EVENT_KEY)
 
     this._element       = null
     this._scrollElement = null
@@ -168,10 +172,10 @@ class ScrollSpy {
     }
 
     if (typeof config.target !== 'string') {
-      let id = $(config.target).attr('id')
+      let id = config.target.id
       if (!id) {
         id = Util.getUID(NAME)
-        $(config.target).attr('id', id)
+        config.target.id = id
       }
       config.target = `#${id}`
     }
@@ -242,32 +246,45 @@ class ScrollSpy {
 
     this._clear()
 
-    const queries = this._selector
-      .split(',')
+    const queries = this._selector.split(',')
       .map((selector) => `${selector}[data-target="${target}"],${selector}[href="${target}"]`)
 
-    const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))
+    const link = SelectorEngine.findOne(queries.join(','))
+
+    if (link.classList.contains(ClassName.DROPDOWN_ITEM)) {
+      SelectorEngine
+        .findOne(Selector.DROPDOWN_TOGGLE, SelectorEngine.closest(link, Selector.DROPDOWN))
+        .classList.add(ClassName.ACTIVE)
 
-    if ($link.hasClass(ClassName.DROPDOWN_ITEM)) {
-      $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE)
-      $link.addClass(ClassName.ACTIVE)
+      link.classList.add(ClassName.ACTIVE)
     } else {
       // Set triggered link as active
-      $link.addClass(ClassName.ACTIVE)
-      // Set triggered links parents as active
-      // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
-      $link.parents(Selector.NAV_LIST_GROUP).prev(`${Selector.NAV_LINKS}, ${Selector.LIST_ITEMS}`).addClass(ClassName.ACTIVE)
-      // Handle special case when .nav-link is inside .nav-item
-      $link.parents(Selector.NAV_LIST_GROUP).prev(Selector.NAV_ITEMS).children(Selector.NAV_LINKS).addClass(ClassName.ACTIVE)
+      link.classList.add(ClassName.ACTIVE)
+
+      SelectorEngine
+        .parents(link, Selector.NAV_LIST_GROUP)
+        .forEach((listGroup) => {
+          // Set triggered links parents as active
+          // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
+          SelectorEngine.prev(listGroup, `${Selector.NAV_LINKS}, ${Selector.LIST_ITEMS}`)
+            .forEach((item) => item.classList.add(ClassName.ACTIVE))
+
+          // Handle special case when .nav-link is inside .nav-item
+          SelectorEngine.prev(listGroup, Selector.NAV_ITEMS)
+            .forEach((navItem) => {
+              SelectorEngine.children(navItem, Selector.NAV_LINKS)
+                .forEach((item) => item.classList.add(ClassName.ACTIVE))
+            })
+        })
     }
 
-    $(this._scrollElement).trigger(Event.ACTIVATE, {
+    EventHandler.trigger(this._scrollElement, Event.ACTIVATE, {
       relatedTarget: target
     })
   }
 
   _clear() {
-    [].slice.call(document.querySelectorAll(this._selector))
+    Util.makeArray(document.querySelectorAll(this._selector))
       .filter((node) => node.classList.contains(ClassName.ACTIVE))
       .forEach((node) => node.classList.remove(ClassName.ACTIVE))
   }
@@ -276,12 +293,11 @@ class ScrollSpy {
 
   static _jQueryInterface(config) {
     return this.each(function () {
-      let data = $(this).data(DATA_KEY)
+      let data = Data.getData(this, DATA_KEY)
       const _config = typeof config === 'object' && config
 
       if (!data) {
         data = new ScrollSpy(this, _config)
-        $(this).data(DATA_KEY, data)
       }
 
       if (typeof config === 'string') {
@@ -300,14 +316,9 @@ class ScrollSpy {
  * ------------------------------------------------------------------------
  */
 
-$(window).on(Event.LOAD_DATA_API, () => {
-  const scrollSpys = [].slice.call(document.querySelectorAll(Selector.DATA_SPY))
-  const scrollSpysLength = scrollSpys.length
-
-  for (let i = scrollSpysLength; i--;) {
-    const $spy = $(scrollSpys[i])
-    ScrollSpy._jQueryInterface.call($spy, $spy.data())
-  }
+EventHandler.on(window, Event.LOAD_DATA_API, () => {
+  Util.makeArray(SelectorEngine.find(Selector.DATA_SPY))
+    .forEach((spy) => new ScrollSpy(spy, Util.getDataAttributes(spy)))
 })
 
 /**
@@ -316,11 +327,15 @@ $(window).on(Event.LOAD_DATA_API, () => {
  * ------------------------------------------------------------------------
  */
 
-$.fn[NAME] = ScrollSpy._jQueryInterface
-$.fn[NAME].Constructor = ScrollSpy
-$.fn[NAME].noConflict = () => {
-  $.fn[NAME] = JQUERY_NO_CONFLICT
-  return ScrollSpy._jQueryInterface
+const $ = Util.jQuery
+if (typeof $ !== 'undefined') {
+  const JQUERY_NO_CONFLICT = $.fn[NAME]
+  $.fn[NAME]               = ScrollSpy._jQueryInterface
+  $.fn[NAME].Constructor   = ScrollSpy
+  $.fn[NAME].noConflict    = () => {
+    $.fn[NAME] = JQUERY_NO_CONFLICT
+    return ScrollSpy._jQueryInterface
+  }
 }
 
 export default ScrollSpy
diff --git a/js/tests/unit/scrollspy.js b/js/tests/unit/scrollspy.js
index 1337e585d4..7470a94c2d 100644
--- a/js/tests/unit/scrollspy.js
+++ b/js/tests/unit/scrollspy.js
@@ -80,7 +80,7 @@ $(function () {
       .show()
       .find('#scrollspy-example')
       .bootstrapScrollspy({
-        target: '#ss-target'
+        target: 'ss-target'
       })
 
     $scrollspy.one('scroll', function () {
@@ -127,7 +127,7 @@ $(function () {
       .show()
       .find('#scrollspy-example')
       .bootstrapScrollspy({
-        target: document.getElementById('#ss-target')
+        target: document.getElementById('ss-target')
       })
 
     $scrollspy.one('scroll', function () {
@@ -557,7 +557,7 @@ $(function () {
     $scrollspy
       .bootstrapScrollspy({
         target: '#navigation',
-        offset: $scrollspy.position().top
+        offset: $scrollspy[0].offsetTop
       })
       .one('scroll', function () {
         assert.strictEqual($('.active').length, 1, '"active" class on only one element present')
@@ -663,11 +663,11 @@ $(function () {
           method: 'offset'
         })
       } else if (type === 'data') {
-        $(window).trigger('load')
+        EventHandler.trigger(window, 'load')
       }
 
       var $target = $('#div-' + type + 'm-2')
-      var scrollspy = $content.data('bs.scrollspy')
+      var scrollspy = Data.getData($content[0], 'bs.scrollspy')
 
       assert.ok(scrollspy._offsets[1] === $target.offset().top, 'offset method with ' + type + ' option')
       assert.ok(scrollspy._offsets[1] !== $target.position().top, 'position method with ' + type + ' option')
@@ -710,11 +710,11 @@ $(function () {
           method: 'position'
         })
       } else if (type === 'data') {
-        $(window).trigger('load')
+        EventHandler.trigger(window, 'load')
       }
 
       var $target = $('#div-' + type + 'm-2')
-      var scrollspy = $content.data('bs.scrollspy')
+      var scrollspy = Data.getData($content[0], 'bs.scrollspy')
 
       assert.ok(scrollspy._offsets[1] !== $target.offset().top, 'offset method with ' + type + ' option')
       assert.ok(scrollspy._offsets[1] === $target.position().top, 'position method with ' + type + ' option')
diff --git a/js/tests/visual/scrollspy.html b/js/tests/visual/scrollspy.html
index f0149198d7..d526af6d2f 100644
--- a/js/tests/visual/scrollspy.html
+++ b/js/tests/visual/scrollspy.html
@@ -88,7 +88,10 @@
 
     <script src="../../../node_modules/jquery/dist/jquery.slim.min.js"></script>
     <script src="../../../site/docs/4.2/assets/js/vendor/popper.min.js"></script>
+    <script src="../../dist/dom/data.js"></script>
     <script src="../../dist/dom/eventHandler.js"></script>
+    <script src="../../dist/dom/manipulator.js"></script>
+    <script src="../../dist/dom/selectorEngine.js"></script>
     <script src="../../dist/util.js"></script>
     <script src="../../dist/scrollspy.js"></script>
     <script src="../../dist/dropdown.js"></script>
-- 
GitLab