const loadingElementId = 'global-loading'; const loadingVisibleClassName = 'loading-visible'; const loadingHtml: Record = { // TODO: Remove dots and boxes because no one is using those dots: '', boxes: '', circle: '
', }; class Loader { private loadingElement: HTMLElement | undefined; constructor() { if (this.isInitialized()) { return; } for (const className in loadingHtml) { if (this.hasClass(className)) { this.setHtml(loadingHtml[className]); break; } } this.setInitialized(); } show() { if (this.incrementCount() === 1) { this.addClass(loadingVisibleClassName); if (!this.hasClass('btn')) { this.fadeIn(); } } } hide() { if (this.getCount() < 1) { return; } if (this.decrementCount() === 0) { this.removeClass(loadingVisibleClassName); if (!this.hasClass('btn')) { this.fadeOut(); } } } private get element() { return this.findOrCreateLoadingElement(); } private addClass(name: string) { this.element.classList.add(name); } private createElement(attributes: Record) { const element = document.createElement('div'); for (const [qualifiedName, value] of Object.entries(attributes)) { element.setAttribute(qualifiedName, value); } document.body.appendChild(element); return element; } private decrementCount() { const count = this.getCount() - 1; this.element.dataset.count = String(count); return count; } private fadeIn() { Object.assign(this.element.style, { display: 'block', opacity: '0' }); setTimeout(() => { Object.assign(this.element.style, { opacity: '1', transition: 'opacity 0.5s' }); }, 0); } private fadeOut() { setTimeout(() => this.handleEndFadeOut(), 500); Object.assign(this.element.style, { opacity: '0', transition: 'opacity 0.5s' }); } private findOrCreateLoadingElement() { this.loadingElement ??= document.getElementById(loadingElementId) ?? this.createElement({ id: loadingElementId, class: 'global-loading circle' }); return this.loadingElement; } private getCount() { return Number(this.element.dataset.count ?? 0); } private handleEndFadeOut() { if (this.getCount() === 0) { Object.assign(this.element.style, { display: null, opacity: null, transition: null }); } } private hasClass(name: string) { return this.element.classList.contains(name); } private incrementCount() { const count = this.getCount() + 1; this.element.dataset.count = String(count); return count; } private isInitialized() { return !!this.element.dataset.initialized; } private removeClass(name: string) { this.element.classList.remove(name); } private setHtml(html: string) { this.element.innerHTML = html; } private setInitialized() { this.element.dataset.initialized = 'true'; } } export const loader = new Loader();