Edit in JSFiddle

console.clear();



/*==========
#TABLEMAKER
==========*/

/*
#TODOS
1. create add table Col <col> functionality
2. create ability to designate which <col> for which colgroup
3. create a "switch in place" method for converting a td into a th
*/
var TableMaker = function(configData) {
  this.config = configData !== undefined ? configData : tmDefaultConfig;
  this.table = document.createElement('table');

  /*========== 
  #CLASSES.
  ==========*/
  /*These should be where we manipulate the elements (Add classes, properties, attributes) */
  
  this._Cell = function(type, row, text, index) {
    var cell = type === 'td' ?  row.insertCell(index) : row.insertBefore(document.createElement('th'), row.children[index]);
    
    if (text) {
      cell.innerText = text;
    }
    
    if (this.config.classes.cols.even && this.config.classes.cols.even !== '' && (row.cells.length ) %2 === 0 ) {
      cell.classList.add(this.config.classes.cols.even);
    }
    
    if (this.config.classes.cols.odd && this.config.classes.cols.odd !== '' && !((row.cells.length ) %2 === 0) ) {
      cell.classList.add(this.config.classes.cols.odd);
    }
    
    if (this.config.classes.cols.nth && this.config.classes.cols.nth !== '' && ((row.cells.length)%this.config.classes.cols.nth === 0) ) {
      cell.classList.add('nth--' + this.config.classes.cols.nth);
    }
    
    return cell;
  };
  
  this._Colgroup = function(index) {
    var colgroup = document.createElement('colgroup');
    return colgroup;
  };
  
  this._Caption = function(text) {
    var caption = this.table.caption !== null ? this.table.caption : this.table.createCaption();

    if (text) {
      caption.innerText = text;
    }
    return caption;
  };

  this._Footer = function() {
    var footer = this.table.createTFoot();
    return footer;
  };

  this._Header = function() {
    var header = this.table.createTHead();
    return header;
  };

  this._Body = function() {
    var body = this.table.createTBody();

    return body;
  };

  this._Row = function(rowContainer, index, nCells) {
    index = index !== undefined ? index : -1;
    
    var row = rowContainer.insertRow(index);
    
    for (var i = 0; i < nCells; i++) {
      this.addCell('td', row, i);
    }
 
    if (this.config.classes.rows.even && this.config.classes.rows.even !== ''  && (rowContainer.rows.length) %2 === 0 ) {
      row.classList.add(this.config.classes.rows.even);
    }
    
    if (this.config.classes.rows.odd && this.config.classes.rows.odd !== '' && !((rowContainer.rows.length) %2 === 0) ) {
      row.classList.add(this.config.classes.rows.odd);
    }
    
    if (this.config.classes.rows.nth  && this.config.classes.rows.nth !== '' && ((rowContainer.rows.length)%this.config.classes.rows.nth === 0) ) {
      row.classList.add('nth--' + this.config.classes.rows.nth);
    }
    
    return row;
  };
  
   this._Range = function(rowContainer, colStart, rowStart, colEnd, rowEnd) {
    var range = [];
      for (var rowI = rowStart, row; rowI <= rowEnd; rowI++) {
        row = rowContainer.rows[rowI];
        var rowRange = [];
        for (var celli = colStart, cell; celli <= colEnd; celli++) {
          cell = row.cells[celli];
          rowRange.push(cell);
        }
        range.push(rowRange);
      }
    return range;
   };
  
  /*==========
  #METHODS
  ==========*/

  this.updateLayout = function (rowContainer, prop, value) {
    var containerName = rowContainer.nodeName;
    var layoutProp;

    switch(containerName) {
        case "TBODY":
          layoutProp = 'body';
          break;
        case 'THEAD':
          layoutProp = 'header';
          break;
        case 'TFOOT':
          layoutProp = 'footer';
          break;
        default:
          break;
    }
    this.config.layout[layoutProp][prop] = value;
    return this.config[layoutProp];
  };
  
  this.addBody = function() {
    this.body = this._Body();
  };

  /*==========
  #METHODS #CELLS
  ==========*/

  this.addCell = function (type, row,index, text) {
    index = index !== undefined ? index : -1;
    var cell = this._Cell(type, row, text, index);
    
    return cell;
  
  };
  this.delCell = function(row, index) {
    row.deleteCell(index);
  };
  
  /*==========
  #METHODS #ROWS
  ==========*/
  //#TODO make sure changes to the rows are changes to the config
  this.addRow = function(rowContainer, index, nCells) {
    nCells = nCells !== undefined ? nCells : rowContainer.rows[rowContainer.rows.length-1].cells.length;
    var row = this._Row(rowContainer, index, nCells);

    this.updateLayout(rowContainer,'rows', rowContainer.rows.length);

    return row;
  };

  this.delRow = function(rowContainer, index) {
    rowContainer.deleteRow(index);
    this.updateLayout(rowContainer,'rows', rowContainer.rows.length);
  };

//#TODO consider merging addrows with addrow?
  this.addRows = function(rowContainer, nRows, nCells) {
    for (var i = 0; i < nRows; i++) {
      this.addRow(rowContainer, -1, nCells);
    }
  };

  this.delRows = function(rowContainer, start, end) {
    end = end !== undefined ? end : rowContainer.rows.length-1;
    for (var i = end; i >= start; i--) {
      this.delRow(rowContainer, i);
    }
  };
  
  /*==========
  #METHODS #COLS
  ==========*/
//#TODO Mke sure that changes to the cols also adjust changes to the config
//#TODO figure out what we're going to call the thing that adds the <col> element
  this.addCol = function(rowContainer, index) {
    var _this = this;
    var rows = rowContainer.rows;
    index = index !== undefined ? index : -1;

    [].forEach.call(rows, function (row) {
      _this.addCell('td', row, index );
      _this.updateLayout(rowContainer, 'cols',  row.cells.length + 1);
    });
  };

  this.addCols = function (rowContainer, nCols, index) {
    for (var i = 0; i < nCols; i++) {
      this.addCol(rowContainer, index);
    }
  };
  
  this.delCol = function(rowContainer, index) {
    var _this = this;
    var rows = rowContainer.rows;

    var end = index;

    [].forEach.call(rows, function (row) {
        _this.delCell(row, index);
    });

    _this.updateLayout(rowContainer, 'cols',  rows[0].cells.length);
  };

  this.delCols = function(rowContainer, start, end) {
    var _this = this;
    var rows = rowContainer.rows;

    end = end !== undefined ? end : rowContainer.rows[0].cells.length -1;

    [].forEach.call(rows, function (row) {

      for (var i = end; i>= start; i--) {
        _this.delCell(row, i)
      }

    });
    _this.updateLayout(rowContainer, 'cols',  rows[0].cells.length);
  };
  /*==========
  #METHODS #COLGROUP
  ==========*/
  
  this.addColGroup = function(index) {
    var colgroup = document.createElement('colgroup');
    this.table.insertBefore(colgroup, this.table.querySelector(':first-child'));
    this.config.layout.colgroups++;
    return colgroup;
  };

  this.delColGroup = function(index) {
    var colgroups = this.table.querySelectorAll('colgroup');
    this.table.removeNode(colgroups[index]);
    this.config.layout.colgroups--;
  };

  this.addColGroups = function(nCols) {
    for (var i = 0; i < nCols; i++) {
      this.addcolGroup(-1);
    }
  };

  /*==========
  #METHODS #HEADER #FOOTER
  ==========*/
  this.addHeader = function(nRows, nCells) {
    if (nRows > 0 && this.table.tHead === null) {
      this.header = this._Header();
      this.addRows(this.header, nRows, nCells);
    }
  };

  this.delHeader = function() {
    this.table.deleteTHead();
  };

  this.addFooter = function(nRows, nCells) {
    if (nRows > 0 && this.table.tFoot === null) {
      this.footer = this._Footer();
      this.addRows(this.footer, nRows, nCells);
    }
  };

  this.delFooter = function() {
    this.table.deleteTFoot();
  };
  
  /*==========
  #METHODS #RANGE
  ==========*/
  //This is the first step in merging cells
  this.getRange = function (rowContainer, startC, startR, endC, endR) {
    //make sure that the start is always less than the end
    endC = endC < startC ? [startC, startC = endC] : endC;
    endR = endR < startR ? [startR, startR = endR] : endR;


   //if the requested range is bigger than the table, stop at the end of the table
    if (endR > rowContainer.rows.length) {
      endR = rowContainer.rows.length;
    }
    // go to the ending row, and check the number of cells there to see if we overreached
    if (endC > rowContainer.rows[endR].cells.length) {
      endC = rowContainer.rows[endR].cells.length;
    }
    var range = this._Range(rowContainer, startC, startR, endC, endR);
    return range;
  };

  this.getRowRange = function (rowContainer, rowIndex, startC, endC) {
    var range = this.getRange(rowContainer, startC, rowIndex, endC, rowIndex);
    return range[0];
  };
  
  this.delRange = function (range) {
    range.forEach(function(item) {
      if (Array.isArray(item)) {
        item.forEach(function(child) {
          child.parentElement.removeChild(child);
        });
      } else {
        item.parentElement.removeChild(item);
      }
    });
  };

  this.mergeRange = function (range) {
    //first, take out the first cell
    var _this = this;
    var is2d = Array.isArray(range[0]);
    var mergeTo =is2d ? range[0].shift() : range.shift();
    //#TODO Sort out what to do when merging an already merged cell
    if (!is2d) {
      this.delRange(range);
      mergeTo.colSpan = range.length + 1;
    } else {
        mergeTo.colSpan =  range[range.length-1].length;
        mergeTo.rowSpan = range.length;
        this.delRange(range);
    }
  };
  
  /*==========
  #METHODS #METADATA
  ==========*/
  
  this.addCaption = function (text) {
    if (text !== undefined && text !== '') {
      this.caption = this._Caption(text);  
      this.config.meta.caption = text;
    }
  };
  
  this.delCaption = function () {
    this.table.deleteCaption();
    this.config.meta.caption = '';
  };

  this.addSummary = function (text) {
    if (text !== undefined && text !== '' ) {
      this.table.summary = text;
      this.config.meta.summary = text;
    }
  };
  
  this.delSummary = function () {
    this.table.removeAttribute('summary');
    this.config.meta.summary = '';
  };

    /*==========
  #METHODS #CLASSES
  ==========*/
  this.addClasses = function (rowContainer, selector, classnames) {
    var collection = rowContainer.querySelectorAll(selector);

    classnames = Array.isArray(classnames) ? classnames : classnames.split(' ');

    [].forEach.call(collection, function (item) {

        classnames.forEach(function (classname) {
          item.classList.add(classname);
        })
    });
  };

  this.delClasses = function (rowContainer, selector, classnames) {
    var collection = rowContainer.querySelectorAll(selector);

    classnames = classnames !== undefined ? classnames : selector.substr(1);
    classnames = Array.isArray(classnames) ? classnames : classnames.split(' ');

    [].forEach.call(collection, function (item) {

        classnames.forEach(function (classname) {
          item.classList.remove(classname);
        })
    });
  };
  
  this.refreshRowClasses = function (rowContainer) {
    var _this = this,
      rowClasses = _this.config.classes.rows;
    var rows = rowContainer.rows;

    [].forEach.call(rows, function (row, i) {
      i = i+1;

      if (rowClasses.even && rowClasses.even !== ''  && i %2 === 0 ) {
        row.classList.add(rowClasses.even);
      }
      
      if (rowClasses.odd && rowClasses.odd !== '' && !(i %2 === 0) ) {
        row.classList.add(rowClasses.odd);
      }
      
      if (rowClasses.nth  && rowClasses.nth !== '' && (i%rowClasses.nth === 0) ) {
        row.classList.add('nth--' + rowClasses.nth);
      }
    });
  };

  this.refreshColClasses = function (rowContainer) {
    var _this = this,
      colClasses = _this.config.classes.cols;

    var rows = rowContainer.rows;

    [].forEach.call(rows, function (row) {

      [].forEach.call(row.cells, function (cell, i) {
        i = i+1;

        if (colClasses.even && colClasses.even !== ''  && i %2 === 0 ) {
          cell.classList.add(colClasses.even);
        }
        
        if (colClasses.odd && colClasses.odd !== '' && !(i %2 === 0) ) {
          cell.classList.add(colClasses.odd);
        }
        
        if (colClasses.nth  && colClasses.nth !== '' && (i%colClasses.nth === 0) ) {
          cell.classList.add('nth--' + colClasses.nth);
        }
      });
    });
  };

  this.refreshClasses = function(rowContainer) {
    this.refreshColClasses(rowContainer);
    this.refreshRowClasses(rowConainer);
  };
  
  /*==========
  #METHODS #INIT
  ==========*/
  
  this.buildTable = function (config) {
      if (config.classes.table !== undefined && config.classes.table !== '') {
        this.table.classList.add(config.classes.table);
      }
      this.addBody();
      this.addHeader(config.layout.header.rows, config.layout.header.cols);
      this.addFooter(config.layout.footer.rows, config.layout.footer.cols);
      this.addRows(this.body, config.layout.body.rows, config.layout.body.cols);
      this.addSummary(config.meta.summary);
      this.addCaption(config.meta.caption);
    };
 
  this.buildTable(this.config);

};
  /*==========
  #TABLEMAKER #CONFIG
  ==========*/

