]>
git.ipfire.org Git - thirdparty/bootstrap.git/blob - js/src/scrollspy.js
2 * --------------------------------------------------------------------------
3 * Bootstrap (v5.1.3): scrollspy.js
4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 * --------------------------------------------------------------------------
11 getSelectorFromElement
,
14 import EventHandler
from './dom/event-handler'
15 import Manipulator
from './dom/manipulator'
16 import SelectorEngine
from './dom/selector-engine'
17 import BaseComponent
from './base-component'
20 * ------------------------------------------------------------------------
22 * ------------------------------------------------------------------------
25 const NAME
= 'scrollspy'
26 const DATA_KEY
= 'bs.scrollspy'
27 const EVENT_KEY
= `.${DATA_KEY}`
28 const DATA_API_KEY
= '.data-api'
39 target
: '(string|element)'
42 const EVENT_ACTIVATE
= `activate${EVENT_KEY}`
43 const EVENT_SCROLL
= `scroll${EVENT_KEY}`
44 const EVENT_LOAD_DATA_API
= `load${EVENT_KEY}${DATA_API_KEY}`
46 const CLASS_NAME_DROPDOWN_ITEM
= 'dropdown-item'
47 const CLASS_NAME_ACTIVE
= 'active'
49 const SELECTOR_DATA_SPY
= '[data-bs-spy="scroll"]'
50 const SELECTOR_NAV_LIST_GROUP
= '.nav, .list-group'
51 const SELECTOR_NAV_LINKS
= '.nav-link'
52 const SELECTOR_NAV_ITEMS
= '.nav-item'
53 const SELECTOR_LIST_ITEMS
= '.list-group-item'
54 const SELECTOR_LINK_ITEMS
= `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}, .${CLASS_NAME_DROPDOWN_ITEM}`
55 const SELECTOR_DROPDOWN
= '.dropdown'
56 const SELECTOR_DROPDOWN_TOGGLE
= '.dropdown-toggle'
58 const METHOD_OFFSET
= 'offset'
59 const METHOD_POSITION
= 'position'
62 * ------------------------------------------------------------------------
64 * ------------------------------------------------------------------------
67 class ScrollSpy
extends BaseComponent
{
68 constructor(element
, config
) {
70 this._scrollElement
= this._element
.tagName
=== 'BODY' ? window
: this._element
71 this._config
= this._getConfig(config
)
74 this._activeTarget
= null
75 this._scrollHeight
= 0
77 EventHandler
.on(this._scrollElement
, EVENT_SCROLL
, () => this._process())
85 static get Default() {
96 const autoMethod
= this._scrollElement
=== this._scrollElement
.window
?
100 const offsetMethod
= this._config
.method
=== 'auto' ?
104 const offsetBase
= offsetMethod
=== METHOD_POSITION
?
105 this._getScrollTop() :
110 this._scrollHeight
= this._getScrollHeight()
112 const targets
= SelectorEngine
.find(SELECTOR_LINK_ITEMS
, this._config
.target
)
114 const targetSelector
= getSelectorFromElement(element
)
115 const target
= targetSelector
? SelectorEngine
.findOne(targetSelector
) : null
118 const targetBCR
= target
.getBoundingClientRect()
119 if (targetBCR
.width
|| targetBCR
.height
) {
121 Manipulator
[offsetMethod
](target
).top
+ offsetBase
,
129 .filter(item
=> item
)
130 .sort((a
, b
) => a
[0] - b
[0])
132 for (const item
of targets
) {
133 this._offsets
.push(item
[0])
134 this._targets
.push(item
[1])
139 EventHandler
.off(this._scrollElement
, EVENT_KEY
)
148 ...Manipulator
.getDataAttributes(this._element
),
149 ...(typeof config
=== 'object' && config
? config
: {})
152 config
.target
= getElement(config
.target
) || document
.documentElement
154 typeCheckConfig(NAME
, config
, DefaultType
)
160 return this._scrollElement
=== window
?
161 this._scrollElement
.pageYOffset
:
162 this._scrollElement
.scrollTop
166 return this._scrollElement
.scrollHeight
|| Math
.max(
167 document
.body
.scrollHeight
,
168 document
.documentElement
.scrollHeight
173 return this._scrollElement
=== window
?
175 this._scrollElement
.getBoundingClientRect().height
179 const scrollTop
= this._getScrollTop() + this._config
.offset
180 const scrollHeight
= this._getScrollHeight()
181 const maxScroll
= this._config
.offset
+ scrollHeight
- this._getOffsetHeight()
183 if (this._scrollHeight
!== scrollHeight
) {
187 if (scrollTop
>= maxScroll
) {
188 const target
= this._targets
[this._targets
.length
- 1]
190 if (this._activeTarget
!== target
) {
191 this._activate(target
)
197 if (this._activeTarget
&& scrollTop
< this._offsets
[0] && this._offsets
[0] > 0) {
198 this._activeTarget
= null
203 for (let i
= this._offsets
.length
; i
--;) {
204 const isActiveTarget
= this._activeTarget
!== this._targets
[i
] &&
205 scrollTop
>= this._offsets
[i
] &&
206 (typeof this._offsets
[i
+ 1] === 'undefined' || scrollTop
< this._offsets
[i
+ 1])
208 if (isActiveTarget
) {
209 this._activate(this._targets
[i
])
215 this._activeTarget
= target
219 const queries
= SELECTOR_LINK_ITEMS
.split(',')
220 .map(selector
=> `${selector}[data-bs-target="${target}"],${selector}[href="${target}"]`)
222 const link
= SelectorEngine
.findOne(queries
.join(','), this._config
.target
)
224 link
.classList
.add(CLASS_NAME_ACTIVE
)
225 if (link
.classList
.contains(CLASS_NAME_DROPDOWN_ITEM
)) {
226 SelectorEngine
.findOne(SELECTOR_DROPDOWN_TOGGLE
, link
.closest(SELECTOR_DROPDOWN
))
227 .classList
.add(CLASS_NAME_ACTIVE
)
229 for (const listGroup
of SelectorEngine
.parents(link
, SELECTOR_NAV_LIST_GROUP
)) {
230 // Set triggered links parents as active
231 // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
232 for (const item
of SelectorEngine
.prev(listGroup
, `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`)) {
233 item
.classList
.add(CLASS_NAME_ACTIVE
)
236 // Handle special case when .nav-link is inside .nav-item
237 for (const navItem
of SelectorEngine
.prev(listGroup
, SELECTOR_NAV_ITEMS
)) {
238 for (const item
of SelectorEngine
.children(navItem
, SELECTOR_NAV_LINKS
)) {
239 item
.classList
.add(CLASS_NAME_ACTIVE
)
245 EventHandler
.trigger(this._scrollElement
, EVENT_ACTIVATE
, {
246 relatedTarget
: target
251 const activeNodes
= SelectorEngine
.find(SELECTOR_LINK_ITEMS
, this._config
.target
)
252 .filter(node
=> node
.classList
.contains(CLASS_NAME_ACTIVE
))
254 for (const node
of activeNodes
) {
255 node
.classList
.remove(CLASS_NAME_ACTIVE
)
261 static jQueryInterface(config
) {
262 return this.each(function () {
263 const data
= ScrollSpy
.getOrCreateInstance(this, config
)
265 if (typeof config
!== 'string') {
269 if (typeof data
[config
] === 'undefined') {
270 throw new TypeError(`No method named "${config}"`)
279 * ------------------------------------------------------------------------
280 * Data Api implementation
281 * ------------------------------------------------------------------------
284 EventHandler
.on(window
, EVENT_LOAD_DATA_API
, () => {
285 for (const spy
of SelectorEngine
.find(SELECTOR_DATA_SPY
)) {
286 new ScrollSpy(spy
) // eslint-disable-line no-new
291 * ------------------------------------------------------------------------
293 * ------------------------------------------------------------------------
294 * add .ScrollSpy to jQuery only if jQuery is present
297 defineJQueryPlugin(ScrollSpy
)
299 export default ScrollSpy