main/LazyLoad.js

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;