/* the tablemaker object is already pretty big. moved the default configs out.
Besides that, this is an example of what the config MUST look like for building a table from JSON*/
var tmDefaultConfig = {
  layout: {
    header: {
      rows: 0,
      cols: 0,
    },
    body: {
      rows: 1,
      cols: 2,
    },
    footer: {
      rows: 0,
      cols: 0,
    },
    colgroups: 0,
  },
  meta: {
    summary: '',
    caption: '',
  },
  classes: {
    table: '',
    rows: {
      even: '',
      odd: '',
      nth: '',
    },
    cols: {
      even: '',
      odd: '',
      nth: '',
    }
  },
};

  /*==========
  #TABLEMAKERUI 
  ==========*/
/*
#TODOS
1. wire up the add table header/footer
2. wire up colgroups
3. wire up the even/odd/ classes
4. wire up the nth classes
5. wire up the table class
6. wire up up table width/ cell padding/spacing
7. wire up column width / row height
*/


var tableMakerUI = tableMakerUI || {};

tableMakerUI.init = function() {
  var config = tmDefaultConfig;
  
  //#TODO move the config changes somewhere else, so the init is less cluttered
  
  // this is really here just to test out different features of TableMaker
  config.layout.body.rows = 4;
  config.layout.body.cols = 4;
  config.classes.rows.even = 'tr--even';
  config.classes.rows.odd = 'tr--odd';
  config.classes.rows.nth = 3;
  config.classes.cols.even = 'td--even';
  config.classes.cols.nth = 3;
  config.classes.cols.odd = 'td--odd';
  
  this.outputTable = new TableMaker(config);
  
  this.bindEvts(this.selectors, this.callbacks);
  this.showTable(this.outputTable.table);
};

