There is a solution with an Intersection Observer.
An advantage of the solution is position-relative
calculations are not required.
jQuery plugin code snippet: (plugin.js)
(function ($) {
class IntersectionDetector {
async isElementPartiallyOutOfViewport(element) {
return new Promise((resolve) => {
const callback = this.handleOutOfViewportObservation.bind(this, resolve);
const options = {
root: null,
threshold: 0,
}
const observer = new IntersectionObserver(callback, options);
observer.observe(element);
})
}
handleOutOfViewportObservation(resolve, [entry], observer) {
const element = entry.target;
observer.unobserve(element);
observer.disconnect();
const ratios = new Map();
ratios.set('inViewportCompletelyRatio', 1);
ratios.set('outOfViewportCompletelyRatio', 0);
ratios.set('actualRatio', entry.intersectionRatio);
const precision = Math.pow(10, 2);
for (const [name, prevRatio] of ratios) {
const nextRatio = precision * prevRatio;
ratios.set(name, nextRatio);
}
const actualRatio = ratios.get('actualRatio');
const inViewportCompletelyRatio = ratios.get('inViewportCompletelyRatio');
const outOfViewportCompletelyRatio = ratios.get('outOfViewportCompletelyRatio');
const isOutOfViewportPartially =
(actualRatio > outOfViewportCompletelyRatio) &&
(actualRatio < inViewportCompletelyRatio);
resolve(isOutOfViewportPartially);
}
}
$.fn.isOnScreen = async function () {
const elements = this;
const promises = elements.map(async (index, element) => {
const detector = new IntersectionDetector();
return await detector.isElementPartiallyOutOfViewport(element);
});
const results = await Promise.all(promises);
return results.every(result => result);
};
}(jQuery));
Usage example: (main.js)
jQuery(async function ($) {
const isOnScreen = await $('#element-to-check').isOnScreen();
console.log(isOnScreen);
});
Explanation:
- jQuery operates nodes collection, thus a better option is to
check every node in a collection. If every node in the collection
is partially visible the function returns true.
- The observer has specified a root argument equal to null,
which means the element's intersection is being detected relative to a browser window.
- The solution uses precision. Potentially, JavaScript can
cause a mistake while processing floating point numbers.
The idea is to compare integer parts of numbers
instead of floats to avoid the incorrect result.
For instance, there is a good answer to this issue:
https://stackoverflow.com/a/50780164/11173494
The code tested for:
- jQuery 3.6.*
- Chrome 103.0.* / Firefox 108.0