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