;(function () { 'use strict'; /** * @namespace */ db.libs.tableEditor = (function($, Mousetrap){ var name = 'tableEditor'; /** * Return table data as JSON * @public * @memberof! db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @returns {json} Table data formated as json */ function getJSON(id){ var $table = $(id); var data = {}; if( $table.find('thead').length ){ data.head = { rows: [] }; $table.find('thead tr').each(function(i, r){ var row = { cells: [], classes: $(r).attr('class') }; $(r).find('td').each(function(i, cell){ row.cells.push({ value: $(cell).html(), classes: $(cell).attr('class') }); }); data.head.rows.push(row); }); } if( $table.find('tbody').length ){ data.body = { rows: [] }; $table.find('tbody tr').each(function(i, r){ var row = { cells: [], classes: $(r).attr('class') }; $(r).find('td').each(function(i, cell){ row.cells.push({ value: $(cell).html(), classes: $(cell).attr('class') }); }); data.body.rows.push(row); }); } return JSON.stringify(data); } /** * Toggles classnames for column * @public * @memberof db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @param {number} [i] Index for the column to alter, if null, last focused is used, if that's not possible, the last column in the table is used * @param {string} addClassnames Classes to toggle * @param {string} removeClassnames Classes to remove * @return {jQuery} the table */ function toggleColumnClass(id, i, toggleClassnames, removeClassnames){ var $table = $(id); if( !$.isNumeric(i) ){ if($table.data('lastFocus')){ i = $($table.data('lastFocus')).index(); } else { i = $table.find('tbody tr td').length; } } $table.find('tr').each(function(r, row){ var $cell = $(row).find('td:nth-child(' + (i + 1) + ')'); $cell.removeClass(removeClassnames); $cell.toggleClass(toggleClassnames); }); return $table; } /** * Toggle classnames for row * @public * @memberof db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @param {number} [i] Index for the row to alter, if null, last focused is used, if that's not possible, the last row in the table is used * @param {string} addClassnames Classes to toggle * @param {string} removeClassnames Classes to remove * @return {jQuery} the table */ function toggleRowClass(id, i, addClassnames, removeClassnames){ var $table = $(id); var $row; if( !$.isNumeric(i) ){ if($table.data('lastFocus')){ $row = $($table.data('lastFocus')).parent(); } else { $row = $table.find('tbody tr').last(); } } else { $row = $table.find('tbody tr:nth-child(' + i + ')'); } $row.removeClass(removeClassnames); $row.toggleClass(addClassnames); return $table; } /** * Toggles the table header/thead * @public * @memberof db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @return {jQuery} the table */ function toggleHeader(id){ var $table = $(id); if($table.find('thead').length){ deleteHeader(id); } else { insertHeader(id); } return $table; } /** * Inserts a header/thead at the top of the table * @public * @memberof db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @return {jQuery} the table */ function insertHeader(id){ var $table = $(id); var $thead = $('<thead><tr></tr></thead>'); for(var x = 0; x < $table.find('tbody tr:first-child td').length; x++){ var $cell = $('<td contenteditable></td>'); $cell.on('focus', bindEvents); $cell.on('blur', unbindEvents); $thead.find('tr').append($cell); } $table.prepend($thead); return $table; } /** * Removed the header/thead from the table * @public * @memberof db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @return {jQuery} the table */ function deleteHeader(id){ var $table = $(id).find('thead').remove(); return $table; } /** * Inserts a new column * @public * @memberof db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @param {number} [i] Where to insert the column, if not set, after last focused is used, if that's not possible, inserted at the end * @return {jQuery} the table */ function insertColumn(id, i){ var $table = $(id); if(i === undefined){ if($table.data('lastFocus')){ i = $($table.data('lastFocus')).index() + 1; $table.data('lastFocus', null); } else { i = $table.find('tbody tr td').length; } } $table.find('tr').each(function(r, row){ var $index = $(row).find('td:nth-child(' + (i + 1) + ')'); var $cell = $('<td contenteditable></td>'); if($index.length === 0){ $(row).append($cell); } else { $index.before($cell); } $cell.on('focus', bindEvents); $cell.on('blur', unbindEvents); }); return $table; } /** * Deletes a column * @public * @memberof db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @param {number} [i] Index for the column to delete. If not set, the last column is selected * @return {jQuery} the table */ function deleteColumn(id, i){ var $table = $(id); if(i === undefined){ if($table.data('lastFocus')){ i = $($table.data('lastFocus')).index(); $table.data('lastFocus', null); } else { i = $table.find('tbody tr:first-child td').length - 1; } } $table.find('tr').each(function(r, row){ $( $(row).find('td')[i] ).remove(); }); return $table; } /** * Insert a new row * @public * @memberof db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @param {number} [i] Where to insert the row, if not set, after last focused is used, if that's not possible, inserted at the end * @return {jQuery} the table */ function insertRow(id, i){ var $table = $(id); var $row = $('<tr></tr>'); var cells = $table.find('tbody tr:first-child td').length; if(cells === 0){ cells = $table.find('thead tr:first-child td').length; } for(var x = 0; x < cells; x++){ var $cell = $('<td contenteditable></td>'); $cell.on('focus', bindEvents); $cell.on('blur', unbindEvents); $row.append($cell); } //FIXME: Should also support inserting row after last index. if(i !== undefined){ var $index = $table.find('tbody tr:nth-child(' + (i+1) + ')'); $index.before($row); } else { $table.find('tbody').append($row); } return $table; } /** * Delete a row * @public * @memberof db.libs.tableEditor * @param {jQuery|string} id The table to be referenced * @param {number} [i] Index for the row to delete. If not set, last focus is used, if that is not possible the last row is selected * @return {jQuery} the table */ function deleteRow(id, i){ var $table = $(id); var $row; if(i === undefined){ if($table.data('lastFocus')){ $row = $($table.data('lastFocus')).parent(); $table.data('lastFocus', null); } else { $row = $table.find('tbody tr').last(); } } else { $row = $table.find('tbody tr:nth-child(' + i + ')'); } if($row.parent().prop('tagName') == 'TBODY'){ $row.remove(); } return $table; } /** * Set focus to the next cell, if there is no "next cell", one is created * @private * @memberof db.libs.tableEditor * @param {event} [event] event passed from keypress */ function nextCell(event){ if(event) event.preventDefault(); var $el = $(':focus'); //If the cell is the last cell in a row, insert a new cell at the end if( ($el.index() + 1) == $el.parent().find('td').length ){ insertColumn( $(':focus').closest('table') ); } $el.next().focus(); } /** * Set focus to the first cell in the next row. If there is no "next row", one is created * @private * @memberof db.libs.tableEditor * @param {event} [event] event passed from keypress */ function nextRow(event){ if(event) event.preventDefault(); var $el = $(':focus'); var $row = $el.parent(); var $table = $(':focus').closest('table'); // If this is the last row in the table, insert a new at the end if( $el.parent().parent().prop('tagName') == 'TBODY' && ($row.index() + 1) == $table.find('tbody tr').length){ insertRow( $table ); } if( $el.parent().parent().prop('tagName') != 'TBODY' ){ $table.find('tbody tr td:first-child').focus(); } else { $row.next().find('td:first-child').focus(); } } /** * Binds events * @private * @memberof db.libs.tableEditor * @param {event} event Event passed from focus or blur */ function bindEvents(event){ $(event.currentTarget).closest('table').data('lastFocus', $(event.currentTarget)); setTimeout(function(){ Mousetrap.bind('tab', nextCell); Mousetrap.bind('enter', nextRow); },0); } /** * Unbind events * @private * @memberof db.libs.tableEditor * @param {event} event Event passed from focus or blur */ function unbindEvents(event){ var $table = $(event.currentTarget).closest('table'); setTimeout(function(){ //If focus not within the table if( $table.find(':focus').length === 0 ){ Mousetrap.unbind('tab', insertColumn); Mousetrap.unbind('enter', insertRow); } }, 0); } /** * Init Table editor * @public * @memberof db.libs.tableEditor * @return {jQuery} all table editors */ function init(){ $('table[data-editor]').each(function(i, table){ if( !db.utils.isInitialized(table, name) ){ $(table).find('td').each(function(i, cell){ $(cell).on('focus', bindEvents); $(cell).on('blur', unbindEvents); }); db.utils.initialized(table, name); } }); return $('table[data-editor]'); } return { init: init, reflow: init, toggleHeader: toggleHeader, insertColumn: insertColumn, deleteColumn: deleteColumn, insertRow: insertRow, deleteRow: deleteRow, toggleColumnClass: toggleColumnClass, toggleRowClass: toggleRowClass, getJSON: getJSON }; })(jQuery, Mousetrap); })();