/**
* --------------------------------------------------------------------------
* Ace (v4.0.0): toaster.js
Wrapper for Bootstrap's toast elements
*/
import Util from './util'
import EventHandler from './event-handler'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'aceToaster'
const VERSION = '4.0.0'
const DATA_KEY = 'ace.toaster'
const EVENT_KEY = `.${DATA_KEY}`
const Event = {
CLEAR: `clear${EVENT_KEY}`,
ADD: `add${EVENT_KEY}`,
ADDED: `added${EVENT_KEY}`
}
const DefaultType = {
placement: 'string',
close: 'boolean',
autoremove: 'boolean',
delay: 'number',
template: 'string',
alert: 'boolean'
}
const Default = {
placement: 'tr',
close: true,
autoremove: true,
delay: 2500,
template: '
',
alert: true
}
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/
class Toaster {
constructor () {
this._lastToastId = 0
this.element = null
this._jQueryBS = typeof window.jQuery !== 'undefined' && typeof window.bootstrap !== 'undefined'
this._tempParent = document.createElement('DIV')
}
static get VERSION () {
return VERSION
}
static get DefaultType () {
return DefaultType
}
static get Default () {
return Default
}
// Public methods
add (config) {
const _config = this._getConfig(config)
const newToast = Util.append(this._tempParent, _config.template)
this._lastToastId++
newToast.classList.add('ace-toaster-item')
newToast.id = `ace-toaster-item-${this._lastToastId}`
newToast.setAttribute('aria-atomic', 'true')
if (_config.alert) {
newToast.setAttribute('role', 'alert')
newToast.setAttribute('aria-live', 'assertive')
} else {
newToast.setAttribute('role', 'status')
newToast.setAttribute('aria-live', 'polite')
}
const toastHeader = newToast.querySelector('.toast-header')
if (_config.title && toastHeader) {
const title = typeof _config.title === 'function' ? _config.title.call(this.element, _config) : _config.title
Util.append(toastHeader, `${title}
`)
}
if (_config.close) {
let close = newToast.querySelector('[data-dismiss="toast"]')
if (close === null) {
close = Util.append(toastHeader, '')
}
close.className += ` ${_config.closeClass || 'close'}`
}
if (_config.body) {
const body = newToast.querySelector('.toast-body')
if (body !== null) {
Util.append(body, typeof _config.body === 'function' ? _config.body.call(this.element, _config) : _config.body)
if (_config.bodyClass) body.className += ` ${_config.bodyClass}`
}
}
if (_config.image) {
const image = newToast.querySelector('.toast-image')
if (image !== null) {
Util.append(image, `
`)
}
}
if (_config.icon) {
const image = newToast.querySelector('.toast-image')
if (image !== null) {
const icon = Util.append(image, _config.icon)
if (!_config.image && _config.imageClass) {
icon.className += ` ${_config.imageClass}`
}
}
}
if (!(_config.image || _config.icon)) newToast.querySelectorAll('.toast-image').forEach((el) => Util.remove(el))
if (_config.className) {
newToast.className += ` ${_config.className}`
}
if (_config.headerClass && toastHeader) {
toastHeader.className += ` ${_config.headerClass}`
}
// if delay is below 30, we consider it as seconds, not milliseconds
_config.delay = _config.delay > 30 ? _config.delay : _config.delay * 1000
if (_config.progress && !_config.sticky && _config.autohide !== false) {
const progress = Util.append(newToast, ``)
progress.style.transitionDuration = `${parseInt(_config.delay * 1.015)}ms`
progress.style.width = _config.progressReverse ? 'calc(100% - 1px)' : 0
// progress.offsetWidth
setTimeout(() => {
progress.style.width = _config.progressReverse ? 0 : 'calc(100% - 2px)'
}, 0)
}
return this._addToContainer(newToast, _config)
}
// add an existing toast element to our container
addEl (element, config) {
const _config = this._getConfig(config)
this.element = element
this.element.classList.add('ace-toaster-item')
if (!this.element.getAttribute('id')) this.element.setAttribute('id', `ace-toaster-item-${++this._lastToastId}`)
this._addToContainer(this.element, _config, false)
}
// add toast element to container
_addToContainer (toast, _config, isNewElement = true) {
// trigger ADD event before adding it to our container
const addEvent = EventHandler.trigger(document, Event.ADD, { target: toast })
if (addEvent.defaultPrevented) {
if (isNewElement) Util.remove(toast)
return null
}
// end of trigger
// add the toaster container to body
let container = document.querySelector(`.ace-toaster-container.position-${_config.placement}`)
if (container === null) {
container = Util.append(document.body, ``)
}
if (_config.belowNav) {
container.classList.add('toaster-below-nav')
}
// this last container should appear above other ones ?
// let topContainer = document.querySelector('.ace-toaster-container-on-top')
// if (topContainer !== null) topContainer.classList.remove('ace-toaster-container-on-top')
// container.classList.add('ace-toaster-container-on-top')
// add to container
Util.append(container, toast)
Util.wrap(toast, '')
// without having an initial .toast element, fade-in animation isn't taking place??!!
let dummy = document.getElementById('ace-toaster-dummy-toast-1')
if (dummy === null) dummy = Util.append(document.body, '')
if (this._jQueryBS) {
window.jQuery(dummy).toast('show')
}
/// ///////////////////////////////////////////////
const _toastOptions = {}
if (_config.sticky === true || _config.autohide === false) _toastOptions.autohide = false
if (_config.animation === false) _toastOptions.animation = false
_toastOptions.delay = _config.delay
if (_config.width) toast.style.width = isNaN(_config.width) ? _config.width : _config.width + 'px'
if (this._jQueryBS) {
window.jQuery(toast)
.toast(_toastOptions)
.toast('show')
.one('hidden.bs.toast.1', function () {
// show it again (invisibly with opacity = 0) and use bootstrap Collapse plugin to hide it, so that other toasts stacked below it move up smoothly
const $toast = window.jQuery(toast)
$toast.removeClass('hide').parent().addClass('show').collapse('hide').one('hidden.bs.collapse', function () {
$toast.toast('dispose')
$toast.parent().collapse('dispose')
if (_config.autoremove) {
$toast.parent().remove()
} else {
if (!isNewElement) {
$toast.unwrap() // remove the wrapper
}
}
})
})
}
// trigger ADDED event before adding it to DOM
EventHandler.trigger(document, Event.ADDED, { target: toast })
return toast
}
// hide toasts
remove (id, instant = false) {
this.hide(id, true, instant)
}
removeAll (placement = null, instant = false) {
this.hideAll(placement, true, instant)
}
// remove toast by ID or element reference
hide (id, remove = false, instant = false) {
const selector = isNaN(id) ? id : '#ace-toaster-item-' + parseInt(id)
this._hideBySelector(selector, remove, instant)
}
// remove ALL toasts
hideAll (placement = null, remove = false, instant = false) {
// trigger CLEAR event before removing ALL
const clearEvent = EventHandler.trigger(document, Event.CLEAR, { detail: { placement: placement || 'all', remove: remove } })
if (clearEvent.defaultPrevented) {
return
}
// end of trigger
let selector = '.toast.ace-toaster-item'
if (placement) selector = `.ace-toaster-container.position-${placement} ${selector}`
this._hideBySelector(selector, remove, instant)
}
_hideBySelector (selector, remove = false, instant = false) {
if (!this._jQueryBS) return
window.jQuery(selector).each(function () {
const $toast = window.jQuery(this)
if (!instant && $toast.is(':visible')) {
// fade out and then remove
$toast.toast('hide')
.off('hidden.bs.toast.1')// remove the previous handler above (because it has autoremove)
.one('hidden.bs.toast.2', function () {
$toast.toast('dispose')
if (remove) $toast.remove()
})
} else {
$toast.toast('dispose')
// instantly remove if not visible
if (remove) $toast.remove()
}
})
}
// Private methods
_getConfig (config) {
config = {
...Default,
...typeof config === 'object' && config ? config : {}
}
if (typeof window.bootstrap !== 'undefined') {
window.bootstrap.Util.typeCheckConfig(
NAME,
config,
this.constructor.DefaultType
)
}
return config
}
// Static methods
static _jQueryInterface (config) {
return this.each(function () {
config = {
...{ autoremove: false }, // don't autoremove it
...window.jQuery(this).data(),
...typeof config === 'object' && config ? config : {}
}
window.jQuery[NAME].addEl(this, config)
// jQuery[NAME] is an instance of Toaster
})
}
}
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
*/
if (typeof window.jQuery !== 'undefined') {
const $ = window.jQuery
$[NAME] = new Toaster()
const JQUERY_NO_CONFLICT = $.fn[NAME]
$.fn[NAME] = Toaster._jQueryInterface
$.fn[NAME].Constructor = Toaster
$.fn[NAME].noConflict = () => {
$.fn[NAME] = JQUERY_NO_CONFLICT
return Toaster._jQueryInterface
}
}
export default Toaster