Source: index-d3.js

/**
 * chatUI constructor
 * @constructor
 * @param {d3-selection} container - Container for the chat interface.
 * @return {object} chatUI object
 */
var chatUI = (function (container) {
  
  var module = {};

  module.container = container.append('div').attr('id', 'cb-container');
  module.config = null;
  module.bubbles = [];
  module.ID = 0;
  module.keys = {};
  module.types = {};
  module.inputState = false;
  module.height = 0;
  module.scroll = module.container.append('div').attr('id', 'cb-flow');
  module.flow = module.scroll.append('div').attr('class', 'cb-inner');
  module.input = module.container.append('div').attr('id', 'cb-input').style('display', 'none');
  module.input.append('div').attr('id','cb-input-container').append('input').attr('type', 'text');
  module.input.append('button').text('+');


  /**
   * updateContainer should be called when height or width changes of the container changes
   * @memberof chatUI
   */
   module.updateContainer = function(){
      module.height = module.container.node().offsetHeight;
      module.flow.style('padding-top', module.height+'px');
      module.scroll.style('height', (module.height-((module.inputState==true)?77:0))+'px');
      module.scrollTo('end');
  };

  /**
   * @memberof chatUI
   * @param {object} options - object containing configs {type:string (e.g. 'text' or 'select'), class:string ('human' || 'bot'), value:depends on type}
   * @param {function} callback - function to be called after everything is done
   * @return {integer} id - id of the bubble 
   */
  module.addBubble = function (options, callback) {
    callback = callback || function () { };

    if (!(options.type in module.types)) {
      throw 'Unknown bubble type';
    } else {

      module.ID++;
      var id = module.ID;
      module.bubbles.push({
        id: id,
        type: options.type
        //additional info
      });
      module.keys[id] = module.bubbles.length - 1;

      //segment container
      var outer = module.flow.append('div')
        .attr('class', 'cb-segment cb-' + options.class + ' cb-bubble-type-' + options.type)
        .attr('id', 'cb-segment-' + id);

      //speaker icon
      outer.append('div').attr('class', 'cb-icon');

      var bubble = outer.append('div')
        .attr('class', 'cb-bubble ' + options.class)
        // .style("height", "50px")
        .append('div')
        .attr('class', 'cb-inner');


      outer.append('hr');

      module.types[options.type](bubble, options, callback);

      module.scrollTo('end');

      return module.ID;
    }
  };

  /**
   * @memberof chatUI
   * @param {d3-selection} bubble - d3 selection of the bubble container
   * @param {object} options - object containing configs {type:'text', class:string ('human' || 'bot'), value:array of objects (e.g. [{label:'yes'}])}
   * @param {function} callback - function to be called after everything is done
   */
  module.types.select = function(bubble, options, callback){
    bubble.selectAll('.cb-choice').data(options.value).enter().append('div')
      .attr('class', 'cb-choice')
      .text(function(d){ return d.label; })
      .on('click', function(d){
        d3.select(this).classed('cb-active', true);
        d3.select(this.parentNode).selectAll('.cb-choice').on('click', function(){});
        callback(d);
      });
  };

  /**
   * @memberof chatUI
   * @param {d3-selection} bubble - d3 selection of the bubble container
   * @param {object} options - object containing configs {type:'text', class:string ('human' || 'bot'), value:string (e.g. 'Hello World')}
   * @param {function} callback - function to be called after everything is done
   */
  module.types.text = function (bubble, options, callback) {
    if (('delay' in options) && options.delay) {
      var animatedCircles = '<div class="circle"></div><div class="circle"></div><div class="circle"></div>';
      bubble.append('div')
        .attr('class', 'cb-waiting')
        .html(animatedCircles);

      setTimeout(function () {

        bubble.select(".cb-waiting").remove();
        module.appendText(bubble, options, callback);

      }, (isNaN(options.delay) ? 1000 : options.delay));
    } else {
      module.appendText(bubble, options, callback);
    }

  };

  /**
   * Helper Function for adding text to a bubble
   * @memberof chatUI
   * @param {d3-selection} bubble - d3 selection of the bubble container
   * @param {object} options - object containing configs {type:'text', class:string ('human' || 'bot'), value:string (e.g. 'Hello World')}
   * @param {function} callback - function to be called after everything is done
   */
  module.appendText = function(bubble, options, callback) {
    bubble.attr('class', 'bubble-ctn-' + options.class).append('p')
      .html(options.value)
      .transition()
      .duration(200)
      .style("width", "auto")
      .style('opacity', 1);

    chat.scrollTo('end');

    callback();
  };

  /**
   * Showing the input module and set cursor into input field
   * @memberof chatUI
   * @param {function} submitCallback - function to be called when user presses enter or submits through the submit-button
   * @param {function} typeCallback - function to when user enters text (on change)
   */
  module.showInput = function (submitCallback, typeCallback) {
    module.inputState = true;

    if (typeCallback) {
      module.input.select('input')
        .on('change', function () {
          typeCallback(d3.select(this).node().value);
        });
    } else {
      module.input.select('input').on('change', function () { });
    }

    module.input.select('input').on('keyup', function () {
        if (d3.event.keyCode == 13) {
          submitCallback(module.input.select('input').node().value);
          module.input.select('input').node().value = '';      
        }
    });

    module.input.select('button')
      .on('click', function () {
        submitCallback(module.input.select('input').node().value);
        module.input.select('input').node().value = '';
      });

    module.input.style('display', 'block');
    module.updateContainer();

    module.input.select('input').node().focus();
    module.scrollTo('end');
  };

  /**
   * Hide the input module
   */
  module.hideInput = function () {
    module.input.select('input').node().blur();
    module.input.style('display', 'none');
    module.inputState = false;
    module.updateContainer();
    module.scrollTo('end');
  };

  /**
   * Remove a bubble from the chat
   * @memberof chatUI
   * @param {integer} id - id of bubble provided by addBubble
   */
  module.removeBubble = function (id) {
    module.flow.select('#cb-segment-' + id).remove();
    module.bubbles.splice(module.keys[id], 1);
    delete module.keys[id];
  };

  /**
   * Remove all bubbles until the bubble with 'id' from the chat
   * @memberof chatUI
   * @param {integer} id - id of bubble provided by addBubble
   */
  module.removeBubbles = function (id) {
    for (var i = module.bubbles.length - 1; i >= module.keys[id]; i--) {
      module.removeBubble(module.bubbles[i].id);
    }
  };

  /**
   * Remove all bubbles until the bubble with 'id' from the chat
   * @memberof chatUI
   * @param {integer} id - id of bubble provided by addBubble
   * @return {object} obj - {el:d3-selection, obj:bubble-data}
   */
  module.getBubble = function (id) {
    return {
      el: module.flow.select('#cb-segment-' + id),
      obj: module.bubbles[module.keys[id]]
    };
  };

  /**
   * Scroll chat flow
   * @memberof chatUI
   * @param {string} position - where to scroll either 'start' or 'end'
   */
  module.scrollTo = function (position) {
    //start
    var s = 0;
    //end
    if (position == 'end') {
      s = module.scroll.property('scrollHeight') - (window.innerHeight-77);
    }
    d3.select('#cb-flow').transition()
      .duration(300)
      .tween("scroll", scrollTween(s));

  };

  function scrollTween(offset) {
    return function () {
      var i = d3.interpolateNumber(module.scroll.property('scrollTop'), offset);
      return function (t) { module.scroll.property('scrollTop', i(t)); };
    };
  }

  function debouncer( func , _timeout ) {
    var timeoutID , timeout = _timeout || 200;
    return function () {
      var scope = this , args = arguments;
      clearTimeout( timeoutID );
      timeoutID = setTimeout( function () {
        func.apply( scope , Array.prototype.slice.call( args ) );
      } , timeout );
    };
  }

  //On Resize scroll to end
  d3.select(window).on('resize', debouncer(function(e){
      module.updateContainer();
  }, 200));

  module.updateContainer();

  return module;
});