Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ansarizafar/12dcb87043fdd5681156ee581c83ce0e to your computer and use it in GitHub Desktop.
Save ansarizafar/12dcb87043fdd5681156ee581c83ce0e to your computer and use it in GitHub Desktop.
An infinite scroller controller of sorts
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