import { queryAll, queryOne } from '@ecl/dom-utils';import { createFocusTrap } from 'focus-trap';/** * @param {HTMLElement} element DOM element for component instantiation and scope * @param {Object} options * @param {String} options.toggleSelector Selector for the modal toggle * @param {String} options.closeSelector Selector for closing the modal * @param {Boolean} options.attachClickListener Whether or not to bind click events on toggle * @param {Boolean} options.attachKeyListener Whether or not to bind keyboard events */export class Modal { /** * @static * Shorthand for instance creation and initialisation. * * @param {HTMLElement} root DOM element for component instantiation and scope * * @return {Modal} An instance of Modal. */ static autoInit(root, { MODAL: defaultOptions = {} } = {}) { const modal = new Modal(root, defaultOptions); modal.init(); root.ECLModal = modal; return modal; } constructor( element, { toggleSelector = '', closeSelector = '[data-ecl-modal-close]', scrollSelector = '[data-ecl-modal-scroll]', attachClickListener = true, attachKeyListener = true, } = {}, ) { // Check element if (!element || element.nodeType !== Node.ELEMENT_NODE) { throw new TypeError( 'DOM element should be given to initialize this widget.', ); } this.element = element; // Options this.toggleSelector = toggleSelector; this.closeSelector = closeSelector; this.scrollSelector = scrollSelector; this.attachClickListener = attachClickListener; this.attachKeyListener = attachKeyListener; // Private variables this.toggle = null; this.close = null; this.scroll = null; this.focusTrap = null; // Bind `this` for use in callbacks this.openModal = this.openModal.bind(this); this.closeModal = this.closeModal.bind(this); this.handleClickOnToggle = this.handleClickOnToggle.bind(this); this.handleKeyboardGlobal = this.handleKeyboardGlobal.bind(this); } /** * Initialise component. */ init() { if (!ECL) { throw new TypeError('Called init but ECL is not present'); } ECL.components = ECL.components || new Map(); // Bind global events if (this.attachKeyListener) { document.addEventListener('keyup', this.handleKeyboardGlobal); } // Get toggle element if (this.toggleSelector === '') { this.toggleSelector = `#${this.element.getAttribute( 'data-ecl-modal-toggle', )}`; } this.toggle = document.querySelector(this.toggleSelector); // Apply aria to toggle if (this.toggle) { this.toggle.setAttribute('aria-controls', this.element.id); if (!this.toggle.getAttribute('aria-haspopup')) { this.toggle.setAttribute('aria-haspopup', 'dialog'); } } // Get other elements this.close = queryAll(this.closeSelector, this.element); this.scroll = queryOne(this.scrollSelector, this.element); // Create focus trap this.focusTrap = createFocusTrap(this.element); // Polyfill to support <dialog> this.isDialogSupported = true; if (!window.HTMLDialogElement) { this.isDialogSupported = false; } // Bind click event on toggle if (this.toggle && this.attachClickListener) { this.toggle.addEventListener('click', this.handleClickOnToggle); } // Bind click event on close buttons if (this.close && this.attachClickListener) { this.close.forEach((close) => { close.addEventListener('click', this.closeModal); }); } // Set ecl initialized attribute this.element.setAttribute('data-ecl-auto-initialized', 'true'); ECL.components.set(this.element, this); } /** * Destroy component. */ destroy() { if (this.toggle && this.attachClickListener) { this.toggle.removeEventListener('click', this.handleClickOnToggle); } if (this.attachKeyListener) { document.removeEventListener('keyup', this.handleKeyboardGlobal); } if (this.close && this.attachClickListener) { this.close.forEach((close) => { close.removeEventListener('click', this.closeModal); }); } this.element.removeAttribute('data-ecl-auto-initialized'); ECL.components.delete(this.element); } /** * Check if there is a scroll and display overflow. */ checkScroll() { if (!this.scroll) return; this.scroll.parentNode.classList.remove('ecl-modal__body--has-scroll'); if (this.scroll.scrollHeight > this.scroll.clientHeight) { this.scroll.parentNode.classList.add('ecl-modal__body--has-scroll'); } } /** * Toggles between collapsed/expanded states. * * @param {Event} e */ handleClickOnToggle(e) { e.preventDefault(); // Get current status const isExpanded = this.toggle.getAttribute('aria-expanded') === 'true'; // Toggle the modal if (isExpanded) { this.closeModal(); return; } this.openModal(); } /** * Open the modal. */ openModal() { if (this.isDialogSupported) { this.element.showModal(); } else { this.element.setAttribute('open', ''); } // Check scroll this.checkScroll(); // Trap focus this.focusTrap.activate(); } /** * Close the modal. */ closeModal() { if (this.isDialogSupported) { this.element.close(); } else { this.element.removeAttribute('open'); } // Untrap focus if (this.focusTrap.active) { this.focusTrap.deactivate(); } } /** * Handles global keyboard events, triggered outside of the modal. * * @param {Event} e */ handleKeyboardGlobal(e) { // Detect press on Escape if (e.key === 'Escape' || e.key === 'Esc') { this.closeModal(); } }}export default Modal;