Source: flowchart/js/flowchart.js

"use strict";
/**
 * @summary Flowchart Library namespace
 * @description This is a library which provides a way to render interactive flowcharts starting from a well formatted xml
 * @license MIT-style license
 * @copyright 2014 Otto srl
 * @author abidibo <dev@abidibo.net> (http://www.abidibo.net)
 * @requires jQuery>=v2.0.3
 * @requires mootools-core>=1.4
 * @requires mootools-more>=1.4
 * @namespace
 */
var flowchart;
if (!flowchart) flowchart = {};
else if( typeof flowchart != 'object') {
  throw new Error('flowchart already exists and is not an object');
}
/**
 * @summary Event dispatcher Object
 * @classdesc Class used to make communicate each other different objects
 * @constructs flowchart.EventDispatcher
 * @memberof flowchart
 */
flowchart.EventDispatcher = {
  _prefix: 'on_',
  listeners: {},
  /**
   * @summary Registers a listener
   * @memberof flowchart.EventDispatcher.prototype
   * @param {String} evt_name The event name
   * @param {Mixed} bind The context passed to the callback function
   * @param {Function} callback Function executed when the event occurres, the event name and an object of properties are the arguiments passed to the function.
   * @return void
   */
  register: function(evt_name, bind, callback) {
    var _evt_name = this._prefix + evt_name;
    if(typeof this.listeners[_evt_name] == 'undefined') {
      this.listeners[_evt_name] = [];
    }
    this.listeners[_evt_name].push([bind === null ? this : bind, callback]);
  },
  /**
   * @summary Emits an event
   * @memberof flowchart.EventDispatcher.prototype
   * @param {String} evt_name The event name
   * @param {Object} params Object parameter to be passed to the invoked function listening to the event
   * @return void
   */
  emit: function(evt_name, params) {
    var _evt_name = this._prefix + evt_name;
    if(typeof this.listeners[_evt_name] != 'undefined') {
      for(var i = 0, l = this.listeners[_evt_name].length; i < l; i++) {
        this.listeners[_evt_name][i][1].call(this.listeners[_evt_name][i][0], evt_name, params);
      }
    }
  }
}
/**
 * @summary Factory method which creates block objects
 * @memberof flowchart
 * @param {String} type The block type
 * @param {Object} node The xml node object
 * @return {Object} a specific Block instance
 */
flowchart.BlockFactory = function(type, node) {
  if(type == 'straight') {
    return new flowchart.StraightBlock(node);
  }
  else if(type == 'conditional') {
    return new flowchart.ConditionalBlock(node);
  }
  else if(type == 'error') {
    return new flowchart.ErrorBlock(node);
  }
  else if(type == 'end') {
    return new flowchart.EndBlock(node);
  }
}

/**
 * @namespace
 * @description Block class which acts as a prototype for all specific block classes
 * @memberof flowchart
 */
flowchart.Block = {
  _status: 'idle',
  /**
   * @summary Initializes the Block instance
   * @memberof flowchart.Block
   * @param {Object} node The xml node object
   * @return void
   */
  init: function(node) {
    this._id = node.attr('id');
  },
  /**
   * @summary Removes the block
   * @memberof flowchart.Block
   * @return void
   */
  remove: function() {
    jQuery(this._block_container).remove();
  }
}

/**
 * @summary Straight Block, no choices
 * @memberof flowchart
 * @constructs flowchart.StraightBlock
 * @extends flowchart.Block
 * @param {Object} node The xml node object
 * @return {Object} A flowchart.StraightBlock instance
 */
