Rev 1 | AutorÃa | Comparar con el anterior | Ultima modificación | Ver Log |
/*** --------------------------------------------------------------------------* Bootstrap scrollspy.js* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)* --------------------------------------------------------------------------*/import BaseComponent from './base-component'import EventHandler from './dom/event-handler'import SelectorEngine from './dom/selector-engine'import {defineJQueryPlugin, getElement, isDisabled, isVisible} from './util/index'/*** Constants*/const NAME = 'scrollspy'const DATA_KEY = 'bs.scrollspy'const EVENT_KEY = `.${DATA_KEY}`const DATA_API_KEY = '.data-api'const EVENT_ACTIVATE = `activate${EVENT_KEY}`const EVENT_CLICK = `click${EVENT_KEY}`const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'const CLASS_NAME_ACTIVE = 'active'const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'const SELECTOR_TARGET_LINKS = '[href]'const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'const SELECTOR_NAV_LINKS = '.nav-link'const SELECTOR_NAV_ITEMS = '.nav-item'const SELECTOR_LIST_ITEMS = '.list-group-item'const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`const SELECTOR_DROPDOWN = '.dropdown'const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'const Default = {offset: null, // TODO: v6 @deprecated, keep it for backwards compatibility reasonsrootMargin: '0px 0px -25%',smoothScroll: false,target: null,threshold: [0.1, 0.5, 1]}const DefaultType = {offset: '(number|null)', // TODO v6 @deprecated, keep it for backwards compatibility reasonsrootMargin: 'string',smoothScroll: 'boolean',target: 'element',threshold: 'array'}/*** Class definition*/class ScrollSpy extends BaseComponent {constructor(element, config) {super(element, config)// this._element is the observablesContainer and config.target the menu links wrapperthis._targetLinks = new Map()this._observableSections = new Map()this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._elementthis._activeTarget = nullthis._observer = nullthis._previousScrollData = {visibleEntryTop: 0,parentScrollTop: 0}this.refresh() // initialize}// Gettersstatic get Default() {return Default}static get DefaultType() {return DefaultType}static get NAME() {return NAME}// Publicrefresh() {this._initializeTargetsAndObservables()this._maybeEnableSmoothScroll()if (this._observer) {this._observer.disconnect()} else {this._observer = this._getNewObserver()}for (const section of this._observableSections.values()) {this._observer.observe(section)}}dispose() {this._observer.disconnect()super.dispose()}// Private_configAfterMerge(config) {// TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} caseconfig.target = getElement(config.target) || document.body// TODO: v6 Only for backwards compatibility reasons. Use rootMargin onlyconfig.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMarginif (typeof config.threshold === 'string') {config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value))}return config}_maybeEnableSmoothScroll() {if (!this._config.smoothScroll) {return}// unregister any previous listenersEventHandler.off(this._config.target, EVENT_CLICK)EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {const observableSection = this._observableSections.get(event.target.hash)if (observableSection) {event.preventDefault()const root = this._rootElement || windowconst height = observableSection.offsetTop - this._element.offsetTopif (root.scrollTo) {root.scrollTo({ top: height, behavior: 'smooth' })return}// Chrome 60 doesn't support `scrollTo`root.scrollTop = height}})}_getNewObserver() {const options = {root: this._rootElement,threshold: this._config.threshold,rootMargin: this._config.rootMargin}return new IntersectionObserver(entries => this._observerCallback(entries), options)}// The logic of selection_observerCallback(entries) {const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`)const activate = entry => {this._previousScrollData.visibleEntryTop = entry.target.offsetTopthis._process(targetElement(entry))}const parentScrollTop = (this._rootElement || document.documentElement).scrollTopconst userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTopthis._previousScrollData.parentScrollTop = parentScrollTopfor (const entry of entries) {if (!entry.isIntersecting) {this._activeTarget = nullthis._clearActiveClass(targetElement(entry))continue}const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop// if we are scrolling down, pick the bigger offsetTopif (userScrollsDown && entryIsLowerThanPrevious) {activate(entry)// if parent isn't scrolled, let's keep the first visible item, breaking the iterationif (!parentScrollTop) {return}continue}// if we are scrolling up, pick the smallest offsetTopif (!userScrollsDown && !entryIsLowerThanPrevious) {activate(entry)}}}_initializeTargetsAndObservables() {this._targetLinks = new Map()this._observableSections = new Map()const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target)for (const anchor of targetLinks) {// ensure that the anchor has an id and is not disabledif (!anchor.hash || isDisabled(anchor)) {continue}const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element)// ensure that the observableSection exists & is visibleif (isVisible(observableSection)) {this._targetLinks.set(decodeURI(anchor.hash), anchor)this._observableSections.set(anchor.hash, observableSection)}}}_process(target) {if (this._activeTarget === target) {return}this._clearActiveClass(this._config.target)this._activeTarget = targettarget.classList.add(CLASS_NAME_ACTIVE)this._activateParents(target)EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })}_activateParents(target) {// Activate dropdown parentsif (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE)return}for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {// Set triggered links parents as active// With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestorfor (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) {item.classList.add(CLASS_NAME_ACTIVE)}}}_clearActiveClass(parent) {parent.classList.remove(CLASS_NAME_ACTIVE)const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent)for (const node of activeNodes) {node.classList.remove(CLASS_NAME_ACTIVE)}}// Staticstatic jQueryInterface(config) {return this.each(function () {const data = ScrollSpy.getOrCreateInstance(this, config)if (typeof config !== 'string') {return}if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {throw new TypeError(`No method named "${config}"`)}data[config]()})}}/*** Data API implementation*/EventHandler.on(window, EVENT_LOAD_DATA_API, () => {for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {ScrollSpy.getOrCreateInstance(spy)}})/*** jQuery*/defineJQueryPlugin(ScrollSpy)export default ScrollSpy