UNPKG

quick-modal

Version:

Generate a custom modal quickly

455 lines (404 loc) 13.8 kB
/* exported QuickModal */ /** * QuickModal class used for the QuickModal plugin */ class QuickModal{ /** * Creates an instance of QuickModal * @param {Object|String} parameters Parameters holder. Use a String if you want a quick initialization * @param {Boolean} [parameters.darkenBackground=true] Should the QuickModal darken the background when shown? * @param {Boolean} [parameters.isForm=true] Is the QuickModal a form? * @param {Boolean} [parameters.keepHidden=false] Keep the modal hidden instead of destroying it * @param {Object} [parameters.form] Form properties holder * @param {String} [parameters.form.action] Form URL * @param {String} [parameters.form.method] Form METHOD attribute (GET/POST/...) * @param {String} [parameters.form.id] Form ID * @param {String[]} [parameters.form.classes] Form classes * @param {String} [parameters.form.submit] Form submit button text * @param {String} [parameters.closeText=OK] Close button text * @param {String[]} [parameters.classes] QuickModal classes * @param {Object<String, String>} [parameters.attributes] QuickModal attributes * @param {String} parameters.header Header content * @param {Object[]} parameters.body Body content * @param {Object[]} parameters.footer Footer content * @param {Document} [parameters.document] Document in which the QuickModal should be opened into, window.document is used by default * @param {Function} [parameters.afterOpen] Callback called after the QuickModal gets displayed * @param {Function} [parameters.beforeClose] Callback called before the QuickModal closes * @param {Function} [parameters.onSubmit] Callback called when the QuickModal form gets submitted * @param {String} [body] Body content if you want a quick initialization */ constructor(parameters, body){ /** * Parameters to initialize the QuickModal with * @private */ this._parameters = { darkenBackground: true, isForm: true, keepHidden: false, form: { action: 'path/to/your/form', method: 'POST', id: 'formId', classes: [], submit: 'OK' }, closeText: 'OK', classes: [], attributes: {}, header: '<div>QuickModal</div>', body: [ { type: 'text', text: 'This is a basic QuickModal. See the <a href="https://zenoo.github.io/quickModal/">documentation</a> for more.', tag: 'p', classes: [] } ], footer: [], document: window.document, afterOpen: () => {}, beforeClose: () => {}, onSubmit: () => {} }; // Initializer shorthand if(typeof parameters == 'string'){ this._parameters = { ...this._parameters, isForm: false, header: '<div>' + parameters + '</div>', body: [ { type: 'text', text: body, tag: 'p', classes: [] } ], footer: [] }; }else{ this._parameters = { ...this._parameters, ...parameters }; } /** * This ID is unique at the time it's accessed */ this.id = 0; while(this._parameters.document.getElementById('quick-modal-' + this.id)) this.id++; /** * QuickModal elements holder * @type {Object.<String, Element>} * @private */ this._elements = { hider: null, body: null, footerLinks: null }; this._build(); this._listen(); // Show the QuickModal this.open(); } /** * Builds the QuickModal in the DOM * @private */ _build(){ this._buildFrame(); // Hider if(this._parameters.darkenBackground){ this._elements.hider = this._parameters.document.createElement('div'); this._elements.hider.id = 'quick-modal-hider-' + this.id; this._elements.hider.classList.add('quick-modal-hider'); this._parameters.document.body.appendChild(this._elements.hider); } this._buildBody(this._elements.body, this._parameters.body); this._buildFooter(); this._parameters.document.body.append(this.modal); } /** * Build the QuickModal frame * @private */ _buildFrame(){ // QuickModal wrapper this.modal = this._parameters.document.createElement('section'); this.modal.id = 'quick-modal-' + this.id; this.modal.classList.add('quick-modal', ...this._parameters.classes); Object.entries(this._parameters.attributes).forEach(([attribute, value]) => { this.modal.setAttribute(attribute, value); }); // QuickModal base DOM tree this.modal.innerHTML = ` ${this._parameters.isForm ? ` <form ${Reflect.ownKeys(this._parameters.form).includes('id') ? 'id="'+this._parameters.form.id+'"' : ''} action="${Reflect.ownKeys(this._parameters.form).includes('action') ? this._parameters.form.action : '#'}" method="${Reflect.ownKeys(this._parameters.form).includes('method') ? this._parameters.form.method : 'POST'}" > ` : ''} <header> ${this._parameters.header} <aside class="quick-modal-close">x</aside> </header> <section class="quick-modal-main"> </section> <footer> <ul> ${this._parameters.isForm ? ` <li> <input type="submit" class="quick-modal-generated-btn quick-modal-submit" value="${Reflect.ownKeys(this._parameters.form).includes('submit') ? this._parameters.form.submit : 'OK'}" /> </li> ` : ''} <li> <a class="quick-modal-generated-btn quick-modal-close" href=""> ${this._parameters.closeText} </a> </li> </ul> </footer> ${this._parameters.isForm ? '</form>' : ''} `; this._elements.body = this.modal.querySelector('.quick-modal-main'); this._elements.footerLinks = this.modal.querySelector('footer>ul'); } /** * Build the QuickModal body * @param {Element} parent Parent element * @param {Object[]} children Children objects to build & append to the parent * @private */ _buildBody(parent, children){ children.forEach(line => { const lineAttributes = Reflect.ownKeys(line); if(line.type == 'text'){ /** @const {Element} */ const element = this._parameters.document.createElement(line.tag); element.innerHTML = line.text; if(lineAttributes.includes('id')) element.id = line.id; if(lineAttributes.includes('classes')) element.classList.add(...line.classes); if(lineAttributes.includes('attributes')){ Object.entries(line.attributes).forEach(([attribute, value]) => { element.setAttribute(attribute, value); }); } parent.appendChild(element); }else if(line.type == 'form'){ switch (line.tag) { case 'select': parent.append(...this._toNodes(` <p> ${lineAttributes.includes('label') ? ` <label for="${lineAttributes.includes('id') ? line.id : line.name}">${line.label}</label> ` : ''} <${line.tag} name="${lineAttributes.includes('name') ? line.name : ''}" id="${lineAttributes.includes('id') ? line.id : line.name}" class="${lineAttributes.includes('classes') ? line.classes.join(' ') : ''}" ${lineAttributes.includes('attributes') ? Object.entries(line.attributes).reduce((acc, [attribute, value]) => acc += attribute + '="'+value+'" ', '') : '' } > ${line.options.reduce((acc, option) => acc += ` <option ${Reflect.ownKeys(option).includes('attributes') ? Object.entries(option.attributes).reduce((accu, [attribute, value]) => accu += attribute + '="'+value+'" ', '') : '' } value="${option.value}" ${option.selected ? 'selected' : ''} > ${option.text} </option> `, '')} </${line.tag}> </p> `)); break; default: parent.append(...this._toNodes(` ${lineAttributes.includes('inputType') && line.inputType == 'hidden' ? '' : '<p>'} ${lineAttributes.includes('label') ? ` <label for="${lineAttributes.includes('id') ? line.id : line.name}">${line.label}</label> ` : ''} <${line.tag} ${line.tag == 'textarea' ? '' : lineAttributes.includes('inputType') ? 'type="'+line.inputType+'"' : ''} name="${lineAttributes.includes('name') ? line.name : ''}" id="${lineAttributes.includes('id') ? line.id : line.name}" class="${lineAttributes.includes('classes') ? line.classes.join(' ') : ''}" ${lineAttributes.includes('attributes') ? Object.entries(line.attributes).reduce((acc, [attribute, value]) => acc += attribute + '="'+value+'" ', '') : '' } placeholder="${lineAttributes.includes('placeholder') ? line.placeholder : ''}" value="${lineAttributes.includes('value') ? line.value : ''}" ></${line.tag}> ${lineAttributes.includes('inputType') && line.inputType == 'hidden' ? '' : '</p>'} `)); break; } }else{ console.warn(line); console.warn('QuickModal: The above element has an invalid `type` attribute. It has been ignored.'); } // Display recursive children if(line.children){ this._buildBody(parent.lastElementChild, line.children); } }); } /** * Build the QuickModal footer * @private */ _buildFooter(){ this._parameters.footer.forEach(link => { const linkAttributes = Reflect.ownKeys(link); this._elements.footerLinks.prepend(...this._toNodes(` <li id="${linkAttributes.includes('id') ? link.id : ''}" class="${linkAttributes.includes('classes') ? link.classes.join(' ') : ''}" ${linkAttributes.includes('attributes') ? Object.entries(link.attributes).reduce((acc, [attribute, value]) => acc += attribute + '="'+value+'" ', '') : '' } > <a class="quick-modal-generated-btn" href="${link.href}"> ${link.text} </a> </li> `)); }); } /** * Attach event listeners for the QuickModal * @private */ _listen(){ // Close action handling const closingHandler = e => { e.preventDefault(); if(this._parameters.keepHidden){ this.close(); }else{ this.destroy(); } }; if(this._parameters.darkenBackground) this._elements.hider.addEventListener('click', closingHandler); this.modal.querySelectorAll('.quick-modal-close').forEach(button => { button.addEventListener('click', closingHandler); }); // Submit action handling if(this._parameters.isForm){ const form = this.modal.querySelector('form'); form.addEventListener('submit', e => { // Callback onSubmit Reflect.apply(this._parameters.onSubmit, this, [e, form]); }); } } /** * Converts an HTML String to a NodeList * @param {String} html String representing the HTML to convert * @return {NodeList} * @private */ _toNodes(html){ const template = this._parameters.document.createElement('template'); template.innerHTML = html; return template.content.childNodes; } /** * Opens the QuickModal * @returns {Promise} A promise resolved once the QuickModal is fully displayed */ open(){ setTimeout(() => { this.modal.classList.remove('done'); this.modal.classList.add('active'); if(this._parameters.darkenBackground){ this._elements.hider.classList.remove('done'); this._elements.hider.classList.add('active'); } // Callback afterOpen Reflect.apply(this._parameters.afterOpen, this, [this.modal]); }, 20); return new Promise(resolve => { setTimeout(() => { resolve(); }, 500); }); } /** * Closes the QuickModal * @returns {Promise} A promise resolved once the QuickModal is fully hidden */ close(){ // Callback beforeClose Reflect.apply(this._parameters.beforeClose, this, [this.modal]); this.modal.classList.remove('active'); if(this._parameters.darkenBackground) this._elements.hider.classList.remove('active'); return new Promise(resolve => { setTimeout(() => { this.modal.classList.add('done'); if(this._parameters.darkenBackground) this._elements.hider.classList.add('done'); resolve(); }, 500); }); } /** * Removes any QuickModal mutation from the DOM */ destroy(){ this.close().then(() => { this.modal.remove(); if(this._parameters.darkenBackground) this._elements.hider.remove(); }); } /** * Removes any QuickModal mutation from the DOM * @param {Integer} id The targeted QuickModal id */ static destroy(id){ const modal = document.getElementById('quick-modal-' + id); if(modal){ modal.querySelector('.quick-modal-close').dispatchEvent(new Event('click')); setTimeout(() => { const hider = document.getElementById('quick-modal-hider-' + id); modal.remove(); if(hider) hider.remove(); }, 500); } } } if(window.jQuery){ // Equivalent jQuery plugin (function($){ 'use strict'; $.fn.extend({ quickModal(parameters) { console.warn('QuickModal: Using `$(...).quickModal(...)` is deprecated. Use `const modal = new QuickModal(...)` instead.'); const modal = new QuickModal(parameters); this.open = () => { modal.open(); }; this.close = () => { modal.close(); }; this.destroy = () => { modal.destroy(); }; return this; } }); }(jQuery)); }