sweetalert2
Version:
A beautiful, responsive, customizable and accessible (WAI-ARIA) replacement for JavaScript's popup boxes, supported fork of sweetalert
376 lines (317 loc) • 11.5 kB
JavaScript
import { default as sweetAlert } from '../sweetalert2.js'
import { swalClasses, iconTypes } from './classes.js'
import { uniqueArray, error, warnOnce } from './utils.js'
// Remember state in cases where opening and handling a modal will fiddle with it.
export const states = {
previousActiveElement: null,
previousBodyPadding: null
}
// Detect Node env
export const isNodeEnv = () => typeof window === 'undefined' || typeof document === 'undefined'
/*
* Add modal + backdrop to DOM
*/
export const init = (params) => {
// Clean up the old popup if it exists
const c = getContainer()
if (c) {
c.parentNode.removeChild(c)
removeClass(
[document.documentElement, document.body],
[
swalClasses['no-backdrop'],
swalClasses['has-input'],
swalClasses['toast-shown']
]
)
}
if (isNodeEnv()) {
error('SweetAlert2 requires document to initialize')
return
}
const container = document.createElement('div')
container.className = swalClasses.container
container.innerHTML = sweetHTML
let targetElement = typeof params.target === 'string' ? document.querySelector(params.target) : params.target
targetElement.appendChild(container)
const popup = getPopup()
const content = getContent()
const input = getChildByClass(content, swalClasses.input)
const file = getChildByClass(content, swalClasses.file)
const range = content.querySelector(`.${swalClasses.range} input`)
const rangeOutput = content.querySelector(`.${swalClasses.range} output`)
const select = getChildByClass(content, swalClasses.select)
const checkbox = content.querySelector(`.${swalClasses.checkbox} input`)
const textarea = getChildByClass(content, swalClasses.textarea)
// a11y
popup.setAttribute('role', params.toast ? 'alert' : 'dialog')
popup.setAttribute('aria-live', params.toast ? 'polite' : 'assertive')
if (!params.toast) {
popup.setAttribute('aria-modal', 'true')
}
const resetValidationError = () => {
sweetAlert.isVisible() && sweetAlert.resetValidationError()
}
input.oninput = resetValidationError
file.onchange = resetValidationError
select.onchange = resetValidationError
checkbox.onchange = resetValidationError
textarea.oninput = resetValidationError
range.oninput = () => {
resetValidationError()
rangeOutput.value = range.value
}
range.onchange = () => {
resetValidationError()
range.nextSibling.value = range.value
}
return popup
}
/*
* Manipulate DOM
*/
const sweetHTML = `
<div aria-labelledby="${swalClasses.title}" aria-describedby="${swalClasses.content}" class="${swalClasses.popup}" tabindex="-1">
<div class="${swalClasses.header}">
<ul class="${swalClasses.progresssteps}"></ul>
<div class="${swalClasses.icon} ${iconTypes.error}">
<span class="swal2-x-mark"><span class="swal2-x-mark-line-left"></span><span class="swal2-x-mark-line-right"></span></span>
</div>
<div class="${swalClasses.icon} ${iconTypes.question}">?</div>
<div class="${swalClasses.icon} ${iconTypes.warning}">!</div>
<div class="${swalClasses.icon} ${iconTypes.info}">i</div>
<div class="${swalClasses.icon} ${iconTypes.success}">
<div class="swal2-success-circular-line-left"></div>
<span class="swal2-success-line-tip"></span> <span class="swal2-success-line-long"></span>
<div class="swal2-success-ring"></div> <div class="swal2-success-fix"></div>
<div class="swal2-success-circular-line-right"></div>
</div>
<img class="${swalClasses.image}" />
<h2 class="${swalClasses.title}" id="${swalClasses.title}"></h2>
<button type="button" class="${swalClasses.close}">×</button>
</div>
<div class="${swalClasses.content}">
<div id="${swalClasses.content}"></div>
<input class="${swalClasses.input}" />
<input type="file" class="${swalClasses.file}" />
<div class="${swalClasses.range}">
<input type="range" />
<output></output>
</div>
<select class="${swalClasses.select}"></select>
<div class="${swalClasses.radio}"></div>
<label for="${swalClasses.checkbox}" class="${swalClasses.checkbox}">
<input type="checkbox" />
</label>
<textarea class="${swalClasses.textarea}"></textarea>
<div class="${swalClasses.validationerror}" id="${swalClasses.validationerror}"></div>
</div>
<div class="${swalClasses.actions}">
<button type="button" class="${swalClasses.confirm}">OK</button>
<button type="button" class="${swalClasses.cancel}">Cancel</button>
</div>
<div class="${swalClasses.footer}">
</div>
</div>
`.replace(/(^|\n)\s*/g, '')
export const getContainer = () => document.body.querySelector('.' + swalClasses.container)
export const getPopup = () => getContainer() ? getContainer().querySelector('.' + swalClasses.popup) : null
export const getIcons = () => {
const popup = getPopup()
return popup.querySelectorAll('.' + swalClasses.icon)
}
export const elementByClass = (className) => getContainer() ? getContainer().querySelector('.' + className) : null
export const getTitle = () => elementByClass(swalClasses.title)
export const getContent = () => elementByClass(swalClasses.content)
export const getImage = () => elementByClass(swalClasses.image)
export const getProgressSteps = () => elementByClass(swalClasses.progresssteps)
export const getValidationError = () => elementByClass(swalClasses.validationerror)
export const getConfirmButton = () => elementByClass(swalClasses.confirm)
export const getCancelButton = () => elementByClass(swalClasses.cancel)
export const getButtonsWrapper = () => {
warnOnce(`swal.getButtonsWrapper() is deprecated and will be removed in the next major release, use swal.getActions() instead`)
return elementByClass(swalClasses.actions)
}
export const getActions = () => elementByClass(swalClasses.actions)
export const getFooter = () => elementByClass(swalClasses.footer)
export const getCloseButton = () => elementByClass(swalClasses.close)
export const getFocusableElements = () => {
const focusableElementsWithTabindex = Array.prototype.slice.call(
getPopup().querySelectorAll('[tabindex]:not([tabindex="-1"]):not([tabindex="0"])')
)
// sort according to tabindex
.sort((a, b) => {
a = parseInt(a.getAttribute('tabindex'))
b = parseInt(b.getAttribute('tabindex'))
if (a > b) {
return 1
} else if (a < b) {
return -1
}
return 0
})
const otherFocusableElements = Array.prototype.slice.call(
getPopup().querySelectorAll('button, input:not([type=hidden]), textarea, select, a, [tabindex="0"]')
)
return uniqueArray(focusableElementsWithTabindex.concat(otherFocusableElements))
}
export const parseHtmlToContainer = (param, target) => {
if (!param) {
return hide(target)
}
if (typeof param === 'object') {
target.innerHTML = ''
if (0 in param) {
for (let i = 0; i in param; i++) {
target.appendChild(param[i].cloneNode(true))
}
} else {
target.appendChild(param.cloneNode(true))
}
} else if (param) {
target.innerHTML = param
} else {}
show(target)
}
export const isModal = () => {
return !document.body.classList.contains(swalClasses['toast-shown'])
}
export const isToast = () => {
return document.body.classList.contains(swalClasses['toast-shown'])
}
export const isLoading = () => {
return getPopup().hasAttribute('data-loading')
}
export const hasClass = (elem, className) => {
if (elem.classList) {
return elem.classList.contains(className)
}
return false
}
export const focusInput = (input) => {
input.focus()
// place cursor at end of text in text input
if (input.type !== 'file') {
// http://stackoverflow.com/a/2345915/1331425
const val = input.value
input.value = ''
input.value = val
}
}
const addOrRemoveClass = (target, classList, add) => {
if (!target || !classList) {
return
}
if (typeof classList === 'string') {
classList = classList.split(/\s+/).filter(Boolean)
}
classList.forEach((className) => {
if (target.forEach) {
target.forEach((elem) => {
add ? elem.classList.add(className) : elem.classList.remove(className)
})
} else {
add ? target.classList.add(className) : target.classList.remove(className)
}
})
}
export const addClass = (target, classList) => {
addOrRemoveClass(target, classList, true)
}
export const removeClass = (target, classList) => {
addOrRemoveClass(target, classList, false)
}
export const getChildByClass = (elem, className) => {
for (let i = 0; i < elem.childNodes.length; i++) {
if (hasClass(elem.childNodes[i], className)) {
return elem.childNodes[i]
}
}
}
export const show = (elem) => {
elem.style.opacity = ''
elem.style.display = (elem.id === swalClasses.content) ? 'block' : 'flex'
}
export const hide = (elem) => {
elem.style.opacity = ''
elem.style.display = 'none'
}
export const empty = (elem) => {
while (elem.firstChild) {
elem.removeChild(elem.firstChild)
}
}
// borrowed from jquery $(elem).is(':visible') implementation
export const isVisible = (elem) => elem && (elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length)
export const removeStyleProperty = (elem, property) => {
if (elem.style.removeProperty) {
elem.style.removeProperty(property)
} else {
elem.style.removeAttribute(property)
}
}
export const animationEndEvent = (() => {
// Prevent run in Node env
if (isNodeEnv()) {
return false
}
const testEl = document.createElement('div')
const transEndEventNames = {
'WebkitAnimation': 'webkitAnimationEnd',
'OAnimation': 'oAnimationEnd oanimationend',
'animation': 'animationend'
}
for (const i in transEndEventNames) {
if (transEndEventNames.hasOwnProperty(i) && typeof testEl.style[i] !== 'undefined') {
return transEndEventNames[i]
}
}
return false
})()
// Reset previous window keydown handler and focued element
export const resetPrevState = () => {
if (states.previousActiveElement && states.previousActiveElement.focus) {
let x = window.scrollX
let y = window.scrollY
states.previousActiveElement.focus()
if (typeof x !== 'undefined' && typeof y !== 'undefined') { // IE doesn't have scrollX/scrollY support
window.scrollTo(x, y)
}
}
}
// Measure width of scrollbar
// https://github.com/twbs/bootstrap/blob/master/js/modal.js#L279-L286
export const measureScrollbar = () => {
const supportsTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints
if (supportsTouch) {
return 0
}
const scrollDiv = document.createElement('div')
scrollDiv.style.width = '50px'
scrollDiv.style.height = '50px'
scrollDiv.style.overflow = 'scroll'
document.body.appendChild(scrollDiv)
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
document.body.removeChild(scrollDiv)
return scrollbarWidth
}
/**
* Inject a string of CSS into the page header
*
* @param {String} css
*/
export const injectCSS = (css = '') => {
// Prevent run in Node env
if (isNodeEnv()) {
return false
}
let head = document.head || document.getElementsByTagName('head')[0]
let style = document.createElement('style')
style.type = 'text/css'
head.appendChild(style)
if (style.styleSheet) {
style.styleSheet.cssText = css
} else {
style.appendChild(document.createTextNode(css))
}
}