Source: table/js/table.js

/*!
 * table
 * https://github.com/Voliware/Template
 * Licensed under the MIT license.
 */

/**
 * Templates and creates tables
 * @extends Template
 */
class Table extends Template {

	/**
	 * Constructor
	 * @param {object} options
	 * @param {string[]} options.rowHeaders - if using the default table
	 * template, pass a string array of row headers
	 * @param {object} options.struct
	 * @param {string} options.struct.$wrapper - css class of the table
	 * @param {string} options.struct.$thead - css class of the header
	 * @param {string} options.struct.$tbody - css class of the body
	 * @param {string} options.struct.$tfoot - css class of the footer
	 * @param {string} options.struct.$tr - css class of the row
	 * @returns {Table}
	 */
	constructor(options){
		var defaults = {
			struct : {
				$wrapper : 'table',
				$thead : 'thead',
				$tbody : 'tbody',
				$tfoot : 'tfoot',
				$tr : 'tbody > tr'
			},
			rowHeaders : []
		};
		super($Util.opts(defaults, options));

		// properties
		this.$rows = [];

		// states
		this.isFirstBuild = true;

		// provide a default empty msg
		this.$empty = $('<tr class="table-empty"><td>There is no data to display.</td></tr>');

		return this;
	}

	/**
	 * Use the provided template and remove
	 * the remplate row from the <tbody>
	 * @param {jQuery|string} [$template=null] - a string or jquery object to use as the template.
	 * If null, will use what is set in this.settings.template
	 * @returns {Table}
	 * @private
	 */
	_useTemplate($template = null){
		super._useTemplate($template);
		// remove template row from the DOM
		this.$tr.remove();
		return this;
	}

	/**
	 * Build a default table structure
	 * @returns {Table}
	 * @private
	 */
	_useDefaultTemplate(){
		var template =
			'<table class="table">' +
				'<thead></thead>' +
				'<tbody>' +
					'<tr></tr>' +
				'</tbody>' +
				'<tfoot></tfoot>' +
			'</table>';

		this._useTemplate($(template));

		// todo: this is a patch for render 
		this.settings.template = null;

		// setup row headers
		var rh = this.settings.rowHeaders;
		var $theadRow = $('<tr></tr>');
		for(var i = 0; i < rh.length; i++){
			var header = rh[i] || '';
			var $header = '<th>'+header+'</th>';
			var $col = '<td></td>';

			this.$tr.append($col);
			$theadRow.append($header);
		}
		this.$thead.append($theadRow);

		return this;
	}

	// data

	/**
	 * Cache originally fed data
	 * @param {object} data
	 * @returns {Table}
	 * @private
	 */
	_cacheData(data){
		if(isArray(data))
			this._cachedData = data;
		else
			this._cachedData = $.extend(true, {}, data);
		return this;
	}

	/**
	 * Optionally process data
	 * @param {number[]|object|object[]|string[]} data
	 * @returns {Table}
	 * @private
	 */
	_processData(data){
		var self = this;
		if(isArray(data))
			this._processedData = data;
		else
			this._processedData = $.extend(true, {}, data);

		$.each(data, function(i, e){
			// add a private _rowId_ for objects
			if(isObject(e)){
				self._processedData[i]._rowId_ = i;
			}
			self._processedData[i] = self._processRow(e);
		});
		return this;
	}

	/**
	 * Optionally process row
	 * @param {*} data
	 * @returns {*}
	 * @private
	 */
	_processRow(data){
		return data;
	}

	// render

	/**
	 * Reneder/build the table rows from supplied data.
	 * This will empty the <tbody> element
	 * @param {object} data
	 * @returns {Table}
	 * @private
	 */
    _render(data){
		var self = this;

		// empty the <tbody>
        this.wipe();

		if(!$.isEmptyObject(data) || (Array.isArray(data) && data.length))
			this.toggleEmpty(false);
		else
			return this;

		// run through data and create rows
		Util.each(data, function(i, e){
			self.addRow(e);
		});
        return this;
    }

	// rows

	/**
	 * Create a new row
	 * @returns {jQuery}
	 */
	createRow(){
		return this.$tr.clone();
	}

	/**
	 * Add the row to the <tobdy>
	 * @param {jQuery} $row
	 * @returns {Table}
	 */
	appendRow($row){
		$row.appendTo(this.$tbody);
		this.$rows.push($row);
		return this;
	}

	/**
	 * Populate a row with data
	 * The <td> elements will be populated
	 * @param {jQuery} $row - row to populate
	 * @param {object[]} data - array of data
	 * @returns {Table}
	 */
	populateRow($row, data){
		var dataArr = [];
		Util.each(data, function(i, e){
			dataArr.push(e);
		});

		var $tds = $row.find('td');
		$.each($tds, function(i, e){
			$(e).html(dataArr[i]);
		});
		return this;
	}

	/**
	 * Add a new row and populate with data
	 * @param {object} data
	 * @returns {Table}
	 */
	addRow(data){
		var useTemplate = !isNull(this.settings.template);
		var $row = this.createRow();

		// if data is an object and a template is used
		if(useTemplate && !Array.isArray(data))
			$row.populateChildren(data);
		// if data is an array
		else
			this.populateRow($row, data);

		this.appendRow($row);
		return this;
	}

	/**
	 * Check if the table is empty based
	 * on the number of trs in the tbody.
	 * This may be useful if rows were
	 * delete from the DOM and not data
	 * @returns {boolean}
	 * @private
	 */
	_isEmptyTable(){
		return this.$tbody.find('tr').length === 0;
	}

	/**
	 * Build the entire table
	 * @param {object|object[]} data
	 * object: an object of objects, where each object is a row of data
	 * All row objects are name-value pairs, where the names equal a [name]
	 * or [data-name] attribute within a row DOM element
	 * object[]: same as object, but instead an object of objects, it is an
	 * array of objects
	 * array: an array of data. This is the most simplest form of data and will
	 * simply be turned into <td>s with the data as the html
	 * @returns {Table}
	 */
	build(data){
		this._cacheData(data);
		this._processData(data);
		this.toggleEmpty(false);
		this._render(this._processedData);
		this.isFirstBuild = false;
		return this;
	}

	/**
	 * Empty the <tbody> and the cached data
	 * @returns {Table}
	 */
	wipe(){
		this.$tbody.empty();
		this.toggleEmpty(true);
		this.$rows = [];
		this._cachedData = {};
		return this;
	}

	/**
	 * Delete a row based on
	 * its index in this.$tr
	 * @param {number} index
	 * @returns {Table}
	 */
	deleteRow(index){
		if(this.$rows[index]){
			this.$rows[index].remove();
			this.$rows.splice(index, 1);
		}
		// check if all rows were deleted
		if(this._isEmptyTable()){
			this.toggleEmpty();
		}
		return this;
	}

	/**
	 * Toggle the empty table message
	 * and hide the thead and tfoot
	 * @param {boolean} [state=true]
	 * @returns {Table}
	 */
	toggleEmpty(state = true){
		this.$thead.toggle(!state);
		this.$tfoot.toggle(!state);
		
		if(state) {
			this.$tbody.append(this.$empty);
			this.$empty.show();
		}
		else {
			this.$empty.remove();
		}
		return this;
	}
}