flowchart.StraightBlock = function(node) {

  this.init(node);
  this._next = node.attr('next');
  /**
   * @summary Renders the block
   * @memberof flowchart.StraightBlock.prototype
   * @method render
   * @return void
   */
  this.render = function() {

    var self = this;

    var block_content = jQuery('<div/>', {
      'class': 'block-content',
      html: node.children('html').text()
    });

    var block_arrow = jQuery('<span/>', {
      'class': 'fa fa-3x fa-arrow-down link'
    }).bind('click', function() {
      self.updateStatus('selected');
      flowchart.EventDispatcher.emit('block-click', {
        from: self._id,
        next: self._next
      });
    });

    var block_controllers = jQuery('<div/>', {
      'class': 'block-controllers',
    }).append(block_arrow);

    this._block_container = jQuery('<div/>', {
      'id': 'block_' + self._id,
      'class': 'block straight'
    }).append(block_content, block_controllers);

    return this._block_container;
  }
  /**
   * @summary updates the block status
   * @memberof flowchart.StraightBlock.prototype
   * @method updateStatus
   * @param {String} status the status
   * @return void
   */
  this.updateStatus = function(status) {
    if(status == 'selected') {
      this._block_container.find('.selected').removeClass('selected');
      this._block_container.addClass(status);
      this._block_container.find('.fa-arrow-down').addClass(status);
    }
  }
}
flowchart.StraightBlock.prototype = flowchart.Block;

/**
 * @summary Conditional Block, n choices
 * @memberof flowchart
 * @constructs flowchart.ConditionalBlock
 * @extends flowchart.Block
 * @param {Object} node The xml node object
 * @return {Object} A flowchart ConditionalBlock instance
 */
flowchart.ConditionalBlock = function(node) {

  this.init(node);
  this._answers = node.children('answer');
  /**
   * @summary Renders the block
   * @memberof flowchart.ConditionalBlock.prototype
   * @method render
   * @return void
   */
  this.render = function() {

    var self = this;

    var block_content = jQuery('<div/>', {
      'class': 'block-content',
      html: node.children('html').text()
    });

    var block_arrows_row = jQuery('<tr/>');
    var block_answers_row = jQuery('<tr/>');
    var block_controllers_table = jQuery('<table/>').append(block_answers_row, block_arrows_row);

    for(var i = 0, l = this._answers.length; i < l; i++) {
      var answer = jQuery(this._answers[i]);
      var block_arrow = jQuery('<span/>', {
        'class': 'fa fa-3x fa-arrow-down link'
      }).attr('data-index', i).bind('click', function() {
        var index = jQuery(this).attr('data-index');
        self.updateStatus('selected-' + index);
        flowchart.EventDispatcher.emit('block-click', {
          from: self._id,
          next: jQuery(self._answers[index]).attr('next')
        });
      }).appendTo(jQuery('<td style="width: ' + (100 / l) + '%">').appendTo(block_arrows_row));
      var block_answer = jQuery('<div/>', {
        'class': 'answer',
        html: answer.children('html').text()
      }).attr('data-answer', i).appendTo(jQuery('<td/>').appendTo(block_answers_row))
    }

    var block_controllers = jQuery('<div/>', {
      'class': 'block-controllers',
    }).append(block_controllers_table);

    this._block_container = jQuery('<div/>', {
      'id': 'block_' + self._id,
      'class': 'block conditional'
    }).append(block_content, block_controllers);

    return this._block_container;
  }
  /**
   * @summary updates the block status
   * @memberof flowchart.ConditionalBlock.prototype
   * @method updateStatus
   * @param {String} status the status
   * @return void
   */
  this.updateStatus = function(status) {
    if(/selected-.*/.test(status)) {
      this._block_container.addClass('selected');
      this._block_container.find('.selected').removeClass('selected');
      this._block_container.find('[data-answer=' + status.replace(/^selected-/, '') + ']').addClass('selected');
      this._block_container.find('[data-index=' + status.replace(/^selected-/, '') + ']').addClass('selected');
    }
  }

}
flowchart.ConditionalBlock.prototype = flowchart.Block;

/**
 * @summary Error Block
 * @memberof flowchart
 * @constructs flowchart.ErrorBlock
 * @extends flowchart.Block
 * @param {Object} node The xml node object
 * @return {Object} A flowchart.ErrorBlock instance
 */
