UNPKG

taboo-table

Version:
706 lines (664 loc) 25.2 kB
var tabooTable = function (elementName, taboo, userOptions) { var defaultOptions = { addRowsButtons: true, addRowHeaderButtons: true, editableRows: true, editableRowHeader: true, autocomplete: true, autocompleteLists: {}, }, globalOptions = {}; if (_.isObject(userOptions)){ _.extend(globalOptions, defaultOptions, userOptions); } else { globalOptions = defaultOptions; } var tableElement = document.querySelector(elementName), _this = this, cloneProperties= ['padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right', 'text-align', 'font', 'font-size', 'font-family', 'font-weight', 'border', 'border-top', 'border-bottom', 'border-left', 'border-right', 'height', 'width']; this.taboo = taboo; if (tableElement === null) { console.warn("Taboo-Table Selector " + elementName + " does not exist"); return; } // When changes occur in the taboo table, this updates the html function syncToHtml(taboo){ clearTable(); _this.taboo.getColumnHeaders().forEach(function(header){ addColumn(header); }); _this.taboo.getRows({objects:false}).forEach(function(row){ addRow({data:row}); }); } this.updateTaboo = function(newTaboo){ this.taboo = newTaboo; syncToHtml(); }; // when changes occur in the html table this updates the taboo table function syncToTaboo(){ taboo.clear({silent:true}); var headers = tableElement.querySelector('thead tr').children; for (var i = 0; i < headers.length; i++) { taboo.addColumns([headers[i].querySelector('span').textContent], {silent:true}); } var rows = tableElement.querySelectorAll('tbody tr'); for (i = 0; i < rows.length; i++){ var row = rows[i]; var cells = []; for (var j = 0; j < row.children.length; j++){ cells.push(row.children[j].querySelector('span').textContent); } taboo.addRow(cells, {silent:true}); } // manually trigger taboo callbacks taboo.triggerCallbacks('update'); } function registerCallbacks(){ taboo.registerCallback('update', syncToHtml); } // copy the style from the copyElement to the newElement function copyStyle(newElement, copyElement){ var activeStyle = window.getComputedStyle(copyElement); cloneProperties.forEach(function(property){ newElement.style[property] = activeStyle[property]; }); } var addRow = this.addRow = function(options) { var index, data; if (typeof options === 'undefined'){ options = {}; } if (options.hasOwnProperty('index')){ index = options.index; } if (options.hasOwnProperty('data')){ data = options.data; } var existingTr = tableElement.querySelector('tbody tr'), existingTd = tableElement.querySelector('tbody td'), tbody = tableElement.querySelector('tbody'), newTr; index = index + 1; // if no index, append at the end, otherwise create at index point if (!index) { newTr = tbody.appendChild(document.createElement('tr')); if (existingTr){ copyStyle(newTr, existingTr); } } else { var before = tbody.querySelector('tr:nth-of-type(' + index +')'); if (typeof before === null){ newTr = tbody.appendChild(document.createElement('tr')); } else { newTr = tbody.insertBefore(document.createElement('tr'), before); } } // create the rows var td, span, headers = tableElement.querySelector('thead tr').children; for (var i = 0; i < headers.length; i++) { td = document.createElement('td'); span = document.createElement('span'); td.appendChild(span); if (existingTd){ copyStyle(td, existingTd); } // if data has been passed need to add that to the newly created cells if (data) { span.textContent = data[i]; } else { span.textContent = ""; } if (globalOptions.editableRows){ td.tabIndex = '1'; } newTr.appendChild(td); } // add the event handlers for the buttons if (globalOptions.addRowsButtons) { addRowButtonsEvents(newTr); } return newTr; }; var addColumn = this.addColumn = function(columnName, columnData, index){ if (typeof columnName === "undefined") { columnName = 'empty'; } if (typeof columnData === 'undefined'){ columnData = []; } // insert column heading var newTh; if (typeof index === 'number'){ index = index + 1; var before = tableElement.querySelector('thead tr :nth-of-type('+ index +')'); if (before === null){ newTh = tableElement.querySelector('thead tr').appendChild(document.createElement('th')); } else { newTh = tableElement.querySelector('thead tr').insertBefore(document.createElement('th'), before); } } else { newTh = tableElement.querySelector('thead tr').appendChild(document.createElement('th')); } // add rows var newSpan = newTh.appendChild(document.createElement('span')), allTrs = tableElement.querySelectorAll('tbody tr'); newSpan.textContent = columnName; for (var i = 0; i < allTrs.length; i++) { var tr = allTrs[i], text; // add data if we have it if (columnData[i] == 'undefined'){ text = ''; } else { text = columnData[i]; } var newTd = document.createElement('td'), span = document.createElement('span'); newTd.appendChild(span); if (typeof index === 'number'){ var beforeTd = tr.querySelector(':nth-of-type('+ index + ')'); tr.insertBefore(newTd, beforeTd); } else { tr.appendChild(newTd); } if (globalOptions.editable){ newTd.tabIndex = '1'; } span.textContent = text; } if (globalOptions.addRowHeaderButtons) { addColumnButtonsEvents(newTh); } }; var deleteColumn = this.deleteColumn = function(index){ var th = tableElement.querySelector('thead tr th:nth-of-type(' + (index + 1) +')'), headTr = tableElement.querySelector('thead tr'); headTr.removeChild(th); // remove the tbody rows var allTrs = tableElement.querySelectorAll('tbody tr'); for (var i = 0; i < allTrs.length; i++) { var tr = allTrs[i]; var td = tr.querySelector(':nth-of-type('+ (index + 1) +')'); tr.removeChild(td); } }; var deleteRow = this.deleteRow = function(index){ var tr = tableElement.querySelector('tbody tr:nth-of-type('+ index + ')'), tbody = tableElement.querySelector('tbody'); tbody.removeChild(tr); }; // adding rows var killEvent = document.createEvent('Event'); killEvent.initEvent('kill', true, true); var stopKillEvent = document.createEvent('Event'); stopKillEvent.initEvent('stopKill', true, true); var createRowButtons = function(event) { var td; if (event.target.tagName === "SPAN"){ td = event.target.parentNode; } else if (event.target.tagName === "TD"){ td = event.target; } else { return undefined; } var tr = td.parentNode, table = tr.parentNode, lastTd = tr.lastChild; // if it already exists, then return early var existingButtonDiv = lastTd.querySelector('.buttonWrapper'); if (existingButtonDiv) { existingButtonDiv.dispatchEvent(stopKillEvent); return existingButtonDiv; } var rect = tr.getBoundingClientRect(); var buttonWrapper = document.createElement('div'), buttonDiv = document.createElement('div'), plus = document.createElement('p'), minus = document.createElement('p'), buttonDivWidth = 20; buttonWrapper.className = 'buttonWrapper'; buttonWrapper.appendChild(buttonDiv); buttonDiv.className = 'buttonsDiv'; buttonDiv.appendChild(plus); buttonDiv.appendChild(minus); buttonDiv.style.top = (rect.top + document.body.scrollTop + 5) + 'px'; buttonDiv.style.left = (rect.right + document.body.scrollLeft - buttonDivWidth - 40) + 'px'; plus.textContent = '+'; minus.textContent = '—'; plus.className = 'plus'; minus.className = 'minus'; // handlers for clicks var plusClickHandler = function(event){ // stop the editor from doing its thing event.stopPropagation(); var plusButton = event.target, bd = plusButton.parentNode, wrappingDiv = bd.parentNode, td = wrappingDiv.parentNode, tr = td.parentNode, tbody = tr.parentNode, index = Array.prototype.indexOf.call(tbody.childNodes, tr); addRow({index:index}); syncToTaboo(); }; var minusClickHandler = function(event){ // stop the editor from doing its thing event.stopPropagation(); var minusButton = event.target, bd = minusButton.parentNode, wrappingDiv = bd.parentNode, td = wrappingDiv.parentNode, tr = td.parentNode, tbody = tr.parentNode, index = Array.prototype.indexOf.call(tbody.childNodes, tr); tbody.removeChild(tr); syncToTaboo(); }; plus.addEventListener('click', plusClickHandler); plus.addEventListener('dblclick', plusClickHandler); minus.addEventListener('click', minusClickHandler); minus.addEventListener('dblclick', minusClickHandler); // handlers for killing the buttonDiv var timeoutID, killing = false; // timed, cancellable kill signal buttonWrapper.addEventListener('kill', function() { // if already killing the buttonDiv, don't add another kill timer if (killing){ return; } else { killing = true; timeoutID = window.setTimeout(function(){ lastTd.removeChild(buttonWrapper); }, 50); } }); buttonWrapper.addEventListener('stopKill', function(){ killing = false; window.clearTimeout(timeoutID); }); // if we mouseover then stop the kill buttonWrapper.addEventListener('mouseover', function(){ buttonWrapper.dispatchEvent(stopKillEvent); }); // if we mouseout then kill the buttonrow buttonWrapper.addEventListener('mouseout', function(){ buttonWrapper.dispatchEvent(killEvent); }); // add the button div to the row lastTd.appendChild(buttonWrapper); return buttonWrapper; }; var removeRowButtons = function(event) { var td = event.target, tr = td.parentNode, lastTd = tr.lastChild; var buttonsDiv = lastTd.querySelector('.buttonWrapper'); if (buttonsDiv) { buttonsDiv.dispatchEvent(killEvent); } }; var addRowButtonsEvents = function(tr){ // create buttons tr.addEventListener('mouseover', createRowButtons); // remove buttons tr.addEventListener('mouseout', removeRowButtons); tr.addEventListener('click', removeRowButtons); }; var createColumnButtons = function(event) { var th; if (event.target.tagName === "TH"){ th = event.target; } else if (event.target.tagName === "SPAN") { th = event.target.parentNode; } else { return undefined; } var tr = th.parentNode, thead = tr.parentNode; if (th.nodeName !== 'TH'){ return undefined; } // if it already exists, then return early var existingButtonDiv = th.querySelector('.buttonsDiv'); if (existingButtonDiv) { existingButtonDiv.dispatchEvent(stopKillEvent); return existingButtonDiv; } var rect = th.getBoundingClientRect(); var buttonDiv = document.createElement('div'), plus = document.createElement('p'), minus = document.createElement('p'), buttonDivWidth = 20; buttonDiv.className = 'buttonsDiv'; buttonDiv.appendChild(plus); buttonDiv.appendChild(minus); buttonDiv.style.top = (rect.top + document.body.scrollTop + 5) + 'px'; buttonDiv.style.left = (rect.right + document.body.scrollLeft - buttonDivWidth - 40) + 'px'; plus.textContent = '+'; minus.textContent = '—'; plus.className = 'plus'; minus.className = 'minus'; var plusClickHandler = function(event){ // stop the editor from doing its thing event.stopPropagation(); var plusButton = event.target, bd = plusButton.parentNode, tr = bd.parentNode, tbody = tr.parentNode, index = Array.prototype.indexOf.call(tbody.childNodes, tr); addColumn("New Column", undefined, index); syncToTaboo(); }; var minusClickHandler = function(event){ // stop the editor from doing its thing event.stopPropagation(); // do the things var minusButton = event.target, bd = minusButton.parentNode, th = bd.parentNode, thead = th.parentNode, index = Array.prototype.indexOf.call(thead.childNodes, th); deleteColumn(index); syncToTaboo(); }; plus.addEventListener('click', plusClickHandler); plus.addEventListener('dblclick', plusClickHandler); minus.addEventListener('click', minusClickHandler); minus.addEventListener('dblclick', minusClickHandler); // handlers for killing the buttonDiv var timeoutID, killing = false; // timed, cancellable kill signal buttonDiv.addEventListener('kill', function() { // if already killing the buttonDiv, don't add another kill timer if (killing){ return; } else { killing = true; timeoutID = window.setTimeout(function(){ th.removeChild(buttonDiv); }, 50); } }); buttonDiv.addEventListener('stopKill', function() { killing = false; window.clearTimeout(timeoutID); }); // if we mouseover then stop the kill buttonDiv.addEventListener('mouseover', function(){ buttonDiv.dispatchEvent(stopKillEvent); }); // if we mouseout then kill the buttonrow buttonDiv.addEventListener('mouseout', function(){ buttonDiv.dispatchEvent(killEvent); }); // add the button div to the row th.appendChild(buttonDiv); return buttonDiv; }; var removeColumnButtons = function(event) { var th = event.target, tr = th.parentNode; var buttonsDiv = th.querySelector('.buttonsDiv'); if (buttonsDiv) { buttonsDiv.dispatchEvent(killEvent); } }; var addColumnButtonsEvents = function(th){ th.addEventListener('mouseover', createColumnButtons); th.addEventListener('mouseout', removeColumnButtons); th.addEventListener('click', removeColumnButtons); }; var clearTable = function(){ tableElement.innerHTML = '<thead><tr></tr></thead><tbody></tbody>'; }; this.updateAutocompleteLists = function(column, autocompleteSuggestions){ globalOptions.autocompleteLists[column] = autocompleteSuggestions; }; // allows cells in the html table to altered var Editor = function(options){ var defaultOptions = { cloneProperties: ['padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right', 'text-align', 'font', 'font-size', 'font-family', 'font-weight', 'border', 'border-top', 'border-bottom', 'border-left', 'border-right', 'height', 'width'] }; var element = tableElement, editor = document.querySelector('body').appendChild(document.createElement('input')), activeOptions = _.extend({}, defaultOptions, options), ARROW_LEFT = 37, ARROW_UP = 38, ARROW_RIGHT = 39, ARROW_DOWN = 40, ENTER = 13, ESC = 27, TAB = 9, F2 = 113, LEFT_WINDOWS = 91, RIGHT_WINDOWS = 92, SELECT = 93, CTRL = 17, BACKSPACE = 8, active, cursorPosition; editor.style.position= 'absolute'; editor.style.display = 'none'; element.style.cursor = 'pointer'; var showEditor = function(event) { active = event.target; if (active === element) { // user has clicked the table itself, not a cell within the table return; } if (active) { if (active.tagName === "SPAN"){ active = active.parentNode; } if (active.parentNode.parentNode.tagName === "TBODY" && !globalOptions.editableRows || active.parentNode.parentNode.tagName === "THEAD" && !globalOptions.editableRowHeader){ return; } editor.value = active.querySelector('span').textContent; var activeStyle = window.getComputedStyle(active); activeOptions.cloneProperties.forEach(function(property){ editor.style[property] = activeStyle[property]; }); editor.classList.remove('error'); editor.style.display = ''; var rect = active.getBoundingClientRect(); editor.style.top = rect.top + document.body.scrollTop + 'px'; editor.style.left = rect.left + document.body.scrollLeft + 'px'; editor.focus(); } }; var setActiveText = function () { var newText = editor.value; if (active.querySelector('span').textContent === newText || editor.classList.contains('error')) { return true; } else { active.querySelector('span').textContent = editor.value; } // update the taboo instance to which this table is linked syncToTaboo(); return undefined; }; var movement = function (cell, keycode) { var currentIndex; if (keycode === ARROW_RIGHT) { return cell.nextElementSibling; } else if (keycode === ARROW_LEFT) { return cell.previousElementSibling; } else if (keycode === ARROW_UP) { currentIndex = Array.prototype.indexOf.call(cell.parentNode.children, cell); var previous = cell.parentNode.previousElementSibling; if (previous){ return previous.children[currentIndex]; } else{ return false; } } else if (keycode === ARROW_DOWN || keycode === ENTER) { currentIndex = Array.prototype.indexOf.call(cell.parentNode.children, cell); var nextRow = cell.parentNode.nextElementSibling; // if there is another row beneath this one, move to it if (nextRow){ return nextRow.children[currentIndex]; } else { // otherwise create a new row below it nextRow = addRow(); return nextRow.children[currentIndex]; } } return false; }; var editorBlur = function (e) { setActiveText(); editor.style.display = 'none'; }; var editorKeydown = function(e) { var atEnd = editor.selectionEnd === editor.value.length, atStart = editor.selectionEnd === 0, currentColumnIndex = Array.prototype.indexOf.call(active.parentNode.children, active), currentRowIndex = Array.prototype.indexOf.call(active.parentNode.parentNode.children, active.parentNode), cell, possibleMove; if (e.which === ENTER) { editor.dispatchEvent(new Event('blur')); e.preventDefault(); e.stopPropagation(); cell = element.querySelector('tr:nth-child(' + (currentRowIndex + 1) + ") td:nth-child(" + (currentColumnIndex + 1) + ")"); var move = movement(cell, ENTER); move.focus(); } else if (e.which === ESC) { // ignore typed values, hide input editor editor.value = active.querySelector('span').textContent; e.preventDefault(); e.stopPropagation(); editor.style.display = 'none'; active.focus(); } else if (e.which === TAB) { setActiveText(); editor.style.display = 'none'; cell = element.querySelector('tr:nth-child(' + (currentRowIndex + 1) + ") td:nth-child(" + (currentColumnIndex + 1) + ")"); possibleMove = movement(cell, ARROW_RIGHT); if (possibleMove) { possibleMove.focus(); } else { cell.focus(); } e.preventDefault(); e.stopPropagation(); } else if ((atEnd && (e.which === ARROW_DOWN || e.which === ARROW_RIGHT)) || atStart && (e.which === ARROW_UP || e.which === ARROW_LEFT)) { possibleMove = movement(active, e.which); if (possibleMove) { possibleMove.focus(); e.preventDefault(); e.stopPropagation(); } } else { insert(e, currentColumnIndex, currentRowIndex); } }; var getMatches = function(currentColumnIndex, currentRowIndex){ var header = element.querySelector('tr th:nth-child(' + (currentColumnIndex + 1) + ') span').textContent, externalSuggestions = globalOptions.autocompleteLists[header]; // get the current column values var cells = element.querySelectorAll('tr td:nth-child(' + (currentColumnIndex + 1) + ') span'); var matches = []; Array.prototype.forEach.call(cells, function(cell, idx) { if (idx === currentRowIndex) return; if (cell.textContent.startsWith(editor.value)){ matches.push(cell.textContent); } }); if (_.isArray(externalSuggestions)){ externalSuggestions.forEach(function(externalSuggestion,idx){ if (externalSuggestion.startsWith(editor.value)){ matches.push(externalSuggestion); } }); } matches.sort(); return matches; }; var insert = function (e, currentColumnIndex, currentRowIndex){ if (globalOptions.autocomplete){ var currentCursorPosition = editor.selectionStart, pressedKey = e.shiftKey ? String.fromCharCode(e.which) : String.fromCharCode(e.which).toLowerCase(), highlightedText = window.getSelection().toString(), alphaNumericRegex = /[a-zA-Z0-9]/; // only do autocomplete if the key is alphanumeric if (alphaNumericRegex.exec(pressedKey)) { var matches; e.preventDefault(); // check if there is a suggestion if (highlightedText.length === 0){ // no suggestion editor.value = editor.value + pressedKey; // check if there is a potential autocomplete matches = getMatches(currentColumnIndex, currentRowIndex); if (matches.length > 0){ // slice the first match in editor.value = editor.value + matches[0].slice(currentCursorPosition + 1, matches[0].length); // highlight suggestion editor.setSelectionRange(currentCursorPosition + 1, matches[0].length); } } else { // suggestion already there // get current suggestion var suggestion = window.getSelection(); editor.value = editor.value.slice(0, editor.selectionStart) + editor.value.slice(editor.selectionEnd); // insert the character before the suggestion editor.value = editor.value + pressedKey; // reupdate the suggestion matches = getMatches(currentColumnIndex, currentRowIndex); if (matches.length > 0) { // slice the first match in editor.value = editor.value + matches[0].slice(currentCursorPosition + 1, matches[0].length); // highlight suggestion editor.setSelectionRange(currentCursorPosition + 1, matches[0].length); } } } } }; // intercept keys for movement var elementKeydown = function(e) { var possibleMove = movement(e.target, e.which); if (possibleMove) { possibleMove.focus(); } else if (e.which === BACKSPACE) { e.preventDefault(); showEditor(e); } }; var validate = function (event) { // TODO do something? }; var windowResize = function (event) { if (editor.style.display !== 'none') { var rect = active.getBoundingClientRect(); editor.style.top = rect.top + document.body.scrollTop + 'px'; editor.style.left = rect.left + document.body.scrollLeft + 'px'; editor.style.width = active.offsetWidth + 'px'; editor.style.height = active.offsetHeight + 'px'; } }; // Add event listeners editor.addEventListener('paste', validate); editor.addEventListener('input', validate); editor.addEventListener('blur', editorBlur); editor.addEventListener('keydown', editorKeydown); element.addEventListener('click', showEditor); element.addEventListener('keypress', showEditor); element.addEventListener('dblclick', showEditor); element.addEventListener('keydown', elementKeydown); window.addEventListener('resize', windowResize); }; // end Editor // init var e = new Editor(); syncToHtml(taboo); // add a single empty row if there are headers but no rows in the taboo if (taboo.numberOfColumns() > 0 && taboo.numberOfRows() === 0){ addRow(); } taboo.registerCallback('update', syncToHtml); };