tableMakerUI.selectors = {
  addHead: '#addHead',
  addFoot: '#addFoot',
  tcaption: '#tcaption',
  tsummary: '#tsummary',
  addCol: '#addCol',
  remCol: '#remCol',
  colNum: '#colNum',
  addRow: '#addRow',
  remRow: '#remRow',
  rowNum: '#rowNum',
  addColG: '#addColG',
  remColG: '#remColG',
  colGNum: 'colGNum',
  evenRow: '#evenRow',
  oddRow: '#oddRow',
  evenCell: '#evenCell',
  oddCell: '#oddCell',
  outputContainer: '#tablemaker',
};

tableMakerUI.functions = {
  updateRows: function(amt) {
    this.outputTable.addRows(this.outputTable.body,amt);
  },
  updateCols: function(amt) {
    this.outputTable.addCols(this.outputTable.body,amt);
  },
  updateColGroups: function(amt) {

  },
  updateClasses: function(selector, className) {

  },
  updateCaption: function (text) {
    if (text !== '' || text !== undefined) {
      this.outputTable.addCaption(text);
    }
    if (text === '' || text === undefined) {
      this.outputTable.delCaption();
    }
  },
  updateSummary: function(text) {
    this.outputTable.addSummary(text);
  },
  updateRowHeight: function() {

  },
  updateColWidth: function() {

  }
};

