174 lines
5.4 KiB
JavaScript
174 lines
5.4 KiB
JavaScript
import { debounce, throttle } from 'lodash';
|
|
|
|
export const placeholderImage =
|
|
'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
|
const SCROLL_THRESHOLD = 500;
|
|
|
|
export default class LazyLoader {
|
|
constructor(options = {}) {
|
|
this.intersectionObserver = null;
|
|
this.lazyImages = [];
|
|
this.observerNode = options.observerNode || '#content-body';
|
|
|
|
const scrollContainer = options.scrollContainer || window;
|
|
scrollContainer.addEventListener('load', () => this.register());
|
|
}
|
|
|
|
static supportsNativeLazyLoading() {
|
|
return 'loading' in HTMLImageElement.prototype;
|
|
}
|
|
|
|
static supportsIntersectionObserver() {
|
|
return Boolean(window.IntersectionObserver);
|
|
}
|
|
|
|
searchLazyImages() {
|
|
window.requestIdleCallback(
|
|
() => {
|
|
const lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
|
|
|
|
if (LazyLoader.supportsNativeLazyLoading()) {
|
|
lazyImages.forEach(img => LazyLoader.loadImage(img));
|
|
} else if (LazyLoader.supportsIntersectionObserver()) {
|
|
if (this.intersectionObserver) {
|
|
lazyImages.forEach(img => this.intersectionObserver.observe(img));
|
|
}
|
|
} else if (lazyImages.length) {
|
|
this.lazyImages = lazyImages;
|
|
this.checkElementsInView();
|
|
}
|
|
},
|
|
{ timeout: 500 },
|
|
);
|
|
}
|
|
|
|
startContentObserver() {
|
|
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
|
|
if (contentNode) {
|
|
this.mutationObserver = new MutationObserver(() => this.searchLazyImages());
|
|
|
|
this.mutationObserver.observe(contentNode, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
stopContentObserver() {
|
|
if (this.mutationObserver) {
|
|
this.mutationObserver.takeRecords();
|
|
this.mutationObserver.disconnect();
|
|
this.mutationObserver = null;
|
|
}
|
|
}
|
|
|
|
unregister() {
|
|
this.stopContentObserver();
|
|
if (this.intersectionObserver) {
|
|
this.intersectionObserver.takeRecords();
|
|
this.intersectionObserver.disconnect();
|
|
this.intersectionObserver = null;
|
|
}
|
|
if (this.throttledScrollCheck) {
|
|
window.removeEventListener('scroll', this.throttledScrollCheck);
|
|
}
|
|
if (this.debouncedElementsInView) {
|
|
window.removeEventListener('resize', this.debouncedElementsInView);
|
|
}
|
|
}
|
|
|
|
register() {
|
|
if (!LazyLoader.supportsNativeLazyLoading()) {
|
|
if (LazyLoader.supportsIntersectionObserver()) {
|
|
this.startIntersectionObserver();
|
|
} else {
|
|
this.startLegacyObserver();
|
|
}
|
|
}
|
|
|
|
this.startContentObserver();
|
|
this.searchLazyImages();
|
|
}
|
|
|
|
startIntersectionObserver = () => {
|
|
this.throttledElementsInView = throttle(() => this.checkElementsInView(), 300);
|
|
this.intersectionObserver = new IntersectionObserver(this.onIntersection, {
|
|
rootMargin: `${SCROLL_THRESHOLD}px 0px`,
|
|
thresholds: 0.1,
|
|
});
|
|
};
|
|
|
|
onIntersection = entries => {
|
|
entries.forEach(entry => {
|
|
// We are using `intersectionRatio > 0` over `isIntersecting`, as some browsers did not ship the latter
|
|
// See: https://gitlab.com/gitlab-org/gitlab-foss/issues/54407
|
|
if (entry.intersectionRatio > 0) {
|
|
this.intersectionObserver.unobserve(entry.target);
|
|
this.lazyImages.push(entry.target);
|
|
}
|
|
});
|
|
this.throttledElementsInView();
|
|
};
|
|
|
|
startLegacyObserver() {
|
|
this.throttledScrollCheck = throttle(() => this.scrollCheck(), 300);
|
|
this.debouncedElementsInView = debounce(() => this.checkElementsInView(), 300);
|
|
window.addEventListener('scroll', this.throttledScrollCheck);
|
|
window.addEventListener('resize', this.debouncedElementsInView);
|
|
}
|
|
|
|
scrollCheck() {
|
|
window.requestAnimationFrame(() => this.checkElementsInView());
|
|
}
|
|
|
|
checkElementsInView() {
|
|
const scrollTop = window.pageYOffset;
|
|
const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD;
|
|
|
|
// Loading Images which are in the current viewport or close to them
|
|
this.lazyImages = this.lazyImages.filter(selectedImage => {
|
|
if (selectedImage.getAttribute('data-src')) {
|
|
const imgBoundRect = selectedImage.getBoundingClientRect();
|
|
const imgTop = scrollTop + imgBoundRect.top;
|
|
const imgBound = imgTop + imgBoundRect.height;
|
|
|
|
if (scrollTop <= imgBound && visHeight >= imgTop) {
|
|
window.requestAnimationFrame(() => {
|
|
LazyLoader.loadImage(selectedImage);
|
|
});
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
If we are scrolling fast, the img we watched intersecting could have left the view port.
|
|
So we are going watch for new intersections.
|
|
*/
|
|
if (LazyLoader.supportsIntersectionObserver()) {
|
|
if (this.intersectionObserver) {
|
|
this.intersectionObserver.observe(selectedImage);
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
static loadImage(img) {
|
|
if (img.getAttribute('data-src')) {
|
|
img.setAttribute('loading', 'lazy');
|
|
let imgUrl = img.getAttribute('data-src');
|
|
// Only adding width + height for avatars for now
|
|
if (imgUrl.indexOf('/avatar/') > -1 && imgUrl.indexOf('?') === -1) {
|
|
const targetWidth = img.getAttribute('width') || img.width;
|
|
imgUrl += `?width=${targetWidth}`;
|
|
}
|
|
img.setAttribute('src', imgUrl);
|
|
img.removeAttribute('data-src');
|
|
img.classList.remove('lazy');
|
|
img.classList.add('js-lazy-loaded');
|
|
img.classList.add('qa-js-lazy-loaded');
|
|
}
|
|
}
|
|
}
|