import InView from '../../utils/classes/InView';
import customEvent from '../../utils/customEvent';
/**
* Detects whether the edge of a container has entered or exited the viewport.
* This is useful because intersection observer has a hard time observing objects
* that are larger than the viewport itself. This is done by adding small sentinal
* objects to the top and bottom of the container that can be observed more easily.
*
* @class InBounds
* @prop {string} boundaryLabel - data field for sentinal elements
* @prop {string} boundaryUpperValue - used to identify the top sentinal element
* @prop {string} boundaryLowerValue - used to identify the bottom sentinal element
* @prop {string} boundaryStyle - style applied to sentinal elements
* @prop {string} adminBarSelector - selector for the wp admin bar (used to calculate topMargin)
* @prop {string} navBarSelector - selector for the nav bar (used to calculate topMargin)
* @prop {element} $container - DOM object representing the container being observed
* @prop {element} $upperbound - DOM object representing the upper sentinal element
* @prop {element} $lowerbound - DOM object representing the lower sentinal element
* @prop {object} observer - the InView object instance we are using to keep watch
* @prop {object} settings - class setting parameters passed to the constructor
* @prop {string} name - name to uniquely identify this object instance
* @prop {int} topMargin - we shrink the viewport by this amount to exclude sticky elements
*/
class InBounds {
boundaryLabel = 'dataBoundary';
boundaryUpperValue = 'upper';
boundaryLowerValue = 'lower';
boundaryStyle = 'width:1px;height:1px;margin-top:-1px;background-color:transparent';
adminBarSelector = '#wpadminbar';
navBarSelector = '.l-navbar';
$container = null;
$upperbound = null;
$lowerbound = null;
observer = null;
settings = null;
topMargin = 0;
/**
* Constructor
*/
constructor( settings ) {
this.settings = settings;
if ( ! this.settings.selector ) {
return;
}
this.$container = document.querySelector( this.settings.selector );
}
/**
* Sets up listeners to watch for upper and lower bounds of container coming in / out of view
*
* @method init
*/
init() {
// We want to treat the boundary below sticky elements as the top of the viewport
this.calculateTopMargin();
// Initialize in view watcher for stickiness
this.observer = new InView({
rootMargin: `-${this.topMargin}px 0px 0px 0px`,
});
this.observer.init();
// Set up boundary sentinals
this.setupBoundary();
}
/**
* Sets up sentinal elements at the top and bottom of the container to serve as signals
*
* @method setupBoundary
*/
setupBoundary() {
this.$upperbound = document.createElement( 'div' );
this.$upperbound.dataset[this.boundaryLabel] = this.boundaryUpperValue;
this.$upperbound.setAttribute( 'style', this.boundaryStyle );
this.$upperbound.dataset.alwaysObserve = 'true';
this.$upperbound.addEventListener( customEvent.IN_VIEW,
evt => this.handleUpperboundInView( evt ) );
this.$lowerbound = document.createElement( 'div' );
this.$lowerbound.dataset[this.boundaryLabel] = this.boundaryLowerValue;
this.$lowerbound.setAttribute( 'style', this.boundaryStyle );
this.$lowerbound.dataset.alwaysObserve = 'true';
this.$lowerbound.addEventListener( customEvent.IN_VIEW,
evt => this.handleLowerboundInView( evt ) );
const $firstChild = this.$container.children[0];
this.$container.insertBefore( this.$upperbound, $firstChild );
this.$container.appendChild( this.$lowerbound );
this.observer.startWatching( this.$upperbound );
}
/**
* Handle upperbound coming in / out of viewport
*
* @method handleUpperboundInView
* @param {object} evt - InView event
*/
handleUpperboundInView( evt ) {
// check if upper bounds has passed below viewport (we can ignore this)
const { bottom } = this.$upperbound.getBoundingClientRect();
if ( bottom >= window.innerHeight ) {
return;
}
let isInBounds = null;
this.observer.stopWatching( this.$lowerbound );
if ( evt.detail.ratioInView > 0 ) {
// The bottom boundary has scrolled into view
isInBounds = false;
} else {
// The bottom boundary has scrolled out of view
// The container is now fully in view
isInBounds = true;
this.observer.startWatching( this.$lowerbound );
}
customEvent.fire( this.$container, customEvent.IN_BOUNDS, {
caller: this,
target: this.$container,
side: 'upper',
isInBounds,
});
}
/**
* Handle lower coming in / out of viewport
*
* @method handleLowerboundInView
* @param {object} evt - InView event
*/
handleLowerboundInView( evt ) {
// check if lower bounds has moved above viewport (we can ignore this)
const { top } = this.$lowerbound.getBoundingClientRect();
if ( top <= this.topMargin ) {
return;
}
let isInBounds = null;
if ( evt.detail.ratioInView > 0 ) {
// The top boundary has scrolled into view
isInBounds = false;
} else {
// The top boundary has scrolled out of view
// The container is now fully in view
isInBounds = true;
}
customEvent.fire( this.$container, customEvent.IN_BOUNDS, {
caller: this,
target: this.$container,
side: 'lower',
isInBounds,
});
}
/**
* Unbind observer and remove sentinal elements
*
* @method stopWatching
*/
stopWatching() {
this.observer.stopWatching( this.$upperbound );
this.observer.stopWatching( this.$lowerbound );
this.$upperbound.parentNode.removeChild( this.$upperbound );
this.$lowerbound.parentNode.removeChild( this.$lowerbound );
}
/**
* This will shorten the viewport based on the sticky nav and WP admin bar height
*
* @method calculateTopMargin
*/
calculateTopMargin() {
const $adminBar = document.querySelector( this.adminBarSelector );
const $navBar = document.querySelector( this.navBarSelector );
if ( $adminBar ) {
this.topMargin += $adminBar.getBoundingClientRect().height;
}
if ( $navBar ) {
this.topMargin += $navBar.getBoundingClientRect().height;
}
}
}
export default InBounds;