source: imaps-frontend/node_modules/bootstrap/js/src/scrollspy.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 8.3 KB
Line 
1/**
2 * --------------------------------------------------------------------------
3 * Bootstrap scrollspy.js
4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 * --------------------------------------------------------------------------
6 */
7
8import BaseComponent from './base-component.js'
9import EventHandler from './dom/event-handler.js'
10import SelectorEngine from './dom/selector-engine.js'
11import {
12 defineJQueryPlugin, getElement, isDisabled, isVisible
13} from './util/index.js'
14
15/**
16 * Constants
17 */
18
19const NAME = 'scrollspy'
20const DATA_KEY = 'bs.scrollspy'
21const EVENT_KEY = `.${DATA_KEY}`
22const DATA_API_KEY = '.data-api'
23
24const EVENT_ACTIVATE = `activate${EVENT_KEY}`
25const EVENT_CLICK = `click${EVENT_KEY}`
26const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
27
28const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'
29const CLASS_NAME_ACTIVE = 'active'
30
31const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'
32const SELECTOR_TARGET_LINKS = '[href]'
33const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'
34const SELECTOR_NAV_LINKS = '.nav-link'
35const SELECTOR_NAV_ITEMS = '.nav-item'
36const SELECTOR_LIST_ITEMS = '.list-group-item'
37const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`
38const SELECTOR_DROPDOWN = '.dropdown'
39const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
40
41const Default = {
42 offset: null, // TODO: v6 @deprecated, keep it for backwards compatibility reasons
43 rootMargin: '0px 0px -25%',
44 smoothScroll: false,
45 target: null,
46 threshold: [0.1, 0.5, 1]
47}
48
49const DefaultType = {
50 offset: '(number|null)', // TODO v6 @deprecated, keep it for backwards compatibility reasons
51 rootMargin: 'string',
52 smoothScroll: 'boolean',
53 target: 'element',
54 threshold: 'array'
55}
56
57/**
58 * Class definition
59 */
60
61class ScrollSpy extends BaseComponent {
62 constructor(element, config) {
63 super(element, config)
64
65 // this._element is the observablesContainer and config.target the menu links wrapper
66 this._targetLinks = new Map()
67 this._observableSections = new Map()
68 this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element
69 this._activeTarget = null
70 this._observer = null
71 this._previousScrollData = {
72 visibleEntryTop: 0,
73 parentScrollTop: 0
74 }
75 this.refresh() // initialize
76 }
77
78 // Getters
79 static get Default() {
80 return Default
81 }
82
83 static get DefaultType() {
84 return DefaultType
85 }
86
87 static get NAME() {
88 return NAME
89 }
90
91 // Public
92 refresh() {
93 this._initializeTargetsAndObservables()
94 this._maybeEnableSmoothScroll()
95
96 if (this._observer) {
97 this._observer.disconnect()
98 } else {
99 this._observer = this._getNewObserver()
100 }
101
102 for (const section of this._observableSections.values()) {
103 this._observer.observe(section)
104 }
105 }
106
107 dispose() {
108 this._observer.disconnect()
109 super.dispose()
110 }
111
112 // Private
113 _configAfterMerge(config) {
114 // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case
115 config.target = getElement(config.target) || document.body
116
117 // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only
118 config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin
119
120 if (typeof config.threshold === 'string') {
121 config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value))
122 }
123
124 return config
125 }
126
127 _maybeEnableSmoothScroll() {
128 if (!this._config.smoothScroll) {
129 return
130 }
131
132 // unregister any previous listeners
133 EventHandler.off(this._config.target, EVENT_CLICK)
134
135 EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {
136 const observableSection = this._observableSections.get(event.target.hash)
137 if (observableSection) {
138 event.preventDefault()
139 const root = this._rootElement || window
140 const height = observableSection.offsetTop - this._element.offsetTop
141 if (root.scrollTo) {
142 root.scrollTo({ top: height, behavior: 'smooth' })
143 return
144 }
145
146 // Chrome 60 doesn't support `scrollTo`
147 root.scrollTop = height
148 }
149 })
150 }
151
152 _getNewObserver() {
153 const options = {
154 root: this._rootElement,
155 threshold: this._config.threshold,
156 rootMargin: this._config.rootMargin
157 }
158
159 return new IntersectionObserver(entries => this._observerCallback(entries), options)
160 }
161
162 // The logic of selection
163 _observerCallback(entries) {
164 const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`)
165 const activate = entry => {
166 this._previousScrollData.visibleEntryTop = entry.target.offsetTop
167 this._process(targetElement(entry))
168 }
169
170 const parentScrollTop = (this._rootElement || document.documentElement).scrollTop
171 const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop
172 this._previousScrollData.parentScrollTop = parentScrollTop
173
174 for (const entry of entries) {
175 if (!entry.isIntersecting) {
176 this._activeTarget = null
177 this._clearActiveClass(targetElement(entry))
178
179 continue
180 }
181
182 const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop
183 // if we are scrolling down, pick the bigger offsetTop
184 if (userScrollsDown && entryIsLowerThanPrevious) {
185 activate(entry)
186 // if parent isn't scrolled, let's keep the first visible item, breaking the iteration
187 if (!parentScrollTop) {
188 return
189 }
190
191 continue
192 }
193
194 // if we are scrolling up, pick the smallest offsetTop
195 if (!userScrollsDown && !entryIsLowerThanPrevious) {
196 activate(entry)
197 }
198 }
199 }
200
201 _initializeTargetsAndObservables() {
202 this._targetLinks = new Map()
203 this._observableSections = new Map()
204
205 const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target)
206
207 for (const anchor of targetLinks) {
208 // ensure that the anchor has an id and is not disabled
209 if (!anchor.hash || isDisabled(anchor)) {
210 continue
211 }
212
213 const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element)
214
215 // ensure that the observableSection exists & is visible
216 if (isVisible(observableSection)) {
217 this._targetLinks.set(decodeURI(anchor.hash), anchor)
218 this._observableSections.set(anchor.hash, observableSection)
219 }
220 }
221 }
222
223 _process(target) {
224 if (this._activeTarget === target) {
225 return
226 }
227
228 this._clearActiveClass(this._config.target)
229 this._activeTarget = target
230 target.classList.add(CLASS_NAME_ACTIVE)
231 this._activateParents(target)
232
233 EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })
234 }
235
236 _activateParents(target) {
237 // Activate dropdown parents
238 if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {
239 SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN))
240 .classList.add(CLASS_NAME_ACTIVE)
241 return
242 }
243
244 for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {
245 // Set triggered links parents as active
246 // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
247 for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) {
248 item.classList.add(CLASS_NAME_ACTIVE)
249 }
250 }
251 }
252
253 _clearActiveClass(parent) {
254 parent.classList.remove(CLASS_NAME_ACTIVE)
255
256 const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent)
257 for (const node of activeNodes) {
258 node.classList.remove(CLASS_NAME_ACTIVE)
259 }
260 }
261
262 // Static
263 static jQueryInterface(config) {
264 return this.each(function () {
265 const data = ScrollSpy.getOrCreateInstance(this, config)
266
267 if (typeof config !== 'string') {
268 return
269 }
270
271 if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
272 throw new TypeError(`No method named "${config}"`)
273 }
274
275 data[config]()
276 })
277 }
278}
279
280/**
281 * Data API implementation
282 */
283
284EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
285 for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {
286 ScrollSpy.getOrCreateInstance(spy)
287 }
288})
289
290/**
291 * jQuery
292 */
293
294defineJQueryPlugin(ScrollSpy)
295
296export default ScrollSpy
Note: See TracBrowser for help on using the repository browser.