/* global SystemJS, UB */
const _ = require('lodash')
const SHOW_TIMEOUT = 300
const HIDE_TIMEOUT = 300
class LoadingOverlayManager {
constructor () {
this._loaderServiceInstance = undefined
this._showTimeout = undefined
this._hideTimeout = undefined
/**
* @type {Object<string, Array>}
* @private
*/
this._loadersByText = {}
}
/**
* Add a loader from loaders list
*
* @param {string} text
*/
show (text) {
const exists = this._loadersByText[text]
if (!exists) {
this._loadersByText[text] = []
}
this._loadersByText[text].push({
text,
started: Date.now()
})
this._calcText()
}
/**
* Remove a loader from loaders list
*
* @param {string} text
*/
hide (text) {
const exists = this._loadersByText[text]
if (!exists) {
console.warn('There is no loader with text %s', text)
return
}
exists.shift()
if (exists.length === 0) {
delete this._loadersByText[text]
}
this._calcText()
}
/**
* Recalculate text to show as a loading text after add or remove a loader
*
* @private
*/
_calcText () {
const text = _.maxBy(
Object.entries(this._loadersByText),
([, loaders]) => _.maxBy(loaders, 'started')
)?.[0]
if (this._text !== text) {
this._text = text
const needShow = !!text
if (needShow) {
if (this._loaderServiceInstance) {
this._loaderServiceInstance.text = this._buildText(text)
if (this._hideTimeout) {
this._removeHideTimeout()
}
} else if (!this._showTimeout) {
this._setShowTimeout()
}
} else {
if (!this._loaderServiceInstance && this._showTimeout) {
this._removeShowTimeout()
}
if (this._loaderServiceInstance && !this._hideTimeout) {
this._setHideTimeout()
}
}
}
}
_setShowTimeout () {
this._showTimeout = setTimeout(
() => {
this._showTimeout = undefined
this._showLoader()
},
SHOW_TIMEOUT
)
}
_removeShowTimeout () {
clearTimeout(this._showTimeout)
this._showTimeout = undefined
}
_setHideTimeout () {
this._hideTimeout = setTimeout(
() => {
this._hideTimeout = undefined
this._hideLoader()
},
HIDE_TIMEOUT
)
}
_removeHideTimeout () {
clearTimeout(this._hideTimeout)
this._hideTimeout = undefined
}
_showLoader () {
if (this._loaderServiceInstance) {
this._loaderServiceInstance.close()
}
const ElementUI = SystemJS.get('element-ui')
this._loaderServiceInstance = ElementUI.Loading.service({ text: this._buildText(this._text) })
// hide all nested spinners when the fullscreen one is displayed
document.body.classList.add('ub-with-loading-overlay')
}
_hideLoader () {
if (this._loaderServiceInstance) {
this._loaderServiceInstance.close()
this._loaderServiceInstance = undefined
document.body.classList.remove('ub-with-loading-overlay')
this._text = undefined
}
}
_buildText (textResourceId) {
return UB.i18n(textResourceId)
}
}
/**
* Subscribe to 'portal:loader' event and manage the loader overlay using ElementUI
*/
function subscribe () {
const manager = new LoadingOverlayManager()
/**
* `portal:loader` event, which shall be set or remove global loader (overlay) over UI, so that user won't trigger
* action in the process loading, for example, big components over a poor network connection.
*
* @example
* try {
* $App.fireEvent('portal:loader', {
* text: 'portal.loader.form',
* show: true
* })
*
* // Do action which takes long. Await for it, if just return unresolved promise, the "finally" block execute
* // before action finish
* } finally {
* // ALWAYS use try-finally to avoid loading overlay hanging in case of errors
* $App.fireEvent('portal:loader', {
* text: 'portal.loader.form',
* show: false
* })
* }
*
* @event 'portal:loader'
* @memberOf module:@unitybase/adminui-vue
*/
window.$App.on('portal:loader', ({ show, text = 'portal.loader.default' }) => {
if (show) {
manager.show(text)
} else {
manager.hide(text)
}
})
}
module.exports = {
subscribe
}