UNPKG

promise-loading-spinner

Version:

Advanced handling of loaders/spinners based on one or multiple Promises

209 lines (208 loc) 8.07 kB
/** * Default config. */ const defaults = { loaderVisibilityCallback: undefined, delay: 300, closeDelay: 10, initDelay: 1000, loaderElement: undefined, classActive: 'is-active', }; /** * Advanced handling of loaders/spinners based on one or multiple Promises. */ export default class Loader { /** * Constructor. * * @param cfg configuration of the loader (optional) */ constructor(cfg = {}) { /** * Contains all promises that aren't settled. */ this.loaderPromises = []; /** * Is the loader currently shown? */ this.loaderShows = false; /** * The promises for which the loader currently shows up. */ this.promisesForShownLoader = []; /** * A promise that resolves after the loader get hidden. */ this.currentLoadingPromise = Promise.resolve([]); this.setCurrentLoadingPromise(); const config = { ...defaults, ...cfg }; this.config = config; const { config: { loaderElement, loaderVisibilityCallback, initDelay } } = this; if (loaderElement !== null && loaderElement !== void 0 ? loaderElement : !loaderVisibilityCallback) { const element = loaderElement !== null && loaderElement !== void 0 ? loaderElement : '#js-page-loader'; this.el = element instanceof HTMLElement ? element : document.querySelector(element); if (!this.el) throw new Error('Element not found'); } if (!this.el && !loaderVisibilityCallback) throw new Error('No loader element or loaderVisibilityCallback provided'); if (loaderVisibilityCallback) loaderVisibilityCallback(this.loaderShows); // suppress loader in a short timeframe after initializing (page load) this.initSuppressTimeout = setTimeout(() => this.stopSuppressLoading(), initDelay); } /** * Add a promise to the loader. * * @param promise a promise * @param options options for this promise (optional) * @returns the used promise */ loader(promise, options) { var _a; const skipDelays = (_a = options === null || options === void 0 ? void 0 : options.skipDelays) !== null && _a !== void 0 ? _a : false; if ((!this.initSuppressTimeout || skipDelays)) { if (this.initSuppressTimeout && skipDelays) this.stopSuppressLoading(); const isFirstLoader = this.loaderPromises.length === 0; this.loaderPromises.push(promise); if (this.loaderShows) this.promisesForShownLoader.push(promise); if (isFirstLoader) { // Only the first loader needs to initialize the show functionality this.showLoader(skipDelays); } void this.handlePromise(promise); } return promise; } /** * Returns a function that wraps the loader functionality around a function call. * * @param fnc A function performing some async operation * @param options options for the operation (optional) * @returns a function that wraps the loader functionality around a function call */ wrapFunction(fnc, options) { // eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment const loaderContext = this; return function (...arguments_) { return loaderContext.loader(fnc.apply(this, arguments_), options); }; } /** * A decorator for methods that wraps loader functionality around a function call. * @param options options for the operation (optional) * @returns a decorator for methods that wraps loader functionality around a function call. */ decorator(options) { /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-this-alias */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable unicorn/no-this-assignment */ const loaderContext = this; return function (target, propertyKey, descriptor) { const oldValue = descriptor.value; descriptor.value = function (...parameters) { return loaderContext.loader(oldValue.apply(this, parameters), options); }; }; /* eslint-enable @typescript-eslint/no-explicit-any */ /* eslint-enable @typescript-eslint/no-this-alias */ /* eslint-enable @typescript-eslint/no-unsafe-argument */ /* eslint-enable @typescript-eslint/no-unsafe-call */ /* eslint-enable @typescript-eslint/no-unsafe-member-access */ /* eslint-enable unicorn/no-this-assignment */ } /** * Stops initial loader suppresion */ stopSuppressLoading() { clearTimeout(this.initSuppressTimeout); this.initSuppressTimeout = undefined; } /** * Create the promise for the currently shown loader. */ setCurrentLoadingPromise() { this.currentLoadingPromise = new Promise((resolve) => { this.loaderShownResolver = resolve; }); } /** * Show or hide the loader. * * @param visible is the loader visible? */ setLoaderVisibility(visible) { if (this.el) this.el.classList[visible ? 'add' : 'remove'](this.config.classActive); if (this.config.loaderVisibilityCallback) this.config.loaderVisibilityCallback(visible); this.loaderShows = visible; if (visible) { this.promisesForShownLoader.push(...this.loaderPromises); } else { /* istanbul ignore else */ if (this.loaderShownResolver) { this.loaderShownResolver(this.promisesForShownLoader.splice(0, this.promisesForShownLoader.length)); } this.setCurrentLoadingPromise(); } } /** * Wait for promise to fulfill or reject and check if the loader can hide. * * @param promise the promise to process */ async handlePromise(promise) { try { await promise; } catch { // nothing to do } const { timeout, loaderPromises, config } = this; if (timeout && loaderPromises.length === 1) { // We close the last operation before the loader was shown. There is no need anymore to show it. clearTimeout(timeout); this.timeout = undefined; } void loaderPromises.splice(loaderPromises.indexOf(promise), 1); if (loaderPromises.length === 0) { // The last operation has finished. Show loader a bit longer so there is no flickering when an operation // starts shortly after. this.closingTimeout = setTimeout(() => { this.setLoaderVisibility(false); this.closingTimeout = undefined; }, config.closeDelay); } } /** * Show the loader. Also adds the loader delay. * * @param skipDelays skip delays? */ showLoader(skipDelays) { if (this.closingTimeout) { // Another operation finished shortly before. To avoid flickering the loader closes later. // But here we don't need to close it because another operation starts. clearTimeout(this.closingTimeout); this.closingTimeout = undefined; } else if (skipDelays) { this.setLoaderVisibility(true); } else { // Show loader after a delay. For operation that are finished fast enough no loader is shown. this.timeout = setTimeout(() => { this.setLoaderVisibility(true); this.timeout = undefined; }, this.config.delay); } } }