Home Manual Reference Source

source/dom/ScriptLoaderSingleton.js

let _instance = null;


/**
 * A script that is loaded or being loaded by {@link ScriptLoaderSingleton}.
 */
export class Script {
  constructor(src) {
    this.state = 'new';
    this.src = src;
    this.scriptTag = null;
    this._loadedCallbacks = [];
    this._onLoad = this._onLoad.bind(this);
  }

  _onLoad() {
    this.state = 'loaded';
    for(let callbackObject of this._loadedCallbacks) {
      callbackObject.resolve(this);
    }
    this._loadedCallbacks = [];
  }

  _loadScript() {
    this.state = 'loading';
    this.scriptTag = document.createElement('script');
    this.scriptTag.src = this.src;
    if (this.scriptTag.readyState) { //IE
      this.scriptTag.onreadystatechange = () => {
        if (this.scriptTag.readyState == "loaded" || this.scriptTag.readyState == "complete") {
          this.scriptTag.onreadystatechange = null;
          this._onLoad();
        }
      };
    } else { //Others
      this.scriptTag.onload = this._onLoad;
    }

    this.scriptTag.onload = this._onLoad;
    this.scriptTag.onreadystatechange = this._onLoad;
    document.body.appendChild(this.scriptTag);
  }

  _load() {
    return new Promise((resolve, reject) => {
      if(this.state == 'loaded') {
        resolve(this);
      } else {
        this._loadedCallbacks.push({
          resolve: resolve,
          reject: reject
        });
        if(this.state == 'new') {
          this._loadScript();
        }
      }
    });
  }
}


/**
 * Asyncronous script loader.
 *
 * @example
 * const scriptLoader = new ScriptLoaderSingleton();
 * scriptLoader.load('//example.com/myscript.js').then((script) => {
 *   console.log(`The ${script.src} script was loaded`);
 * });
 */
export default class ScriptLoaderSingleton {
  /**
   * Get an instance of the singleton.
   *
   * The first time this is called, we create a new instance.
   * For all subsequent calls, we return the instance that was
   * created on the first call.
   */
  constructor() {
    if(!_instance) {
      _instance = this;
    }
    this._scriptsMap = new Map(); // Maps src to Script objects
    return _instance;
  }

  /**
   * Load a script.
   *
   * @param {string} src The source URL of the script.
   * @returns {Promise} A Promise that resolves when the script is loaded.
   *    We do not handle errors (not possible with the insert script tag approach),
   *    so this promise will never be rejected.
   */
  load(src) {
    let script;
    if(this._scriptsMap.has(src)) {
      script = this._scriptsMap.get(src);
    } else {
      script = new Script(src);
      this._scriptsMap.set(src, script);
    }
    return script._load();
  }
}