tableMakerUI.showTable = function (table) {
  var output = document.querySelector(this.selectors.outputContainer);
  output.appendChild(table);
};

tableMakerUI.callbacks = {
  addCol : function () {
    var _this = tableMakerUI,
        colNum = document.querySelector(_this.selectors.colNum);
        
    colNum.value = parseInt(colNum.value, 10) + 1;
    
      if ("createEvent" in document) {
          var evt = document.createEvent("HTMLEvents");
          evt.initEvent("change", false, true);
          colNum.dispatchEvent(evt);
      }
      else {
          colNum.fireEvent("onchange");
      }
  },
  remCol : function () {
    var _this = tableMakerUI,
        colNum = document.querySelector(_this.selectors.colNum);
        
     colNum.value = parseInt(colNum.value, 10) -1;
     
      if ("createEvent" in document) {
          var evt = document.createEvent("HTMLEvents");
          evt.initEvent("change", false, true);
          colNum.dispatchEvent(evt);
      }
      else {
          colNum.fireEvent("onchange");
      }
  },
  changeCols: function () {
    var _this = tableMakerUI,
        colNum = document.querySelector(_this.selectors.colNum),
        cols = parseInt(colNum.value, 10),
        dif = cols - _this.outputTable.body.rows[0].cells.length;

    // todo: move this into the functions.updatecols
    if (cols > _this.outputTable.body.rows[0].cells.length) {
      _this.outputTable.addCols(_this.outputTable.body, dif);
    }

    if (cols < _this.outputTable.body.rows[0].cells.length) {
      _this.outputTable.delCols(_this.outputTable.body, _this.outputTable.body.rows[0].cells.length  + dif);
    }
    
  },
  addRow : function () {
    var _this = tableMakerUI,
      rowNum = document.querySelector(_this.selectors.rowNum);
      
    rowNum.value = parseInt(rowNum.value, 10) + 1;
    
       if ("createEvent" in document) {
          var evt = document.createEvent("HTMLEvents");
          evt.initEvent("change", false, true);
          rowNum.dispatchEvent(evt);
      } else {
          rowNum.fireEvent("onchange");
      }
  },
  remRow : function () {
    var _this = tableMakerUI,
        rowNum = document.querySelector(_this.selectors.rowNum);
        
     rowNum.value = parseInt(rowNum.value, 10) -1 ;
     
    if ("createEvent" in document) {
      var evt = document.createEvent("HTMLEvents");
      evt.initEvent("change", false, true);
      rowNum.dispatchEvent(evt);
    } else {
      rowNum.fireEvent("onchange");
    }
  },
  changeRows: function () {
    var _this = tableMakerUI,
        rowNum = document.querySelector(_this.selectors.rowNum),
        rows = parseInt(rowNum.value, 10),
        dif = rows - _this.outputTable.body.rows.length;
    
    //todo: move this into the functions.updaterows
    if (rows > _this.outputTable.body.rows.length) {
      _this.outputTable.addRows(_this.outputTable.body, dif);
    }

    if (rows < _this.outputTable.body.rows.length) {
      _this.outputTable.delRows( _this.outputTable.body,(_this.outputTable.body.rows.length ) + dif );
    }
  },
  changeCaption: function (e) {
    var _this = tableMakerUI,
        text = e.target.value;
        
    _this.functions.updateCaption.call(tableMakerUI, text);
  },
  changeSummary: function (e) {
    var _this = tableMakerUI,
        text = e.target.value;
        
    _this.functions.updateSummary.call(tableMakerUI, text);
  },
  changeRowClass: function (e) {
    var _this = tableMakerUI;
    if (_this.outputTable.config.classes.rows[e.target.name] !== '') {
      _this.outputTable.delClasses(_this.outputTable.body,'.'+_this.outputTable.config.classes.rows[e.target.name]);
    }

    _this.outputTable.config.classes.rows[e.target.name] = e.target.value;
    _this.outputTable.refreshRowClasses(_this.outputTable.body);
  },
  changeColClass: function (e) {
    var _this = tableMakerUI;

    if (_this.outputTable.config.classes.cols[e.target.name] !== '') {
      _this.outputTable.delClasses(_this.outputTable.body,'.'+_this.outputTable.config.classes.cols[e.target.name]);
    }
      
    _this.outputTable.config.classes.cols[e.target.name] = e.target.value;
    _this.outputTable.refreshColClasses(_this.outputTable.body);
  },
};

tableMakerUI.bindEvts = function(selectors, callbacks) {
  document.querySelector(selectors.colNum).addEventListener('change', callbacks.changeCols);
  document.querySelector(selectors.addCol).addEventListener('click', callbacks.addCol);
  document.querySelector(selectors.remCol).addEventListener('click', callbacks.remCol);  

  document.querySelector(selectors.rowNum).addEventListener('change', callbacks.changeRows);
  document.querySelector(selectors.addRow).addEventListener('click', callbacks.addRow);
  document.querySelector(selectors.remRow).addEventListener('click', callbacks.remRow);

  document.querySelector(selectors.tcaption).addEventListener('change', callbacks.changeCaption);
  document.querySelector(selectors.tsummary).addEventListener('change', callbacks.changeSummary);

  document.querySelector(selectors.evenRow).addEventListener('change', callbacks.changeRowClass);
  document.querySelector(selectors.oddRow).addEventListener('change', callbacks.changeRowClass);

  document.querySelector(selectors.evenCell).addEventListener('change', callbacks.changeColClass);
  document.querySelector(selectors.oddCell).addEventListener('change', callbacks.changeColClass);
};

tableMakerUI.init();