main/LoadMore.js

import LazyLoad from './LazyLoad';
import customEvent from '../utils/customEvent';
import ImageContainer from './ImageContainer';
import SophiTagWrapper from './SophiTagWrapper';

/**
 * Created 'Load More' button
 *
 * @module LoadMore
 * @prop {Integer} lastPostId - Id of the last post loaded on the page
 * @prop {String} ad_class - Ad item identifying class
 * @prop {String} ad_unit_class - Ad inner div identifying class
 * @prop {string} loadmore_item_class - css class for post item
 * @prop {string} loadmore_button_selector - load more button selector
 * @prop {string} loadmore_ended_selector - selector of end of load more list
 * @prop {string} data_region - data attribute for region
 * @prop {String} data_post_id - data attribute for post id
 */
const LoadMore = {
	lastPostId: null, // TO BE DELETED
	ad_class: 'c-posts__ad',
	ad_unit_class: 'c-ad__unit',
	loadmore_item_class: 'c-posts__loadmore',
	loadmore_button_selector: '[data-load-more-button]',
	loadmore_ended_selector: '[data-load-more-ended]',
	data_region: 'data-region',
	data_post_id: 'data-post-id',

	/**
	 * Initialize 'Load More' button and bind click event.
	 * @method init
	 *
	 */
	init() {
		const $buttons = document.querySelectorAll( this.loadmore_button_selector );

		[].forEach.call( $buttons, ( $button ) => {
			const $resultList = document.querySelector( `#${$button.dataset.loadMoreTarget}` );

			// Get last post id loaded on the page
			const $renderedPosts = $resultList.querySelectorAll( `[${this.data_post_id}]` );
			const $lastRenderedPost = $renderedPosts[$renderedPosts.length - 1];
			this.lastPostId = $lastRenderedPost.getAttribute( this.data_post_id );

			// New version of load more button
			const adFrequency = JSON.parse( $button.dataset.adFrequency );
			const adBpOffset = JSON.parse( $button.dataset.adOffset );
			const breakpoints = Object.keys( adFrequency );
			const numPosts = $renderedPosts.length;
			[].forEach.call( breakpoints, ( bp ) => {
				adBpOffset[bp] = adFrequency[bp] - ( ( numPosts - adBpOffset[bp]) % adFrequency[bp]);
			});

			// Set initial state for button
			this.setButtonAttributes( $button, {
				lastPostId: $lastRenderedPost.getAttribute( this.data_post_id ),
				adOffset: JSON.stringify( adBpOffset ),
				page: 1,
			});

			// Bind new click event listener to 'Load More' button
			$button.addEventListener( 'click', ( event ) => {
				LoadMore.handleButtonClick( event );
			});
		});
	},

	/**
	 * Updates button attributes for page result and ad position
	 * Calls ajax
	 * @method handleButtonClick
	 *
	 */
	handleButtonClick( evt ) {
		if ( ! evt.currentTarget.dataset.action ) {
			return;
		}

		const keys = Object.keys( evt.currentTarget.dataset );
		const params = {};
		[].forEach.call( keys, ( k ) => {
			if ( k.indexOf( 'load-more' ) < 0 ) {
				if ( /\{.+\}/.test( evt.currentTarget.dataset[k]) ) {
					params[k] = JSON.parse( evt.currentTarget.dataset[k]);
				} else {
					params[k] = evt.currentTarget.dataset[k];
				}
			}
		});

		this.toggleButtonAndShimmer( evt.currentTarget );

		// Then call ajax function.
		this.loadMoreResults( evt.currentTarget, params.action, params );

		// Track analytics
		this.track( evt.currentTarget );
	},

	/**
	 * Updates button attributes for page result and ad position
	 * Calls ajax
	 * @method getMoreResults
	 * @param {HTMLElement} $button - html element of load more button
	 * @param {String} action - ajax action
	 * @param {Object} params - params to be sent via ajax call
	 *
	 */
	loadMoreResults( $button, action, params ) {
		fetch( `/gnca-ajax-redesign/${params.action}/${encodeURI( JSON.stringify( params ) )}` )
			.then( response => response.text() )
			.then( ( content ) => {
				const $htmlContent = new DOMParser().parseFromString( content, 'text/html' );
				const $listItems = $htmlContent.querySelectorAll( `.${this.loadmore_item_class}` );
				const stopLoadMore = $htmlContent.querySelector( `${this.loadmore_ended_selector}` );
				const $resultList = document.querySelector( `#${$button.dataset.loadMoreTarget}` );
				const { adPosition, adOffset, queryValue } = params;

				if ( $resultList ) {
					let $currentPostItem = null;
					[].forEach.call( $listItems, ( $listItem ) => {
						$resultList.appendChild( $listItem );

						// Check if this unit is an ad
						if ( $listItem.classList.contains( this.ad_class ) ) {
							// If so get the inner div
							const adInnerDiv = $listItem.querySelector( `.${this.ad_unit_class}` );
							const { adBp: breakpoint, adPos } = adInnerDiv.dataset;

							/* global gn_monetize */
							/* eslint-disable camelcase */
							if ( adInnerDiv && 'undefined' !== typeof gn_monetize && 'undefined' !== typeof gn_monetize.Ads ) {
								// Initiate dynamic ad
								gn_monetize.Ads.create({
									sizes: '[300,250]',
									biddable: true,
									id: `${adInnerDiv.getAttribute( 'id' )}`,
									lazy: true,
									targeting: {
										pos: `${adPos}`,
									},
								});

								adPosition[breakpoint] = adPos;
							}
							/* eslint-enable camelcase */
						} else {
							// watch for lazy load images
							LazyLoad.startWatching( $listItem.querySelector( `img${LazyLoad.selector}` ) );
							ImageContainer.init( $listItem.querySelectorAll( ImageContainer.selector ) );
							$currentPostItem = $listItem;
						}
					});

					// Place focus on first new rendered story
					// for improved screenreader and keyboard navigation experience
					const lastRenderedPostId = $button.getAttribute( 'data-last-post-id' );
					const lastRenderedItem = document.querySelector( `[${this.data_post_id}="${lastRenderedPostId}"]` );
					if ( lastRenderedItem ) {
						let firstNewItem = lastRenderedItem.nextElementSibling;
						if ( firstNewItem ) {
							// First new item can be an ad. In this case skip to the item after it
							if ( firstNewItem.classList.contains( 'c-posts__ad' ) ) {
								firstNewItem = firstNewItem.nextElementSibling;
							}

							// Focus on the link in first new rendered story
							if ( firstNewItem.querySelector( 'a' ) ) {
								firstNewItem.querySelector( 'a' ).focus();
							}
						}
					}

					// Calculate new ad placement offset
					const numPosts = parseInt( params.number, 10 );
					[].forEach.call( Object.keys( params.adPosition ), ( bp ) => {
						const frequency = params.adFrequency[bp];
						adOffset[bp] = frequency - ( ( numPosts - adOffset[bp]) % frequency );
					});

					// Update load more button attributes, if load more returns a valid response
					if ( content ) {
						const buttonParams = {
							lastPostId: $currentPostItem ? $currentPostItem.dataset.postId : 0,
							adPosition: JSON.stringify( adPosition ),
							adOffset: JSON.stringify( adOffset ),
							page: parseInt( params.page, 10 ) + 1,
						};

						// Update paged value for querying next page.
						if ( 'object' === typeof ( queryValue ) && queryValue.paged ) {
							queryValue.paged += 1;
							buttonParams.queryValue = JSON.stringify( queryValue );
						}

						this.setButtonAttributes( $button, buttonParams );
					}

					this.toggleButtonAndShimmer( $button );

					SophiTagWrapper.refreshTracking();

					customEvent.fire( window, customEvent.MORE_LOADED );

					// If there are no more posts in the query for next load,
					// hide 'Load More' button
					if ( stopLoadMore ) {
						$button.style.display = 'none'; /* eslint-disable-line no-param-reassign */
					}
				}
			});
	},

	/**
	 * Toggle hide/show class on 'Load More' button and placeholder list
	 * @method toggleButtonAndShimmer
	 *
	 */
	toggleButtonAndShimmer( $button ) {
		document.querySelector( `#${$button.dataset.loadMoreTarget}-skeleton` ).classList.toggle( 'is-hidden' );
		$button.classList.toggle( 'is-hidden' );
	},

	/**
	 * Set data attributes for load more button
	 *
	 * @method setButtonAttributes
	 * @param {HTMLElement} $button - button element
	 * @param {Object} data - parameters to be sent via ajax call
	 */
	setButtonAttributes( $button, data ) {
		const keys = Object.keys( data );
		[].forEach.call( keys, ( key ) => {
			$button.dataset[key] = data[key]; /* eslint-disable-line no-param-reassign */
		});
	},

	/**
	 * Track analytics when load more clicked
	 *
	 * @method track
	 * @param {HTMLElement} $target - button element
	 */
	track( $target ) {
		const params = $target.dataset;

		/* global gn_analytics */
		/* eslint-disable camelcase */
		if ( 'undefined' !== typeof ( gn_analytics ) ) {
			gn_analytics.Analytics.track(['ga', 'adobe'], {
				eventType: 'loadMore',
				action: params.page,
				target: $target,
				data: {
					loadMore: params.page,
				},
			});
		}
		/* eslint-enable camelcase */
	},
};

export default LoadMore;