UNPKG

vex-js

Version:

Beautiful, functional dialogs in vanilla JavaScript

346 lines (299 loc) 10.4 kB
// classList polyfill for old browsers require('classlist-polyfill') // Object.assign polyfill require('es6-object-assign').polyfill() // String to DOM function var domify = require('domify') // Use the DOM's HTML parsing to escape any dangerous strings var escapeHtml = function escapeHtml (str) { if (typeof str !== 'undefined') { var div = document.createElement('div') div.appendChild(document.createTextNode(str)) return div.innerHTML } else { return '' } } // Utility function to add space-delimited class strings to a DOM element's classList var addClasses = function addClasses (el, classStr) { if (typeof classStr !== 'string' || classStr.length === 0) { return } var classes = classStr.split(' ') for (var i = 0; i < classes.length; i++) { var className = classes[i] if (className.length) { el.classList.add(className) } } } // Detect CSS Animation End Support // https://github.com/limonte/sweetalert2/blob/99bd539f85e15ac170f69d35001d12e092ef0054/src/utils/dom.js#L194 var animationEndEvent = (function detectAnimationEndEvent () { var el = document.createElement('div') var eventNames = { 'animation': 'animationend', 'WebkitAnimation': 'webkitAnimationEnd', 'MozAnimation': 'animationend', 'OAnimation': 'oanimationend', 'msAnimation': 'MSAnimationEnd' } for (var i in eventNames) { if (el.style[i] !== undefined) { return eventNames[i] } } return false })() // vex base CSS classes var baseClassNames = { vex: 'vex', content: 'vex-content', overlay: 'vex-overlay', close: 'vex-close', closing: 'vex-closing', open: 'vex-open' } // Private lookup table of all open vex objects, keyed by id var vexes = {} var globalId = 1 // Private boolean to assist the escapeButtonCloses option var isEscapeActive = false // vex itself is an object that exposes a simple API to open and close vex objects in various ways var vex = { open: function open (opts) { // Check for usage of deprecated options, and log a warning var warnDeprecated = function warnDeprecated (prop) { console.warn('The "' + prop + '" property is deprecated in vex 3. Use CSS classes and the appropriate "ClassName" options, instead.') console.warn('See http://github.hubspot.com/vex/api/advanced/#options') } if (opts.css) { warnDeprecated('css') } if (opts.overlayCSS) { warnDeprecated('overlayCSS') } if (opts.contentCSS) { warnDeprecated('contentCSS') } if (opts.closeCSS) { warnDeprecated('closeCSS') } // The dialog instance var vexInstance = {} // Set id vexInstance.id = globalId++ // Store internally vexes[vexInstance.id] = vexInstance // Set state vexInstance.isOpen = true // Close function on the vex instance // This is how all API functions should close individual vexes vexInstance.close = function instanceClose () { // Check state if (!this.isOpen) { return true } var options = this.options // escapeButtonCloses is checked first if (isEscapeActive && !options.escapeButtonCloses) { return false } // Allow the user to validate any info or abort the close with the beforeClose callback var shouldClose = (function shouldClose () { // Call before close callback if (options.beforeClose) { return options.beforeClose.call(this) } // Otherwise indicate that it's ok to continue with close return true }.bind(this)()) // If beforeClose() fails, abort the close if (shouldClose === false) { return false } // Update state this.isOpen = false // Detect if the content el has any CSS animations defined var style = window.getComputedStyle(this.contentEl) function hasAnimationPre (prefix) { return style.getPropertyValue(prefix + 'animation-name') !== 'none' && style.getPropertyValue(prefix + 'animation-duration') !== '0s' } var hasAnimation = hasAnimationPre('') || hasAnimationPre('-webkit-') || hasAnimationPre('-moz-') || hasAnimationPre('-o-') // Define the function that will actually close the instance var close = function close () { if (!this.rootEl.parentNode) { return } // Run once this.rootEl.removeEventListener(animationEndEvent, close) this.overlayEl.removeEventListener(animationEndEvent, close) // Remove from lookup table (prevent memory leaks) delete vexes[this.id] // Remove the dialog from the DOM this.rootEl.parentNode.removeChild(this.rootEl) // Remove the overlay from the DOM this.bodyEl.removeChild(this.overlayEl) // Call after close callback if (options.afterClose) { options.afterClose.call(this) } // Remove styling from the body, if no more vexes are open if (Object.keys(vexes).length === 0) { document.body.classList.remove(baseClassNames.open) } }.bind(this) // Close the vex if (animationEndEvent && hasAnimation) { // Setup the end event listener, to remove the el from the DOM this.rootEl.addEventListener(animationEndEvent, close) this.overlayEl.addEventListener(animationEndEvent, close) // Add the closing class to the dialog, showing the close animation this.rootEl.classList.add(baseClassNames.closing) this.overlayEl.classList.add(baseClassNames.closing) } else { close() } return true } // Allow strings as content if (typeof opts === 'string') { opts = { content: opts } } // `content` is unsafe internally, so translate // safe default: HTML-escape the content before passing it through if (opts.unsafeContent && !opts.content) { opts.content = opts.unsafeContent } else if (opts.content) { opts.content = escapeHtml(opts.content) } // Store options on instance for future reference var options = vexInstance.options = Object.assign({}, vex.defaultOptions, opts) // Get Body Element var bodyEl = vexInstance.bodyEl = document.getElementsByTagName('body')[0] // vex root var rootEl = vexInstance.rootEl = document.createElement('div') rootEl.classList.add(baseClassNames.vex) addClasses(rootEl, options.className) // Overlay var overlayEl = vexInstance.overlayEl = document.createElement('div') overlayEl.classList.add(baseClassNames.overlay) addClasses(overlayEl, options.overlayClassName) if (options.overlayClosesOnClick) { rootEl.addEventListener('click', function overlayClickListener (e) { if (e.target === rootEl) { vexInstance.close() } }) } bodyEl.appendChild(overlayEl) // Content var contentEl = vexInstance.contentEl = document.createElement('div') contentEl.classList.add(baseClassNames.content) addClasses(contentEl, options.contentClassName) contentEl.appendChild(options.content instanceof window.Node ? options.content : domify(options.content)) rootEl.appendChild(contentEl) // Close button if (options.showCloseButton) { var closeEl = vexInstance.closeEl = document.createElement('div') closeEl.classList.add(baseClassNames.close) addClasses(closeEl, options.closeClassName) closeEl.addEventListener('click', vexInstance.close.bind(vexInstance)) contentEl.appendChild(closeEl) } // Add to DOM document.querySelector(options.appendLocation).appendChild(rootEl) // Call after open callback if (options.afterOpen) { options.afterOpen.call(vexInstance) } // Apply styling to the body document.body.classList.add(baseClassNames.open) // Return the created vex instance return vexInstance }, // A top-level vex.close function to close dialogs by reference or id close: function close (vexOrId) { var id if (vexOrId.id) { id = vexOrId.id } else if (typeof vexOrId === 'string') { id = vexOrId } else { throw new TypeError('close requires a vex object or id string') } if (!vexes[id]) { return false } return vexes[id].close() }, // Close the most recently created/opened vex closeTop: function closeTop () { var ids = Object.keys(vexes) if (!ids.length) { return false } return vexes[ids[ids.length - 1]].close() }, // Close every vex! closeAll: function closeAll () { for (var id in vexes) { this.close(id) } return true }, // A getter for the internal lookup table getAll: function getAll () { return vexes }, // A getter for the internal lookup table getById: function getById (id) { return vexes[id] } } // Close top vex on escape window.addEventListener('keyup', function vexKeyupListener (e) { if (e.keyCode === 27) { isEscapeActive = true vex.closeTop() isEscapeActive = false } }) // Close all vexes on history pop state (useful in single page apps) window.addEventListener('popstate', function () { if (vex.defaultOptions.closeAllOnPopState) { vex.closeAll() } }) vex.defaultOptions = { content: '', showCloseButton: true, escapeButtonCloses: true, overlayClosesOnClick: true, appendLocation: 'body', className: '', overlayClassName: '', contentClassName: '', closeClassName: '', closeAllOnPopState: true } // TODO Loading symbols? // Include escapeHtml function on the library object Object.defineProperty(vex, '_escapeHtml', { configurable: false, enumerable: false, writable: false, value: escapeHtml }) // Plugin system! vex.registerPlugin = function registerPlugin (pluginFn, name) { var plugin = pluginFn(vex) var pluginName = name || plugin.name if (vex[pluginName]) { throw new Error('Plugin ' + name + ' is already registered.') } vex[pluginName] = plugin } module.exports = vex