9
import Util from './util'
15
const NAME = 'scrollspy'
16
const VERSION = '4.6.1'
17
const DATA_KEY = 'bs.scrollspy'
18
const EVENT_KEY = `.${DATA_KEY}`
19
const DATA_API_KEY = '.data-api'
20
const JQUERY_NO_CONFLICT = $.fn[NAME]
22
const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'
23
const CLASS_NAME_ACTIVE = 'active'
25
const EVENT_ACTIVATE = `activate${EVENT_KEY}`
26
const EVENT_SCROLL = `scroll${EVENT_KEY}`
27
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
29
const METHOD_OFFSET = 'offset'
30
const METHOD_POSITION = 'position'
32
const SELECTOR_DATA_SPY = '[data-spy="scroll"]'
33
const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'
34
const SELECTOR_NAV_LINKS = '.nav-link'
35
const SELECTOR_NAV_ITEMS = '.nav-item'
36
const SELECTOR_LIST_ITEMS = '.list-group-item'
37
const SELECTOR_DROPDOWN = '.dropdown'
38
const SELECTOR_DROPDOWN_ITEMS = '.dropdown-item'
39
const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
50
target: '(string|element)'
58
constructor(element, config) {
59
this._element = element
60
this._scrollElement = element.tagName === 'BODY' ? window : element
61
this._config = this._getConfig(config)
62
this._selector = `${this._config.target} ${SELECTOR_NAV_LINKS},` +
63
`${this._config.target} ${SELECTOR_LIST_ITEMS},` +
64
`${this._config.target} ${SELECTOR_DROPDOWN_ITEMS}`
67
this._activeTarget = null
68
this._scrollHeight = 0
70
$(this._scrollElement).on(EVENT_SCROLL, event => this._process(event))
77
static get VERSION() {
81
static get Default() {
87
const autoMethod = this._scrollElement === this._scrollElement.window ?
88
METHOD_OFFSET : METHOD_POSITION
90
const offsetMethod = this._config.method === 'auto' ?
91
autoMethod : this._config.method
93
const offsetBase = offsetMethod === METHOD_POSITION ?
94
this._getScrollTop() : 0
99
this._scrollHeight = this._getScrollHeight()
101
const targets = [].slice.call(document.querySelectorAll(this._selector))
106
const targetSelector = Util.getSelectorFromElement(element)
108
if (targetSelector) {
109
target = document.querySelector(targetSelector)
113
const targetBCR = target.getBoundingClientRect()
114
if (targetBCR.width || targetBCR.height) {
117
$(target)[offsetMethod]().top + offsetBase,
125
.filter(item => item)
126
.sort((a, b) => a[0] - b[0])
128
this._offsets.push(item[0])
129
this._targets.push(item[1])
134
$.removeData(this._element, DATA_KEY)
135
$(this._scrollElement).off(EVENT_KEY)
138
this._scrollElement = null
140
this._selector = null
143
this._activeTarget = null
144
this._scrollHeight = null
151
...(typeof config === 'object' && config ? config : {})
154
if (typeof config.target !== 'string' && Util.isElement(config.target)) {
155
let id = $(config.target).attr('id')
157
id = Util.getUID(NAME)
158
$(config.target).attr('id', id)
161
config.target = `#${id}`
164
Util.typeCheckConfig(NAME, config, DefaultType)
170
return this._scrollElement === window ?
171
this._scrollElement.pageYOffset : this._scrollElement.scrollTop
175
return this._scrollElement.scrollHeight || Math.max(
176
document.body.scrollHeight,
177
document.documentElement.scrollHeight
182
return this._scrollElement === window ?
183
window.innerHeight : this._scrollElement.getBoundingClientRect().height
187
const scrollTop = this._getScrollTop() + this._config.offset
188
const scrollHeight = this._getScrollHeight()
189
const maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight()
191
if (this._scrollHeight !== scrollHeight) {
195
if (scrollTop >= maxScroll) {
196
const target = this._targets[this._targets.length - 1]
198
if (this._activeTarget !== target) {
199
this._activate(target)
205
if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {
206
this._activeTarget = null
211
for (let i = this._offsets.length; i--;) {
212
const isActiveTarget = this._activeTarget !== this._targets[i] &&
213
scrollTop >= this._offsets[i] &&
214
(typeof this._offsets[i + 1] === 'undefined' ||
215
scrollTop < this._offsets[i + 1])
217
if (isActiveTarget) {
218
this._activate(this._targets[i])
224
this._activeTarget = target
228
const queries = this._selector
230
.map(selector => `${selector}[data-target="${target}"],${selector}[href="${target}"]`)
232
const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))
234
if ($link.hasClass(CLASS_NAME_DROPDOWN_ITEM)) {
235
$link.closest(SELECTOR_DROPDOWN)
236
.find(SELECTOR_DROPDOWN_TOGGLE)
237
.addClass(CLASS_NAME_ACTIVE)
238
$link.addClass(CLASS_NAME_ACTIVE)
241
$link.addClass(CLASS_NAME_ACTIVE)
244
$link.parents(SELECTOR_NAV_LIST_GROUP)
245
.prev(`${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`)
246
.addClass(CLASS_NAME_ACTIVE)
248
$link.parents(SELECTOR_NAV_LIST_GROUP)
249
.prev(SELECTOR_NAV_ITEMS)
250
.children(SELECTOR_NAV_LINKS)
251
.addClass(CLASS_NAME_ACTIVE)
254
$(this._scrollElement).trigger(EVENT_ACTIVATE, {
255
relatedTarget: target
260
[].slice.call(document.querySelectorAll(this._selector))
261
.filter(node => node.classList.contains(CLASS_NAME_ACTIVE))
262
.forEach(node => node.classList.remove(CLASS_NAME_ACTIVE))
266
static _jQueryInterface(config) {
267
return this.each(function () {
268
let data = $(this).data(DATA_KEY)
269
const _config = typeof config === 'object' && config
272
data = new ScrollSpy(this, _config)
273
$(this).data(DATA_KEY, data)
276
if (typeof config === 'string') {
277
if (typeof data[config] === 'undefined') {
278
throw new TypeError(`No method named "${config}"`)
291
$(window).on(EVENT_LOAD_DATA_API, () => {
292
const scrollSpys = [].slice.call(document.querySelectorAll(SELECTOR_DATA_SPY))
293
const scrollSpysLength = scrollSpys.length
295
for (let i = scrollSpysLength; i--;) {
296
const $spy = $(scrollSpys[i])
297
ScrollSpy._jQueryInterface.call($spy, $spy.data())
305
$.fn[NAME] = ScrollSpy._jQueryInterface
306
$.fn[NAME].Constructor = ScrollSpy
307
$.fn[NAME].noConflict = () => {
308
$.fn[NAME] = JQUERY_NO_CONFLICT
309
return ScrollSpy._jQueryInterface
312
export default ScrollSpy