Source: chariot.js

/* global
history, location
*/

/* Please refer to example apge to see how a typical configuration is structured */

import Tutorial from './tutorial';
import QueryParse from 'query-parse';

require('./ie-shim');

let initialState = true;

class Chariot {
  /**
   * The master Chariot configuration dictionary can consist of multiple
   *  tutorial configurations.
   * @typedef ChariotConfiguration
   * @property {Object.<string, TutorialConfig>} config - The main configuration
   *  containing all tutorials.
   *
   */

  /**
   * The delegate optionally responds to lifecycle callbacks from Chariot.
   * @typedef ChariotDelegate
   * @property {Object} delegate - The object that responds to the
   *  following lifecycle callbacks.
   *
   * <ol>
   *   <li>willBeginTutorial</li>
   *   <li>The following are repeated for each step.</li>
   *   <ol>
   *     <li>willBeginStep</li>
   *     <li>willRenderOverlay</li>
   *     <li>didShowOverlay</li>
   *     <li>willRenderTooltip</li>
   *     <li>didRenderTooltip</li>
   *     <li>didFinishStep</li>
   *   </ol>
   *   <li>didFinishTutorial</li>
   * </ol>
   */

  /**
   * Called once before a tutorial begins.
   * @callback willBeginTutorial
   * @param {Tutorial} tutorial - The Tutorial object
   */

  /**
   * Called once after a tutorial is finished.
   * @callback didFinishTutorial tutorial
   * @param {Tutorial} tutorial - The Tutorial object
   * @param {boolean} forced - Indicates whether tutorial was forced to end
   */

  /**
   * Called once before each step begins.
   * Return a promise here if you have async callbacks you want resolved before
   * continuing.
   * @callback willBeginStep
   * @param {Step} step - The current Step object
   * @param {int} stepIndex - Index of current Step
   * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
   * @returns {Promise} [promise] Return a promise if you have async callbacks
   *   that must be resolved before continuing.
   */

  /**
   * Called once after each step is finished.
   * @callback didFinishStep
   * @param {Step} step - The current Step object
   * @param {int} stepIndex - Index of current Step
   * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
   * @returns {Promise} [promise] Return a promise if you have async callbacks
   *   that must be resolved before continuing.
   */

  /**
   * Called once before each overlay is shown.
   * @callback willShowOverlay
   * @param {Overlay} overlay - The current Overlay object
   * @param {int} stepIndex - Index of current Step
   * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
   * @returns {Promise} [promise] Return a promise if you have async callbacks
   *   that must be resolved before continuing.
   */

  /**
   * Called once after each overlay is shown.
   * @callback didShowOverlay
   * @param {Overlay} overlay - The current Overlay object
   * @param {int} stepIndex - Index of current Step
   * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
   * @returns {Promise} [promise] Return a promise if you have async callbacks
   *   that must be resolved before continuing.
   */

  /**
   * Called once before each tooltip is rendered.
   * @callback willRenderTooltip
   * @param {Tooltip} tooltip - The current Tooltip object
   * @param {int} stepIndex - Index of current Step
   * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
   * @returns {Promise} [promise] Return a promise if you have async callbacks
   *   that must be resolved before continuing.
   */

  /**
   * Called once after each tooltip is rendered.
   * @callback didRenderTooltip
   * @param {Tooltip} tooltip - The current Tooltip object
   * @param {int} stepIndex - Index of current Step
   * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
   * @returns {Promise} [promise] Return a promise if you have async callbacks
   *   that must be resolved before continuing.
   */

  /**
   * <p>Called when handling browser popState events.</p>
   *
   * <p>The four delegate methods (handlePopState, handlePushState,
   * handleReplaceState, handleHashChange) are called during browser
   * navigation events.
   * Routing libraries might use one or more of these methods to implement
   * routing for single page applications.</p>
   * <p>The default handler for these events  will force quit the current
   * tutorial. If you choose to override any of these methods, you will be
   * responsibile for calling `tutorial.end()`.</p>
   *
   * @callback handlePopState
   */

  /**
   * <p>Called when handling browser pushState events.</p>
   *
   * <p>The four delegate methods (handlePopState, handlePushState,
   * handleReplaceState, handleHashChange) are called during browser
   * navigation events.
   * Routing libraries might use one or more of these methods to implement
   * routing for single page applications.</p>
   * <p>The default handler for these events  will force quit the current
   * tutorial. If you choose to override any of these methods, you will be
   * responsibile for calling `tutorial.end()`.</p>
   *
   * @callback handlePushState
   */

  /**
   * <p>Called when handling browser replaceState events.</p>
   *
   * <p>The four delegate methods (handlePopState, handlePushState,
   * handleReplaceState, handleHashChange) are called during browser
   * navigation events.
   * Routing libraries might use one or more of these methods to implement
   * routing for single page applications.</p>
   * <p>The default handler for these events  will force quit the current
   * tutorial. If you choose to override any of these methods, you will be
   * responsibile for calling `tutorial.end()`.</p>
   *
   * @callback handleReplaceState
   */

