codekingpro/portable-devtools
114k
1/**2 * --------------------------------------------------------------------------3 * Bootstrap (v4.6.2): scrollspy.js4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)5 * --------------------------------------------------------------------------6 */7 8import $ from 'jquery'9import Util from './util'10 11/**12 * Constants13 */14 15const NAME = 'scrollspy'16const VERSION = '4.6.2'17const DATA_KEY = 'bs.scrollspy'18const EVENT_KEY = `.${DATA_KEY}`19const DATA_API_KEY = '.data-api'20const JQUERY_NO_CONFLICT = $.fn[NAME]21 22const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'23const CLASS_NAME_ACTIVE = 'active'24 25const EVENT_ACTIVATE = `activate${EVENT_KEY}`26const EVENT_SCROLL = `scroll${EVENT_KEY}`27const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`28 29const METHOD_OFFSET = 'offset'30const METHOD_POSITION = 'position'31 32const SELECTOR_DATA_SPY = '[data-spy="scroll"]'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_DROPDOWN = '.dropdown'38const SELECTOR_DROPDOWN_ITEMS = '.dropdown-item'39const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'40 41const Default = {42 offset: 10,43 method: 'auto',44 target: ''45}46 47const DefaultType = {48 offset: 'number',49 method: 'string',50 target: '(string|element)'51}52 53/**54 * Class definition55 */56 57class ScrollSpy {58 constructor(element, config) {59 this._element = element60 this._scrollElement = element.tagName === 'BODY' ? window : element61 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 = null68 this._scrollHeight = 069 70 $(this._scrollElement).on(EVENT_SCROLL, event => this._process(event))71 72 this.refresh()73 this._process()74 }75 76 // Getters77 static get VERSION() {78 return VERSION79 }80 81 static get Default() {82 return Default83 }84 85 // Public86 refresh() {87 const autoMethod = this._scrollElement === this._scrollElement.window ?88 METHOD_OFFSET : METHOD_POSITION89 90 const offsetMethod = this._config.method === 'auto' ?91 autoMethod : this._config.method92 93 const offsetBase = offsetMethod === METHOD_POSITION ?94 this._getScrollTop() : 095 96 this._offsets = []97 this._targets = []98 99 this._scrollHeight = this._getScrollHeight()100 101 const targets = [].slice.call(document.querySelectorAll(this._selector))102 103 targets104 .map(element => {105 let target106 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/offset116 return [117 $(target)[offsetMethod]().top + offsetBase,118 targetSelector119 ]120 }121 }122 123 return null124 })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 = null138 this._scrollElement = null139 this._config = null140 this._selector = null141 this._offsets = null142 this._targets = null143 this._activeTarget = null144 this._scrollHeight = null145 }146 147 // Private148 _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 config167 }168 169 _getScrollTop() {170 return this._scrollElement === window ?171 this._scrollElement.pageYOffset : this._scrollElement.scrollTop172 }173 174 _getScrollHeight() {175 return this._scrollElement.scrollHeight || Math.max(176 document.body.scrollHeight,177 document.documentElement.scrollHeight178 )179 }180 181 _getOffsetHeight() {182 return this._scrollElement === window ?183 window.innerHeight : this._scrollElement.getBoundingClientRect().height184 }185 186 _process() {187 const scrollTop = this._getScrollTop() + this._config.offset188 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 return203 }204 205 if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {206 this._activeTarget = null207 this._clear()208 return209 }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 = target225 226 this._clear()227 228 const queries = this._selector229 .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 active241 $link.addClass(CLASS_NAME_ACTIVE)242 // Set triggered links parents as active243 // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor244 $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-item248 $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: target256 })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 // Static266 static _jQueryInterface(config) {267 return this.each(function () {268 let data = $(this).data(DATA_KEY)269 const _config = typeof config === 'object' && config270 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 implementation289 */290 291$(window).on(EVENT_LOAD_DATA_API, () => {292 const scrollSpys = [].slice.call(document.querySelectorAll(SELECTOR_DATA_SPY))293 const scrollSpysLength = scrollSpys.length294 295 for (let i = scrollSpysLength; i--;) {296 const $spy = $(scrollSpys[i])297 ScrollSpy._jQueryInterface.call($spy, $spy.data())298 }299})300 301/**302 * jQuery303 */304 305$.fn[NAME] = ScrollSpy._jQueryInterface306$.fn[NAME].Constructor = ScrollSpy307$.fn[NAME].noConflict = () => {308 $.fn[NAME] = JQUERY_NO_CONFLICT309 return ScrollSpy._jQueryInterface310}311 312export default ScrollSpy313 