@randstad-design/orbit-multitheme
Version:
multitheme Front-end code based on Randstad Human Forward components
349 lines (321 loc) • 10.6 kB
JavaScript
/**
* modal.js
* Script to toggle modal
*/
import ElementHelpers from '../helpers/element-helpers';
import trapFocus from '../helpers/trapFocus';
/**
* Declare constants
*/
const attributeBase = 'data-rs-modal';
export class Modal {
constructor(element) {
this.element = element;
this.openTrigger = ElementHelpers.getElementByAttribute(
this.attributes.trigger,
this.element.getAttribute(attributeBase)
);
this.closeTrigger = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.closeTrigger
);
this.doNotCloseOnClickOverlay =
ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.doNotCloseOnClickOverlay
);
this.dialog = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.dialog
);
this.main = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.main
);
this.header = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.header
);
this.footer = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.footer
);
this.disableHistory =
this.element.getAttribute(this.attributes.disableHistory) !== null;
this.popupDelay = this.element.getAttribute(this.attributes.popupDelay);
this.defaultPopupDelay = 5;
this.scrollbarWidth =
window.innerWidth - document.documentElement.clientWidth;
this.scrollPosition = 3;
this.bodyMarginTop = Modal.initializeBodyMarginTop();
this.addEventHandlers();
this.handleDividers();
this.handleQueryStringParameters();
this.initializePopup();
this.keystroke = 0;
this.modalOpen;
}
/**
* Declare classes
*/
get classes() {
return {
modalActive: 'modal--active',
modalOpen: 'modal-open',
modalHeaderDivider: 'divider',
modalHeaderDividerIn: 'modal__header--divider-in',
modalHeaderDividerOut: 'modal__header--divider-out',
modalMainOverflow: 'modal__main--overflow',
divider: 'divider',
dividerTop: 'divider--top'
};
}
/**
* Declare attribute constants
*/
get attributes() {
return {
trigger: `${attributeBase}-trigger`,
popupDelay: `${attributeBase}-popup-delay`,
closeTrigger: `${attributeBase}-close-trigger`,
dialog: `${attributeBase}-dialog`,
main: `${attributeBase}-main`,
doNotCloseOnClickOverlay: `${attributeBase}-do-not-close-on-click-overlay`,
header: `${attributeBase}-header`,
footer: `${attributeBase}-footer`,
disableHistory: `${attributeBase}-disable-history`,
modalOpen: 'modal_open',
hidden: 'aria-hidden'
};
}
/**
* Declare key codes
*/
get keyCodes() {
return {
Escape: 'Escape',
Tab: 'Tab'
};
}
/**
* Initalize body margin top
*/
static initializeBodyMarginTop() {
const marginTop = document.body.style.marginTop;
return !marginTop ? 0 : marginTop.slice(0, marginTop.length - 2);
}
/**
* Add event handlers
*/
addEventHandlers() {
// toggle modal on click trigger
this.openTrigger.addEventListener('click', (event) => {
this.modalOpen = true;
event.preventDefault();
this.scrollPosition = window.pageYOffset;
this.toggleModal(!this.disableHistory);
});
// close/toggle modal on click close trigger
this.closeTrigger.addEventListener('click', (event) => {
event.preventDefault();
this.toggleModal(!this.disableHistory);
document.body.style.marginTop = `${this.bodyMarginTop}px`;
window.scrollTo(0, this.scrollPosition);
this.modalOpen = false;
});
// if clicked outside modal/on overlay close/toggle modal
this.element.addEventListener('mousedown', (clickEvent) => {
if (
!this.doNotCloseOnClickOverlay &&
clickEvent.target !== this.dialog &&
!this.dialog.contains(clickEvent.target)
) {
this.toggleModal(!this.disableHistory);
document.body.style.marginTop = `${this.bodyMarginTop}px`;
window.scrollTo(0, this.scrollPosition);
}
});
// on resize, handle dividers of modal
window.addEventListener('resize', () => {
this.handleDividers();
});
// on scroll, check position to set/unset dividers
this.main.addEventListener(
'scroll',
() => {
this.checkPosition();
},
{ passive: true }
);
// on popstate event (i.e. back button in browser), handle querystring parameters
window.addEventListener('popstate', () => {
this.handleQueryStringParameters(false);
});
// handle key down event - close modal if escape is pressed
document.addEventListener('keydown', (keydownEvent) => {
this.handleButtonKeys(keydownEvent);
});
}
/**
* Toggle modal
*/
toggleModal(changePushState = true) {
this.element.classList.contains(this.classes.modalActive)
? this.closeModal(changePushState)
: this.openModal(changePushState);
}
/**
* Close modal - closes modal, set styling and edit pushstate
* @param {boolean} changePushState - determines if history push state should be adjusted
*/
closeModal(changePushState) {
// remove width of scrollbar to body and modal
document.body.style.paddingRight = '0px';
this.element.style.paddingRight = '';
// if changePushState is true, remove querystring parameters from url
if (changePushState) {
window.history.pushState({}, '', window.location.pathname);
}
document.querySelector('html').classList.remove(this.classes.modalOpen);
this.element.setAttribute(this.attributes.hidden, 'true');
this.element.classList.remove(this.classes.modalActive);
this.keystroke = 0;
// If the modal is active and there is a trigger,When closing the modal focus on the trigger again
if (this.modalOpen) {
this.openTrigger.focus();
}
this.modalOpen = false;
}
/**
* Open modal - opens modal, set styling and edit pushstate
* @param {boolean} changePushState - determines if history push state should be adjusted
*/
openModal(changePushState) {
this.modalOpen = true;
document.body.style.marginTop = `${
this.bodyMarginTop - this.scrollPosition
}px`;
document.body.style.paddingRight = `${this.scrollbarWidth}px`;
this.element.style.paddingRight = `${this.scrollbarWidth}px`;
// if changePushState is true, add querystring parameters to url
if (changePushState) {
window.history.pushState(
{},
'',
`${window.location.pathname}?${
this.attributes.modalOpen
}=${this.element.getAttribute(attributeBase)}`
);
}
document.querySelector('html').classList.add(this.classes.modalOpen);
this.element.removeAttribute(this.attributes.hidden);
this.element.classList.add(this.classes.modalActive);
}
/**
* Handle dividers - add or remove dividers
*/
handleDividers() {
if (this.main.scrollHeight > this.main.offsetHeight) {
// article element has a scrollbar, add divider classes
this.addDivider();
} else if (
this.header &&
this.header.classList.contains(this.classes.divider)
) {
// article element has no scrollbar, remove divider classes
this.removeDivider();
}
}
/**
* Add divider - add divider classes if main content is scrollable
*/
addDivider() {
this.main.classList.add(this.classes.modalMainOverflow);
if (this.footer !== null) {
this.footer.classList.add(this.classes.divider, this.classes.dividerTop);
}
}
/**
* Remove divider - remove divider classes if main content is not scrollable
*/
removeDivider() {
this.header.classList.remove(this.classes.divider);
this.main.classList.remove(this.classes.modalMainOverflow);
if (this.footer !== null) {
this.footer.classList.remove(this.classes.dividerTop);
}
}
/**
* Check position
*/
checkPosition() {
const modalMainY = this.main.scrollTop;
if (this.header && modalMainY > this.scrollPosition) {
this.header.classList.add(this.classes.divider);
this.header.classList.add(this.classes.modalHeaderDividerIn);
this.header.classList.remove(this.classes.modalHeaderDividerOut);
}
if (
(this.header && modalMainY < this.scrollPosition) ||
(this.header && modalMainY === 0)
) {
this.header.classList.add(this.classes.modalHeaderDividerOut);
this.header.classList.remove(this.classes.divider);
this.header.classList.remove(this.classes.modalHeaderDividerIn);
}
}
/**
* Handle querystring parameters - check querystring parameters to show or hide modal
*/
handleQueryStringParameters() {
if (window.location.href.includes(`?${this.attributes.modalOpen}`)) {
let urlParams = new URLSearchParams(window.location.search);
// open modal if element is equal to value in querystring parameter
this.openModalElement = urlParams.get(this.attributes.modalOpen);
if (this.openModalElement === this.element.getAttribute(attributeBase)) {
this.openModal(false);
}
} else {
this.closeModal(false);
}
}
/**
* Initialize popup - if element contains popup-delay attribute => show popup with delay, if no delay found -> set to 5 sec
*/
initializePopup() {
if (this.popupDelay !== null) {
let popupDelay = this.element.getAttribute(this.attributes.popupDelay);
setTimeout(() => {
this.openModal(false);
}, (popupDelay === '' ? this.defaultPopupDelay : popupDelay) * 1000);
}
}
/**
* Handle button keys - handle key presses escape
*
* @param {Event} event - event triggered
*/
handleButtonKeys(event) {
if (event != null) {
switch (ElementHelpers.getKeyCodeOnKeyDownEvent(event)) {
case this.keyCodes.Escape:
event.preventDefault();
this.closeModal(true);
document.body.style.marginTop = `${this.bodyMarginTop}px`;
window.scrollTo(0, this.scrollPosition);
break;
case this.keyCodes.Tab:
trapFocus(event, this.element, this.modalOpen, this.keystroke);
if (this.modalOpen) {
this.keystroke++;
}
}
}
}
/**
* Get selector
*/
static getSelector() {
return `[${attributeBase}]`;
}
}