import InView from '../utils/classes/InView';
import customEvent from '../utils/customEvent';
/**
* Quick links for anchoring on the page
*
* @module QuickLinks
* @prop {Array} headings - Section headings found in article
* @prop {Object} selectors - DOM element selectors
* @prop {Object} css - quick link elements class names
*/
const QuickLinks = {
headings: [],
selectors: {
list: '.c-quickLinks__list',
select: '.c-quickLinks__select',
heading: '.l-article__sectionHeading',
anchor: '.l-article__sectionAnchor',
scroll: '.c-quickLinks--scroll',
section: '.l-article__section',
stickyDropdown: 'c-quickLinks--sticky',
articleDropdown: '.c-quickLinks--article',
},
css: {
link: 'c-quickLinks__link',
item: 'c-quickLinks__item',
highlight: 'c-quickLinks__item--highlight',
hidden: 'is-hidden',
},
/**
* Look for section headings in the article body
* and generate quick links accordingly.
*
* @method init
*/
init() {
const $lists = document.querySelectorAll( this.selectors.list );
const $selects = document.querySelectorAll( this.selectors.select );
this.headings = document.querySelectorAll( this.selectors.heading );
if ( this.headings && $lists ) {
[].forEach.call( $lists, ( $list ) => {
if ( this.headings.length > 0 ) {
[].forEach.call( this.headings, ( $heading ) => {
const $anchor = $heading.querySelector( this.selectors.anchor );
if ( $anchor ) {
const $item = document.createElement( 'li' );
$item.classList.add( this.css.item );
const $link = document.createElement( 'a' );
$link.textContent = $heading.textContent;
$link.classList.add( this.css.link );
$link.setAttribute( 'href', `#${$anchor.getAttribute( 'id' )}` );
$item.appendChild( $link );
$list.appendChild( $item );
const $addedLink = $item.querySelector( `.${this.css.link}` );
$addedLink.addEventListener( 'click', evt => this.track( evt ) );
}
});
} else {
$list.parentNode.classList.add( this.css.hidden );
}
});
}
if ( this.headings && $selects ) {
[].forEach.call( $selects, ( $select ) => {
if ( this.headings.length > 0 ) {
// Create 1st generic option
const $firstOption = document.createElement( 'option' );
$firstOption.textContent = 'Go to section...';
$firstOption.setAttribute( 'disabled', true );
$firstOption.setAttribute( 'selected', true );
$firstOption.setAttribute( 'value', '' );
$select.appendChild( $firstOption );
[].forEach.call( this.headings, ( $heading ) => {
const $anchor = $heading.querySelector( this.selectors.anchor );
if ( $anchor ) {
const $option = document.createElement( 'option' );
$option.textContent = $heading.textContent;
$option.classList.add( this.css.link );
$option.setAttribute( 'value', `#${$anchor.getAttribute( 'id' )}` );
$select.appendChild( $option );
}
});
$select.addEventListener( 'change', ( evt ) => {
if ( '' !== $select.value ) {
window.location.hash = $select.value;
this.track( evt );
// Update the value on the other select for consistency
[].forEach.call( $selects, ( $select2 ) => {
const $selectDropdown = $select2;
$selectDropdown.value = $select.value;
});
}
});
} else {
$select.parentNode.classList.add( this.css.hidden );
}
});
}
// If at least one quickLinks menu has a scroll class
// attach scrolling trigger to the sections
// currently the scrolling menu is only on 'Issues' article template
if ( document.querySelector( this.selectors.scroll ) ) {
// Trigger to add contrst class to sticky nav on custom template
this.attachTriggers(
this.selectors.section,
[{
handler: this.menuHighlight,
key: 'menuHighlight',
}],
);
}
this.$sectionsDropdown = document.querySelector( this.selectors.articleDropdown );
this.$stickyQuickLinks = document.querySelector( `.${this.selectors.stickyDropdown}` );
// Section dropdown menu transitions on scroll on 'Featured' and 'Issues' templates
if ( this.$sectionsDropdown && this.$stickyQuickLinks ) {
// Runs only on tablet-portrait and below
if ( window.innerWidth <= 1024 ) {
this.attachTriggers(
this.selectors.articleDropdown,
[{
handler: this.transitionSectionsDropdown,
key: 'transitionSectionsDropdown',
}],
);
}
}
},
/**
* Attaches Intersection Observer trigger to all sections within the article
*
* @method attachTriggers
* @param {string} selector - CSS selector for the transition trigger element.
* @param {array} callbacks - Array of callback handlers and keys.
*/
attachTriggers( selector, callback ) {
const $targets = document.querySelectorAll( selector );
// Don't run if there are no targers in the article
if ( ! $targets ) {
return;
}
[].forEach.call( $targets, ( target ) => {
if ( target ) {
const $target = target;
// We need to continue observing the breakpoint no matter how often the user scrolls past
$target.dataset.alwaysObserve = 'true';
// Threshold height
const boundary = $target.getBoundingClientRect();
// Bind observer handler
$target.addEventListener(
customEvent.IN_VIEW,
callback[0].handler.bind( this, boundary, callback[0].key ),
);
}
});
// Initialize Intersection Observer
const observer = new InView({
selector,
threshold: 0.1,
});
observer.init();
},
/**
* Hightlights section menu item when corresponding section is visible
*
* @method menuHighlight
* @param {object} boundary - The bounding box values of the trigger element.
* @param {string} key - String to select the appropriate status setting.
* @param {object} event - Intersection Observer callback object with InView.
*/
menuHighlight( boundary, key, event ) {
if ( event && event.detail ) {
const $section = event.target;
// Check if this section has a heading anchor
const $sectionAnchor = $section.querySelector( this.selectors.anchor );
let anchorId = '';
if ( $sectionAnchor ) {
anchorId = $sectionAnchor.getAttribute( 'id' );
// Find corresponding menu item
const $menuLink = document.querySelector( `a[href="#${anchorId}"]` );
const $menuItem = $menuLink.parentNode;
// If the section with a heading is now in view
if ( event.detail.isInView ) {
$menuItem.classList.add( this.css.highlight );
} else {
$menuItem.classList.remove( this.css.highlight );
}
}
}
},
/**
* Handles sticky behavior of the quickLinks article menu select
* Used on the 'Issues' and 'Featured' template
*
* @method transitionSectionsDropdown
* @param {object} boundary - The bounding box values of the trigger element.
* @param {string} key - String to select the appropriate status setting.
* @param {object} event - Intersection Observer callback object with InView.
*/
transitionSectionsDropdown( boundary, key, event ) {
const dropdownTop = this.$sectionsDropdown.getBoundingClientRect().top;
const aboveMenu = dropdownTop > 0;
if ( event
&& event.detail
&& event.detail.ratioInView < 0.1
&& aboveMenu ) { // In-article dropdown is below viewport
setTimeout( () => {
this.$stickyQuickLinks.classList.add( this.css.hidden );
customEvent.fire( window, customEvent.STICKY_OFF, {
target: this.$stickyQuickLinks,
selector: this.selectors.stickyDropdown,
});
}, 500 );
} else if ( event
&& event.detail
&& event.detail.ratioInView < 0.1
&& ! aboveMenu ) { // In-article dropdown is above viewport
setTimeout( () => {
if ( this.headings.length > 0 ) {
this.$stickyQuickLinks.classList.remove( this.css.hidden );
customEvent.fire( window, customEvent.STICKY_ON, {
target: this.$stickyQuickLinks,
selector: this.selectors.stickyDropdown,
});
}
}, 500 );
} else if ( event
&& event.detail
&& event.detail.ratioInView >= 0.1 ) { // In-article dropdown is currently visible
setTimeout( () => {
this.$stickyQuickLinks.classList.add( this.css.hidden );
customEvent.fire( window, customEvent.STICKY_OFF, {
target: this.$stickyQuickLinks,
selector: this.selectors.stickyDropdown,
});
}, 500 );
}
},
/**
* Track link click
*/
track( event ) {
/* global gn_analytics */
/* eslint-disable camelcase */
if ( 'undefined' !== typeof ( gn_analytics ) ) {
const $target = event.currentTarget;
gn_analytics.Analytics.track(['adobe'], {
eventType: 'article quick link',
action: $target.textContent.trim(),
target: $target,
});
}
/* eslint-enable camelcase */
},
};
export default QuickLinks;