flowchart.ErrorBlock = function(node) {

  this.init(node);
  /**
   * @summary Renders the block
   * @memberof flowchart.ErrorBlock.prototype
   * @method render
   * @return void
   */
  this.render = function() {

    var self = this;

    var block_content = jQuery('<div/>', {
      'class': 'block-content',
      html: node.children('html').text()
    });

    this._block_container = jQuery('<div/>', {
      'id': 'block_' + self._id,
      'class': 'block error'
    }).append(block_content);

    return this._block_container;
  }

}
flowchart.ErrorBlock.prototype = flowchart.Block;

/**
 * @summary End Block
 * @memberof flowchart
 * @constructs flowchart.EndBlock
 * @extends flowchart.Block
 * @param {Object} node The xml node object
 * @return {Object} A flowchart.EndBlock instance
 */
flowchart.EndBlock = function(node) {

  this.init(node);
  /**
   * @summary Renders the block
   * @memberof flowchart.EndBlock.prototype
   * @method render
   * @return void
   */
  this.render = function() {

    var self = this;

    var block_content = jQuery('<div/>', {
      'class': 'block-content',
      html: node.children('html').text()
    });

    this._block_container = jQuery('<div/>', {
      'id': 'block_' + self._id,
      'class': 'block end'
    }).append(block_content);

    return this._block_container;
  }

}
flowchart.EndBlock.prototype = flowchart.Block;

/**
 * @summary Chart class
 * @classdesc Loads, renders and handles the chart flow
 * @memberof flowchart
 * @constructs flowchart.Chart
 * @return {Object} A flowchart.Chart instance
 */
flowchart.Chart = function() {

  this._history = [];
  this._history_obj = {};
  /**
   * @summary Loads the xml from path
   * @memberof flowchart.Chart.prototype
   * @method getXmlObject
   * @param {String} path The xml path
   * @return {Object} The xml object
   */
  this.getXmlObject = function(path) {
    var xml_object = null;
    var xmltext = jQuery.ajax({
        async: false,
        type:"GET",
        context: this,
        url: path,
        dataType:"xml"
      }).responseText;

    var parser = new DOMParser();
    xml_object = parser.parseFromString(xmltext, "text/xml");

    return xml_object;

  }
  /**
   * @summary Starts the chart flow
   * @memberof flowchart.Chart.prototype
   * @method start
   * @param {String} path The xml path
   * @return void
   */
  this.start = function(path) {

    // retrieve xml object
    this._xml_object = this.getXmlObject(path);
    // create html container element
    this._chart = jQuery('<div id="chart"></div>').appendTo('body');
    // retrieve root element
    this._root = jQuery(this._xml_object).children('chart');
    // register event listening
    flowchart.EventDispatcher.register('block-click', this, this.listen);
    // run the first block
    this.run(0, 1);
  }
  /**
   * @summary Callback called when passing from a block to the next one @see flowchart.EventDispatcher
   * @memberof flowchart.Chart.prototype
   * @method listen
   * @param {String} evt_name The event name
   * @param {Object} params The params passed by the EventDispatcher
   * @param {String} params.from The id of the previous block
   * @param {String} params.next The id of the next block
   * @return void
   */
  this.listen = function(evt_name, params) {
    this.run(params.from, params.next);
  }
  /**
   * @summary Renders a block
   * @memberof flowchart.Chart.prototype
   * @method run
   * @param {String} from The id of the previous block
   * @param {String} id The id of the next block
   * @return void
   */
  this.run = function(from, id) {

    id = parseInt(id);
    from = parseInt(from);

    var history_index = jQuery(this._history).index(from);

    if(history_index > -1) {
      // second time click, delete all story backward till there
      var l = this._history.length;
      for(var i = l - 1; i > history_index; i--) {
        var index = this._history.pop();
        this._history_obj[index].remove();
        delete this._history_obj[index];
      }
      //
    }

    var node = this._root.children('block[id=' + id + ']');
    var type = node.attr('type');
    var block_obj = flowchart.BlockFactory(type, node);

    var block_element = block_obj.render();
    block_element.appendTo(this._chart);

    this._history.push(id);
    this._history_obj[id] = block_obj;
  }

}