import customEvent from '../utils/customEvent';
import InView from '../utils/classes/InView';
/**
* Lazy load iframes and images with attribute loading="lazy"
*
* @module LazyLoad
* @prop {array} targetedHtmlTags - list of html tags to enable lazy load for
* @prop {string} selector - targets elements on to set up lazy loading
*/
const LazyLoad = {
selector: '[loading="lazy"]',
targetedHtmlTags: ['img', 'iframe'],
liveBlogSelector: '.liveblog-feed',
/**
* Initialize InView watcher for handling lazy load image / iframes
*
* @method init
*/
init() {
// If this article has live blog, observe entries being loaded
if ( document.querySelector( this.liveBlogSelector ) ) {
this.observeLiveBlog();
}
// Set up in view watchers for targeted html tags
[].forEach.call( this.targetedHtmlTags, ( nodeName ) => {
if ( ! this.hasNativeSupport( nodeName ) ) {
const watcher = new InView({
threshold: 0.1,
rootMargin: `${window.innerHeight * 0.4}px`,
}, this.getWatcherName( nodeName ) );
watcher.init();
}
let $targets = document.querySelectorAll( `${nodeName}${this.selector}` );
// remove $targets that don't include `data-src` attribute
// since `loading="lazy"` is natively added by WP in v5.5
$targets = [].filter.call( $targets, $target => 'undefined' !== typeof $target.dataset.src );
[].forEach.call( $targets, ( $target ) => {
$target.addEventListener( 'load', ( evt ) => {
evt.currentTarget.dataset.loaded = 'true'; /* eslint-disable-line no-param-reassign */
const parentId = evt.currentTarget.dataset.parent;
if ( parentId ) {
const $parent = document.querySelector( `#${parentId}` );
if ( $parent ) {
$parent.dataset.loaded = 'true';
}
}
});
this.startWatching( $target );
});
});
},
/**
* Listen to `InView` event fired by $target element,
*
* @method startWatching
* @param {element} $target
*/
startWatching( $target ) {
const nodeName = $target.nodeName.toLowerCase();
if ( this.hasNativeSupport( nodeName ) ) {
// Native lazy load feature is available
$target.src = $target.dataset.src; /* eslint-disable-line no-param-reassign */
if ( $target.dataset.srcset ) {
$target.srcset = $target.dataset.srcset; /* eslint-disable-line no-param-reassign */
}
if ( $target.dataset.sizes ) {
$target.sizes = $target.dataset.sizes; /* eslint-disable-line no-param-reassign */
}
} else {
const watcher = InView.getWatcher( this.getWatcherName( nodeName ) );
$target.addEventListener( customEvent.IN_VIEW, ( event ) => {
if ( event && event.detail && event.detail.isInView ) {
if ( $target.dataset.src ) {
this.load( $target, 'src', $target.dataset.src );
}
if ( $target.dataset.srcset ) {
this.load( $target, 'srcset', $target.dataset.srcset );
}
if ( $target.dataset.sizes ) {
this.load( $target, 'sizes', $target.dataset.sizes );
}
watcher.stopWatching( $target );
}
});
watcher.startWatching( $target );
}
},
/**
* Loads content referenced by src url into designated element
*
* @method load
* @param {element} $target - target element
* @param {string} key - attribute name (e.g. src, srcset, sizes)
* @param {string} value - attribute value (e.g. source URL, srcset list, etc.)
*/
load( $target, key, value ) {
$target.setAttribute( key, value );
},
/**
* Detect if lazy load is natively supported for specific element
*
* @method hasNativeSupport
* @param {string} nodeName - element node name
*/
hasNativeSupport( nodeName ) {
let result = false;
switch ( nodeName ) {
case 'img':
result = 'loading' in HTMLImageElement.prototype;
break;
case 'iframe':
result = 'loading' in HTMLIFrameElement.prototype;
break;
default:
break;
}
return result;
},
/**
* Get in view watcher name for given element type
*
* @method getWatcherName
* @param {string} nodeName - element node name
*/
getWatcherName( nodeName ) {
return `${nodeName}LazyLoadWatcher`;
},
/**
* Creates MutationObserver for live blog, as the entries are loaded after the page renders
* This ensures that images and iframes get lazyloaded properly in liveblog
*
* @method observeLiveBlog
*/
observeLiveBlog() {
// Set up mutation observer for live blog entry list
const targetNode = document.querySelector( this.liveBlogSelector );
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Create an observer instance linked to the callback function
const observer = new MutationObserver( ( mutationsList ) => {
mutationsList.forEach( ( mutation ) => {
// If this is a childList mutation (entries added or removed)
if ( 'childList' === mutation.type ) {
// If a new entries are being added in this mutation
if ( 0 < mutation.addedNodes.length ) {
// If an item that's beeing added is an entry
if ( mutation.addedNodes[0].classList && mutation.addedNodes[0].classList.contains( 'liveblog-entry' ) ) {
// Set up in view watchers for targeted html tags
[].forEach.call( this.targetedHtmlTags, ( nodeName ) => {
const $targets = mutation.addedNodes[0].querySelectorAll( `${nodeName}${this.selector}` );
[].forEach.call( $targets, ( $target ) => {
this.startWatching( $target );
});
});
}
}
}
});
});
// Start observing the target node for configured mutations
observer.observe( targetNode, config );
},
};
export default LazyLoad;