| 1 | efrain | 1 | /**
 | 
        
           |  |  | 2 |  * --------------------------------------------------------------------------
 | 
        
           | 1441 | ariadna | 3 |  * Bootstrap scrollspy.js
 | 
        
           | 1 | efrain | 4 |  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
 | 
        
           |  |  | 5 |  * --------------------------------------------------------------------------
 | 
        
           |  |  | 6 |  */
 | 
        
           |  |  | 7 |   | 
        
           | 1441 | ariadna | 8 | import BaseComponent from './base-component'
 | 
        
           |  |  | 9 | import EventHandler from './dom/event-handler'
 | 
        
           |  |  | 10 | import SelectorEngine from './dom/selector-engine'
 | 
        
           |  |  | 11 | import {
 | 
        
           |  |  | 12 |   defineJQueryPlugin, getElement, isDisabled, isVisible
 | 
        
           |  |  | 13 | } from './util/index'
 | 
        
           | 1 | efrain | 14 |   | 
        
           |  |  | 15 | /**
 | 
        
           |  |  | 16 |  * Constants
 | 
        
           |  |  | 17 |  */
 | 
        
           |  |  | 18 |   | 
        
           |  |  | 19 | const NAME = 'scrollspy'
 | 
        
           |  |  | 20 | const DATA_KEY = 'bs.scrollspy'
 | 
        
           |  |  | 21 | const EVENT_KEY = `.${DATA_KEY}`
 | 
        
           |  |  | 22 | const DATA_API_KEY = '.data-api'
 | 
        
           |  |  | 23 |   | 
        
           |  |  | 24 | const EVENT_ACTIVATE = `activate${EVENT_KEY}`
 | 
        
           | 1441 | ariadna | 25 | const EVENT_CLICK = `click${EVENT_KEY}`
 | 
        
           | 1 | efrain | 26 | const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
 | 
        
           |  |  | 27 |   | 
        
           | 1441 | ariadna | 28 | const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'
 | 
        
           |  |  | 29 | const CLASS_NAME_ACTIVE = 'active'
 | 
        
           | 1 | efrain | 30 |   | 
        
           | 1441 | ariadna | 31 | const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'
 | 
        
           |  |  | 32 | const SELECTOR_TARGET_LINKS = '[href]'
 | 
        
           | 1 | efrain | 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'
 | 
        
           | 1441 | ariadna | 37 | const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`
 | 
        
           | 1 | efrain | 38 | const SELECTOR_DROPDOWN = '.dropdown'
 | 
        
           |  |  | 39 | const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
 | 
        
           |  |  | 40 |   | 
        
           |  |  | 41 | const Default = {
 | 
        
           | 1441 | ariadna | 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]
 | 
        
           | 1 | efrain | 47 | }
 | 
        
           |  |  | 48 |   | 
        
           |  |  | 49 | const DefaultType = {
 | 
        
           | 1441 | ariadna | 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'
 | 
        
           | 1 | efrain | 55 | }
 | 
        
           |  |  | 56 |   | 
        
           |  |  | 57 | /**
 | 
        
           |  |  | 58 |  * Class definition
 | 
        
           |  |  | 59 |  */
 | 
        
           |  |  | 60 |   | 
        
           | 1441 | ariadna | 61 | class ScrollSpy extends BaseComponent {
 | 
        
           | 1 | efrain | 62 |   constructor(element, config) {
 | 
        
           | 1441 | ariadna | 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
 | 
        
           | 1 | efrain | 69 |     this._activeTarget = null
 | 
        
           | 1441 | ariadna | 70 |     this._observer = null
 | 
        
           |  |  | 71 |     this._previousScrollData = {
 | 
        
           |  |  | 72 |       visibleEntryTop: 0,
 | 
        
           |  |  | 73 |       parentScrollTop: 0
 | 
        
           |  |  | 74 |     }
 | 
        
           |  |  | 75 |     this.refresh() // initialize
 | 
        
           | 1 | efrain | 76 |   }
 | 
        
           |  |  | 77 |   | 
        
           |  |  | 78 |   // Getters
 | 
        
           |  |  | 79 |   static get Default() {
 | 
        
           |  |  | 80 |     return Default
 | 
        
           |  |  | 81 |   }
 | 
        
           |  |  | 82 |   | 
        
           | 1441 | ariadna | 83 |   static get DefaultType() {
 | 
        
           |  |  | 84 |     return DefaultType
 | 
        
           |  |  | 85 |   }
 | 
        
           |  |  | 86 |   | 
        
           |  |  | 87 |   static get NAME() {
 | 
        
           |  |  | 88 |     return NAME
 | 
        
           |  |  | 89 |   }
 | 
        
           |  |  | 90 |   | 
        
           | 1 | efrain | 91 |   // Public
 | 
        
           |  |  | 92 |   refresh() {
 | 
        
           | 1441 | ariadna | 93 |     this._initializeTargetsAndObservables()
 | 
        
           |  |  | 94 |     this._maybeEnableSmoothScroll()
 | 
        
           | 1 | efrain | 95 |   | 
        
           | 1441 | ariadna | 96 |     if (this._observer) {
 | 
        
           |  |  | 97 |       this._observer.disconnect()
 | 
        
           |  |  | 98 |     } else {
 | 
        
           |  |  | 99 |       this._observer = this._getNewObserver()
 | 
        
           |  |  | 100 |     }
 | 
        
           | 1 | efrain | 101 |   | 
        
           | 1441 | ariadna | 102 |     for (const section of this._observableSections.values()) {
 | 
        
           |  |  | 103 |       this._observer.observe(section)
 | 
        
           |  |  | 104 |     }
 | 
        
           |  |  | 105 |   }
 | 
        
           | 1 | efrain | 106 |   | 
        
           | 1441 | ariadna | 107 |   dispose() {
 | 
        
           |  |  | 108 |     this._observer.disconnect()
 | 
        
           |  |  | 109 |     super.dispose()
 | 
        
           |  |  | 110 |   }
 | 
        
           | 1 | efrain | 111 |   | 
        
           | 1441 | ariadna | 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
 | 
        
           | 1 | efrain | 116 |   | 
        
           | 1441 | ariadna | 117 |     // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only
 | 
        
           |  |  | 118 |     config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin
 | 
        
           | 1 | efrain | 119 |   | 
        
           | 1441 | ariadna | 120 |     if (typeof config.threshold === 'string') {
 | 
        
           |  |  | 121 |       config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value))
 | 
        
           |  |  | 122 |     }
 | 
        
           | 1 | efrain | 123 |   | 
        
           | 1441 | ariadna | 124 |     return config
 | 
        
           |  |  | 125 |   }
 | 
        
           | 1 | efrain | 126 |   | 
        
           | 1441 | ariadna | 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
 | 
        
           | 1 | efrain | 144 |         }
 | 
        
           |  |  | 145 |   | 
        
           | 1441 | ariadna | 146 |         // Chrome 60 doesn't support `scrollTo`
 | 
        
           |  |  | 147 |         root.scrollTop = height
 | 
        
           |  |  | 148 |       }
 | 
        
           |  |  | 149 |     })
 | 
        
           | 1 | efrain | 150 |   }
 | 
        
           |  |  | 151 |   | 
        
           | 1441 | ariadna | 152 |   _getNewObserver() {
 | 
        
           |  |  | 153 |     const options = {
 | 
        
           |  |  | 154 |       root: this._rootElement,
 | 
        
           |  |  | 155 |       threshold: this._config.threshold,
 | 
        
           |  |  | 156 |       rootMargin: this._config.rootMargin
 | 
        
           |  |  | 157 |     }
 | 
        
           | 1 | efrain | 158 |   | 
        
           | 1441 | ariadna | 159 |     return new IntersectionObserver(entries => this._observerCallback(entries), options)
 | 
        
           | 1 | efrain | 160 |   }
 | 
        
           |  |  | 161 |   | 
        
           | 1441 | ariadna | 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))
 | 
        
           | 1 | efrain | 168 |     }
 | 
        
           |  |  | 169 |   | 
        
           | 1441 | ariadna | 170 |     const parentScrollTop = (this._rootElement || document.documentElement).scrollTop
 | 
        
           |  |  | 171 |     const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop
 | 
        
           |  |  | 172 |     this._previousScrollData.parentScrollTop = parentScrollTop
 | 
        
           | 1 | efrain | 173 |   | 
        
           | 1441 | ariadna | 174 |     for (const entry of entries) {
 | 
        
           |  |  | 175 |       if (!entry.isIntersecting) {
 | 
        
           |  |  | 176 |         this._activeTarget = null
 | 
        
           |  |  | 177 |         this._clearActiveClass(targetElement(entry))
 | 
        
           | 1 | efrain | 178 |   | 
        
           | 1441 | ariadna | 179 |         continue
 | 
        
           |  |  | 180 |       }
 | 
        
           | 1 | efrain | 181 |   | 
        
           | 1441 | ariadna | 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 |         }
 | 
        
           | 1 | efrain | 190 |   | 
        
           | 1441 | ariadna | 191 |         continue
 | 
        
           |  |  | 192 |       }
 | 
        
           | 1 | efrain | 193 |   | 
        
           | 1441 | ariadna | 194 |       // if we are scrolling up, pick the smallest offsetTop
 | 
        
           |  |  | 195 |       if (!userScrollsDown && !entryIsLowerThanPrevious) {
 | 
        
           |  |  | 196 |         activate(entry)
 | 
        
           |  |  | 197 |       }
 | 
        
           |  |  | 198 |     }
 | 
        
           | 1 | efrain | 199 |   }
 | 
        
           |  |  | 200 |   | 
        
           | 1441 | ariadna | 201 |   _initializeTargetsAndObservables() {
 | 
        
           |  |  | 202 |     this._targetLinks = new Map()
 | 
        
           |  |  | 203 |     this._observableSections = new Map()
 | 
        
           | 1 | efrain | 204 |   | 
        
           | 1441 | ariadna | 205 |     const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target)
 | 
        
           | 1 | efrain | 206 |   | 
        
           | 1441 | ariadna | 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 |       }
 | 
        
           | 1 | efrain | 212 |   | 
        
           | 1441 | ariadna | 213 |       const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element)
 | 
        
           | 1 | efrain | 214 |   | 
        
           | 1441 | ariadna | 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)
 | 
        
           | 1 | efrain | 219 |       }
 | 
        
           |  |  | 220 |     }
 | 
        
           | 1441 | ariadna | 221 |   }
 | 
        
           | 1 | efrain | 222 |   | 
        
           | 1441 | ariadna | 223 |   _process(target) {
 | 
        
           |  |  | 224 |     if (this._activeTarget === target) {
 | 
        
           | 1 | efrain | 225 |       return
 | 
        
           |  |  | 226 |     }
 | 
        
           |  |  | 227 |   | 
        
           | 1441 | ariadna | 228 |     this._clearActiveClass(this._config.target)
 | 
        
           |  |  | 229 |     this._activeTarget = target
 | 
        
           |  |  | 230 |     target.classList.add(CLASS_NAME_ACTIVE)
 | 
        
           |  |  | 231 |     this._activateParents(target)
 | 
        
           | 1 | efrain | 232 |   | 
        
           | 1441 | ariadna | 233 |     EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })
 | 
        
           | 1 | efrain | 234 |   }
 | 
        
           |  |  | 235 |   | 
        
           | 1441 | ariadna | 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 |     }
 | 
        
           | 1 | efrain | 243 |   | 
        
           | 1441 | ariadna | 244 |     for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {
 | 
        
           | 1 | efrain | 245 |       // Set triggered links parents as active
 | 
        
           |  |  | 246 |       // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
 | 
        
           | 1441 | ariadna | 247 |       for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) {
 | 
        
           |  |  | 248 |         item.classList.add(CLASS_NAME_ACTIVE)
 | 
        
           |  |  | 249 |       }
 | 
        
           | 1 | efrain | 250 |     }
 | 
        
           |  |  | 251 |   }
 | 
        
           |  |  | 252 |   | 
        
           | 1441 | ariadna | 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 |     }
 | 
        
           | 1 | efrain | 260 |   }
 | 
        
           |  |  | 261 |   | 
        
           |  |  | 262 |   // Static
 | 
        
           | 1441 | ariadna | 263 |   static jQueryInterface(config) {
 | 
        
           | 1 | efrain | 264 |     return this.each(function () {
 | 
        
           | 1441 | ariadna | 265 |       const data = ScrollSpy.getOrCreateInstance(this, config)
 | 
        
           | 1 | efrain | 266 |   | 
        
           | 1441 | ariadna | 267 |       if (typeof config !== 'string') {
 | 
        
           |  |  | 268 |         return
 | 
        
           | 1 | efrain | 269 |       }
 | 
        
           |  |  | 270 |   | 
        
           | 1441 | ariadna | 271 |       if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
 | 
        
           |  |  | 272 |         throw new TypeError(`No method named "${config}"`)
 | 
        
           |  |  | 273 |       }
 | 
        
           | 1 | efrain | 274 |   | 
        
           | 1441 | ariadna | 275 |       data[config]()
 | 
        
           | 1 | efrain | 276 |     })
 | 
        
           |  |  | 277 |   }
 | 
        
           |  |  | 278 | }
 | 
        
           |  |  | 279 |   | 
        
           |  |  | 280 | /**
 | 
        
           |  |  | 281 |  * Data API implementation
 | 
        
           |  |  | 282 |  */
 | 
        
           |  |  | 283 |   | 
        
           | 1441 | ariadna | 284 | EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
 | 
        
           |  |  | 285 |   for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {
 | 
        
           |  |  | 286 |     ScrollSpy.getOrCreateInstance(spy)
 | 
        
           | 1 | efrain | 287 |   }
 | 
        
           |  |  | 288 | })
 | 
        
           |  |  | 289 |   | 
        
           |  |  | 290 | /**
 | 
        
           |  |  | 291 |  * jQuery
 | 
        
           |  |  | 292 |  */
 | 
        
           |  |  | 293 |   | 
        
           | 1441 | ariadna | 294 | defineJQueryPlugin(ScrollSpy)
 | 
        
           | 1 | efrain | 295 |   | 
        
           |  |  | 296 | export default ScrollSpy
 |