Last active
August 1, 2023 11:36
-
-
Save dliebner/25f1a30d2020deb4f16f17fe407b3752 to your computer and use it in GitHub Desktop.
An infinite scroller controller of sorts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class BottomIntersectionObserverController { | |
/** @type {HTMLElement} */ | |
host; | |
/** @type {string} */ | |
_bottomThreshold; | |
/** @type {CallableFunction} */ | |
bottomThresholdReachedCallback; | |
/** @type {IntersectionObserver} */ | |
observer; | |
/** @type {HTMLElement} */ | |
thresholdElement; | |
get bottomThreshold() { | |
return this._bottomThreshold; | |
} | |
set bottomThreshold( newVal ) { | |
this._bottomThreshold = newVal; | |
if( this.thresholdElement ) this.thresholdElement.style.height = newVal; | |
} | |
/** | |
* @param {ControllerHost} host | |
* @param {string} bottomThreshold in CSS units (i.e. 400px, 100vh) | |
* @param {typeof this.bottomThresholdReachedCallback} bottomThresholdReachedCallback | |
*/ | |
constructor(host, bottomThreshold, bottomThresholdReachedCallback) { | |
// Store a reference to the host | |
this.host = host; | |
// Register for lifecycle updates | |
host.addController(this); | |
this.bottomThreshold = bottomThreshold; | |
this.bottomThresholdReachedCallback = bottomThresholdReachedCallback; | |
this.observer = new IntersectionObserver((entries) => this._observerCallback(entries), { | |
threshold: 0 | |
}); | |
} | |
/** @param {IntersectionObserverEntry[]} entries */ | |
_observerCallback( entries ) { | |
// It's possible to scroll so fast that we skip over the thresholdElement, | |
// never triggering the observer. Therefore, we also observe the host | |
// element to see if it scrolls out of view. | |
// There can be more than one entry for the same target, | |
// therefore the threshold can be triggered and then | |
// subsequently un-triggered in the same "observation" | |
let bottomThresholdReached = false, | |
bottomThresholdReachedByTarget = new Map(); | |
entries.forEach(entry => { | |
if( entry.target === this.thresholdElement ) { | |
if( entry.isIntersecting ) { | |
//console.log('bottom of threshold element is near the bottom of the viewport'); | |
bottomThresholdReached = true; | |
bottomThresholdReachedByTarget.set( entry.target, true ); | |
} else { | |
bottomThresholdReachedByTarget.delete( entry.target ); | |
} | |
} else if( entry.target === this.host ) { | |
if( !entry.isIntersecting ) { | |
// the host element is no longer in view | |
// is the host element above the viewport? | |
const windowTop = window.scrollY, | |
hostTop = this.host.offsetTop; | |
if( hostTop < windowTop ) { | |
//console.log('host element is above the viewport'); | |
bottomThresholdReached = true; | |
bottomThresholdReachedByTarget.set( entry.target, true ); | |
} else { | |
bottomThresholdReachedByTarget.delete( entry.target ); | |
} | |
} | |
} | |
}); | |
this.bottomThresholdReached = bottomThresholdReached && bottomThresholdReachedByTarget.size > 0; | |
if( bottomThresholdReached ) this.bottomThresholdReachedCallback?.(); | |
} | |
// create the threshold element and append it to the target element when the host is connected to the component tree | |
hostConnected() { | |
this.host.classList.add('bottom-io-controller-host'); | |
this.thresholdElement = document.createElement('div'); | |
this.thresholdElement.style.cssText = ` | |
bottom: 0; | |
height: ${this.bottomThreshold}; | |
width: 1px; | |
position: absolute; | |
pointer-events: none; | |
`; | |
this.host.appendChild( this.thresholdElement ); | |
this.observer.observe( this.thresholdElement ); | |
this.observer.observe( this.host ); | |
} | |
// remove the threshold element and stop observing it when the host is disconnected from the component tree | |
hostDisconnected() { | |
this.host.classList.remove('bottom-io-controller-host'); | |
this.observer.unobserve( this.thresholdElement ); | |
this.observer.unobserve( this.host ); | |
this.thresholdElement.remove(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment