main/RegionPicker.js

import Cookies from '../vendor/jscookie';
import ImageContainer from './ImageContainer';
import customEvent from '../utils/customEvent';
import SophiTagWrapper from './SophiTagWrapper';
import '../polyfills/Fetch';

/**
 * Sets up region selection
 *
 * @example
 * <section data-region-picker>{{region menu here}}</section>
 *
 * @module RegionPicker
 * @prop {object} selectors - DOM query selectors
 * @prop {object} states - CSS classes
 */
const RegionPicker = {
	selectors: {
		main: '[data-region-picker]',
		posts: '.c-posts',
		postItem: '.c-posts__item:not(.c-posts__ad)',
		regionLinks: '[data-region-link]',
		linkLabel: '.c-link__label',
	},

	states: {
		shimmer: 'c-shimmer',
		skeleton: 'c-posts--skeleton',
		animateIn: 'animate-fadeIn',
		hidden: 'is-hidden',
	},

	cookieNames: {
		geo: '_wpcom_geo',
		geoExp: '_wpcom_geo_exp',
	},

	currentRegion: false,

	cookieCheck: 0,

	/**
	 * Search for any region selection modules and bind click events
	 * @method init
	 */
	init() {
		const $regionPickers = document.querySelectorAll( this.selectors.main );
		let cookiedRegion = Cookies.get( this.cookieNames.geo );
		if ( cookiedRegion ) {
			cookiedRegion = cookiedRegion.replace( 'gnca-', '' );
		}

		/* global gnca_settings */
		/* eslint-disable camelcase */
		const detectedRegion = gnca_settings && gnca_settings.user_region ? gnca_settings.user_region.replace( 'gnca-', '' ) : 'national';
		/* eslint-enable camelcase */

		this.currentRegion = detectedRegion || cookiedRegion;
		this.saveRegion( this.currentRegion );

		[].forEach.call( $regionPickers, ( $regionPicker ) => {
			const targetSelector = $regionPicker.dataset.regionPickerTarget;
			const $target = document.querySelector( targetSelector );
			const clickListener = this.handleClick.bind( this, $target, $regionPicker.getAttribute( 'id' ) );
			$regionPicker.addEventListener( 'click', clickListener );
		});
	},

	/**
	 * Verify that the user has clicked on a link within the region selection module
	 * Then pass along the region name that was clicked on
	 * @method handleClick
	 * @param {element} $changeTarget - update content in $changeTarget based on new region
	 * @param {string} pickerId - picker id
	 * @param {event} event
	 */
	handleClick( $changeTarget, pickerId, event ) {
		let $link = false;

		if ( 'a' === event.target.nodeName.toLowerCase() ) {
			$link = event.target;
		} else if ( 'a' === event.target.parentNode.nodeName.toLowerCase() ) {
			$link = event.target.parentNode;
		}

		if ( $link && 'false' !== $link.dataset.regionChange ) {
			// don't follow anchor tag, we will do this via JS after resetting the cookie
			event.preventDefault();

			let region = $link.innerText;
			region = this.parseRegion( region );
			this.changeRegion( region );
			this.trackRegionChange( region, event );

			if ( $changeTarget ) {
				const $toggleButton = document.querySelector( `[data-expand="#${pickerId}"]` );
				$toggleButton.click();
				this.changeRegionContent( region, $changeTarget );
			} else {
				this.postRegion( region, $link.getAttribute( 'href' ) );
			}
		}
	},

	/**
	 * Slugify the region name
	 * @method parseRegion
	 */
	parseRegion( region ) {
		return region.trim().toLowerCase().replace( ' ', '-' );
	},

	/**
	 * Update the user's region cookie
	 * @method changeRegion
	 */
	changeRegion( region ) {
		this.saveRegion( region );
		customEvent.fire( window, customEvent.REGION_CHANGE, {
			region,
		});
	},

	/**
	 * Save region cookie - save two cookies in staggered expiry to keep region cookie alive
	 * to prevent infinite geo detection loop.
	 *
	 * @method saveRegion
	 */
	saveRegion( region ) {
		Cookies.set( this.cookieNames.geo, `gnca-${region}`, { expires: 90, path: '/' });
		Cookies.set( this.cookieNames.geoExp, `gnca-${region}`, { expires: 60, path: '/' });
	},

	/**
	 * Send region as a POST request for vary cache
	 *
	 * @method postRegion
	 * @param {string} region - region to remember
	 * @param {string} redirectUrl - optional URL to redirect to when post completed
	 */
	postRegion( region, redirectUrl = '' ) {
		if ( ! redirectUrl ) {
			const xhttp = new XMLHttpRequest();
			xhttp.open( 'POST', redirectUrl, true );
			xhttp.setRequestHeader( 'Content-type', 'application/x-www-form-urlencoded' );
			xhttp.send( `gnca-region=gnca-${region}` );
		} else {
			// Create a temporary form for redirecting to the target URL as a POST request
			const template = `
				<form id="regionPickerForm" class="is-hidden" action="${redirectUrl}" method="POST">
					<input type="hidden" name="gnca-region" value="gnca-${region}"/>
				</form>
			`;

			const $form = document.createElement( 'div' );
			$form.innerHTML = template;
			document.querySelector( 'body' ).appendChild( $form );

			this.redirectUser( region );
		}
	},

	/**
	 * Redirect user to the target page once region cookie is set
	 */
	redirectUser( region ) {
		const $form = document.querySelector( '#regionPickerForm' );

		// Ensure cookie set before leaving the page
		clearInterval( this.cookieCheck );
		this.cookieCheck = setInterval( () => {
			if ( `gnca-${region}` === Cookies.get( this.cookieNames.geo ) ) {
				$form.submit();
				clearInterval( this.cookieCheck );
			}
		}, 100 );

		// Timeout when cookie still not set within 1 second.
		setTimeout( () => {
			$form.submit();
			clearInterval( this.cookieCheck );
		}, 1000 );
	},

	/**
	 * Make an ajax request to get content for selected region
	 *
	 * @method changeRegionContent
	 * @param {string} region - selected region
	 * @param {element} $target - target element where new content should be added
	 */
	changeRegionContent( region, $target ) {
		const $posts = $target.querySelector( this.selectors.posts );
		const keys = Object.keys( $posts.dataset );
		const params = {};
		[].forEach.call( keys, ( k ) => {
			if ( /\{.+\}/.test( $posts.dataset[k]) ) {
				params[k] = JSON.parse( $posts.dataset[k]);
			} else {
				params[k] = $posts.dataset[k];
			}
		});
		params.region = `gnca-${region}`;

		// Set loading style
		$posts.classList.add( this.states.skeleton );
		$posts.classList.add( this.states.shimmer );
		$posts.classList.remove( this.states.animateIn );

		$posts.dataset.sophiFeature = `local-news-${region}`;

		// Region name
		let regionName = region.replace( '-', ' ' );
		regionName = regionName.length > 2 ? regionName.replace( /(^|\s)\S/g, t => t.toUpperCase() ) : regionName.toUpperCase();

		const $regionLinks = document.querySelectorAll( this.selectors.regionLinks );
		const link = `/${region}`;
		let overrideLink = link;

		/* global gnca_settings */
		/* eslint-disable camelcase */

		// check for override link
		if ( gnca_settings && gnca_settings.override_links ) {
			if ( gnca_settings.override_links[link]) {
				overrideLink = gnca_settings.override_links[link];
			}
		}
		/* eslint-enable camelcase */

		[].forEach.call( $regionLinks, ( $link ) => {
			$link.setAttribute( 'href', $link.dataset.regionLinkOverride ? overrideLink : link );
			if ( Object.keys( $link.dataset ).indexOf( 'regionLabel' ) >= 0 ) {
				const $label = $link.querySelector( this.selectors.linkLabel );
				$label.textContent = regionName; /* eslint-disable-line no-param-reassign */
				$link.setAttribute( 'title', regionName );
			}
		});

		this.postRegion( region );

		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.selectors.postItem );
				const $oldListItems = $posts.querySelectorAll( this.selectors.postItem );
				const numOldItems = $oldListItems.length;
				const numNewItems = $listItems.length;

				if ( numNewItems > numOldItems ) {
					[].forEach.call( $listItems, ( $newItem, index ) => {
						const $oldItem = $oldListItems[index];
						if ( $oldItem ) {
							// Only replace the item if it isn't a hidden ad fallback item
							if ( ! $oldItem.dataset.adFallback
								|| ! $oldItem.classList.contains( this.states.hidden ) ) {
								$posts.replaceChild( $newItem, $oldItem );
							}
						} else {
							$posts.appendChild( $newItem );
						}
					});
				} else {
					[].forEach.call( $oldListItems, ( $oldItem, index ) => {
						if ( $listItems[index]) {
							// Only replace the item if it isn't a hidden ad fallback item
							if ( ! $oldItem.dataset.adFallback
								|| ! $oldItem.classList.contains( this.states.hidden ) ) {
								$posts.replaceChild( $listItems[index], $oldItem );
							}
						} else {
							$oldItem.classList.add( this.states.hidden );
						}
					});
				}

				ImageContainer.init( $posts.querySelectorAll( ImageContainer.selector ) );
				$posts.classList.remove( this.states.skeleton );
				$posts.classList.remove( this.states.shimmer );
				$posts.classList.add( this.states.animateIn );

				SophiTagWrapper.refreshTracking();
			});
	},

	/**
	 * Track region change in Google and Adobe analytics
	 * @method trackRegionChange
	 */
	trackRegionChange( region, event ) {
		/* global gn_analytics */
		/* eslint-disable camelcase */
		if ( 'undefined' !== typeof ( gn_analytics ) && 'undefined' !== typeof ( gn_analytics.Analytics ) ) {
			const trackAction = `${this.currentRegion} > ${region}`;
			gn_analytics.Analytics.track(['ga', 'adobe'], {
				eventType: 'Region Change',
				action: trackAction,
				target: event.currentTarget,
			});
		}
		/* eslint-enable camelcase */
	},
};

export default RegionPicker;