You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Scrollspy component was broken when switching to the observer api. I decided to look into it. Basically, I understood why the tests were passed, which tests did not work as they should, what exactly broke, and so on. I started fixing the component while getting rid of the old functionality, rewriting, adding and correcting the tests. I've managed to make some progress with the component. But I have a question, are you interested in this and what are your plans for it? Can I continue working on it and send a PR, what do you say?
Motivation and context
The component has been in a broken state for a long time. I'd like to fix it.
Here's what's ready at the moment. I just have to fix a couple of bugs and add scroll-offset-top for the header, getting rid of the offset parameter. It can even be made dynamic by passing an overlapping absolute positioning element in the configuration and adding styles. There's a lot to think about.
first.webmsecond.webmthird.webm
For now, I'll just leave the code of the new component:
/** * -------------------------------------------------------------------------- * Bootstrap scrollspy.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */importBaseComponentfrom'./base-component.js'importEventHandlerfrom'./dom/event-handler.js'importSelectorEnginefrom'./dom/selector-engine.js'import{getElement,isDisabled,isVisible}from'./util/index.js'/** * Constants */constNAME='scrollspy'constDATA_KEY='bs.scrollspy'constEVENT_KEY=`.${DATA_KEY}`constDATA_API_KEY='.data-api'constEVENT_ACTIVATE=`activate${EVENT_KEY}`constEVENT_LOAD_DATA_API=`load${EVENT_KEY}${DATA_API_KEY}`constCLASS_NAME_DROPDOWN_ITEM='dropdown-item'constCLASS_NAME_ACTIVE='active'constSELECTOR_DATA_SPY='[data-bs-spy="scroll"]'constSELECTOR_TARGET_LINKS='[href]'constSELECTOR_NAV_LIST_GROUP='.nav, .list-group'constSELECTOR_NAV_LINKS='.nav-link'constSELECTOR_NAV_ITEMS='.nav-item'constSELECTOR_LIST_ITEMS='.list-group-item'constSELECTOR_LINK_ITEMS=`${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`constSELECTOR_DROPDOWN='.dropdown'constSELECTOR_DROPDOWN_TOGGLE='.dropdown-toggle'exportconstSPY_ENGINE_CONFIG={rootMargin: '-2% 0px -98% 0px',threshold: [0]}exportconstSPY_SENTRY_CONFIG={rootMargin: '0px 0px 0px 0px',threshold: [0]}constDefault={rootMargin: SPY_ENGINE_CONFIG.rootMargin,threshold: SPY_ENGINE_CONFIG.threshold,smoothScroll: false,target: null}constDefaultType={rootMargin: 'string',threshold: 'array',smoothScroll: 'boolean',target: 'element'}/** * Class definition */classScrollSpyextendsBaseComponent{constructor(element,config){super(element,config)// this._element is the observablesContainer and config.target the menu links wrapperthis._targetLinks=newMap()this._observableSections=newMap()this._rootElement=getComputedStyle(this._element).overflowY==='visible' ? null : this._elementthis._activeTarget=nullthis._observer=nullthis._sentryObserver=nullthis._sentryObserverElement=nullthis.refresh()// initialize}// GettersstaticgetDefault(){returnDefault}staticgetDefaultType(){returnDefaultType}staticgetNAME(){returnNAME}// Publicrefresh(){this._initializeTargets()this._captureTargets()this._customizeScrollBehavior()this._activateAnchor()this._observer?.disconnect()this._observer=this._getNewObserver()this._sentryObserver?.disconnect()this._sentryObserver=this._getNewSentryObserver()for(constsectionofthis._observableSections.values()){this._observer.observe(section)}this._sentryObserver.observe(this._sentryObserverElement)}dispose(){this._observer.disconnect()super.dispose()}// Private_configAfterMerge(config){config.target=getElement(config.target)if(!config.target){thrownewTypeError('Bootstrap ScrollSpy: You must specify a valid "target" element')}returnconfig}_initializeTargets(){this._targetLinks=newMap()this._observableSections=newMap()this._sentryObserverElement=SelectorEngine.findOne('.sentry-observer',this._element)if(!this._sentryObserverElement){constsentryObserverElement=document.createElement('div')sentryObserverElement.classList.add('sentry-observer','visibility-hidden')sentryObserverElement.style.height='1px'sentryObserverElement.style.width='1px'this._element.append(sentryObserverElement)}}_captureTargets(){consttargetLinks=SelectorEngine.find(SELECTOR_TARGET_LINKS,this._config.target)for(constanchoroftargetLinks){// ensure that the anchor has an id and is not disabledif(!anchor.hash||isDisabled(anchor)){continue}constobservableSection=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)}}this._sentryObserverElement=SelectorEngine.findOne('.sentry-observer',this._element)}_customizeScrollBehavior(){if(this._rootElement&&this._config.smoothScroll){this._rootElement.classList.add('scroll-smooth')}if(this._rootElement&&!this._config.smoothScroll){this._rootElement.classList.remove('scroll-smooth')}}_activateAnchor(){const[,firstObservableSection]=[...this._observableSections].shift()const[,firstAnchorElement]=[...this._targetLinks].shift()constrect=firstObservableSection.getBoundingClientRect()if(rect.top>0){this._setActiveClass(firstAnchorElement)}}_getNewSentryObserver(){constoptions={root: this._rootElement,threshold: SPY_SENTRY_CONFIG.threshold,rootMargin: SPY_SENTRY_CONFIG.rootMargin}returnnewIntersectionObserver(entries=>this._sentryObserverCallback(entries),options)}_sentryObserverCallback(entries){constvisibleEntry=entries.find(entry=>entry.isIntersecting)if(!visibleEntry){return}consttargets=[...this._targetLinks]const[,lastAnchorElement]=targets.pop()if(!lastAnchorElement.classList.contains(CLASS_NAME_ACTIVE)){for(const[,element]oftargets){this._clearActiveClass(element)}this._setActiveClass(lastAnchorElement)}}_getNewObserver(){constoptions={root: this._rootElement,threshold: this._config.threshold,rootMargin: this._config.rootMargin}returnnewIntersectionObserver(entries=>this._observerCallback(entries),options)}_observerCallback(entries){constvisibleEntry=entries.find(entry=>entry.isIntersecting)if(!visibleEntry){return}constelement=this._targetLinks.get(`#${visibleEntry.target.id}`)this._process(element)}_process(target){if(this._activeTarget===target){return}this._clearActiveClass(this._config.target)this._setActiveClass(target)this._activateDropdownParentElement(target)this._activateListGroupParentElement(target)EventHandler.trigger(this._element,EVENT_ACTIVATE,{relatedTarget: target})}_clearActiveClass(parent){parent.classList.remove(CLASS_NAME_ACTIVE)constactiveNodes=SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`,parent)for(constnodeofactiveNodes){node.classList.remove(CLASS_NAME_ACTIVE)}}_setActiveClass(target){this._activeTarget=targettarget.classList.add(CLASS_NAME_ACTIVE)}_activateDropdownParentElement(target){// Set the parent active dropdown class if dropdown is the target of the clicked link or the current viewportif(target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)){SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE,target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE)}}_activateListGroupParentElement(target){for(constlistGroupofSelectorEngine.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(constitemofSelectorEngine.prev(listGroup,SELECTOR_LINK_ITEMS)){item.classList.add(CLASS_NAME_ACTIVE)}}}}/** * Data API implementation */EventHandler.on(window,EVENT_LOAD_DATA_API,()=>{for(constspyofSelectorEngine.find(SELECTOR_DATA_SPY)){ScrollSpy.getOrCreateInstance(spy)}})exportdefaultScrollSpy
Prerequisites
Proposal
The Scrollspy component was broken when switching to the observer api. I decided to look into it. Basically, I understood why the tests were passed, which tests did not work as they should, what exactly broke, and so on. I started fixing the component while getting rid of the old functionality, rewriting, adding and correcting the tests. I've managed to make some progress with the component. But I have a question, are you interested in this and what are your plans for it? Can I continue working on it and send a PR, what do you say?
Motivation and context
The component has been in a broken state for a long time. I'd like to fix it.
Here's what's ready at the moment. I just have to fix a couple of bugs and add scroll-offset-top for the header, getting rid of the offset parameter. It can even be made dynamic by passing an overlapping absolute positioning element in the configuration and adding styles. There's a lot to think about.
first.webm
second.webm
third.webm
For now, I'll just leave the code of the new component: