Module.js

/**
 * Modules parent class.
 * @author Moez Bouhlel <bmoez.j@gmail.com>
 * @license MPL-2.0
 * @copyright 2014-2017 Moez Bouhlel
 * @module
 */

import Options from './Options.js';
import {
  logInject,
} from './statics.js';

/**
 * Base class for all modules used to support websites.
 *
 * A module is a content-script executed on a website to replace its custom
 * video player with the browser native HTML5 video player. This class abstract
 * module/backgroun-script messages/options handling. See
 * {@tutorial h5vew-adding-new-website-support} for detailed steps to add new
 * website support.
 */
class Module {
  /**
   * Define module unique name.
   * @public
   *
   * @param {!string} name - Module unique name.
   */
  constructor(name /* , redirect = [], block = []*/ ) {
    /**
     * The unique name of module.
     * @member {string}
     */
    this.name = name;
    /*
    this.redirect = redirect;
    this.block = block;
    */

    /**
     * The webExtension port to communicate with the background script.
     * @member {chrome.runtime.Port}
     */
    this.port = chrome.runtime.connect({
      "name": "h5vew",
    });
    this.port.postMessage({
      "type": "inject",
      "module": this.name,
    });
    /**
     * Mapping of event and its handlers.
     * @member {Object.<string, Array.<Function>>}
     * @see {@link addMessageListener} for how to add event listener.
     */
    this.messageListeners = {};
    this.port.onMessage.addListener((msg) => this.onMessage(msg));
  }

  /**
   * The module routine execution function. It ensures module options has been
   * recived from the background script and execute module functions
   * (onLoading, onInteractive, onComplete).
   * Ensure to call this function at the end of module file the following way:
   *
   * @example <caption>A example of starting a website module.</caption>
   * new ChildModuleX().start();
   *
   * @public
   */
  start() {
    this.log("start()");
    if (this.options && !this.options.isDisabled()) {
      new Promise((resolve, reject) => resolve(this.onLoading()))
        .then(() => new Promise((resolve, reject) => {
          if (document.readyState != "loading") {
            resolve(this.onInteractive());
          } else {
            document.addEventListener("DOMContentLoaded", () => {
              resolve(this.onInteractive());
            });
          }
        }))
        .then(() => {
          if (document.readyState === "complete") {
            this.onComplete();
          } else {
            document.addEventListener("load", () => this.onComplete());
          }
          logInject(this.name);
        })
        .catch((err) => this.log("Error start():", err));
    } else if (!this.options) {
      this.addMessageListener("options", (msg) => this.start());
    } else { // this.options.isDisabled()
      this.log("Module disabled. Exitings.");
      return;
    }
  }

  /**
   * Module code to execute when page loading starts (before DOM tree is ready)
   * @abstract
   */
  onLoading() {
    this.log("onLoading()");
  }

  /**
   * Module code to execute when DOM tree is ready but other resources are
   * still loading
   * @abstract
   */
  onInteractive() {
    this.log("onInteractive()");
  }

  /**
   * Module code to execute after full page is loaded (DOM ready and other
   * resources are loaded)
   * @abstract
   */
  onComplete() {
    this.log("onComplete()");
  }

  /**
   * Event handler for new messages send from the background script. It mainly
   * execute registered event handlers for the message type.
   * @private
   *
   * @param {!Object} msg - Message send from the background script containing
   *                        at least the 'type' field.
   *
   * @private
   */
  onMessage(msg) {
    this.log("Message", msg);
    switch (msg.type) {
      case "options":
        /**
         * Options instance of this module to get/set extension general options
         * and module special options.
         * @member {Options}
         */
        this.options = new Options(msg.options, this.name);
        break;
      default:
        break;
    }
    for (const fn of (this.messageListeners[msg.type] || [])) {
      fn(msg);
    }
  }

  /**
   * Register a callback function to execute when a message with provided type
   * has been recived from the background script.
   * @public
   *
   * @param {!string} type - The type of message to handle.
   * @param {!function} fn - The callback function to execute when a message
   *                         with provided type is recived.
   */
  addMessageListener(type, fn) {
    this.log("Add msg listener:", type);
    if (!this.messageListeners[type]) {
      this.messageListeners[type] = [];
    }
    this.messageListeners[type].push(fn);
  }
  onOptionChange(opt, val) {}

  getOption(opt) {}

  async setOption(opt, val) {}

  /**
   * Log a message to the console if debug mode is enabled.
   * @public
   *
   * @param {...string} args - Strings to print on the debug message.
   */
  log(...args) {
    console.log("[h5vew:" + this.name + "]", ...args);
  }
}

export default Module;