first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:33:39 +03:00
commit 4362c3b83f
1991 changed files with 285411 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
/** @type {Record<DismissReason, DismissReason>} */
export const DismissReason = Object.freeze({
cancel: 'cancel',
backdrop: 'backdrop',
close: 'close',
esc: 'esc',
timer: 'timer',
})

View File

@@ -0,0 +1,94 @@
// Source: https://gist.github.com/mudge/5830382?permalink_comment_id=2691957#gistcomment-2691957
export default class EventEmitter {
constructor() {
/** @type {Events} */
this.events = {}
}
/**
* @param {string} eventName
* @returns {EventHandlers}
*/
_getHandlersByEventName(eventName) {
if (typeof this.events[eventName] === 'undefined') {
// not Set because we need to keep the FIFO order
// https://github.com/sweetalert2/sweetalert2/pull/2763#discussion_r1748990334
this.events[eventName] = []
}
return this.events[eventName]
}
/**
* @param {string} eventName
* @param {EventHandler} eventHandler
*/
on(eventName, eventHandler) {
const currentHandlers = this._getHandlersByEventName(eventName)
if (!currentHandlers.includes(eventHandler)) {
currentHandlers.push(eventHandler)
}
}
/**
* @param {string} eventName
* @param {EventHandler} eventHandler
*/
once(eventName, eventHandler) {
/**
* @param {...any} args
*/
const onceFn = (...args) => {
this.removeListener(eventName, onceFn)
// @ts-ignore
eventHandler.apply(this, args)
}
this.on(eventName, onceFn)
}
/**
* @param {string} eventName
* @param {...any} args
*/
emit(eventName, ...args) {
this._getHandlersByEventName(eventName).forEach(
/**
* @param {EventHandler} eventHandler
*/
(eventHandler) => {
try {
// @ts-ignore
eventHandler.apply(this, args)
} catch (error) {
console.error(error)
}
}
)
}
/**
* @param {string} eventName
* @param {EventHandler} eventHandler
*/
removeListener(eventName, eventHandler) {
const currentHandlers = this._getHandlersByEventName(eventName)
const index = currentHandlers.indexOf(eventHandler)
if (index > -1) {
currentHandlers.splice(index, 1)
}
}
/**
* @param {string} eventName
*/
removeAllListeners(eventName) {
if (this.events[eventName] !== undefined) {
// https://github.com/sweetalert2/sweetalert2/pull/2763#discussion_r1749239222
this.events[eventName].length = 0
}
}
reset() {
this.events = {}
}
}

View File

@@ -0,0 +1,71 @@
export default class Timer {
/**
* @param {() => void} callback
* @param {number} delay
*/
constructor(callback, delay) {
this.callback = callback
this.remaining = delay
this.running = false
this.start()
}
/**
* @returns {number}
*/
start() {
if (!this.running) {
this.running = true
this.started = new Date()
this.id = setTimeout(this.callback, this.remaining)
}
return this.remaining
}
/**
* @returns {number}
*/
stop() {
if (this.started && this.running) {
this.running = false
clearTimeout(this.id)
this.remaining -= new Date().getTime() - this.started.getTime()
}
return this.remaining
}
/**
* @param {number} n
* @returns {number}
*/
increase(n) {
const running = this.running
if (running) {
this.stop()
}
this.remaining += n
if (running) {
this.start()
}
return this.remaining
}
/**
* @returns {number}
*/
getTimerLeft() {
if (this.running) {
this.stop()
this.start()
}
return this.remaining
}
/**
* @returns {boolean}
*/
isRunning() {
return this.running
}
}

View File

@@ -0,0 +1,33 @@
import { getContainer } from './dom/getters.js'
// From https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-modal-dialog-accessibility/
// Adding aria-hidden="true" to elements outside of the active modal dialog ensures that
// elements not within the active modal dialog will not be surfaced if a user opens a screen
// readers list of elements (headings, form controls, landmarks, etc.) in the document.
export const setAriaHidden = () => {
const container = getContainer()
const bodyChildren = Array.from(document.body.children)
bodyChildren.forEach((el) => {
if (el.contains(container)) {
return
}
if (el.hasAttribute('aria-hidden')) {
el.setAttribute('data-previous-aria-hidden', el.getAttribute('aria-hidden') || '')
}
el.setAttribute('aria-hidden', 'true')
})
}
export const unsetAriaHidden = () => {
const bodyChildren = Array.from(document.body.children)
bodyChildren.forEach((el) => {
if (el.hasAttribute('data-previous-aria-hidden')) {
el.setAttribute('aria-hidden', el.getAttribute('data-previous-aria-hidden') || '')
el.removeAttribute('data-previous-aria-hidden')
} else {
el.removeAttribute('aria-hidden')
}
})
}

View File

@@ -0,0 +1,97 @@
export const swalPrefix = 'swal2-'
/**
* @typedef {Record<SwalClass, string>} SwalClasses
*/
/**
* @typedef {'success' | 'warning' | 'info' | 'question' | 'error'} SwalIcon
* @typedef {Record<SwalIcon, string>} SwalIcons
*/
/** @type {SwalClass[]} */
const classNames = [
'container',
'shown',
'height-auto',
'iosfix',
'popup',
'modal',
'no-backdrop',
'no-transition',
'toast',
'toast-shown',
'show',
'hide',
'close',
'title',
'html-container',
'actions',
'confirm',
'deny',
'cancel',
'footer',
'icon',
'icon-content',
'image',
'input',
'file',
'range',
'select',
'radio',
'checkbox',
'label',
'textarea',
'inputerror',
'input-label',
'validation-message',
'progress-steps',
'active-progress-step',
'progress-step',
'progress-step-line',
'loader',
'loading',
'styled',
'top',
'top-start',
'top-end',
'top-left',
'top-right',
'center',
'center-start',
'center-end',
'center-left',
'center-right',
'bottom',
'bottom-start',
'bottom-end',
'bottom-left',
'bottom-right',
'grow-row',
'grow-column',
'grow-fullscreen',
'rtl',
'timer-progress-bar',
'timer-progress-bar-container',
'scrollbar-measure',
'icon-success',
'icon-warning',
'icon-info',
'icon-question',
'icon-error',
'draggable',
'dragging',
]
export const swalClasses = classNames.reduce((acc, className) => {
acc[className] = swalPrefix + className
return acc
}, /** @type {SwalClasses} */ ({}))
/** @type {SwalIcon[]} */
const icons = ['success', 'warning', 'info', 'question', 'error']
export const iconTypes = icons.reduce((acc, icon) => {
acc[icon] = swalPrefix + icon
return acc
}, /** @type {SwalIcons} */ ({}))

View File

@@ -0,0 +1,23 @@
export default {
/**
* @param {string} string
* @param {string} [validationMessage]
* @returns {Promise<string | void>}
*/
email: (string, validationMessage) => {
return /^[a-zA-Z0-9.+_'-]+@[a-zA-Z0-9.-]+\.[a-zA-Z0-9-]+$/.test(string)
? Promise.resolve()
: Promise.resolve(validationMessage || 'Invalid email address')
},
/**
* @param {string} string
* @param {string} [validationMessage]
* @returns {Promise<string | void>}
*/
url: (string, validationMessage) => {
// taken from https://stackoverflow.com/a/3809435 with a small change from #1306 and #2013
return /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)$/.test(string)
? Promise.resolve()
: Promise.resolve(validationMessage || 'Invalid URL')
},
}

View File

