import populateTemplate from '../../utils/populateTemplate';
import customEvent from '../../utils/customEvent';
/**
* Handles Material-UI style Snackbar prompts that slide in at the bottom of the page
*
* @class Snackbar
* @prop {object} selectors - Set of selectors that can be used to select snackbar features.
* @prop {object} states - Set of classes that serve as flags for snackbar states.
* @prop {string} template - Snackbar markup template.
* @prop {object} settings - Set of settings that can be used to customize the snackbar.
* @prop {element} $element - DOM reference to the snackbar element.
* @prop {element} $element - DOM reference to the snackbar container (i.e. the body element).
* @prop {int} height - Snackbar height (used to compute transitions).
* @prop {int} startY - Initial cursor Y position when swiping (to compute swipe dismiss gesture).
*/
class Snackbar {
static selectors = {
container: 'body',
element: '.c-snackbar',
dismiss: '.c-snackbar__dismiss',
accept: '.c-snackbar__accept',
};
static states = {
active: 'c-snackbar--active',
};
static template = `
<div class="c-snackbar__inner">
<div class="c-snackbar__text">{{prompt}}</div>
<div class="c-snackbar__buttons">
<button class="c-button c-button--tertiary c-snackbar__button c-snackbar__dismiss">{{dismiss}}</button>
<button class="c-button c-snackbar__button c-snackbar__accept">{{accept}}</button>
</div>
</div>
`;
settings = {
text: {
prompt: '',
dismiss: 'Dismiss',
accept: 'Accept',
},
};
$container = null;
$element = null;
height = null;
startY = null;
/**
* @constructs
* @param {object} settings - Snackbar settings to be merged with defaults
*/
constructor( settings ) {
// custom snackbar text
if ( settings && settings.text ) {
this.settings.text.prompt = settings.text.prompt;
this.settings.text.dismiss = settings.text.dismiss;
this.settings.text.accept = settings.text.accept;
}
}
/**
* Initialize snackbar, and add to page.
*
* @method init
*/
init() {
// remove existing Snackbar(s)
const $elements = document.querySelectorAll( Snackbar.selectors.element );
[].forEach.call( $elements, ( $element ) => {
$element.parentNode.removeChild( $element );
});
// create element
this.$element = document.createElement( 'div' );
this.$element.innerHTML = populateTemplate( Snackbar.template, this.settings.text, true );
this.$element.classList.add( Snackbar.selectors.element.replace( '.', '' ) );
// add event listeners
this.listeners();
// append to markup
this.$container = document.querySelector( Snackbar.selectors.container );
this.$container.appendChild( this.$element );
// set height
this.height = this.$element.getBoundingClientRect().height;
this.$element.style.setProperty( '--snackbarHeight', `${this.height}px` );
// fade in
setTimeout( () => {
this.$element.classList.add( Snackbar.states.active );
}, 300 );
}
/**
* Add snackbar event listeners for snackbar actions.
*
* @method listeners
*/
listeners() {
// dismiss button
const $dismiss = this.$element.querySelector( `${Snackbar.selectors.dismiss}` );
$dismiss.addEventListener( 'click', () => { this.dismiss(); });
// accept button
const $accept = this.$element.querySelector( `${Snackbar.selectors.accept}` );
$accept.addEventListener( 'click', () => { this.accept(); });
this.$element.addEventListener( 'pointerdown', ( e ) => { this.startSwipe( e ); });
}
/**
* Setup and bind listeners to handle the swipe to dismiss gesture.
*
* @method startSwipe
* @param {object} event - Pointerdown event instance.
*/
startSwipe( event ) {
this.startY = event.clientY;
this.boundPointerMove = e => this.handleSwipe( e );
this.boundPointerUp = e => this.endSwipe( e );
this.boundPointerCancel = e => this.endSwipe( e );
this.boundToggleScroll = ( e ) => {
if ( e.cancelable ) {
e.preventDefault();
}
};
window.addEventListener( 'pointermove', this.boundPointerMove );
window.addEventListener( 'pointerup', this.boundPointerUp );
window.addEventListener( 'pointercancel', this.boundPointerCancel );
window.addEventListener( 'touchmove', this.boundToggleScroll, { passive: false });
this.$element.style.transition = 'none';
}
/**
* Move snackbar with pointer if user is moving downwards to dismiss.
*
* @method handleSwipe
* @param {object} event - Pointermove event instance.
*/
handleSwipe( event ) {
const diff = this.startY - event.clientY;
if ( diff <= 0 ) {
const offset = Math.abs( diff );
this.$element.style.transform = `translate3d( 0, ${offset}px, 0 )`;
} else {
const offset = 8 * Math.log( diff );
this.$element.style.transform = `translate3d( 0, -${offset}px, 0 )`;
}
event.preventDefault();
}
/**
* Once the user has lifted the pointer, determine if a swipe gestures was completed.
* If so, get rid of the snackbar.
*
* @method endSwipe
* @param {object} event - Pointerup or pointercancel event instance.
*/
endSwipe( event ) {
const diff = this.startY - event.clientY;
window.removeEventListener( 'pointermove', this.boundPointerMove );
window.removeEventListener( 'pointerup', this.boundPointerUp );
window.removeEventListener( 'pointercancel', this.boundPointerCancel );
window.removeEventListener( 'touchmove', this.boundToggleScroll );
this.$element.style.transform = '';
this.$element.style.transition = '';
// remove snackbar if user has swiped 10% or more of the elements height
if ( diff <= ( this.height * - 0.1 ) ) {
this.dismiss();
}
}
/**
* Dismiss the snackbar and fire dismissal event.
*
* @method dismiss
*/
dismiss() {
customEvent.fire( this.$element, customEvent.SNACKBAR_DISMISSED );
this.destroy();
}
/**
* Fire acceptance event and get rid of the snackbar.
*
* @method accept
*/
accept() {
customEvent.fire( this.$element, customEvent.SNACKBAR_ACCEPTED );
this.destroy();
}
/**
* Slide the snackbar out and remove the element from the DOM.
*
* @method @destroy
*/
destroy() {
this.$element.classList.remove( Snackbar.states.active );
setTimeout( () => {
this.$element.parentNode.removeChild( this.$element );
}, 500 );
}
}
export default Snackbar;