vex-js
Version:
Beautiful, functional dialogs in vanilla JavaScript
346 lines (299 loc) • 10.4 kB
JavaScript
// 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