diff options
Diffstat (limited to 'js/src/scrollspy')
| -rw-r--r-- | js/src/scrollspy/scrollspy.js | 357 | ||||
| -rw-r--r-- | js/src/scrollspy/scrollspy.spec.js | 653 |
2 files changed, 0 insertions, 1010 deletions
diff --git a/js/src/scrollspy/scrollspy.js b/js/src/scrollspy/scrollspy.js deleted file mode 100644 index fa52a1289..000000000 --- a/js/src/scrollspy/scrollspy.js +++ /dev/null @@ -1,357 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): scrollspy.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -import { - getjQuery, - getSelectorFromElement, - getUID, - makeArray, - typeCheckConfig -} from '../util/index' -import Data from '../dom/data' -import EventHandler from '../dom/event-handler' -import Manipulator from '../dom/manipulator' -import SelectorEngine from '../dom/selector-engine' - -/** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - -const NAME = 'scrollspy' -const VERSION = '4.3.1' -const DATA_KEY = 'bs.scrollspy' -const EVENT_KEY = `.${DATA_KEY}` -const DATA_API_KEY = '.data-api' - -const Default = { - offset: 10, - method: 'auto', - target: '' -} - -const DefaultType = { - offset: 'number', - method: 'string', - target: '(string|element)' -} - -const Event = { - ACTIVATE: `activate${EVENT_KEY}`, - SCROLL: `scroll${EVENT_KEY}`, - LOAD_DATA_API: `load${EVENT_KEY}${DATA_API_KEY}` -} - -const ClassName = { - DROPDOWN_ITEM: 'dropdown-item', - ACTIVE: 'active' -} - -const Selector = { - DATA_SPY: '[data-spy="scroll"]', - NAV_LIST_GROUP: '.nav, .list-group', - NAV_LINKS: '.nav-link', - NAV_ITEMS: '.nav-item', - LIST_ITEMS: '.list-group-item', - DROPDOWN: '.dropdown', - DROPDOWN_TOGGLE: '.dropdown-toggle' -} - -const OffsetMethod = { - OFFSET: 'offset', - POSITION: 'position' -} - -/** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - -class ScrollSpy { - constructor(element, config) { - this._element = element - this._scrollElement = element.tagName === 'BODY' ? window : element - this._config = this._getConfig(config) - this._selector = `${this._config.target} ${Selector.NAV_LINKS},` + - `${this._config.target} ${Selector.LIST_ITEMS},` + - `${this._config.target} .${ClassName.DROPDOWN_ITEM}` - this._offsets = [] - this._targets = [] - this._activeTarget = null - this._scrollHeight = 0 - - EventHandler.on(this._scrollElement, Event.SCROLL, event => this._process(event)) - - this.refresh() - this._process() - - Data.setData(element, DATA_KEY, this) - } - - // Getters - - static get VERSION() { - return VERSION - } - - static get Default() { - return Default - } - - // Public - - refresh() { - const autoMethod = this._scrollElement === this._scrollElement.window ? - OffsetMethod.OFFSET : - OffsetMethod.POSITION - - const offsetMethod = this._config.method === 'auto' ? - autoMethod : - this._config.method - - const offsetBase = offsetMethod === OffsetMethod.POSITION ? - this._getScrollTop() : - 0 - - this._offsets = [] - this._targets = [] - - this._scrollHeight = this._getScrollHeight() - - const targets = makeArray(SelectorEngine.find(this._selector)) - - targets - .map(element => { - let target - const targetSelector = getSelectorFromElement(element) - - if (targetSelector) { - target = SelectorEngine.findOne(targetSelector) - } - - if (target) { - const targetBCR = target.getBoundingClientRect() - if (targetBCR.width || targetBCR.height) { - return [ - Manipulator[offsetMethod](target).top + offsetBase, - targetSelector - ] - } - } - - return null - }) - .filter(item => item) - .sort((a, b) => a[0] - b[0]) - .forEach(item => { - this._offsets.push(item[0]) - this._targets.push(item[1]) - }) - } - - dispose() { - Data.removeData(this._element, DATA_KEY) - EventHandler.off(this._scrollElement, EVENT_KEY) - - this._element = null - this._scrollElement = null - this._config = null - this._selector = null - this._offsets = null - this._targets = null - this._activeTarget = null - this._scrollHeight = null - } - - // Private - - _getConfig(config) { - config = { - ...Default, - ...typeof config === 'object' && config ? config : {} - } - - if (typeof config.target !== 'string') { - let { id } = config.target - if (!id) { - id = getUID(NAME) - config.target.id = id - } - - config.target = `#${id}` - } - - typeCheckConfig(NAME, config, DefaultType) - - return config - } - - _getScrollTop() { - return this._scrollElement === window ? - this._scrollElement.pageYOffset : - this._scrollElement.scrollTop - } - - _getScrollHeight() { - return this._scrollElement.scrollHeight || Math.max( - document.body.scrollHeight, - document.documentElement.scrollHeight - ) - } - - _getOffsetHeight() { - return this._scrollElement === window ? - window.innerHeight : - this._scrollElement.getBoundingClientRect().height - } - - _process() { - const scrollTop = this._getScrollTop() + this._config.offset - const scrollHeight = this._getScrollHeight() - const maxScroll = this._config.offset + - scrollHeight - - this._getOffsetHeight() - - if (this._scrollHeight !== scrollHeight) { - this.refresh() - } - - if (scrollTop >= maxScroll) { - const target = this._targets[this._targets.length - 1] - - if (this._activeTarget !== target) { - this._activate(target) - } - - return - } - - if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) { - this._activeTarget = null - this._clear() - return - } - - const offsetLength = this._offsets.length - for (let i = offsetLength; i--;) { - const isActiveTarget = this._activeTarget !== this._targets[i] && - scrollTop >= this._offsets[i] && - (typeof this._offsets[i + 1] === 'undefined' || - scrollTop < this._offsets[i + 1]) - - if (isActiveTarget) { - this._activate(this._targets[i]) - } - } - } - - _activate(target) { - this._activeTarget = target - - this._clear() - - const queries = this._selector.split(',') - .map(selector => `${selector}[data-target="${target}"],${selector}[href="${target}"]`) - - 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) - - link.classList.add(ClassName.ACTIVE) - } else { - // Set triggered link as 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)) - }) - }) - } - - EventHandler.trigger(this._scrollElement, Event.ACTIVATE, { - relatedTarget: target - }) - } - - _clear() { - makeArray(SelectorEngine.find(this._selector)) - .filter(node => node.classList.contains(ClassName.ACTIVE)) - .forEach(node => node.classList.remove(ClassName.ACTIVE)) - } - - // Static - - static jQueryInterface(config) { - return this.each(function () { - let data = Data.getData(this, DATA_KEY) - const _config = typeof config === 'object' && config - - if (!data) { - data = new ScrollSpy(this, _config) - } - - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } - - data[config]() - } - }) - } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } -} - -/** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ - -EventHandler.on(window, Event.LOAD_DATA_API, () => { - makeArray(SelectorEngine.find(Selector.DATA_SPY)) - .forEach(spy => new ScrollSpy(spy, Manipulator.getDataAttributes(spy))) -}) - -const $ = getjQuery() - -/** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - */ -/* istanbul ignore if */ -if ($) { - 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/src/scrollspy/scrollspy.spec.js b/js/src/scrollspy/scrollspy.spec.js deleted file mode 100644 index a019a6d1a..000000000 --- a/js/src/scrollspy/scrollspy.spec.js +++ /dev/null @@ -1,653 +0,0 @@ -import ScrollSpy from './scrollspy' -import Manipulator from '../dom/manipulator' -import EventHandler from '../dom/event-handler' - -/** Test helpers */ -import { getFixture, clearFixture, createEvent, jQueryMock } from '../../tests/helpers/fixture' - -describe('ScrollSpy', () => { - let fixtureEl - - const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, spy, cb }) => { - const element = fixtureEl.querySelector(elementSelector) - const target = fixtureEl.querySelector(targetSelector) - - // add top padding to fix Chrome on Android failures - const paddingTop = 5 - const scrollHeight = Math.ceil(contentEl.scrollTop + Manipulator.position(target).top) + paddingTop - - function listener() { - expect(element.classList.contains('active')).toEqual(true) - contentEl.removeEventListener('scroll', listener) - expect(scrollSpy._process).toHaveBeenCalled() - spy.calls.reset() - cb() - } - - contentEl.addEventListener('scroll', listener) - contentEl.scrollTop = scrollHeight - } - - beforeAll(() => { - fixtureEl = getFixture() - }) - - afterEach(() => { - clearFixture() - }) - - describe('VERSION', () => { - it('should return plugin version', () => { - expect(ScrollSpy.VERSION).toEqual(jasmine.any(String)) - }) - }) - - describe('Default', () => { - it('should return plugin default config', () => { - expect(ScrollSpy.Default).toEqual(jasmine.any(Object)) - }) - }) - - describe('constructor', () => { - it('should generate an id when there is not one', () => { - fixtureEl.innerHTML = [ - '<nav></nav>', - '<div class="content"></div>' - ].join('') - - const navEl = fixtureEl.querySelector('nav') - const scrollSpy = new ScrollSpy(fixtureEl.querySelector('.content'), { - target: navEl - }) - - expect(scrollSpy).toBeDefined() - expect(navEl.getAttribute('id')).not.toEqual(null) - }) - - it('should not process element without target', () => { - fixtureEl.innerHTML = [ - '<nav id="navigation" class="navbar">', - ' <ul class="navbar-nav">', - ' <li class="nav-item active"><a class="nav-link" id="one-link" href="#">One</a></li>', - ' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>', - ' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>', - ' </ul>', - '</nav>', - '<div id="content" style="height: 200px; overflow-y: auto;">', - ' <div id="two" style="height: 300px;"></div>', - ' <div id="three" style="height: 10px;"></div>', - '</div>' - ].join('') - - const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { - target: '#navigation' - }) - - expect(scrollSpy._targets.length).toEqual(2) - }) - - it('should only switch "active" class on current target', done => { - fixtureEl.innerHTML = [ - '<div id="root" class="active" style="display: block">', - ' <div class="topbar">', - ' <div class="topbar-inner">', - ' <div class="container" id="ss-target">', - ' <ul class="nav">', - ' <li class="nav-item"><a href="#masthead">Overview</a></li>', - ' <li class="nav-item"><a href="#detail">Detail</a></li>', - ' </ul>', - ' </div>', - ' </div>', - ' </div>', - ' <div id="scrollspy-example" style="height: 100px; overflow: auto;">', - ' <div style="height: 200px;">', - ' <h4 id="masthead">Overview</h4>', - ' <p style="height: 200px;"></p>', - ' </div>', - ' <div style="height: 200px;">', - ' <h4 id="detail">Detail</h4>', - ' <p style="height: 200px;"></p>', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') - const rootEl = fixtureEl.querySelector('#root') - const scrollSpy = new ScrollSpy(scrollSpyEl, { - target: 'ss-target' - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - scrollSpyEl.addEventListener('scroll', () => { - expect(rootEl.classList.contains('active')).toEqual(true) - expect(scrollSpy._process).toHaveBeenCalled() - done() - }) - - scrollSpyEl.scrollTop = 350 - }) - - it('should only switch "active" class on current target specified w element', done => { - fixtureEl.innerHTML = [ - '<div id="root" class="active" style="display: block">', - ' <div class="topbar">', - ' <div class="topbar-inner">', - ' <div class="container" id="ss-target">', - ' <ul class="nav">', - ' <li class="nav-item"><a href="#masthead">Overview</a></li>', - ' <li class="nav-item"><a href="#detail">Detail</a></li>', - ' </ul>', - ' </div>', - ' </div>', - ' </div>', - ' <div id="scrollspy-example" style="height: 100px; overflow: auto;">', - ' <div style="height: 200px;">', - ' <h4 id="masthead">Overview</h4>', - ' <p style="height: 200px;"></p>', - ' </div>', - ' <div style="height: 200px;">', - ' <h4 id="detail">Detail</h4>', - ' <p style="height: 200px;"></p>', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') - const rootEl = fixtureEl.querySelector('#root') - const scrollSpy = new ScrollSpy(scrollSpyEl, { - target: fixtureEl.querySelector('#ss-target') - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - scrollSpyEl.addEventListener('scroll', () => { - expect(rootEl.classList.contains('active')).toEqual(true) - expect(scrollSpy._process).toHaveBeenCalled() - done() - }) - - scrollSpyEl.scrollTop = 350 - }) - - it('should correctly select middle navigation option when large offset is used', done => { - fixtureEl.innerHTML = [ - '<div id="header" style="height: 500px;"></div>', - '<nav id="navigation" class="navbar">', - ' <ul class="navbar-nav">', - ' <li class="nav-item active"><a class="nav-link" id="one-link" href="#one">One</a></li>', - ' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>', - ' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>', - ' </ul>', - '</nav>', - '<div id="content" style="height: 200px; overflow-y: auto;">', - ' <div id="one" style="height: 500px;"></div>', - ' <div id="two" style="height: 300px;"></div>', - ' <div id="three" style="height: 10px;"></div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: Manipulator.position(contentEl).top - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - contentEl.addEventListener('scroll', () => { - expect(fixtureEl.querySelector('#one-link').classList.contains('active')).toEqual(false) - expect(fixtureEl.querySelector('#two-link').classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('#three-link').classList.contains('active')).toEqual(false) - expect(scrollSpy._process).toHaveBeenCalled() - done() - }) - - contentEl.scrollTop = 550 - }) - - it('should add the active class to the correct element', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <ul class="nav">', - ' <li class="nav-item"><a class="nav-link" id="a-1" href="#div-1">div 1</a></li>', - ' <li class="nav-item"><a class="nav-link" id="a-2" href="#div-2">div 2</a></li>', - ' </ul>', - '</nav>', - '<div class="content" style="overflow: auto; height: 50px">', - ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - testElementIsActiveAfterScroll({ - elementSelector: '#a-1', - targetSelector: '#div-1', - contentEl, - scrollSpy, - spy, - cb: () => { - testElementIsActiveAfterScroll({ - elementSelector: '#a-2', - targetSelector: '#div-2', - contentEl, - scrollSpy, - spy, - cb: () => done() - }) - } - }) - }) - - it('should add the active class to the correct element (nav markup)', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <nav class="nav">', - ' <a class="nav-link" id="a-1" href="#div-1">div 1</a>', - ' <a class="nav-link" id="a-2" href="#div-2">div 2</a>', - ' </nav>', - '</nav>', - '<div class="content" style="overflow: auto; height: 50px">', - ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - testElementIsActiveAfterScroll({ - elementSelector: '#a-1', - targetSelector: '#div-1', - contentEl, - scrollSpy, - spy, - cb: () => { - testElementIsActiveAfterScroll({ - elementSelector: '#a-2', - targetSelector: '#div-2', - contentEl, - scrollSpy, - spy, - cb: () => done() - }) - } - }) - }) - - it('should add the active class to the correct element (list-group markup)', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <div class="list-group">', - ' <a class="list-group-item" id="a-1" href="#div-1">div 1</a>', - ' <a class="list-group-item" id="a-2" href="#div-2">div 2</a>', - ' </div>', - '</nav>', - '<div class="content" style="overflow: auto; height: 50px">', - ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - testElementIsActiveAfterScroll({ - elementSelector: '#a-1', - targetSelector: '#div-1', - contentEl, - scrollSpy, - spy, - cb: () => { - testElementIsActiveAfterScroll({ - elementSelector: '#a-2', - targetSelector: '#div-2', - contentEl, - scrollSpy, - spy, - cb: () => done() - }) - } - }) - }) - - it('should clear selection if above the first section', done => { - fixtureEl.innerHTML = [ - '<div id="header" style="height: 500px;"></div>', - '<nav id="navigation" class="navbar">', - ' <ul class="navbar-nav">', - ' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>', - ' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>', - ' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>', - ' </ul>', - '</nav>', - '<div id="content" style="height: 200px; overflow-y: auto;">', - ' <div id="spacer" style="height: 100px;"></div>', - ' <div id="one" style="height: 100px;"></div>', - ' <div id="two" style="height: 100px;"></div>', - ' <div id="three" style="height: 100px;"></div>', - ' <div id="spacer" style="height: 100px;"></div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: Manipulator.position(contentEl).top - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - let firstTime = true - - contentEl.addEventListener('scroll', () => { - const active = fixtureEl.querySelector('.active') - - expect(spy).toHaveBeenCalled() - spy.calls.reset() - if (firstTime) { - expect(fixtureEl.querySelectorAll('.active').length).toEqual(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = 0 - } else { - expect(active).toBeNull() - done() - } - }) - - contentEl.scrollTop = 201 - }) - - it('should not clear selection if above the first section and first section is at the top', done => { - fixtureEl.innerHTML = [ - '<div id="header" style="height: 500px;"></div>', - '<nav id="navigation" class="navbar">', - ' <ul class="navbar-nav">', - ' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>', - ' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>', - ' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>', - ' </ul>', - '</nav>', - '<div id="content" style="height: 200px; overflow-y: auto;">', - ' <div id="one" style="height: 100px;"></div>', - ' <div id="two" style="height: 100px;"></div>', - ' <div id="three" style="height: 100px;"></div>', - ' <div id="spacer" style="height: 100px;"></div>', - '</div>' - ].join('') - - const negativeHeight = -10 - const startOfSectionTwo = 101 - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: contentEl.offsetTop - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - let firstTime = true - - contentEl.addEventListener('scroll', () => { - const active = fixtureEl.querySelector('.active') - - expect(spy).toHaveBeenCalled() - spy.calls.reset() - if (firstTime) { - expect(fixtureEl.querySelectorAll('.active').length).toEqual(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = negativeHeight - } else { - expect(fixtureEl.querySelectorAll('.active').length).toEqual(1) - expect(active.getAttribute('id')).toEqual('one-link') - done() - } - }) - - contentEl.scrollTop = startOfSectionTwo - }) - - it('should correctly select navigation element on backward scrolling when each target section height is 100%', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <ul class="nav">', - ' <li class="nav-item"><a id="li-100-1" class="nav-link" href="#div-100-1">div 1</a></li>', - ' <li class="nav-item"><a id="li-100-2" class="nav-link" href="#div-100-2">div 2</a></li>', - ' <li class="nav-item"><a id="li-100-3" class="nav-link" href="#div-100-3">div 3</a></li>', - ' <li class="nav-item"><a id="li-100-4" class="nav-link" href="#div-100-4">div 4</a></li>', - ' <li class="nav-item"><a id="li-100-5" class="nav-link" href="#div-100-5">div 5</a></li>', - ' </ul>', - '</nav>', - '<div class="content" style="position: relative; overflow: auto; height: 100px">', - ' <div id="div-100-1" style="position: relative; height: 100%; padding: 0; margin: 0">div 1</div>', - ' <div id="div-100-2" style="position: relative; height: 100%; padding: 0; margin: 0">div 2</div>', - ' <div id="div-100-3" style="position: relative; height: 100%; padding: 0; margin: 0">div 3</div>', - ' <div id="div-100-4" style="position: relative; height: 100%; padding: 0; margin: 0">div 4</div>', - ' <div id="div-100-5" style="position: relative; height: 100%; padding: 0; margin: 0">div 5</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-5', - targetSelector: '#div-100-5', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-4', - targetSelector: '#div-100-4', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-3', - targetSelector: '#div-100-3', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-2', - targetSelector: '#div-100-2', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-1', - targetSelector: '#div-100-1', - scrollSpy, - spy, - contentEl, - cb: done - }) - } - }) - } - }) - } - }) - } - }) - }) - - it('should allow passed in option offset method: offset', () => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <ul class="nav">', - ' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>', - ' <li class="nav-item"><a id="li-jsm-2" class="nav-link" href="#div-jsm-2">div 2</a></li>', - ' <li class="nav-item"><a id="li-jsm-3" class="nav-link" href="#div-jsm-3">div 3</a></li>', - ' </ul>', - '</nav>', - '<div class="content" style="position: relative; overflow: auto; height: 100px">', - ' <div id="div-jsm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-jsm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>', - ' <div id="div-jsm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const targetEl = fixtureEl.querySelector('#div-jsm-2') - const scrollSpy = new ScrollSpy(contentEl, { - target: '.navbar', - offset: 0, - method: 'offset' - }) - - expect(scrollSpy._offsets[1]).toEqual(Manipulator.offset(targetEl).top) - expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.position(targetEl).top) - }) - - it('should allow passed in option offset method: position', () => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <ul class="nav">', - ' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>', - ' <li class="nav-item"><a id="li-jsm-2" class="nav-link" href="#div-jsm-2">div 2</a></li>', - ' <li class="nav-item"><a id="li-jsm-3" class="nav-link" href="#div-jsm-3">div 3</a></li>', - ' </ul>', - '</nav>', - '<div class="content" style="position: relative; overflow: auto; height: 100px">', - ' <div id="div-jsm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-jsm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>', - ' <div id="div-jsm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const targetEl = fixtureEl.querySelector('#div-jsm-2') - const scrollSpy = new ScrollSpy(contentEl, { - target: '.navbar', - offset: 0, - method: 'position' - }) - - expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.offset(targetEl).top) - expect(scrollSpy._offsets[1]).toEqual(Manipulator.position(targetEl).top) - }) - }) - - describe('dispose', () => { - it('should dispose a scrollspy', () => { - spyOn(EventHandler, 'off') - fixtureEl.innerHTML = '<div style="display: none;"></div>' - - const divEl = fixtureEl.querySelector('div') - const scrollSpy = new ScrollSpy(divEl) - - scrollSpy.dispose() - expect(EventHandler.off).toHaveBeenCalledWith(divEl, '.bs.scrollspy') - }) - }) - - describe('jQueryInterface', () => { - it('should create a scrollspy', () => { - fixtureEl.innerHTML = '<div></div>' - - const div = fixtureEl.querySelector('div') - - jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.scrollspy.call(jQueryMock) - - expect(ScrollSpy.getInstance(div)).toBeDefined() - }) - - it('should not re create a scrollspy', () => { - fixtureEl.innerHTML = '<div></div>' - - const div = fixtureEl.querySelector('div') - const scrollSpy = new ScrollSpy(div) - - jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.scrollspy.call(jQueryMock) - - expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy) - }) - - it('should call a scrollspy method', () => { - fixtureEl.innerHTML = '<div></div>' - - const div = fixtureEl.querySelector('div') - const scrollSpy = new ScrollSpy(div) - - spyOn(scrollSpy, 'refresh') - - jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface - jQueryMock.elements = [div] - - jQueryMock.fn.scrollspy.call(jQueryMock, 'refresh') - - expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy) - expect(scrollSpy.refresh).toHaveBeenCalled() - }) - - it('should throw error on undefined method', () => { - fixtureEl.innerHTML = '<div></div>' - - const div = fixtureEl.querySelector('div') - const action = 'undefinedMethod' - - jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface - jQueryMock.elements = [div] - - try { - jQueryMock.fn.scrollspy.call(jQueryMock, action) - } catch (error) { - expect(error.message).toEqual(`No method named "${action}"`) - } - }) - }) - - describe('getInstance', () => { - it('should return null if there is no instance', () => { - expect(ScrollSpy.getInstance(fixtureEl)).toEqual(null) - }) - }) - - describe('event handler', () => { - it('should create scrollspy on window load event', () => { - fixtureEl.innerHTML = '<div data-spy="scroll"></div>' - - const scrollSpyEl = fixtureEl.querySelector('div') - - window.dispatchEvent(createEvent('load')) - - expect(ScrollSpy.getInstance(scrollSpyEl)).not.toBeNull() - }) - }) -}) |
