UNPKG

wtc-modal-view

Version:
455 lines (379 loc) 12.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: wtc-modal-view.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: wtc-modal-view.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/** * A Modal class which can display programatically-generated content, or pull in content from an existing DOM node. * * @example * const myModal = new Modal(); * const triggerButton = document.querySelector('trigger'); * * myModal.optionalClass = "modal--myModal"; * myModal.content = '&lt;p>Some sample content!&lt;/p>'; * myModal.focusOnClose = triggerButton; * * triggerButton.addEventListener('click', () => { * myModal.open(); * }); */ class Modal { constructor() { this.state = false; this.modal = document.createElement("div"); this.modalOverlay = document.createElement("div"); this.modalFocusStart = document.createElement("div"); this.modalFocusEnd = document.createElement("div"); this.modalClose = document.createElement("button"); this.modalClose.innerHTML = "&lt;span>Close&lt;/span>"; this.modalWrapper = document.createElement("div"); this.modalContent = document.createElement("div"); this.wrapperOfContent = document.createElement("div"); this.className = "modal"; this.classNameOpen = "modal--open"; this.appended = false; this.storeContent = false; this.inOutDuration = 400; // getters and setters variables this._onOpen = null; this._onClose = null; this._onCloseStart = null; this._focusOnClose = null; this._content = null; // add the classes and focus attributes this.modal.classList.add(this.className); this.modalOverlay.classList.add(`${this.className}__overlay`); this.modalFocusStart.classList.add(`${this.className}__focus-start`); this.modalFocusEnd.classList.add(`${this.className}__focus-end`); this.modalClose.classList.add(`${this.className}__close`); this.modalWrapper.classList.add(`${this.className}__wrapper`); this.modalContent.classList.add(`${this.className}__content`); this.wrapperOfContent.classList.add(`${this.className}-content-wrapper`); // adds role of dialog for a11y // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role this.modal.setAttribute("role", "dialog"); this.modalFocusEnd.setAttribute("tabindex", 0); this.modalFocusStart.setAttribute("tabindex", 0); this.modalContent.setAttribute("tabindex", -1); // create the markup structure this.modalWrapper.appendChild(this.modalFocusStart); this.modalWrapper.appendChild(this.modalClose); this.modalWrapper.appendChild(this.modalContent); this.modalWrapper.appendChild(this.modalFocusEnd); this.modal.appendChild(this.modalOverlay); this.modal.appendChild(this.modalWrapper); document.body.appendChild(this.wrapperOfContent); this.modalFocusEnd.addEventListener("focus", () => { this.modalClose.focus(); }); this.modalFocusStart.addEventListener( "focus", this.focusLastElement.bind(this) ); this.modalClose.addEventListener("click", this.close.bind(this)); this.modalOverlay.addEventListener("click", this.close.bind(this)); } /** * Closes modal. * * If `onCloseStart`is defined that is called and waits for the callback. * Otherwise it removes content and optional class, * and shifts user focus back to triggering element, if specified. * */ close() { if (this.state) { if(this._onCloseStart) { this._onCloseStart(this.modal, () => { this._completeClose() }) } else { this._completeClose() } } } _completeClose() { if (this.state) { this.modal.classList.remove(this.classNameOpen); // if a focusOnClose element was passed in when the modal opened, focus it! if (this.focusOnClose) this.focusOnClose.focus(); // This gives us time to animate and transition setTimeout(() => { this.state = false; // Setting the modal to display: none; when closed, just to prevent anything from still being // focussable. Mainly the close button. this.modal.style.display = "none"; // We only remove the optional classNames when the timeout is complete, // and everything is "gone" from view. // This way, we're able to target our various custom modals in CSS, // via their specific classNames (i.e. ".modal--video", ".modal--video.modal--open"): if (this.optionalClass) { if (typeof this.optionalClass === "string") this.modal.classList.remove(this.optionalClass); else if (this.optionalClass instanceof Array) this.modal.classList.remove(...this.optionalClass); } // On close, we take the content from the modal and apply it to our static modal wrapper. // This prevents the content from stil being tabbable in the DOM. if (this.storeContent) this.wrapperOfContent.appendChild(this._content); else this.modalContent.innerHTML = ""; if (this.onClose) this.onClose(); }, this.inOutDuration); if (Modal.hash) history.replaceState("", document.title, window.location.pathname); } } /** * Get current url hash * @static * @return {String} hash string or null if none. */ static get hash() { let URLhash = /#\!?\/(.+)\//i.exec(window.location.hash); if (URLhash &amp;&amp; URLhash.length > 1) { return URLhash[1]; } else { return null; } } /** * Opens modal, adds content and optional CSS class */ open() { if (!this.state) { if (this.optionalClass) { if (typeof this.optionalClass === "string") this.modal.classList.add(this.optionalClass); else if (this.optionalClass instanceof Array) this.modal.classList.add(...this.optionalClass); } document.body.appendChild(this.modal); // This is here to avoid a flash of content for the first time let delay = this.appended ? 0 : 100; if (!this.appended) this.appended = true; // Appending the content back to the modal. this.modalContent.append(this._content); // Setting the modal back to block. this.modal.style.display = "block"; setTimeout(() => { this.modal.classList.add(this.classNameOpen); this.focusFirstElement(); if (this.onOpen) this.onOpen(this.modal); }, delay); this.state = true; const onKeyDown = (e) => { if (e.keyCode == 27) { this.close(); document.removeEventListener("keydown", onKeyDown.bind(this), false); } }; document.addEventListener("keydown", onKeyDown.bind(this), false); } } /** * Shifts focus to the first element inside the content */ focusFirstElement() { // traverse tree down const findFirst = (parent) => { if (!parent.firstElementChild) return parent; return findFirst(parent.firstElementChild); }; let finalElement = findFirst(this.modalContent.firstElementChild); finalElement.setAttribute("tabindex", -1); finalElement.focus(); } /** * Shifts focus to the last element inside the content */ focusLastElement() { const focusableElements = this.modalContent.querySelectorAll( '[href], button, [tabindex="0"], [role="button"]' ); if (focusableElements.length > 0) { const lastFocusableElement = focusableElements[focusableElements.length - 1]; lastFocusableElement.focus(); } else { this.modalClose.focus(); } } /** * Gets the element that will be focused when the modal closes * * @return {HTMLElement} */ get focusOnClose() { return this._focusOnClose; } /** * Sets the element that will be focused when the modal closes. * Setter. Usage: `modalInstance.focusOnClose = myElement` * * @param {HTMLElement} element Must be a focusable element */ set focusOnClose(element) { if ( !element instanceof HTMLButtonElement &amp;&amp; !element instanceof HTMLAnchorElement &amp;&amp; !element.getAttribute("tabindex") ) return; this._focusOnClose = element; } /** * Gets the function that is called when the modal opens * * @return {Function} */ get onOpen() { return this._onOpen; } /** * Sets the function that is called when the modal opens. * The function gets called with the modals DOM element. * Setter. Usage: `modalInstance.onOpen = (modalElement) => {}` * * @param {Function} callback */ set onOpen(callback) { if (!callback || typeof callback !== "function") return; this._onOpen = callback; } /** * Get the function that is called when the modal closes * * @return {Function} */ get onClose() { return this._onClose; } /** * Sets the function that is called when the modal closes. * Setter. Usage: `modalInstance.onClose = myFunction` * * @param {Function} callback */ set onClose(callback) { if (!callback || typeof callback !== "function") return; this._onClose = callback; } /** * Get the function that is called just before the modal closes * * @return {Function} */ get onCloseStart() { return this._onCloseStart; } /** * Sets the function that is called just before the modal closes. * If this is set, when modalInstance.close()` is called it will * run the set function with a the modal DOM element and a callback. * It will then wait for that callback to be run before completing * the close function and calling onClose. * * Setter. Usage: * `modalInstance.onCloseStart = (modalElement, cb) => { * // do some animation with modalElement * cb(); * } * * modalInstance.close(); * ` * * @param {Function} callback */ set onCloseStart(callback) { if (!callback || typeof callback !== "function") return; this._onCloseStart = callback; } /** * Sets an optional class name on the modal for custom styling. * Setter. Usage: `modalInstance.optionalClass = "modal--myclass"` * * @param {String|Array} className */ set optionalClass(className) { if (!className) return; this._optionalClass = className; } /** * Gets the optional class name * * @return {String|Array} optionalClass */ get optionalClass() { return this._optionalClass || ""; } /** * Sets the content of the close button, useful for localizing. * Setter. Usage: `modalInstance.closeButtonContent = "&lt;String of HTML!>"` * * @param {string|HTMLElement} content */ set closeButtonContent(content) { if (!content) return; if (typeof content === "string") { this.modalClose.innerHTML = content; return; } if (content instanceof HTMLElement) { this.modalClose.innerHTML = ""; this.modalClose.appendChild(content); return; } } /** * Sets the content of the modal. * Setter. Usage: `modalInstance.content = MyHTMLElement` * * @param {string|HTMLElement} content */ set content(content) { if (!content) return; if (typeof content !== "string" &amp;&amp; !content instanceof HTMLElement) return; if (this.storeContent) this.wrapperOfContent.appendChild(this._content); this._content = content; if (typeof content === "string") { this.storeContent = false; this.modalContent.innerHTML = this._content; } else { this.storeContent = true; this.modalContent.innerHTML = ""; this.modalContent.appendChild(this._content); return; } } } export default Modal; </code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Modal.html">Modal</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.4</a> on Mon Feb 07 2022 21:52:18 GMT+0000 (Coordinated Universal Time) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>