@@ -0,0 +1,365 @@
import { iconTypes, swalClasses } from '../classes.js'
import { warn } from '../utils.js'
import { getCancelButton, getConfirmButton, getDenyButton, getTimerProgressBar } from './getters.js'
/**
* Securely set innerHTML of an element
* https://github.com/sweetalert2/sweetalert2/issues/1926
*
* @param {HTMLElement} elem
* @param {string} html
*/
export const setInnerHtml = (elem, html) => {
elem.textContent = ''
if (html) {
const parser = new DOMParser()
const parsed = parser.parseFromString(html, `text/html`)
const head = parsed.querySelector('head')
if (head) {
Array.from(head.childNodes).forEach((child) => {
elem.appendChild(child)
})
}
const body = parsed.querySelector('body')
if (body) {
Array.from(body.childNodes).forEach((child) => {
if (child instanceof HTMLVideoElement || child instanceof HTMLAudioElement) {
elem.appendChild(child.cloneNode(true)) // https://github.com/sweetalert2/sweetalert2/issues/2507
} else {
elem.appendChild(child)
}
})
}
}
}
/**
* @param {HTMLElement} elem
* @param {string} className
* @returns {boolean}
*/
export const hasClass = (elem, className) => {
if (!className) {
return false
}
const classList = className.split(/\s+/)
for (let i = 0; i < classList.length; i++) {
if (!elem.classList.contains(classList[i])) {
return false
}
}
return true
}
/**
* @param {HTMLElement} elem
* @param {SweetAlertOptions} params
*/
const removeCustomClasses = (elem, params) => {
Array.from(elem.classList).forEach((className) => {
if (
!Object.values(swalClasses).includes(className) &&
!Object.values(iconTypes).includes(className) &&
!Object.values(params.showClass || {}).includes(className)
) {
elem.classList.remove(className)
}
})
}
/**
* @param {HTMLElement} elem
* @param {SweetAlertOptions} params
* @param {string} className
*/
export const applyCustomClass = (elem, params, className) => {
removeCustomClasses(elem, params)
if (!params.customClass) {
return
}
const customClass = params.customClass[/** @type {keyof SweetAlertCustomClass} */ (className)]
if (!customClass) {
return
}
if (typeof customClass !== 'string' && !customClass.forEach) {
warn(`Invalid type of customClass.${className}! Expected string or iterable object, got "${typeof customClass}"`)
return
}
addClass(elem, customClass)
}
/**
* @param {HTMLElement} popup
* @param {import('./renderers/renderInput').InputClass | SweetAlertInput} inputClass
* @returns {HTMLInputElement | null}
*/
export const getInput = (popup, inputClass) => {
if (!inputClass) {
return null
}
switch (inputClass) {
case 'select':
case 'textarea':
case 'file':
return popup.querySelector(`.${swalClasses.popup} > .${swalClasses[inputClass]}`)
case 'checkbox':
return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.checkbox} input`)
case 'radio':
return (
popup.querySelector(`.${swalClasses.popup} > .${swalClasses.radio} input:checked`) ||
popup.querySelector(`.${swalClasses.popup} > .${swalClasses.radio} input:first-child`)
)
case 'range':
return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.range} input`)
default:
return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.input}`)
}
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement} input
*/
export const focusInput = (input) => {
input.focus()
// place cursor at end of text in text input
if (input.type !== 'file') {
// http://stackoverflow.com/a/2345915
const val = input.value
input.value = ''
input.value = val
}
}
/**
* @param {HTMLElement | HTMLElement[] | null} target
* @param {string | string[] | readonly string[] | undefined} classList
* @param {boolean} condition
*/
export const toggleClass = (target, classList, condition) => {
if (!target || !classList) {
return
}
if (typeof classList === 'string') {
classList = classList.split(/\s+/).filter(Boolean)
}
classList.forEach((className) => {
if (Array.isArray(target)) {
target.forEach((elem) => {
if (condition) {
elem.classList.add(className)
} else {
elem.classList.remove(className)
}
})
} else {
if (condition) {
target.classList.add(className)
} else {
target.classList.remove(className)
}
}
})
}
/**
* @param {HTMLElement | HTMLElement[] | null} target
* @param {string | string[] | readonly string[] | undefined} classList
*/
export const addClass = (target, classList) => {
toggleClass(target, classList, true)
}
/**
* @param {HTMLElement | HTMLElement[] | null} target
* @param {string | string[] | readonly string[] | undefined} classList
*/
export const removeClass = (target, classList) => {
toggleClass(target, classList, false)
}
/**
* Get direct child of an element by class name
*
* @param {HTMLElement} elem
* @param {string} className
* @returns {HTMLElement | undefined}
*/
export const getDirectChildByClass = (elem, className) => {
const children = Array.from(elem.children)
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child instanceof HTMLElement && hasClass(child, className)) {
return child
}
}
}
/**
* @param {HTMLElement} elem
* @param {string} property
* @param {string | number | null | undefined} value
*/
export const applyNumericalStyle = (elem, property, value) => {
if (value === `${parseInt(`${value}`)}`) {
value = parseInt(value)
}
if (value || parseInt(`${value}`) === 0) {
elem.style.setProperty(property, typeof value === 'number' ? `${value}px` : /** @type {string} */ (value))
} else {
elem.style.removeProperty(property)
}
}
/**
* @param {HTMLElement | null} elem
* @param {string} display
*/
export const show = (elem, display = 'flex') => {
if (!elem) {
return
}
elem.style.display = display
}
/**
* @param {HTMLElement | null} elem
*/
export const hide = (elem) => {
if (!elem) {
return
}
elem.style.display = 'none'
}
/**
* @param {HTMLElement | null} elem
* @param {string} display
*/
export const showWhenInnerHtmlPresent = (elem, display = 'block') => {
if (!elem) {
return
}
new MutationObserver(() => {
toggle(elem, elem.innerHTML, display)
}).observe(elem, { childList: true, subtree: true })
}
/**
* @param {HTMLElement} parent
* @param {string} selector
* @param {string} property
* @param {string} value
*/
export const setStyle = (parent, selector, property, value) => {
/** @type {HTMLElement | null} */
const el = parent.querySelector(selector)
if (el) {
el.style.setProperty(property, value)
}
}
/**
* @param {HTMLElement} elem
* @param {boolean | string | null | undefined} condition
* @param {string} display
*/
export const toggle = (elem, condition, display = 'flex') => {
if (condition) {
show(elem, display)
} else {
hide(elem)
}
}
/**
* borrowed from jquery $(elem).is(':visible') implementation
*
* @param {HTMLElement | null} elem
* @returns {boolean}
*/
export const isVisible = (elem) => Boolean(elem && (elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length))
/**
* @returns {boolean}
*/
export const allButtonsAreHidden = () =>
!isVisible(getConfirmButton()) && !isVisible(getDenyButton()) && !isVisible(getCancelButton())
/**
* @param {HTMLElement} elem
* @returns {boolean}
*/
export const isScrollable = (elem) => Boolean(elem.scrollHeight > elem.clientHeight)
/**
* @param {HTMLElement} element
* @param {HTMLElement} stopElement
* @returns {boolean}
*/
export const selfOrParentIsScrollable = (element, stopElement) => {
let parent = /** @type {HTMLElement | null} */ (element)
while (parent && parent !== stopElement) {
if (isScrollable(parent)) {
return true
}
parent = parent.parentElement
}
return false
}
/**
* borrowed from https://stackoverflow.com/a/46352119
*
* @param {HTMLElement} elem
* @returns {boolean}
*/
export const hasCssAnimation = (elem) => {
const style = window.getComputedStyle(elem)
const animDuration = parseFloat(style.getPropertyValue('animation-duration') || '0')
const transDuration = parseFloat(style.getPropertyValue('transition-duration') || '0')
return animDuration > 0 || transDuration > 0
}
/**
* @param {number} timer
* @param {boolean} reset
*/
export const animateTimerProgressBar = (timer, reset = false) => {
const timerProgressBar = getTimerProgressBar()
if (!timerProgressBar) {
return
}
if (isVisible(timerProgressBar)) {
if (reset) {
timerProgressBar.style.transition = 'none'
timerProgressBar.style.width = '100%'
}
setTimeout(() => {
timerProgressBar.style.transition = `width ${timer / 1000}s linear`
timerProgressBar.style.width = '0%'
}, 10)
}
}
export const stopTimerProgressBar = () => {
const timerProgressBar = getTimerProgressBar()
if (!timerProgressBar) {
return
}
const timerProgressBarWidth = parseInt(window.getComputedStyle(timerProgressBar).width)
timerProgressBar.style.removeProperty('transition')
timerProgressBar.style.width = '100%'
const timerProgressBarFullWidth = parseInt(window.getComputedStyle(timerProgressBar).width)
const timerProgressBarPercent = (timerProgressBarWidth / timerProgressBarFullWidth) * 100
timerProgressBar.style.width = `${timerProgressBarPercent}%`
}

View File

@@ -0,0 +1,198 @@
import { swalClasses } from '../classes.js'
import { hasClass, isVisible } from './domUtils.js'
/**
* Gets the popup container which contains the backdrop and the popup itself.
*
* @returns {HTMLElement | null}
*/
export const getContainer = () => document.body.querySelector(`.${swalClasses.container}`)
/**
* @param {string} selectorString
* @returns {HTMLElement | null}
*/
export const elementBySelector = (selectorString) => {
const container = getContainer()
return container ? container.querySelector(selectorString) : null
}
/**
* @param {string} className
* @returns {HTMLElement | null}
*/
const elementByClass = (className) => {
return elementBySelector(`.${className}`)
}
/**
* @returns {HTMLElement | null}
*/
export const getPopup = () => elementByClass(swalClasses.popup)
/**
* @returns {HTMLElement | null}
*/
export const getIcon = () => elementByClass(swalClasses.icon)
/**
* @returns {HTMLElement | null}
*/
export const getIconContent = () => elementByClass(swalClasses['icon-content'])
/**
* @returns {HTMLElement | null}
*/
export const getTitle = () => elementByClass(swalClasses.title)
/**
* @returns {HTMLElement | null}
*/
export const getHtmlContainer = () => elementByClass(swalClasses['html-container'])
/**
* @returns {HTMLElement | null}
*/
export const getImage = () => elementByClass(swalClasses.image)
/**
* @returns {HTMLElement | null}
*/
export const getProgressSteps = () => elementByClass(swalClasses['progress-steps'])
/**
* @returns {HTMLElement | null}
*/
export const getValidationMessage = () => elementByClass(swalClasses['validation-message'])
/**
* @returns {HTMLButtonElement | null}
*/
export const getConfirmButton = () =>
/** @type {HTMLButtonElement} */ (elementBySelector(`.${swalClasses.actions} .${swalClasses.confirm}`))
/**
* @returns {HTMLButtonElement | null}
*/
export const getCancelButton = () =>
/** @type {HTMLButtonElement} */ (elementBySelector(`.${swalClasses.actions} .${swalClasses.cancel}`))
/**
* @returns {HTMLButtonElement | null}
*/
export const getDenyButton = () =>
/** @type {HTMLButtonElement} */ (elementBySelector(`.${swalClasses.actions} .${swalClasses.deny}`))
/**
* @returns {HTMLElement | null}
*/
export const getInputLabel = () => elementByClass(swalClasses['input-label'])
/**
* @returns {HTMLElement | null}
*/
export const getLoader = () => elementBySelector(`.${swalClasses.loader}`)
/**
* @returns {HTMLElement | null}
*/
export const getActions = () => elementByClass(swalClasses.actions)
/**
* @returns {HTMLElement | null}
*/
export const getFooter = () => elementByClass(swalClasses.footer)
/**
* @returns {HTMLElement | null}
*/
export const getTimerProgressBar = () => elementByClass(swalClasses['timer-progress-bar'])
/**
* @returns {HTMLElement | null}
*/
export const getCloseButton = () => elementByClass(swalClasses.close)
// https://github.com/jkup/focusable/blob/master/index.js
const focusable = `
a[href],
area[href],
input:not([disabled]),
select:not([disabled]),
textarea:not([disabled]),
button:not([disabled]),
iframe,
object,
embed,
[tabindex="0"],
[contenteditable],
audio[controls],
video[controls],
summary
`
/**
* @returns {HTMLElement[]}
*/
export const getFocusableElements = () => {
const popup = getPopup()
if (!popup) {
return []
}
/** @type {NodeListOf<HTMLElement>} */
const focusableElementsWithTabindex = popup.querySelectorAll('[tabindex]:not([tabindex="-1"]):not([tabindex="0"])')
const focusableElementsWithTabindexSorted = Array.from(focusableElementsWithTabindex)
// sort according to tabindex
.sort((a, b) => {
const tabindexA = parseInt(a.getAttribute('tabindex') || '0')
const tabindexB = parseInt(b.getAttribute('tabindex') || '0')
if (tabindexA > tabindexB) {
return 1
} else if (tabindexA < tabindexB) {
return -1
}
return 0
})
/** @type {NodeListOf<HTMLElement>} */
const otherFocusableElements = popup.querySelectorAll(focusable)
const otherFocusableElementsFiltered = Array.from(otherFocusableElements).filter(
(el) => el.getAttribute('tabindex') !== '-1'
)
return [...new Set(focusableElementsWithTabindexSorted.concat(otherFocusableElementsFiltered))].filter((el) =>
isVisible(el)
)
}
/**
* @returns {boolean}
*/
export const isModal = () => {
return (
hasClass(document.body, swalClasses.shown) &&
!hasClass(document.body, swalClasses['toast-shown']) &&
!hasClass(document.body, swalClasses['no-backdrop'])
)
}
/**
* @returns {boolean}
*/
export const isToast = () => {
const popup = getPopup()
if (!popup) {
return false
}
return hasClass(popup, swalClasses.toast)
}
/**
* @returns {boolean}
*/
export const isLoading = () => {
const popup = getPopup()
if (!popup) {
return false
}
return popup.hasAttribute('data-loading')
}

View File

@@ -0,0 +1,5 @@
export * from './domUtils.js'
export * from './init.js'
export * from './getters.js'
export * from './parseHtmlToContainer.js'
export * from './renderers/render.js'

View File

@@ -0,0 +1,193 @@
import globalState from '../../globalState.js'
import { swalClasses } from '../classes.js'
import { isNodeEnv } from '../isNodeEnv.js'
import { error } from '../utils.js'
import { addClass, getDirectChildByClass, removeClass, setInnerHtml } from './domUtils.js'
import { getContainer, getPopup } from './getters.js'
const sweetHTML = `
<div aria-labelledby="${swalClasses.title}" aria-describedby="${swalClasses['html-container']}" class="${swalClasses.popup}" tabindex="-1">
<button type="button" class="${swalClasses.close}"></button>
<ul class="${swalClasses['progress-steps']}"></ul>
<div class="${swalClasses.icon}"></div>
<img class="${swalClasses.image}" />
<h2 class="${swalClasses.title}" id="${swalClasses.title}"></h2>
<div class="${swalClasses['html-container']}" id="${swalClasses['html-container']}"></div>
<input class="${swalClasses.input}" id="${swalClasses.input}" />
<input type="file" class="${swalClasses.file}" />
<div class="${swalClasses.range}">
<input type="range" />
<output></output>
</div>
<select class="${swalClasses.select}" id="${swalClasses.select}"></select>
<div class="${swalClasses.radio}"></div>
<label class="${swalClasses.checkbox}">
<input type="checkbox" id="${swalClasses.checkbox}" />
<span class="${swalClasses.label}"></span>
</label>
<textarea class="${swalClasses.textarea}" id="${swalClasses.textarea}"></textarea>
<div class="${swalClasses['validation-message']}" id="${swalClasses['validation-message']}"></div>
<div class="${swalClasses.actions}">
<div class="${swalClasses.loader}"></div>
<button type="button" class="${swalClasses.confirm}"></button>
<button type="button" class="${swalClasses.deny}"></button>
<button type="button" class="${swalClasses.cancel}"></button>
</div>
<div class="${swalClasses.footer}"></div>
<div class="${swalClasses['timer-progress-bar-container']}">
<div class="${swalClasses['timer-progress-bar']}"></div>
</div>
</div>
`.replace(/(^|\n)\s*/g, '')
/**
* @returns {boolean}
*/
const resetOldContainer = () => {
const oldContainer = getContainer()
if (!oldContainer) {
return false
}
oldContainer.remove()
removeClass(
[document.documentElement, document.body],
[
swalClasses['no-backdrop'],
swalClasses['toast-shown'],
// @ts-ignore: 'has-column' is not defined in swalClasses but may be set dynamically
swalClasses['has-column'],
]
)
return true
}
const resetValidationMessage = () => {
if (globalState.currentInstance) {
globalState.currentInstance.resetValidationMessage()
}
}
const addInputChangeListeners = () => {
const popup = getPopup()
if (!popup) {
return
}
const input = getDirectChildByClass(popup, swalClasses.input)
const file = getDirectChildByClass(popup, swalClasses.file)
/** @type {HTMLInputElement | null} */
const range = popup.querySelector(`.${swalClasses.range} input`)
/** @type {HTMLOutputElement | null} */
const rangeOutput = popup.querySelector(`.${swalClasses.range} output`)
const select = getDirectChildByClass(popup, swalClasses.select)
/** @type {HTMLInputElement | null} */
const checkbox = popup.querySelector(`.${swalClasses.checkbox} input`)
const textarea = getDirectChildByClass(popup, swalClasses.textarea)
if (input) {
input.oninput = resetValidationMessage
}
if (file) {
file.onchange = resetValidationMessage
}
if (select) {
select.onchange = resetValidationMessage
}
if (checkbox) {
checkbox.onchange = resetValidationMessage
}
if (textarea) {
textarea.oninput = resetValidationMessage
}
if (range && rangeOutput) {
range.oninput = () => {
resetValidationMessage()
rangeOutput.value = range.value
}
range.onchange = () => {
resetValidationMessage()
rangeOutput.value = range.value
}
}
}
/**
* @param {string | HTMLElement} target
* @returns {HTMLElement}
*/
const getTarget = (target) => {
if (typeof target === 'string') {
const element = document.querySelector(target)
if (!element) {
throw new Error(`Target element "${target}" not found`)
}
return /** @type {HTMLElement} */ (element)
}
return target
}
/**
* @param {SweetAlertOptions} params
*/
const setupAccessibility = (params) => {
const popup = getPopup()
if (!popup) {
return
}
popup.setAttribute('role', params.toast ? 'alert' : 'dialog')
popup.setAttribute('aria-live', params.toast ? 'polite' : 'assertive')
if (!params.toast) {
popup.setAttribute('aria-modal', 'true')
}
}
/**
* @param {HTMLElement} targetElement
*/
const setupRTL = (targetElement) => {
if (window.getComputedStyle(targetElement).direction === 'rtl') {
addClass(getContainer(), swalClasses.rtl)
globalState.isRTL = true
}
}
/**
* Add modal + backdrop to DOM
*
* @param {SweetAlertOptions} params
*/
export const init = (params) => {
// Clean up the old popup container if it exists
const oldContainerExisted = resetOldContainer()
if (isNodeEnv()) {
error('SweetAlert2 requires document to initialize')
return
}
const container = document.createElement('div')
container.className = swalClasses.container
if (oldContainerExisted) {
addClass(container, swalClasses['no-transition'])
}
setInnerHtml(container, sweetHTML)
container.dataset['swal2Theme'] = params.theme
const targetElement = getTarget(params.target || 'body')
targetElement.appendChild(container)
if (params.topLayer) {
container.setAttribute('popover', '')
container.showPopover()
}
setupAccessibility(params)
setupRTL(targetElement)
addInputChangeListeners()
}

View File

@@ -0,0 +1,239 @@
import { showLoading } from '../../staticMethods/showLoading.js'
import { swalClasses } from '../classes.js'
import { asPromise, error, hasToPromiseFn, isPromise } from '../utils.js'
import { getDirectChildByClass } from './domUtils.js'
import * as dom from './index.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const handleInputOptionsAndValue = (instance, params) => {
if (params.input === 'select' || params.input === 'radio') {
handleInputOptions(instance, params)
} else if (
['text', 'email', 'number', 'tel', 'textarea'].some((i) => i === params.input) &&
(hasToPromiseFn(params.inputValue) || isPromise(params.inputValue))
) {
showLoading(dom.getConfirmButton())
handleInputValue(instance, params)
}
}
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} innerParams
* @returns {SweetAlertInputValue}
*/
export const getInputValue = (instance, innerParams) => {
const input = instance.getInput()
if (!input) {
return null
}
switch (innerParams.input) {
case 'checkbox':
return getCheckboxValue(input)
case 'radio':
return getRadioValue(input)
case 'file':
return getFileValue(input)
default:
return innerParams.inputAutoTrim ? input.value.trim() : input.value
}
}
/**
* @param {HTMLInputElement} input
* @returns {number}
*/
const getCheckboxValue = (input) => (input.checked ? 1 : 0)
/**
* @param {HTMLInputElement} input
* @returns {string | null}
*/
const getRadioValue = (input) => (input.checked ? input.value : null)
/**
* @param {HTMLInputElement} input
* @returns {FileList | File | null}
*/
const getFileValue = (input) =>
input.files && input.files.length ? (input.getAttribute('multiple') !== null ? input.files : input.files[0]) : null
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const handleInputOptions = (instance, params) => {
const popup = dom.getPopup()
if (!popup) {
return
}
/**
* @param {*} inputOptions
*/
const processInputOptions = (inputOptions) => {
if (params.input === 'select') {
populateSelectOptions(popup, formatInputOptions(inputOptions), params)
} else if (params.input === 'radio') {
populateRadioOptions(popup, formatInputOptions(inputOptions), params)
}
}
if (hasToPromiseFn(params.inputOptions) || isPromise(params.inputOptions)) {
showLoading(dom.getConfirmButton())
asPromise(params.inputOptions).then((inputOptions) => {
instance.hideLoading()
processInputOptions(inputOptions)
})
} else if (typeof params.inputOptions === 'object') {
processInputOptions(params.inputOptions)
} else {
error(`Unexpected type of inputOptions! Expected object, Map or Promise, got ${typeof params.inputOptions}`)
}
}
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
const handleInputValue = (instance, params) => {
const input = instance.getInput()
if (!input) {
return
}
dom.hide(input)
asPromise(params.inputValue)
.then((inputValue) => {
input.value = params.input === 'number' ? `${parseFloat(inputValue) || 0}` : `${inputValue}`
dom.show(input)
input.focus()
instance.hideLoading()
})
.catch((err) => {
error(`Error in inputValue promise: ${err}`)
input.value = ''
dom.show(input)
input.focus()
instance.hideLoading()
})
}
/**
* @param {HTMLElement} popup
* @param {InputOptionFlattened[]} inputOptions
* @param {SweetAlertOptions} params
*/
function populateSelectOptions(popup, inputOptions, params) {
const select = getDirectChildByClass(popup, swalClasses.select)
if (!select) {
return
}
/**
* @param {HTMLElement} parent
* @param {string} optionLabel
* @param {string} optionValue
*/
const renderOption = (parent, optionLabel, optionValue) => {
const option = document.createElement('option')
option.value = optionValue
dom.setInnerHtml(option, optionLabel)
option.selected = isSelected(optionValue, params.inputValue)
parent.appendChild(option)
}
inputOptions.forEach((inputOption) => {
const optionValue = inputOption[0]
const optionLabel = inputOption[1]
// <optgroup> spec:
// https://www.w3.org/TR/html401/interact/forms.html#h-17.6
// "...all OPTGROUP elements must be specified directly within a SELECT element (i.e., groups may not be nested)..."
// check whether this is a <optgroup>
if (Array.isArray(optionLabel)) {
// if it is an array, then it is an <optgroup>
const optgroup = document.createElement('optgroup')
optgroup.label = optionValue
optgroup.disabled = false // not configurable for now
select.appendChild(optgroup)
optionLabel.forEach((o) => renderOption(optgroup, o[1], o[0]))
} else {
// case of <option>
renderOption(select, optionLabel, optionValue)
}
})
select.focus()
}
/**
* @param {HTMLElement} popup
* @param {InputOptionFlattened[]} inputOptions
* @param {SweetAlertOptions} params
*/
function populateRadioOptions(popup, inputOptions, params) {
const radio = getDirectChildByClass(popup, swalClasses.radio)
if (!radio) {
return
}
inputOptions.forEach((inputOption) => {
const radioValue = inputOption[0]
const radioLabel = inputOption[1]
const radioInput = document.createElement('input')
const radioLabelElement = document.createElement('label')
radioInput.type = 'radio'
radioInput.name = swalClasses.radio
radioInput.value = radioValue
if (isSelected(radioValue, params.inputValue)) {
radioInput.checked = true
}
const label = document.createElement('span')
dom.setInnerHtml(label, radioLabel)
label.className = swalClasses.label
radioLabelElement.appendChild(radioInput)
radioLabelElement.appendChild(label)
radio.appendChild(radioLabelElement)
})
const radios = radio.querySelectorAll('input')
if (radios.length) {
radios[0].focus()
}
}
/**
* Converts `inputOptions` into an array of `[value, label]`s
*
* @param {*} inputOptions
* @typedef {string[]} InputOptionFlattened
* @returns {InputOptionFlattened[]}
*/
const formatInputOptions = (inputOptions) => {
/** @type {InputOptionFlattened[]} */
const result = []
if (inputOptions instanceof Map) {
inputOptions.forEach((value, key) => {
let valueFormatted = value
if (typeof valueFormatted === 'object') {
// case of <optgroup>
valueFormatted = formatInputOptions(valueFormatted)
}
result.push([key, valueFormatted])
})
} else {
Object.keys(inputOptions).forEach((key) => {
let valueFormatted = inputOptions[key]
if (typeof valueFormatted === 'object') {
// case of <optgroup>
valueFormatted = formatInputOptions(valueFormatted)
}
result.push([key, valueFormatted])
})
}
return result
}
/**
* @param {string} optionValue
* @param {SweetAlertInputValue} inputValue
* @returns {boolean}
*/
const isSelected = (optionValue, inputValue) => {
return Boolean(inputValue) && (inputValue !== null && inputValue !== undefined) && inputValue.toString() === optionValue.toString()
}

View File

@@ -0,0 +1,53 @@
import { setInnerHtml } from './domUtils.js'
/**
* @param {HTMLElement | object | string} param
* @param {HTMLElement} target
*/
export const parseHtmlToContainer = (param, target) => {
// DOM element
if (param instanceof HTMLElement) {
target.appendChild(param)
}
// Object
else if (typeof param === 'object') {
handleObject(param, target)
}
// Plain string
else if (param) {
setInnerHtml(target, param)
}
}
/**
* @param {object} param
* @param {HTMLElement} target
*/
const handleObject = (param, target) => {
// JQuery element(s)
if ('jquery' in param) {
handleJqueryElem(target, param)
}
// For other objects use their string representation
else {
setInnerHtml(target, param.toString())
}
}
/**
* @param {HTMLElement} target
* @param {any} elem
*/
const handleJqueryElem = (target, elem) => {
target.textContent = ''
if (0 in elem) {
for (let i = 0; i in elem; i++) {
target.appendChild(elem[i].cloneNode(true))
}
} else {
target.appendChild(elem.cloneNode(true))
}
}

View File

@@ -0,0 +1,37 @@
import globalState from '../../../globalState.js'
import { getPopup } from '../getters.js'
import { renderActions } from './renderActions.js'
import { renderCloseButton } from './renderCloseButton.js'
import { renderContainer } from './renderContainer.js'
import { renderContent } from './renderContent.js'
import { renderFooter } from './renderFooter.js'
import { renderIcon } from './renderIcon.js'
import { renderImage } from './renderImage.js'
import { renderPopup } from './renderPopup.js'
import { renderProgressSteps } from './renderProgressSteps.js'
import { renderTitle } from './renderTitle.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const render = (instance, params) => {
renderPopup(instance, params)
renderContainer(instance, params)
renderProgressSteps(instance, params)
renderIcon(instance, params)
renderImage(instance, params)
renderTitle(instance, params)
renderCloseButton(instance, params)
renderContent(instance, params)
renderActions(instance, params)
renderFooter(instance, params)
const popup = getPopup()
if (typeof params.didRender === 'function' && popup) {
params.didRender(popup)
}
globalState.eventEmitter?.emit('didRender', popup)
}

View File

@@ -0,0 +1,127 @@
import { swalClasses } from '../../classes.js'
import * as dom from '../../dom/index.js'
import { capitalizeFirstLetter } from '../../utils.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderActions = (instance, params) => {
const actions = dom.getActions()
const loader = dom.getLoader()
if (!actions || !loader) {
return
}
// Actions (buttons) wrapper
if (!params.showConfirmButton && !params.showDenyButton && !params.showCancelButton) {
dom.hide(actions)
} else {
dom.show(actions)
}
// Custom class
dom.applyCustomClass(actions, params, 'actions')
// Render all the buttons
renderButtons(actions, loader, params)
// Loader
dom.setInnerHtml(loader, params.loaderHtml || '')
dom.applyCustomClass(loader, params, 'loader')
}
/**
* @param {HTMLElement} actions
* @param {HTMLElement} loader
* @param {SweetAlertOptions} params
*/
function renderButtons(actions, loader, params) {
const confirmButton = dom.getConfirmButton()
const denyButton = dom.getDenyButton()
const cancelButton = dom.getCancelButton()
if (!confirmButton || !denyButton || !cancelButton) {
return
}
// Render buttons
renderButton(confirmButton, 'confirm', params)
renderButton(denyButton, 'deny', params)
renderButton(cancelButton, 'cancel', params)
handleButtonsStyling(confirmButton, denyButton, cancelButton, params)
if (params.reverseButtons) {
if (params.toast) {
actions.insertBefore(cancelButton, confirmButton)
actions.insertBefore(denyButton, confirmButton)
} else {
actions.insertBefore(cancelButton, loader)
actions.insertBefore(denyButton, loader)
actions.insertBefore(confirmButton, loader)
}
}
}
/**
* @param {HTMLElement} confirmButton
* @param {HTMLElement} denyButton
* @param {HTMLElement} cancelButton
* @param {SweetAlertOptions} params
*/
function handleButtonsStyling(confirmButton, denyButton, cancelButton, params) {
if (!params.buttonsStyling) {
dom.removeClass([confirmButton, denyButton, cancelButton], swalClasses.styled)
return
}
dom.addClass([confirmButton, denyButton, cancelButton], swalClasses.styled)
// Apply custom background colors to action buttons
if (params.confirmButtonColor) {
confirmButton.style.setProperty('--swal2-confirm-button-background-color', params.confirmButtonColor)
}
if (params.denyButtonColor) {
denyButton.style.setProperty('--swal2-deny-button-background-color', params.denyButtonColor)
}
if (params.cancelButtonColor) {
cancelButton.style.setProperty('--swal2-cancel-button-background-color', params.cancelButtonColor)
}
// Apply the outline color to action buttons
applyOutlineColor(confirmButton)
applyOutlineColor(denyButton)
applyOutlineColor(cancelButton)
}
/**
* @param {HTMLElement} button
*/
function applyOutlineColor(button) {
const buttonStyle = window.getComputedStyle(button)
if (buttonStyle.getPropertyValue('--swal2-action-button-focus-box-shadow')) {
// If the button already has a custom outline color, no need to change it
return
}
const outlineColor = buttonStyle.backgroundColor.replace(/rgba?\((\d+), (\d+), (\d+).*/, 'rgba($1, $2, $3, 0.5)')
button.style.setProperty(
'--swal2-action-button-focus-box-shadow',
buttonStyle.getPropertyValue('--swal2-outline').replace(/ rgba\(.*/, ` ${outlineColor}`)
)
}
/**
* @param {HTMLElement} button
* @param {'confirm' | 'deny' | 'cancel'} buttonType
* @param {SweetAlertOptions} params
*/
function renderButton(button, buttonType, params) {
const buttonName = /** @type {'Confirm' | 'Deny' | 'Cancel'} */ (capitalizeFirstLetter(buttonType))
dom.toggle(button, params[`show${buttonName}Button`], 'inline-block')
dom.setInnerHtml(button, params[`${buttonType}ButtonText`] || '') // Set caption text
button.setAttribute('aria-label', params[`${buttonType}ButtonAriaLabel`] || '') // ARIA label
// Add buttons custom classes
button.className = swalClasses[buttonType]
dom.applyCustomClass(button, params, `${buttonType}Button`)
}

View File

@@ -0,0 +1,20 @@
import * as dom from '../../dom/index.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderCloseButton = (instance, params) => {
const closeButton = dom.getCloseButton()
if (!closeButton) {
return
}
dom.setInnerHtml(closeButton, params.closeButtonHtml || '')
// Custom class
dom.applyCustomClass(closeButton, params, 'closeButton')
dom.toggle(closeButton, params.showCloseButton)
closeButton.setAttribute('aria-label', params.closeButtonAriaLabel || '')
}

View File

@@ -0,0 +1,62 @@
import { swalClasses } from '../../classes.js'
import * as dom from '../../dom/index.js'
import { warn } from '../../utils.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderContainer = (instance, params) => {
const container = dom.getContainer()
if (!container) {
return
}
handleBackdropParam(container, params.backdrop)
handlePositionParam(container, params.position)
handleGrowParam(container, params.grow)
// Custom class
dom.applyCustomClass(container, params, 'container')
}
/**
* @param {HTMLElement} container
* @param {SweetAlertOptions['backdrop']} backdrop
*/
function handleBackdropParam(container, backdrop) {
if (typeof backdrop === 'string') {
container.style.background = backdrop
} else if (!backdrop) {
dom.addClass([document.documentElement, document.body], swalClasses['no-backdrop'])
}
}
/**
* @param {HTMLElement} container
* @param {SweetAlertOptions['position']} position
*/
function handlePositionParam(container, position) {
if (!position) {
return
}
if (position in swalClasses) {
dom.addClass(container, swalClasses[position])
} else {
warn('The "position" parameter is not valid, defaulting to "center"')
dom.addClass(container, swalClasses.center)
}
}
/**
* @param {HTMLElement} container
* @param {SweetAlertOptions['grow']} grow
*/
function handleGrowParam(container, grow) {
if (!grow) {
return
}
dom.addClass(container, swalClasses[`grow-${grow}`])
}

View File

@@ -0,0 +1,36 @@
import * as dom from '../../dom/index.js'
import { renderInput } from './renderInput.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderContent = (instance, params) => {
const htmlContainer = dom.getHtmlContainer()
if (!htmlContainer) {
return
}
dom.showWhenInnerHtmlPresent(htmlContainer)
dom.applyCustomClass(htmlContainer, params, 'htmlContainer')
// Content as HTML
if (params.html) {
dom.parseHtmlToContainer(params.html, htmlContainer)
dom.show(htmlContainer, 'block')
}
// Content as plain text
else if (params.text) {
htmlContainer.textContent = params.text
dom.show(htmlContainer, 'block')
}
// No content
else {
dom.hide(htmlContainer)
}
renderInput(instance, params)
}

View File

@@ -0,0 +1,23 @@
import * as dom from '../../dom/index.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderFooter = (instance, params) => {
const footer = dom.getFooter()
if (!footer) {
return
}
dom.showWhenInnerHtmlPresent(footer)
dom.toggle(footer, Boolean(params.footer), 'block')
if (params.footer) {
dom.parseHtmlToContainer(params.footer, footer)
}
// Custom class
dom.applyCustomClass(footer, params, 'footer')
}

View File

@@ -0,0 +1,164 @@
import privateProps from '../../../privateProps.js'
import { iconTypes, swalClasses } from '../../classes.js'
import * as dom from '../../dom/index.js'
import { error } from '../../utils.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderIcon = (instance, params) => {
const innerParams = privateProps.innerParams.get(instance)
const icon = dom.getIcon()
if (!icon) {
return
}
// if the given icon already rendered, apply the styling without re-rendering the icon
if (innerParams && params.icon === innerParams.icon) {
// Custom or default content
setContent(icon, params)
applyStyles(icon, params)
return
}
if (!params.icon && !params.iconHtml) {
dom.hide(icon)
return
}
if (params.icon && Object.keys(iconTypes).indexOf(params.icon) === -1) {
error(`Unknown icon! Expected "success", "error", "warning", "info" or "question", got "${params.icon}"`)
dom.hide(icon)
return
}
dom.show(icon)
// Custom or default content
setContent(icon, params)
applyStyles(icon, params)
// Animate icon
dom.addClass(icon, params.showClass && params.showClass.icon)
// Re-adjust the success icon on system theme change
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: dark)')
colorSchemeQueryList.addEventListener('change', adjustSuccessIconBackgroundColor)
}
/**
* @param {HTMLElement} icon
* @param {SweetAlertOptions} params
*/
const applyStyles = (icon, params) => {
for (const [iconType, iconClassName] of Object.entries(iconTypes)) {
if (params.icon !== iconType) {
dom.removeClass(icon, iconClassName)
}
}
dom.addClass(icon, params.icon && iconTypes[params.icon])
// Icon color
setColor(icon, params)
// Success icon background color
adjustSuccessIconBackgroundColor()
// Custom class
dom.applyCustomClass(icon, params, 'icon')
}
// Adjust success icon background color to match the popup background color
const adjustSuccessIconBackgroundColor = () => {
const popup = dom.getPopup()
if (!popup) {
return
}
const popupBackgroundColor = window.getComputedStyle(popup).getPropertyValue('background-color')
/** @type {NodeListOf<HTMLElement>} */
const successIconParts = popup.querySelectorAll('[class^=swal2-success-circular-line], .swal2-success-fix')
for (let i = 0; i < successIconParts.length; i++) {
successIconParts[i].style.backgroundColor = popupBackgroundColor
}
}
/**
*
* @param {SweetAlertOptions} params
* @returns {string}
*/
const successIconHtml = (params) => `
${params.animation ? '<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>
${params.animation ? '<div class="swal2-success-fix"></div>' : ''}
${params.animation ? '<div class="swal2-success-circular-line-right"></div>' : ''}
`
const errorIconHtml = `
<span class="swal2-x-mark">
<span class="swal2-x-mark-line-left"></span>
<span class="swal2-x-mark-line-right"></span>
</span>
`
/**
* @param {HTMLElement} icon
* @param {SweetAlertOptions} params
*/
const setContent = (icon, params) => {
if (!params.icon && !params.iconHtml) {
return
}
let oldContent = icon.innerHTML
let newContent = ''
if (params.iconHtml) {
newContent = iconContent(params.iconHtml)
} else if (params.icon === 'success') {
newContent = successIconHtml(params)
oldContent = oldContent.replace(/ style=".*?"/g, '') // undo adjustSuccessIconBackgroundColor()
} else if (params.icon === 'error') {
newContent = errorIconHtml
} else if (params.icon) {
const defaultIconHtml = {
question: '?',
warning: '!',
info: 'i',
}
newContent = iconContent(defaultIconHtml[params.icon])
}
if (oldContent.trim() !== newContent.trim()) {
dom.setInnerHtml(icon, newContent)
}
}
/**
* @param {HTMLElement} icon
* @param {SweetAlertOptions} params
*/
const setColor = (icon, params) => {
if (!params.iconColor) {
return
}
icon.style.color = params.iconColor
icon.style.borderColor = params.iconColor
for (const sel of [
'.swal2-success-line-tip',
'.swal2-success-line-long',
'.swal2-x-mark-line-left',
'.swal2-x-mark-line-right',
]) {
dom.setStyle(icon, sel, 'background-color', params.iconColor)
}
dom.setStyle(icon, '.swal2-success-ring', 'border-color', params.iconColor)
}
/**
* @param {string} content
* @returns {string}
*/
const iconContent = (content) => `<div class="${swalClasses['icon-content']}">${content}</div>`

View File

@@ -0,0 +1,32 @@
import { swalClasses } from '../../classes.js'
import * as dom from '../../dom/index.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderImage = (instance, params) => {
const image = dom.getImage()
if (!image) {
return
}
if (!params.imageUrl) {
dom.hide(image)
return
}
dom.show(image, '')
// Src, alt
image.setAttribute('src', params.imageUrl)
image.setAttribute('alt', params.imageAlt || '')
// Width, height
dom.applyNumericalStyle(image, 'width', params.imageWidth)
dom.applyNumericalStyle(image, 'height', params.imageHeight)
// Class
image.className = swalClasses.image
dom.applyCustomClass(image, params, 'image')
}

View File

@@ -0,0 +1,353 @@
/// <reference path="../../../../sweetalert2.d.ts"/>
/**
* @typedef { HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement } Input
* @typedef { 'input' | 'file' | 'range' | 'select' | 'radio' | 'checkbox' | 'textarea' } InputClass
*/
import privateProps from '../../../privateProps.js'
import { swalClasses } from '../../classes.js'
import * as dom from '../../dom/index.js'
import { error, isPromise, warn } from '../../utils.js'
/** @type {InputClass[]} */
const inputClasses = ['input', 'file', 'range', 'select', 'radio', 'checkbox', 'textarea']
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderInput = (instance, params) => {
const popup = dom.getPopup()
if (!popup) {
return
}
const innerParams = privateProps.innerParams.get(instance)
const rerender = !innerParams || params.input !== innerParams.input
inputClasses.forEach((inputClass) => {
const inputContainer = dom.getDirectChildByClass(popup, swalClasses[inputClass])
if (!inputContainer) {
return
}
// set attributes
setAttributes(inputClass, params.inputAttributes)
// set class
inputContainer.className = swalClasses[inputClass]
if (rerender) {
dom.hide(inputContainer)
}
})
if (params.input) {
if (rerender) {
showInput(params)
}
// set custom class
setCustomClass(params)
}
}
/**
* @param {SweetAlertOptions} params
*/
const showInput = (params) => {
if (!params.input) {
return
}
if (!renderInputType[params.input]) {
error(`Unexpected type of input! Expected ${Object.keys(renderInputType).join(' | ')}, got "${params.input}"`)
return
}
const inputContainer = getInputContainer(params.input)
if (!inputContainer) {
return
}
const input = renderInputType[params.input](inputContainer, params)
dom.show(inputContainer)
// input autofocus
if (params.inputAutoFocus) {
setTimeout(() => {
dom.focusInput(input)
})
}
}
/**
* @param {HTMLInputElement} input
*/
const removeAttributes = (input) => {
for (let i = 0; i < input.attributes.length; i++) {
const attrName = input.attributes[i].name
if (!['id', 'type', 'value', 'style'].includes(attrName)) {
input.removeAttribute(attrName)
}
}
}
/**
* @param {InputClass} inputClass
* @param {SweetAlertOptions['inputAttributes']} inputAttributes
*/
const setAttributes = (inputClass, inputAttributes) => {
const popup = dom.getPopup()
if (!popup) {
return
}
const input = dom.getInput(popup, inputClass)
if (!input) {
return
}
removeAttributes(input)
for (const attr in inputAttributes) {
input.setAttribute(attr, inputAttributes[attr])
}
}
/**
* @param {SweetAlertOptions} params
*/
const setCustomClass = (params) => {
if (!params.input) {
return
}
const inputContainer = getInputContainer(params.input)
if (inputContainer) {
dom.applyCustomClass(inputContainer, params, 'input')
}
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @param {SweetAlertOptions} params
*/
const setInputPlaceholder = (input, params) => {
if (!input.placeholder && params.inputPlaceholder) {
input.placeholder = params.inputPlaceholder
}
}
/**
* @param {Input} input
* @param {Input} prependTo
* @param {SweetAlertOptions} params
*/
const setInputLabel = (input, prependTo, params) => {
if (params.inputLabel) {
const label = document.createElement('label')
const labelClass = swalClasses['input-label']
label.setAttribute('for', input.id)
label.className = labelClass
if (typeof params.customClass === 'object') {
dom.addClass(label, params.customClass.inputLabel)
}
label.innerText = params.inputLabel
prependTo.insertAdjacentElement('beforebegin', label)
}
}
/**
* @param {SweetAlertInput} inputType
* @returns {HTMLElement | undefined}
*/
const getInputContainer = (inputType) => {
const popup = dom.getPopup()
if (!popup) {
return
}
return dom.getDirectChildByClass(popup, swalClasses[/** @type {SwalClass} */ (inputType)] || swalClasses.input)
}
/**
* @param {HTMLInputElement | HTMLOutputElement | HTMLTextAreaElement} input
* @param {SweetAlertOptions['inputValue']} inputValue
*/
const checkAndSetInputValue = (input, inputValue) => {
if (['string', 'number'].includes(typeof inputValue)) {
input.value = `${inputValue}`
} else if (!isPromise(inputValue)) {
warn(`Unexpected type of inputValue! Expected "string", "number" or "Promise", got "${typeof inputValue}"`)
}
}
/** @type {Record<SweetAlertInput, (input: Input | HTMLElement, params: SweetAlertOptions) => Input>} */
const renderInputType = {}
/**
* @param {Input | HTMLElement} input
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.text =
renderInputType.email =
renderInputType.password =
renderInputType.number =
renderInputType.tel =
renderInputType.url =
renderInputType.search =
renderInputType.date =
renderInputType['datetime-local'] =
renderInputType.time =
renderInputType.week =
renderInputType.month =
/** @type {(input: Input | HTMLElement, params: SweetAlertOptions) => Input} */
(input, params) => {
const inputElement = /** @type {HTMLInputElement} */ (input)
checkAndSetInputValue(inputElement, params.inputValue)
setInputLabel(inputElement, inputElement, params)
setInputPlaceholder(inputElement, params)
inputElement.type = /** @type {string} */ (params.input)
return inputElement
}
/**
* @param {Input | HTMLElement} input
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.file = (input, params) => {
const inputElement = /** @type {HTMLInputElement} */ (input)
setInputLabel(inputElement, inputElement, params)
setInputPlaceholder(inputElement, params)
return inputElement
}
/**
* @param {Input | HTMLElement} range
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.range = (range, params) => {
const rangeContainer = /** @type {HTMLElement} */ (range)
const rangeInput = rangeContainer.querySelector('input')
const rangeOutput = rangeContainer.querySelector('output')
if (rangeInput) {
checkAndSetInputValue(rangeInput, params.inputValue)
rangeInput.type = /** @type {string} */ (params.input)
setInputLabel(rangeInput, /** @type {Input} */ (range), params)
}
if (rangeOutput) {
checkAndSetInputValue(rangeOutput, params.inputValue)
}
return /** @type {Input} */ (range)
}
/**
* @param {Input | HTMLElement} select
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.select = (select, params) => {
const selectElement = /** @type {HTMLSelectElement} */ (select)
selectElement.textContent = ''
if (params.inputPlaceholder) {
const placeholder = document.createElement('option')
dom.setInnerHtml(placeholder, params.inputPlaceholder)
placeholder.value = ''
placeholder.disabled = true
placeholder.selected = true
selectElement.appendChild(placeholder)
}
setInputLabel(selectElement, selectElement, params)
return selectElement
}
/**
* @param {Input | HTMLElement} radio
* @returns {Input}
*/
renderInputType.radio = (radio) => {
const radioElement = /** @type {HTMLElement} */ (radio)
radioElement.textContent = ''
return /** @type {Input} */ (radio)
}
/**
* @param {Input | HTMLElement} checkboxContainer
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.checkbox = (checkboxContainer, params) => {
const popup = dom.getPopup()
if (!popup) {
throw new Error('Popup not found')
}
const checkbox = dom.getInput(popup, 'checkbox')
if (!checkbox) {
throw new Error('Checkbox input not found')
}
checkbox.value = '1'
checkbox.checked = Boolean(params.inputValue)
const containerElement = /** @type {HTMLElement} */ (checkboxContainer)
const label = containerElement.querySelector('span')
if (label) {
const placeholderOrLabel = params.inputPlaceholder || params.inputLabel
if (placeholderOrLabel) {
dom.setInnerHtml(label, placeholderOrLabel)
}
}
return checkbox
}
/**
* @param {Input | HTMLElement} textarea
* @param {SweetAlertOptions} params
* @returns {Input}
*/
renderInputType.textarea = (textarea, params) => {
const textareaElement = /** @type {HTMLTextAreaElement} */ (textarea)
checkAndSetInputValue(textareaElement, params.inputValue)
setInputPlaceholder(textareaElement, params)
setInputLabel(textareaElement, textareaElement, params)
/**
* @param {HTMLElement} el
* @returns {number}
*/
const getMargin = (el) =>
parseInt(window.getComputedStyle(el).marginLeft) + parseInt(window.getComputedStyle(el).marginRight)
// https://github.com/sweetalert2/sweetalert2/issues/2291
setTimeout(() => {
// https://github.com/sweetalert2/sweetalert2/issues/1699
if ('MutationObserver' in window) {
const popup = dom.getPopup()
if (!popup) {
return
}
const initialPopupWidth = parseInt(window.getComputedStyle(popup).width)
const textareaResizeHandler = () => {
// check if texarea is still in document (i.e. popup wasn't closed in the meantime)
if (!document.body.contains(textareaElement)) {
return
}
const textareaWidth = textareaElement.offsetWidth + getMargin(textareaElement)
const popupElement = dom.getPopup()
if (popupElement) {
if (textareaWidth > initialPopupWidth) {
popupElement.style.width = `${textareaWidth}px`
} else {
dom.applyNumericalStyle(popupElement, 'width', params.width)
}
}
}
new MutationObserver(textareaResizeHandler).observe(textareaElement, {
attributes: true,
attributeFilter: ['style'],
})
}
})
return textareaElement
}

View File

@@ -0,0 +1,83 @@
import { swalClasses } from '../../classes.js'
import { addDraggableListeners, removeDraggableListeners } from '../../draggable.js'
import * as dom from '../../dom/index.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderPopup = (instance, params) => {
const container = dom.getContainer()
const popup = dom.getPopup()
if (!container || !popup) {
return
}
// Width
// https://github.com/sweetalert2/sweetalert2/issues/2170
if (params.toast) {
dom.applyNumericalStyle(container, 'width', params.width)
popup.style.width = '100%'
const loader = dom.getLoader()
if (loader) {
popup.insertBefore(loader, dom.getIcon())
}
} else {
dom.applyNumericalStyle(popup, 'width', params.width)
}
// Padding
dom.applyNumericalStyle(popup, 'padding', params.padding)
// Color
if (params.color) {
popup.style.color = params.color
}
// Background
if (params.background) {
popup.style.background = params.background
}
dom.hide(dom.getValidationMessage())
// Classes
addClasses(popup, params)
if (params.draggable && !params.toast) {
dom.addClass(popup, swalClasses.draggable)
addDraggableListeners(popup)
} else {
dom.removeClass(popup, swalClasses.draggable)
removeDraggableListeners(popup)
}
}
/**
* @param {HTMLElement} popup
* @param {SweetAlertOptions} params
*/
const addClasses = (popup, params) => {
const showClass = params.showClass || {}
// Default Class + showClass when updating Swal.update({})
popup.className = `${swalClasses.popup} ${dom.isVisible(popup) ? showClass.popup : ''}`
if (params.toast) {
dom.addClass([document.documentElement, document.body], swalClasses['toast-shown'])
dom.addClass(popup, swalClasses.toast)
} else {
dom.addClass(popup, swalClasses.modal)
}
// Custom class
dom.applyCustomClass(popup, params, 'popup')
// TODO: remove in the next major
if (typeof params.customClass === 'string') {
dom.addClass(popup, params.customClass)
}
// Icon class (#1842)
if (params.icon) {
dom.addClass(popup, swalClasses[`icon-${params.icon}`])
}
}

View File

@@ -0,0 +1,67 @@
import { swalClasses } from '../../classes.js'
import * as dom from '../../dom/index.js'
import { warn } from '../../utils.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderProgressSteps = (instance, params) => {
const progressStepsContainer = dom.getProgressSteps()
if (!progressStepsContainer) {
return
}
const { progressSteps, currentProgressStep } = params
if (!progressSteps || progressSteps.length === 0 || currentProgressStep === undefined) {
dom.hide(progressStepsContainer)
return
}
dom.show(progressStepsContainer)
progressStepsContainer.textContent = ''
if (currentProgressStep >= progressSteps.length) {
warn(
'Invalid currentProgressStep parameter, it should be less than progressSteps.length ' +
'(currentProgressStep like JS arrays starts from 0)'
)
}
progressSteps.forEach((step, index) => {
const stepEl = createStepElement(step)
progressStepsContainer.appendChild(stepEl)
if (index === currentProgressStep) {
dom.addClass(stepEl, swalClasses['active-progress-step'])
}
if (index !== progressSteps.length - 1) {
const lineEl = createLineElement(params)
progressStepsContainer.appendChild(lineEl)
}
})
}
/**
* @param {string} step
* @returns {HTMLLIElement}
*/
const createStepElement = (step) => {
const stepEl = document.createElement('li')
dom.addClass(stepEl, swalClasses['progress-step'])
dom.setInnerHtml(stepEl, step)
return stepEl
}
/**
* @param {SweetAlertOptions} params
* @returns {HTMLLIElement}
*/
const createLineElement = (params) => {
const lineEl = document.createElement('li')
dom.addClass(lineEl, swalClasses['progress-step-line'])
if (params.progressStepsDistance) {
dom.applyNumericalStyle(lineEl, 'width', params.progressStepsDistance)
}
return lineEl
}

View File

@@ -0,0 +1,27 @@
import * as dom from '../../dom/index.js'
/**
* @param {SweetAlert} instance
* @param {SweetAlertOptions} params
*/
export const renderTitle = (instance, params) => {
const title = dom.getTitle()
if (!title) {
return
}
dom.showWhenInnerHtmlPresent(title)
dom.toggle(title, Boolean(params.title || params.titleText), 'block')
if (params.title) {
dom.parseHtmlToContainer(params.title, title)
}
if (params.titleText) {
title.innerText = params.titleText
}
// Custom class
dom.applyCustomClass(title, params, 'title')
}

View File

@@ -0,0 +1,99 @@
import globalState from '../globalState.js'
import * as dom from './dom/index.js'
let dragging = false
let mousedownX = 0
let mousedownY = 0
let initialX = 0
let initialY = 0
/**
* @param {HTMLElement} popup
*/
export const addDraggableListeners = (popup) => {
popup.addEventListener('mousedown', down)
document.body.addEventListener('mousemove', move)
popup.addEventListener('mouseup', up)
popup.addEventListener('touchstart', down)
document.body.addEventListener('touchmove', move)
popup.addEventListener('touchend', up)
}
/**
* @param {HTMLElement} popup
*/
export const removeDraggableListeners = (popup) => {
popup.removeEventListener('mousedown', down)
document.body.removeEventListener('mousemove', move)
popup.removeEventListener('mouseup', up)
popup.removeEventListener('touchstart', down)
document.body.removeEventListener('touchmove', move)
popup.removeEventListener('touchend', up)
}
/**
* @param {MouseEvent | TouchEvent} event
*/
const down = (event) => {
const popup = dom.getPopup()
if (!popup) {
return
}
const icon = dom.getIcon()
if (event.target === popup || (icon && icon.contains(/** @type {HTMLElement} */ (event.target)))) {
dragging = true
const clientXY = getClientXY(event)
mousedownX = clientXY.clientX
mousedownY = clientXY.clientY
initialX = parseInt(popup.style.insetInlineStart) || 0
initialY = parseInt(popup.style.insetBlockStart) || 0
dom.addClass(popup, 'swal2-dragging')
}
}
/**
* @param {MouseEvent | TouchEvent} event
*/
const move = (event) => {
const popup = dom.getPopup()
if (!popup) {
return
}
if (dragging) {
let { clientX, clientY } = getClientXY(event)
const deltaX = clientX - mousedownX
// In RTL mode, negate the horizontal delta since insetInlineStart refers to the right edge
popup.style.insetInlineStart = `${initialX + (globalState.isRTL ? -deltaX : deltaX)}px`
popup.style.insetBlockStart = `${initialY + (clientY - mousedownY)}px`
}
}
const up = () => {
const popup = dom.getPopup()
dragging = false
dom.removeClass(popup, 'swal2-dragging')
}
/**
* @param {MouseEvent | TouchEvent} event
* @returns {{ clientX: number, clientY: number }}
*/
const getClientXY = (event) => {
let clientX = 0,
clientY = 0
if (event.type.startsWith('mouse')) {
clientX = /** @type {MouseEvent} */ (event).clientX
clientY = /** @type {MouseEvent} */ (event).clientY
} else if (event.type.startsWith('touch')) {
clientX = /** @type {TouchEvent} */ (event).touches[0].clientX
clientY = /** @type {TouchEvent} */ (event).touches[0].clientY
}
return { clientX, clientY }
}

View File

@@ -0,0 +1,265 @@
import defaultParams from './params.js'
import { capitalizeFirstLetter, warn } from './utils.js'
const swalStringParams = ['swal-title', 'swal-html', 'swal-footer']
/**
* @param {SweetAlertOptions} params
* @returns {SweetAlertOptions}
*/
export const getTemplateParams = (params) => {
const template =
typeof params.template === 'string'
? /** @type {HTMLTemplateElement} */ (document.querySelector(params.template))
: params.template
if (!template) {
return {}
}
/** @type {DocumentFragment} */
const templateContent = template.content
showWarningsForElements(templateContent)
const result = Object.assign(
getSwalParams(templateContent),
getSwalFunctionParams(templateContent),
getSwalButtons(templateContent),
getSwalImage(templateContent),
getSwalIcon(templateContent),
getSwalInput(templateContent),
getSwalStringParams(templateContent, swalStringParams)
)
return result
}
/**
* @param {DocumentFragment} templateContent
* @returns {Record<string, string | boolean | number>}
*/
const getSwalParams = (templateContent) => {
/** @type {Record<string, string | boolean | number>} */
const result = {}
/** @type {HTMLElement[]} */
const swalParams = Array.from(templateContent.querySelectorAll('swal-param'))
swalParams.forEach((param) => {
showWarningsForAttributes(param, ['name', 'value'])
const paramName = /** @type {keyof SweetAlertOptions} */ (param.getAttribute('name'))
const value = param.getAttribute('value')
if (!paramName || !value) {
return
}
if (
paramName in defaultParams &&
typeof defaultParams[/** @type {keyof typeof defaultParams} */ (paramName)] === 'boolean'
) {
result[paramName] = value !== 'false'
} else if (
paramName in defaultParams &&
typeof defaultParams[/** @type {keyof typeof defaultParams} */ (paramName)] === 'object'
) {
result[paramName] = JSON.parse(value)
} else {
result[paramName] = value
}
})
return result
}
/**
* @param {DocumentFragment} templateContent
* @returns {Record<string, () => void>}
*/
const getSwalFunctionParams = (templateContent) => {
/** @type {Record<string, () => void>} */
const result = {}
/** @type {HTMLElement[]} */
const swalFunctions = Array.from(templateContent.querySelectorAll('swal-function-param'))
swalFunctions.forEach((param) => {
const paramName = /** @type {keyof SweetAlertOptions} */ param.getAttribute('name')
const value = param.getAttribute('value')
if (!paramName || !value) {
return
}
result[paramName] = new Function(`return ${value}`)()
})
return result
}
/**
* @param {DocumentFragment} templateContent
* @returns {Record<string, string | boolean>}
*/
const getSwalButtons = (templateContent) => {
/** @type {Record<string, string | boolean>} */
const result = {}
/** @type {HTMLElement[]} */
const swalButtons = Array.from(templateContent.querySelectorAll('swal-button'))
swalButtons.forEach((button) => {
showWarningsForAttributes(button, ['type', 'color', 'aria-label'])
const type = button.getAttribute('type')
if (!type || !['confirm', 'cancel', 'deny'].includes(type)) {
return
}
result[`${type}ButtonText`] = button.innerHTML
result[`show${capitalizeFirstLetter(type)}Button`] = true
if (button.hasAttribute('color')) {
const color = button.getAttribute('color')
if (color !== null) {
result[`${type}ButtonColor`] = color
}
}
if (button.hasAttribute('aria-label')) {
const ariaLabel = button.getAttribute('aria-label')
if (ariaLabel !== null) {
result[`${type}ButtonAriaLabel`] = ariaLabel
}
}
})
return result
}
/**
* @param {DocumentFragment} templateContent
* @returns {Pick<SweetAlertOptions, 'imageUrl' | 'imageWidth' | 'imageHeight' | 'imageAlt'>}
*/
const getSwalImage = (templateContent) => {
const result = {}
/** @type {HTMLElement | null} */
const image = templateContent.querySelector('swal-image')
if (image) {
showWarningsForAttributes(image, ['src', 'width', 'height', 'alt'])
if (image.hasAttribute('src')) {
result.imageUrl = image.getAttribute('src') || undefined
}
if (image.hasAttribute('width')) {
result.imageWidth = image.getAttribute('width') || undefined
}
if (image.hasAttribute('height')) {
result.imageHeight = image.getAttribute('height') || undefined
}
if (image.hasAttribute('alt')) {
result.imageAlt = image.getAttribute('alt') || undefined
}
}
return result
}
/**
* @param {DocumentFragment} templateContent
* @returns {object}
*/
const getSwalIcon = (templateContent) => {
const result = {}
/** @type {HTMLElement | null} */
const icon = templateContent.querySelector('swal-icon')
if (icon) {
showWarningsForAttributes(icon, ['type', 'color'])
if (icon.hasAttribute('type')) {
result.icon = icon.getAttribute('type')
}
if (icon.hasAttribute('color')) {
result.iconColor = icon.getAttribute('color')
}
result.iconHtml = icon.innerHTML
}
return result
}
/**
* @param {DocumentFragment} templateContent
* @returns {object}
*/
const getSwalInput = (templateContent) => {
/** @type {Record<string, any>} */
const result = {}
/** @type {HTMLElement | null} */
const input = templateContent.querySelector('swal-input')
if (input) {
showWarningsForAttributes(input, ['type', 'label', 'placeholder', 'value'])
result.input = input.getAttribute('type') || 'text'
if (input.hasAttribute('label')) {
result.inputLabel = input.getAttribute('label')
}
if (input.hasAttribute('placeholder')) {
result.inputPlaceholder = input.getAttribute('placeholder')
}
if (input.hasAttribute('value')) {
result.inputValue = input.getAttribute('value')
}
}
/** @type {HTMLElement[]} */
const inputOptions = Array.from(templateContent.querySelectorAll('swal-input-option'))
if (inputOptions.length) {
result.inputOptions = {}
inputOptions.forEach((option) => {
showWarningsForAttributes(option, ['value'])
const optionValue = option.getAttribute('value')
if (!optionValue) {
return
}
const optionName = option.innerHTML
result.inputOptions[optionValue] = optionName
})
}
return result
}
/**
* @param {DocumentFragment} templateContent
* @param {string[]} paramNames
* @returns {Record<string, string>}
*/
const getSwalStringParams = (templateContent, paramNames) => {
/** @type {Record<string, string>} */
const result = {}
for (const i in paramNames) {
const paramName = paramNames[i]
/** @type {HTMLElement | null} */
const tag = templateContent.querySelector(paramName)
if (tag) {
showWarningsForAttributes(tag, [])
result[paramName.replace(/^swal-/, '')] = tag.innerHTML.trim()
}
}
return result
}
/**
* @param {DocumentFragment} templateContent
*/
const showWarningsForElements = (templateContent) => {
const allowedElements = swalStringParams.concat([
'swal-param',
'swal-function-param',
'swal-button',
'swal-image',
'swal-icon',
'swal-input',
'swal-input-option',
])
Array.from(templateContent.children).forEach((el) => {
const tagName = el.tagName.toLowerCase()
if (!allowedElements.includes(tagName)) {
warn(`Unrecognized element <${tagName}>`)
}
})
}
/**
* @param {HTMLElement} el
* @param {string[]} allowedAttributes
*/
const showWarningsForAttributes = (el, allowedAttributes) => {
Array.from(el.attributes).forEach((attribute) => {
if (allowedAttributes.indexOf(attribute.name) === -1) {
warn([
`Unrecognized attribute "${attribute.name}" on <${el.tagName.toLowerCase()}>.`,
`${
allowedAttributes.length
? `Allowed attributes are: ${allowedAttributes.join(', ')}`
: 'To set the value, use HTML within the element.'
}`,
])
}
})
}

View File

@@ -0,0 +1,112 @@
import { swalClasses } from '../utils/classes.js'
import * as dom from './dom/index.js'
// @ts-ignore
export const isSafariOrIOS = typeof window !== 'undefined' && Boolean(window.GestureEvent) // true for Safari desktop + all iOS browsers https://stackoverflow.com/a/70585394
/**
* Fix iOS scrolling
* http://stackoverflow.com/q/39626302
*/
export const iOSfix = () => {
if (isSafariOrIOS && !dom.hasClass(document.body, swalClasses.iosfix)) {
const offset = document.body.scrollTop
document.body.style.top = `${offset * -1}px`
dom.addClass(document.body, swalClasses.iosfix)
lockBodyScroll()
}
}
/**
* https://github.com/sweetalert2/sweetalert2/issues/1246
*/
const lockBodyScroll = () => {
const container = dom.getContainer()
if (!container) {
return
}
/** @type {boolean} */
let preventTouchMove
/**
* @param {TouchEvent} event
*/
container.ontouchstart = (event) => {
preventTouchMove = shouldPreventTouchMove(event)
}
/**
* @param {TouchEvent} event
*/
container.ontouchmove = (event) => {
if (preventTouchMove) {
event.preventDefault()
event.stopPropagation()
}
}
}
/**
* @param {TouchEvent} event
* @returns {boolean}
*/
const shouldPreventTouchMove = (event) => {
const target = event.target
const container = dom.getContainer()
const htmlContainer = dom.getHtmlContainer()
if (!container || !htmlContainer) {
return false
}
if (isStylus(event) || isZoom(event)) {
return false
}
if (target === container) {
return true
}
if (
!dom.isScrollable(container) &&
target instanceof HTMLElement &&
!dom.selfOrParentIsScrollable(target, htmlContainer) && // #2823
target.tagName !== 'INPUT' && // #1603
target.tagName !== 'TEXTAREA' && // #2266
!(
dom.isScrollable(htmlContainer) && // #1944
htmlContainer.contains(target)
)
) {
return true
}
return false
}
/**
* https://github.com/sweetalert2/sweetalert2/issues/1786
*
* @param {TouchEvent} event
* @returns {boolean}
*/
const isStylus = (event) => {
return Boolean(
event.touches &&
event.touches.length &&
// @ts-ignore - touchType is not a standard property
event.touches[0].touchType === 'stylus'
)
}
/**
* https://github.com/sweetalert2/sweetalert2/issues/1891
*
* @param {TouchEvent} event
* @returns {boolean}
*/
const isZoom = (event) => {
return event.touches && event.touches.length > 1
}
export const undoIOSfix = () => {
if (dom.hasClass(document.body, swalClasses.iosfix)) {
const offset = parseInt(document.body.style.top, 10)
dom.removeClass(document.body, swalClasses.iosfix)
document.body.style.top = ''
document.body.scrollTop = offset * -1
}
}

View File

@@ -0,0 +1,6 @@
/**
* Detect Node env
*
* @returns {boolean}
*/
export const isNodeEnv = () => typeof window === 'undefined' || typeof document === 'undefined'

View File

@@ -0,0 +1,139 @@
import globalState from '../globalState.js'
import { setAriaHidden } from './aria.js'
import { swalClasses } from './classes.js'
import * as dom from './dom/index.js'
import { iOSfix } from './iosFix.js'
import { replaceScrollbarWithPadding } from './scrollbar.js'
export const SHOW_CLASS_TIMEOUT = 10
/**
* Open popup, add necessary classes and styles, fix scrollbar
*
* @param {SweetAlertOptions} params
*/
export const openPopup = (params) => {
const container = dom.getContainer()
const popup = dom.getPopup()
if (!container || !popup) {
return
}
if (typeof params.willOpen === 'function') {
params.willOpen(popup)
}
globalState.eventEmitter?.emit('willOpen', popup)
const bodyStyles = window.getComputedStyle(document.body)
const initialBodyOverflow = bodyStyles.overflowY
addClasses(container, popup, params)
// scrolling is 'hidden' until animation is done, after that 'auto'
setTimeout(() => {
setScrollingVisibility(container, popup)
}, SHOW_CLASS_TIMEOUT)
if (dom.isModal()) {
// Using ternary instead of ?? operator for Webpack 4 compatibility
fixScrollContainer(
container,
params.scrollbarPadding !== undefined ? params.scrollbarPadding : false,
initialBodyOverflow
)
setAriaHidden()
}
if (!dom.isToast() && !globalState.previousActiveElement) {
globalState.previousActiveElement = document.activeElement
}
if (typeof params.didOpen === 'function') {
const didOpen = params.didOpen
setTimeout(() => didOpen(popup))
}
globalState.eventEmitter?.emit('didOpen', popup)
}
/**
* @param {Event} event
*/
const swalOpenAnimationFinished = (event) => {
const popup = dom.getPopup()
if (!popup || event.target !== popup) {
return
}
const container = dom.getContainer()
if (!container) {
return
}
popup.removeEventListener('animationend', swalOpenAnimationFinished)
popup.removeEventListener('transitionend', swalOpenAnimationFinished)
container.style.overflowY = 'auto'
// no-transition is added in init() in case one swal is opened right after another
dom.removeClass(container, swalClasses['no-transition'])
}
/**
* @param {HTMLElement} container
* @param {HTMLElement} popup
*/
const setScrollingVisibility = (container, popup) => {
if (dom.hasCssAnimation(popup)) {
container.style.overflowY = 'hidden'
popup.addEventListener('animationend', swalOpenAnimationFinished)
popup.addEventListener('transitionend', swalOpenAnimationFinished)
} else {
container.style.overflowY = 'auto'
}
}
/**
* @param {HTMLElement} container
* @param {boolean} scrollbarPadding
* @param {string} initialBodyOverflow
*/
const fixScrollContainer = (container, scrollbarPadding, initialBodyOverflow) => {
iOSfix()
if (scrollbarPadding && initialBodyOverflow !== 'hidden') {
replaceScrollbarWithPadding(initialBodyOverflow)
}
// sweetalert2/issues/1247
setTimeout(() => {
container.scrollTop = 0
})
}
/**
* @param {HTMLElement} container
* @param {HTMLElement} popup
* @param {SweetAlertOptions} params
*/
const addClasses = (container, popup, params) => {
if (params.showClass?.backdrop) {
dom.addClass(container, params.showClass.backdrop)
}
if (params.animation) {
// this workaround with opacity is needed for https://github.com/sweetalert2/sweetalert2/issues/2059
popup.style.setProperty('opacity', '0', 'important')
dom.show(popup, 'grid')
setTimeout(() => {
// Animate popup right after showing it
if (params.showClass?.popup) {
dom.addClass(popup, params.showClass.popup)
}
// and remove the opacity workaround
popup.style.removeProperty('opacity')
}, SHOW_CLASS_TIMEOUT) // 10ms in order to fix #2062
} else {
dom.show(popup, 'grid')
}
dom.addClass([document.documentElement, document.body], swalClasses.shown)
if (params.heightAuto && params.backdrop && !params.toast) {
dom.addClass([document.documentElement, document.body], swalClasses['height-auto'])
}
}

View File

@@ -0,0 +1,269 @@
import { warn, warnAboutDeprecation } from '../utils/utils.js'
export const defaultParams = {
title: '',
titleText: '',
text: '',
html: '',
footer: '',
icon: undefined,
iconColor: undefined,
iconHtml: undefined,
template: undefined,
toast: false,
draggable: false,
animation: true,
theme: 'light',
showClass: {
popup: 'swal2-show',
backdrop: 'swal2-backdrop-show',
icon: 'swal2-icon-show',
},
hideClass: {
popup: 'swal2-hide',
backdrop: 'swal2-backdrop-hide',
icon: 'swal2-icon-hide',
},
customClass: {},
target: 'body',
color: undefined,
backdrop: true,
heightAuto: true,
allowOutsideClick: true,
allowEscapeKey: true,
allowEnterKey: true,
stopKeydownPropagation: true,
keydownListenerCapture: false,
showConfirmButton: true,
showDenyButton: false,
showCancelButton: false,
preConfirm: undefined,
preDeny: undefined,
confirmButtonText: 'OK',
confirmButtonAriaLabel: '',
confirmButtonColor: undefined,
denyButtonText: 'No',
denyButtonAriaLabel: '',
denyButtonColor: undefined,
cancelButtonText: 'Cancel',
cancelButtonAriaLabel: '',
cancelButtonColor: undefined,
buttonsStyling: true,
reverseButtons: false,
focusConfirm: true,
focusDeny: false,
focusCancel: false,
returnFocus: true,
showCloseButton: false,
closeButtonHtml: '&times;',
closeButtonAriaLabel: 'Close this dialog',
loaderHtml: '',
showLoaderOnConfirm: false,
showLoaderOnDeny: false,
imageUrl: undefined,
imageWidth: undefined,
imageHeight: undefined,
imageAlt: '',
timer: undefined,
timerProgressBar: false,
width: undefined,
padding: undefined,
background: undefined,
input: undefined,
inputPlaceholder: '',
inputLabel: '',
inputValue: '',
inputOptions: {},
inputAutoFocus: true,
inputAutoTrim: true,
inputAttributes: {},
inputValidator: undefined,
returnInputValueOnDeny: false,
validationMessage: undefined,
grow: false,
position: 'center',
progressSteps: [],
currentProgressStep: undefined,
progressStepsDistance: undefined,
willOpen: undefined,
didOpen: undefined,
didRender: undefined,
willClose: undefined,
didClose: undefined,
didDestroy: undefined,
scrollbarPadding: true,
topLayer: false,
}
export const updatableParams = [
'allowEscapeKey',
'allowOutsideClick',
'background',
'buttonsStyling',
'cancelButtonAriaLabel',
'cancelButtonColor',
'cancelButtonText',
'closeButtonAriaLabel',
'closeButtonHtml',
'color',
'confirmButtonAriaLabel',
'confirmButtonColor',
'confirmButtonText',
'currentProgressStep',
'customClass',
'denyButtonAriaLabel',
'denyButtonColor',
'denyButtonText',
'didClose',
'didDestroy',
'draggable',
'footer',
'hideClass',
'html',
'icon',
'iconColor',
'iconHtml',
'imageAlt',
'imageHeight',
'imageUrl',
'imageWidth',
'preConfirm',
'preDeny',
'progressSteps',
'returnFocus',
'reverseButtons',
'showCancelButton',
'showCloseButton',
'showConfirmButton',
'showDenyButton',
'text',
'title',
'titleText',
'theme',
'willClose',
]
/** @type {Record<string, string | undefined>} */
export const deprecatedParams = {
allowEnterKey: undefined,
}
const toastIncompatibleParams = [
'allowOutsideClick',
'allowEnterKey',
'backdrop',
'draggable',
'focusConfirm',
'focusDeny',
'focusCancel',
'returnFocus',
'heightAuto',
'keydownListenerCapture',
]
/**
* Is valid parameter
*
* @param {string} paramName
* @returns {boolean}
*/
export const isValidParameter = (paramName) => {
return Object.prototype.hasOwnProperty.call(defaultParams, paramName)
}
/**
* Is valid parameter for Swal.update() method
*
* @param {string} paramName
* @returns {boolean}
*/
export const isUpdatableParameter = (paramName) => {
return updatableParams.indexOf(paramName) !== -1
}
/**
* Is deprecated parameter
*
* @param {string} paramName
* @returns {string | undefined}
*/
export const isDeprecatedParameter = (paramName) => {
return deprecatedParams[paramName]
}
/**
* @param {string} param
*/
const checkIfParamIsValid = (param) => {
if (!isValidParameter(param)) {
warn(`Unknown parameter "${param}"`)
}
}
/**
* @param {string} param
*/
const checkIfToastParamIsValid = (param) => {
if (toastIncompatibleParams.includes(param)) {
warn(`The parameter "${param}" is incompatible with toasts`)
}
}
/**
* @param {string} param
*/
const checkIfParamIsDeprecated = (param) => {
const isDeprecated = isDeprecatedParameter(param)
if (isDeprecated) {
warnAboutDeprecation(param, isDeprecated)
}
}
/**
* Show relevant warnings for given params
*
* @param {SweetAlertOptions} params
*/
export const showWarningsForParams = (params) => {
if (params.backdrop === false && params.allowOutsideClick) {
warn('"allowOutsideClick" parameter requires `backdrop` parameter to be set to `true`')
}
if (
params.theme &&
![
'light',
'dark',
'auto',
'minimal',
'borderless',
'bootstrap-4',
'bootstrap-4-light',
'bootstrap-4-dark',
'bootstrap-5',
'bootstrap-5-light',
'bootstrap-5-dark',
'material-ui',
'material-ui-light',
'material-ui-dark',
'embed-iframe',
'bulma',
'bulma-light',
'bulma-dark',
].includes(params.theme)
) {
warn(`Invalid theme "${params.theme}"`)
}
for (const param in params) {
checkIfParamIsValid(param)
if (params.toast) {
checkIfToastParamIsValid(param)
}
checkIfParamIsDeprecated(param)
}
}
export default defaultParams

View File

@@ -0,0 +1,48 @@
import { swalClasses } from './classes.js'
/**
* Measure scrollbar width for padding body during modal show/hide
* https://github.com/twbs/bootstrap/blob/master/js/src/modal.js
*
* @returns {number}
*/
export const measureScrollbar = () => {
const scrollDiv = document.createElement('div')
scrollDiv.className = swalClasses['scrollbar-measure']
document.body.appendChild(scrollDiv)
const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
document.body.removeChild(scrollDiv)
return scrollbarWidth
}
/**
* Remember state in cases where opening and handling a modal will fiddle with it.
* @type {number | null}
*/
let previousBodyPadding = null
/**
* @param {string} initialBodyOverflow
*/
export const replaceScrollbarWithPadding = (initialBodyOverflow) => {
// for queues, do not do this more than once
if (previousBodyPadding !== null) {
return
}
// if the body has overflow
if (
document.body.scrollHeight > window.innerHeight ||
initialBodyOverflow === 'scroll' // https://github.com/sweetalert2/sweetalert2/issues/2663
) {
// add padding so the content doesn't shift after removal of scrollbar
previousBodyPadding = parseInt(window.getComputedStyle(document.body).getPropertyValue('padding-right'))
document.body.style.paddingRight = `${previousBodyPadding + measureScrollbar()}px`
}
}
export const undoReplaceScrollbarWithPadding = () => {
if (previousBodyPadding !== null) {
document.body.style.paddingRight = `${previousBodyPadding}px`
previousBodyPadding = null
}
}

View File

@@ -0,0 +1,61 @@
import defaultInputValidators from './defaultInputValidators.js'
import * as dom from './dom/index.js'
import { warn } from './utils.js'
/**
* @param {SweetAlertOptions} params
*/
function setDefaultInputValidators(params) {
// Use default `inputValidator` for supported input types if not provided
if (params.inputValidator) {
return
}
if (params.input === 'email') {
params.inputValidator = defaultInputValidators['email']
}
if (params.input === 'url') {
params.inputValidator = defaultInputValidators['url']
}
}
/**
* @param {SweetAlertOptions} params
*/
function validateCustomTargetElement(params) {
// Determine if the custom target element is valid
if (
!params.target ||
(typeof params.target === 'string' && !document.querySelector(params.target)) ||
(typeof params.target !== 'string' && !params.target.appendChild)
) {
warn('Target parameter is not valid, defaulting to "body"')
params.target = 'body'
}
}
/**
* Set type, text and actions on popup
*
* @param {SweetAlertOptions} params
*/
export default function setParameters(params) {
setDefaultInputValidators(params)
// showLoaderOnConfirm && preConfirm
if (params.showLoaderOnConfirm && !params.preConfirm) {
warn(
'showLoaderOnConfirm is set to true, but preConfirm is not defined.\n' +
'showLoaderOnConfirm should be used together with preConfirm, see usage example:\n' +
'https://sweetalert2.github.io/#ajax-request'
)
}
validateCustomTargetElement(params)
// Replace newlines with <br> in title
if (typeof params.title === 'string') {
params.title = params.title.split('\n').join('<br />')
}
dom.init(params)
}

View File

@@ -0,0 +1,88 @@
export const consolePrefix = 'SweetAlert2:'
/**
* Capitalize the first letter of a string
*
* @param {string} str
* @returns {string}
*/
export const capitalizeFirstLetter = (str) => str.charAt(0).toUpperCase() + str.slice(1)
/**
* Standardize console warnings
*
* @param {string | string[]} message
*/
export const warn = (message) => {
console.warn(`${consolePrefix} ${typeof message === 'object' ? message.join(' ') : message}`)
}
/**
* Standardize console errors
*
* @param {string} message
*/
export const error = (message) => {
console.error(`${consolePrefix} ${message}`)
}
/**
* Private global state for `warnOnce`
*
* @type {string[]}
* @private
*/
const previousWarnOnceMessages = []
/**
* Show a console warning, but only if it hasn't already been shown
*
* @param {string} message
*/
export const warnOnce = (message) => {
if (!previousWarnOnceMessages.includes(message)) {
previousWarnOnceMessages.push(message)
warn(message)
}
}
/**
* Show a one-time console warning about deprecated params/methods
*
* @param {string} deprecatedParam
* @param {string?} useInstead
*/
export const warnAboutDeprecation = (deprecatedParam, useInstead = null) => {
warnOnce(
`"${deprecatedParam}" is deprecated and will be removed in the next major release.${
useInstead ? ` Use "${useInstead}" instead.` : ''
}`
)
}
/**
* If `arg` is a function, call it (with no arguments or context) and return the result.
* Otherwise, just pass the value through
*
* @param {(() => *) | *} arg
* @returns {*}
*/
export const callIfFunction = (arg) => (typeof arg === 'function' ? arg() : arg)
/**
* @param {*} arg
* @returns {boolean}
*/
export const hasToPromiseFn = (arg) => arg && typeof arg.toPromise === 'function'
/**
* @param {*} arg
* @returns {Promise<*>}
*/
export const asPromise = (arg) => (hasToPromiseFn(arg) ? arg.toPromise() : Promise.resolve(arg))
/**
* @param {*} arg
* @returns {boolean}
*/
export const isPromise = (arg) => arg && Promise.resolve(arg) === arg