  /**
   * <p>Called when handling browser hashChange events.</p>
   *
   * <p>The four delegate methods (handlePopState, handlePushState,
   * handleReplaceState, handleHashChange) are called during browser
   * navigation events.
   * Routing libraries might use one or more of these methods to implement
   * routing for single page applications.</p>
   * <p>The default handler for these events  will force quit the current
   * tutorial. If you choose to override any of these methods, you will be
   * responsibile for calling `tutorial.end()`.</p>
   *
   * @callback handleHashChange
   */

  /**
   * @constructor
   * @param {ChariotConfiguration} config - The main configuration for all
   *  tutorials
   * @param {ChariotDelegate} [delegate] - An optional delegate that responds to
   *  lifecycle callbacks
   */
  constructor(config, delegate) {
    this.config = config;
    this.delegate = delegate;
    this.tutorials = {};
    this._readConfig(config);
    this._listenForPushState();
  }

  /**
   * Sets the chariot delegate.
   * @param {ChariotDelegate} [delegate] - An object that responds to
   *  lifecycle callbacks
   */
  setDelegate(delegate) {
    this.delegate = delegate;
  }

  /**
   * Starts a tutorial with the given name.
   * Won't start a tutorial if one is currently running.
   * @param {string} name - Name of the tutorial to start
   * @returns {Tutorial} tutorial - The Tutorial object, or undefined if
   *  another tutorial is currently active.
   */
  startTutorial(name) {
    if (this.currentTutorial()) {
      return;
    }
    let tutorial = this.tutorials[name];
    tutorial.start();
    return tutorial;
  }

  /**
   * Ends the current tutorial.
   * @returns {undefined}
   */
  endTutorial() {
    let tutorial = this.currentTutorial();
    tutorial.end(true);
  }

  /**
   * Returns the current tutorial, if any.
   * @returns {Tutorial} tutorial - The current tutorial, or null if none active
   */
  currentTutorial() {
    for (let tutorialName in this.tutorials) {
      let tutorial = this.tutorials[tutorialName];
      if (tutorial.isActive()) return tutorial;
    }
  }

  /**
   * Static method for creating a Tutorial object without needing to instantiate
   * chariot with a large configuration and named tutorials.
   * @param {TutorialConfiguration} config - The tutorial configuration
   * @param {ChariotDelegate} [delegate] - An optional delegate that responds to
   *  lifecycle callbacks
   */
  static createTutorial(config, delegate) {
    return new Tutorial(config, '', delegate);
  }

  /**
   * Static method for creating and starting a Tutorial object without needing
   * to instantiate chariot with a large configuration and named tutorials.
   * @param {TutorialConfiguration} config - The tutorial configuration
   * @param {ChariotDelegate} [delegate] - An optional delegate that responds to
   *  lifecycle callbacks
   */
  static startTutorial(config, delegate) {
    const tutorial = this.createTutorial(config, delegate);
    tutorial.start();
    return tutorial;
  }

  toString() {
    return `[Chariot - config: ${this.config}, tutorials: {this.tutorials}]`;
  }

  //// PRIVATE

  _readConfig(config) {
    if (!config || typeof config !== 'object') {
      throw new Error(`Config must contains a tutorials hash.\n${this}`);
    }
    for (let tutorialName in config) {
      this.tutorials[tutorialName] = new Tutorial(
        config[tutorialName], tutorialName, this.delegate);
    }
  }

  _listenForPushState() {
    // override pushState to listen for url
    // sample url to listen for: agent/tickets/1?tutorial=ticketing
    let processGetParams = () => {
      let parameter = QueryParse.toObject(window.location.search);
      let match = location.hash.match(/\?.*tutorial=([^&]*)/)
      let tutorialName = parameter['?tutorial'] || (match ? match[1] : null);
      if (tutorialName) {
        this.startTutorial(tutorialName);
      }
    };

    let pushState = history.pushState;
    history.pushState = function(state) {
      initialState = false;
      let res = null;
      if (typeof pushState === 'function') {
        res = pushState.apply(history, arguments);
      }
      processGetParams();
      return res;
    };

    let replaceState = history.replaceState;
    history.replaceState = function(state) {
      initialState = false;
      let res = null;
      if (typeof replaceState === 'function') {
        res = replaceState.apply(history, arguments);
      }
      processGetParams();
      return res;
    };

    window.addEventListener('hashchange', argument => {
      let tutorial = this.currentTutorial();
      if (tutorial) {
        tutorial.tearDown();
      }
      processGetParams();
    });

    let popState = window.onpopstate;
    window.onpopstate = () => {
      if (initialState) return;
      let res = null;
      if (typeof popState === 'function') {
        res = popState.apply(arguments);
      }
      let tutorial = this.currentTutorial();
      if (tutorial) {
        tutorial.tearDown();
      }
      processGetParams();
      return res;
    };

    if (!navigator.userAgent.match(/msie 9/i)) {
      processGetParams();
    }
  }
}

export
default Chariot;