1 |
efrain |
1 |
/**
|
|
|
2 |
* --------------------------------------------------------------------------
|
|
|
3 |
* Bootstrap (v4.6.2): scrollspy.js
|
|
|
4 |
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
|
|
5 |
* --------------------------------------------------------------------------
|
|
|
6 |
*/
|
|
|
7 |
|
|
|
8 |
import $ from 'jquery'
|
|
|
9 |
import Util from './util'
|
|
|
10 |
|
|
|
11 |
/**
|
|
|
12 |
* Constants
|
|
|
13 |
*/
|
|
|
14 |
|
|
|
15 |
const NAME = 'scrollspy'
|
|
|
16 |
const VERSION = '4.6.2'
|
|
|
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]
|
|
|
21 |
|
|
|
22 |
const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'
|
|
|
23 |
const CLASS_NAME_ACTIVE = 'active'
|
|
|
24 |
|
|
|
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}`
|
|
|
28 |
|
|
|
29 |
const METHOD_OFFSET = 'offset'
|
|
|
30 |
const METHOD_POSITION = 'position'
|
|
|
31 |
|
|
|
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'
|
|
|
40 |
|
|
|
41 |
const Default = {
|
|
|
42 |
offset: 10,
|
|
|
43 |
method: 'auto',
|
|
|
44 |
target: ''
|
|
|
45 |
}
|
|
|
46 |
|
|
|
47 |
const DefaultType = {
|
|
|
48 |
offset: 'number',
|
|
|
49 |
method: 'string',
|
|
|
50 |
target: '(string|element)'
|
|
|
51 |
}
|
|
|
52 |
|
|
|
53 |
/**
|
|
|
54 |
* Class definition
|
|
|
55 |
*/
|
|
|
56 |
|
|
|
57 |
class ScrollSpy {
|
|
|
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}`
|
|
|
65 |
this._offsets = []
|
|
|
66 |
this._targets = []
|
|
|
67 |
this._activeTarget = null
|
|
|
68 |
this._scrollHeight = 0
|
|
|
69 |
|
|
|
70 |
$(this._scrollElement).on(EVENT_SCROLL, event => this._process(event))
|
|
|
71 |
|
|
|
72 |
this.refresh()
|
|
|
73 |
this._process()
|
|
|
74 |
}
|
|
|
75 |
|
|
|
76 |
// Getters
|
|
|
77 |
static get VERSION() {
|
|
|
78 |
return VERSION
|
|
|
79 |
}
|
|
|
80 |
|
|
|
81 |
static get Default() {
|
|
|
82 |
return Default
|
|
|
83 |
}
|
|
|
84 |
|
|
|
85 |
// Public
|
|
|
86 |
refresh() {
|
|
|
87 |
const autoMethod = this._scrollElement === this._scrollElement.window ?
|
|
|
88 |
METHOD_OFFSET : METHOD_POSITION
|
|
|
89 |
|
|
|
90 |
const offsetMethod = this._config.method === 'auto' ?
|
|
|
91 |
autoMethod : this._config.method
|
|
|
92 |
|
|
|
93 |
const offsetBase = offsetMethod === METHOD_POSITION ?
|
|
|
94 |
this._getScrollTop() : 0
|
|
|
95 |
|
|
|
96 |
this._offsets = []
|
|
|
97 |
this._targets = []
|
|
|
98 |
|
|
|
99 |
this._scrollHeight = this._getScrollHeight()
|
|
|
100 |
|
|
|
101 |
const targets = [].slice.call(document.querySelectorAll(this._selector))
|
|
|
102 |
|
|
|
103 |
targets
|
|
|
104 |
.map(element => {
|
|
|
105 |
let target
|
|
|
106 |
const targetSelector = Util.getSelectorFromElement(element)
|
|
|
107 |
|
|
|
108 |
if (targetSelector) {
|
|
|
109 |
target = document.querySelector(targetSelector)
|
|
|
110 |
}
|
|
|
111 |
|
|
|
112 |
if (target) {
|
|
|
113 |
const targetBCR = target.getBoundingClientRect()
|
|
|
114 |
if (targetBCR.width || targetBCR.height) {
|
|
|
115 |
// TODO (fat): remove sketch reliance on jQuery position/offset
|
|
|
116 |
return [
|
|
|
117 |
$(target)[offsetMethod]().top + offsetBase,
|
|
|
118 |
targetSelector
|
|
|
119 |
]
|
|
|
120 |
}
|
|
|
121 |
}
|
|
|
122 |
|
|
|
123 |
return null
|
|
|
124 |
})
|
|
|
125 |
.filter(Boolean)
|
|
|
126 |
.sort((a, b) => a[0] - b[0])
|
|
|
127 |
.forEach(item => {
|
|
|
128 |
this._offsets.push(item[0])
|
|
|
129 |
this._targets.push(item[1])
|
|
|
130 |
})
|
|
|
131 |
}
|
|
|
132 |
|
|
|
133 |
dispose() {
|
|
|
134 |
$.removeData(this._element, DATA_KEY)
|
|
|
135 |
$(this._scrollElement).off(EVENT_KEY)
|
|
|
136 |
|
|
|
137 |
this._element = null
|
|
|
138 |
this._scrollElement = null
|
|
|
139 |
this._config = null
|
|
|
140 |
this._selector = null
|
|
|
141 |
this._offsets = null
|
|
|
142 |
this._targets = null
|
|
|
143 |
this._activeTarget = null
|
|
|
144 |
this._scrollHeight = null
|
|
|
145 |
}
|
|
|
146 |
|
|
|
147 |
// Private
|
|
|
148 |
_getConfig(config) {
|
|
|
149 |
config = {
|
|
|
150 |
...Default,
|
|
|
151 |
...(typeof config === 'object' && config ? config : {})
|
|
|
152 |
}
|
|
|
153 |
|
|
|
154 |
if (typeof config.target !== 'string' && Util.isElement(config.target)) {
|
|
|
155 |
let id = $(config.target).attr('id')
|
|
|
156 |
if (!id) {
|
|
|
157 |
id = Util.getUID(NAME)
|
|
|
158 |
$(config.target).attr('id', id)
|
|
|
159 |
}
|
|
|
160 |
|
|
|
161 |
config.target = `#${id}`
|
|
|
162 |
}
|
|
|
163 |
|
|
|
164 |
Util.typeCheckConfig(NAME, config, DefaultType)
|
|
|
165 |
|
|
|
166 |
return config
|
|
|
167 |
}
|
|
|
168 |
|
|
|
169 |
_getScrollTop() {
|
|
|
170 |
return this._scrollElement === window ?
|
|
|
171 |
this._scrollElement.pageYOffset : this._scrollElement.scrollTop
|
|
|
172 |
}
|
|
|
173 |
|
|
|
174 |
_getScrollHeight() {
|
|
|
175 |
return this._scrollElement.scrollHeight || Math.max(
|
|
|
176 |
document.body.scrollHeight,
|
|
|
177 |
document.documentElement.scrollHeight
|
|
|
178 |
)
|
|
|
179 |
}
|
|
|
180 |
|
|
|
181 |
_getOffsetHeight() {
|
|
|
182 |
return this._scrollElement === window ?
|
|
|
183 |
window.innerHeight : this._scrollElement.getBoundingClientRect().height
|
|
|
184 |
}
|
|
|
185 |
|
|
|
186 |
_process() {
|
|
|
187 |
const scrollTop = this._getScrollTop() + this._config.offset
|
|
|
188 |
const scrollHeight = this._getScrollHeight()
|
|
|
189 |
const maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight()
|
|
|
190 |
|
|
|
191 |
if (this._scrollHeight !== scrollHeight) {
|
|
|
192 |
this.refresh()
|
|
|
193 |
}
|
|
|
194 |
|
|
|
195 |
if (scrollTop >= maxScroll) {
|
|
|
196 |
const target = this._targets[this._targets.length - 1]
|
|
|
197 |
|
|
|
198 |
if (this._activeTarget !== target) {
|
|
|
199 |
this._activate(target)
|
|
|
200 |
}
|
|
|
201 |
|
|
|
202 |
return
|
|
|
203 |
}
|
|
|
204 |
|
|
|
205 |
if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {
|
|
|
206 |
this._activeTarget = null
|
|
|
207 |
this._clear()
|
|
|
208 |
return
|
|
|
209 |
}
|
|
|
210 |
|
|
|
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])
|
|
|
216 |
|
|
|
217 |
if (isActiveTarget) {
|
|
|
218 |
this._activate(this._targets[i])
|
|
|
219 |
}
|
|
|
220 |
}
|
|
|
221 |
}
|
|
|
222 |
|
|
|
223 |
_activate(target) {
|
|
|
224 |
this._activeTarget = target
|
|
|
225 |
|
|
|
226 |
this._clear()
|
|
|
227 |
|
|
|
228 |
const queries = this._selector
|
|
|
229 |
.split(',')
|
|
|
230 |
.map(selector => `${selector}[data-target="${target}"],${selector}[href="${target}"]`)
|
|
|
231 |
|
|
|
232 |
const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))
|
|
|
233 |
|
|
|
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)
|
|
|
239 |
} else {
|
|
|
240 |
// Set triggered link as active
|
|
|
241 |
$link.addClass(CLASS_NAME_ACTIVE)
|
|
|
242 |
// Set triggered links parents as active
|
|
|
243 |
// With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
|
|
|
244 |
$link.parents(SELECTOR_NAV_LIST_GROUP)
|
|
|
245 |
.prev(`${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`)
|
|
|
246 |
.addClass(CLASS_NAME_ACTIVE)
|
|
|
247 |
// Handle special case when .nav-link is inside .nav-item
|
|
|
248 |
$link.parents(SELECTOR_NAV_LIST_GROUP)
|
|
|
249 |
.prev(SELECTOR_NAV_ITEMS)
|
|
|
250 |
.children(SELECTOR_NAV_LINKS)
|
|
|
251 |
.addClass(CLASS_NAME_ACTIVE)
|
|
|
252 |
}
|
|
|
253 |
|
|
|
254 |
$(this._scrollElement).trigger(EVENT_ACTIVATE, {
|
|
|
255 |
relatedTarget: target
|
|
|
256 |
})
|
|
|
257 |
}
|
|
|
258 |
|
|
|
259 |
_clear() {
|
|
|
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))
|
|
|
263 |
}
|
|
|
264 |
|
|
|
265 |
// Static
|
|
|
266 |
static _jQueryInterface(config) {
|
|
|
267 |
return this.each(function () {
|
|
|
268 |
let data = $(this).data(DATA_KEY)
|
|
|
269 |
const _config = typeof config === 'object' && config
|
|
|
270 |
|
|
|
271 |
if (!data) {
|
|
|
272 |
data = new ScrollSpy(this, _config)
|
|
|
273 |
$(this).data(DATA_KEY, data)
|
|
|
274 |
}
|
|
|
275 |
|
|
|
276 |
if (typeof config === 'string') {
|
|
|
277 |
if (typeof data[config] === 'undefined') {
|
|
|
278 |
throw new TypeError(`No method named "${config}"`)
|
|
|
279 |
}
|
|
|
280 |
|
|
|
281 |
data[config]()
|
|
|
282 |
}
|
|
|
283 |
})
|
|
|
284 |
}
|
|
|
285 |
}
|
|
|
286 |
|
|
|
287 |
/**
|
|
|
288 |
* Data API implementation
|
|
|
289 |
*/
|
|
|
290 |
|
|
|
291 |
$(window).on(EVENT_LOAD_DATA_API, () => {
|
|
|
292 |
const scrollSpys = [].slice.call(document.querySelectorAll(SELECTOR_DATA_SPY))
|
|
|
293 |
const scrollSpysLength = scrollSpys.length
|
|
|
294 |
|
|
|
295 |
for (let i = scrollSpysLength; i--;) {
|
|
|
296 |
const $spy = $(scrollSpys[i])
|
|
|
297 |
ScrollSpy._jQueryInterface.call($spy, $spy.data())
|
|
|
298 |
}
|
|
|
299 |
})
|
|
|
300 |
|
|
|
301 |
/**
|
|
|
302 |
* jQuery
|
|
|
303 |
*/
|
|
|
304 |
|
|
|
305 |
$.fn[NAME] = ScrollSpy._jQueryInterface
|
|
|
306 |
$.fn[NAME].Constructor = ScrollSpy
|
|
|
307 |
$.fn[NAME].noConflict = () => {
|
|
|
308 |
$.fn[NAME] = JQUERY_NO_CONFLICT
|
|
|
309 |
return ScrollSpy._jQueryInterface
|
|
|
310 |
}
|
|
|
311 |
|
|
|
312 |
export default ScrollSpy
|