UNPKG

tabulator-tables

Version:

Interactive table generation JavaScript library

823 lines (639 loc) 20.7 kB
import Module from '../../core/Module.js'; import Helpers from '../../core/tools/Helpers.js'; import defaultEditors from './defaults/editors.js'; export default class Edit extends Module{ static moduleName = "edit"; //load defaults static editors = defaultEditors; constructor(table){ super(table); this.currentCell = false; //hold currently editing cell this.mouseClick = false; //hold mousedown state to prevent click binding being overridden by editor opening this.recursionBlock = false; //prevent focus recursion this.invalidEdit = false; this.editedCells = []; this.convertEmptyValues = false; this.editors = Edit.editors; this.registerTableOption("editTriggerEvent", "focus"); this.registerTableOption("editorEmptyValue"); this.registerTableOption("editorEmptyValueFunc", this.emptyValueCheck.bind(this)); this.registerColumnOption("editable"); this.registerColumnOption("editor"); this.registerColumnOption("editorParams"); this.registerColumnOption("editorEmptyValue"); this.registerColumnOption("editorEmptyValueFunc"); this.registerColumnOption("cellEditing"); this.registerColumnOption("cellEdited"); this.registerColumnOption("cellEditCancelled"); this.registerTableFunction("getEditedCells", this.getEditedCells.bind(this)); this.registerTableFunction("clearCellEdited", this.clearCellEdited.bind(this)); this.registerTableFunction("navigatePrev", this.navigatePrev.bind(this)); this.registerTableFunction("navigateNext", this.navigateNext.bind(this)); this.registerTableFunction("navigateLeft", this.navigateLeft.bind(this)); this.registerTableFunction("navigateRight", this.navigateRight.bind(this)); this.registerTableFunction("navigateUp", this.navigateUp.bind(this)); this.registerTableFunction("navigateDown", this.navigateDown.bind(this)); this.registerComponentFunction("cell", "isEdited", this.cellIsEdited.bind(this)); this.registerComponentFunction("cell", "clearEdited", this.clearEdited.bind(this)); this.registerComponentFunction("cell", "edit", this.editCell.bind(this)); this.registerComponentFunction("cell", "cancelEdit", this.cellCancelEdit.bind(this)); this.registerComponentFunction("cell", "navigatePrev", this.navigatePrev.bind(this)); this.registerComponentFunction("cell", "navigateNext", this.navigateNext.bind(this)); this.registerComponentFunction("cell", "navigateLeft", this.navigateLeft.bind(this)); this.registerComponentFunction("cell", "navigateRight", this.navigateRight.bind(this)); this.registerComponentFunction("cell", "navigateUp", this.navigateUp.bind(this)); this.registerComponentFunction("cell", "navigateDown", this.navigateDown.bind(this)); } initialize(){ this.subscribe("cell-init", this.bindEditor.bind(this)); this.subscribe("cell-delete", this.clearEdited.bind(this)); this.subscribe("cell-value-changed", this.updateCellClass.bind(this)); this.subscribe("column-layout", this.initializeColumnCheck.bind(this)); this.subscribe("column-delete", this.columnDeleteCheck.bind(this)); this.subscribe("row-deleting", this.rowDeleteCheck.bind(this)); this.subscribe("row-layout", this.rowEditableCheck.bind(this)); this.subscribe("data-refreshing", this.cancelEdit.bind(this)); this.subscribe("clipboard-paste", this.pasteBlocker.bind(this)); this.subscribe("keybinding-nav-prev", this.navigatePrev.bind(this, undefined)); this.subscribe("keybinding-nav-next", this.keybindingNavigateNext.bind(this)); // this.subscribe("keybinding-nav-left", this.navigateLeft.bind(this, undefined)); // this.subscribe("keybinding-nav-right", this.navigateRight.bind(this, undefined)); this.subscribe("keybinding-nav-up", this.navigateUp.bind(this, undefined)); this.subscribe("keybinding-nav-down", this.navigateDown.bind(this, undefined)); if(Object.keys(this.table.options).includes("editorEmptyValue")){ this.convertEmptyValues = true; } } /////////////////////////////////// ///////// Paste Negation ////////// /////////////////////////////////// pasteBlocker(e){ if(this.currentCell){ return true; } } /////////////////////////////////// ////// Keybinding Functions /////// /////////////////////////////////// keybindingNavigateNext(e){ var cell = this.currentCell, newRow = this.options("tabEndNewRow"); if(cell){ if(!this.navigateNext(cell, e)){ if(newRow){ cell.getElement().firstChild.blur(); if(!this.invalidEdit){ if(newRow === true){ newRow = this.table.addRow({}); }else{ if(typeof newRow == "function"){ newRow = this.table.addRow(newRow(cell.row.getComponent())); }else{ newRow = this.table.addRow(Object.assign({}, newRow)); } } newRow.then(() => { setTimeout(() => { cell.getComponent().navigateNext(); }); }); } } } } } /////////////////////////////////// ///////// Cell Functions ////////// /////////////////////////////////// cellIsEdited(cell){ return !! cell.modules.edit && cell.modules.edit.edited; } cellCancelEdit(cell){ if(cell === this.currentCell){ this.table.modules.edit.cancelEdit(); }else{ console.warn("Cancel Editor Error - This cell is not currently being edited "); } } /////////////////////////////////// ///////// Table Functions ///////// /////////////////////////////////// updateCellClass(cell){ if(this.allowEdit(cell)) { cell.getElement().classList.add("tabulator-editable"); } else { cell.getElement().classList.remove("tabulator-editable"); } } clearCellEdited(cells){ if(!cells){ cells = this.table.modules.edit.getEditedCells(); } if(!Array.isArray(cells)){ cells = [cells]; } cells.forEach((cell) => { this.table.modules.edit.clearEdited(cell._getSelf()); }); } navigatePrev(cell = this.currentCell, e){ var nextCell, prevRow; if(cell){ if(e){ e.preventDefault(); } nextCell = this.navigateLeft(); if(nextCell){ return true; }else{ prevRow = this.table.rowManager.prevDisplayRow(cell.row, true); if(prevRow){ nextCell = this.findPrevEditableCell(prevRow, prevRow.cells.length); if(nextCell){ nextCell.getComponent().edit(); return true; } } } } return false; } navigateNext(cell = this.currentCell, e){ var nextCell, nextRow; if(cell){ if(e){ e.preventDefault(); } nextCell = this.navigateRight(); if(nextCell){ return true; }else{ nextRow = this.table.rowManager.nextDisplayRow(cell.row, true); if(nextRow){ nextCell = this.findNextEditableCell(nextRow, -1); if(nextCell){ nextCell.getComponent().edit(); return true; } } } } return false; } navigateLeft(cell = this.currentCell, e){ var index, nextCell; if(cell){ if(e){ e.preventDefault(); } index = cell.getIndex(); nextCell = this.findPrevEditableCell(cell.row, index); if(nextCell){ nextCell.getComponent().edit(); return true; } } return false; } navigateRight(cell = this.currentCell, e){ var index, nextCell; if(cell){ if(e){ e.preventDefault(); } index = cell.getIndex(); nextCell = this.findNextEditableCell(cell.row, index); if(nextCell){ nextCell.getComponent().edit(); return true; } } return false; } navigateUp(cell = this.currentCell, e){ var index, nextRow; if(cell){ if(e){ e.preventDefault(); } index = cell.getIndex(); nextRow = this.table.rowManager.prevDisplayRow(cell.row, true); if(nextRow){ nextRow.cells[index].getComponent().edit(); return true; } } return false; } navigateDown(cell = this.currentCell, e){ var index, nextRow; if(cell){ if(e){ e.preventDefault(); } index = cell.getIndex(); nextRow = this.table.rowManager.nextDisplayRow(cell.row, true); if(nextRow){ nextRow.cells[index].getComponent().edit(); return true; } } return false; } findNextEditableCell(row, index){ var nextCell = false; if(index < row.cells.length-1){ for(var i = index+1; i < row.cells.length; i++){ let cell = row.cells[i]; if(cell.column.modules.edit && Helpers.elVisible(cell.getElement())){ let allowEdit = this.allowEdit(cell); if(allowEdit){ nextCell = cell; break; } } } } return nextCell; } findPrevEditableCell(row, index){ var prevCell = false; if(index > 0){ for(var i = index-1; i >= 0; i--){ let cell = row.cells[i]; if(cell.column.modules.edit && Helpers.elVisible(cell.getElement())){ let allowEdit = this.allowEdit(cell); if(allowEdit){ prevCell = cell; break; } } } } return prevCell; } /////////////////////////////////// ///////// Internal Logic ////////// /////////////////////////////////// initializeColumnCheck(column){ if(typeof column.definition.editor !== "undefined"){ this.initializeColumn(column); } } columnDeleteCheck(column){ if(this.currentCell && this.currentCell.column === column){ this.cancelEdit(); } } rowDeleteCheck(row){ if(this.currentCell && this.currentCell.row === row){ this.cancelEdit(); } } rowEditableCheck(row){ row.getCells().forEach((cell) => { if(cell.column.modules.edit && typeof cell.column.modules.edit.check === "function"){ this.updateCellClass(cell); } }); } //initialize column editor initializeColumn(column){ var convertEmpty = Object.keys(column.definition).includes("editorEmptyValue"); var config = { editor:false, blocked:false, check:column.definition.editable, params:column.definition.editorParams || {}, convertEmptyValues:convertEmpty, editorEmptyValue:column.definition.editorEmptyValue, editorEmptyValueFunc:column.definition.editorEmptyValueFunc, }; //set column editor config.editor = this.lookupEditor(column.definition.editor, column); if(config.editor){ column.modules.edit = config; } } lookupEditor(editor, column){ var editorFunc; switch(typeof editor){ case "string": if(this.editors[editor]){ editorFunc = this.editors[editor]; }else{ console.warn("Editor Error - No such editor found: ", editor); } break; case "function": editorFunc = editor; break; case "boolean": if(editor === true){ if(typeof column.definition.formatter !== "function"){ if(this.editors[column.definition.formatter]){ editorFunc = this.editors[column.definition.formatter]; }else{ editorFunc = this.editors["input"]; } }else{ console.warn("Editor Error - Cannot auto lookup editor for a custom formatter: ", column.definition.formatter); } } break; } return editorFunc; } getCurrentCell(){ return this.currentCell ? this.currentCell.getComponent() : false; } clearEditor(cancel){ var cell = this.currentCell, cellEl; this.invalidEdit = false; if(cell){ this.currentCell = false; cellEl = cell.getElement(); this.dispatch("edit-editor-clear", cell, cancel); cellEl.classList.remove("tabulator-editing"); while(cellEl.firstChild) cellEl.removeChild(cellEl.firstChild); cell.row.getElement().classList.remove("tabulator-editing"); cell.table.element.classList.remove("tabulator-editing"); } } cancelEdit(){ if(this.currentCell){ var cell = this.currentCell; var component = this.currentCell.getComponent(); this.clearEditor(true); cell.setValueActual(cell.getValue()); cell.cellRendered(); if(cell.column.definition.editor == "textarea" || cell.column.definition.variableHeight){ cell.row.normalizeHeight(true); } if(cell.column.definition.cellEditCancelled){ cell.column.definition.cellEditCancelled.call(this.table, component); } this.dispatch("edit-cancelled", cell); this.dispatchExternal("cellEditCancelled", component); } } //return a formatted value for a cell bindEditor(cell){ if(cell.column.modules.edit){ var self = this, element = cell.getElement(true); this.updateCellClass(cell); element.setAttribute("tabindex", 0); element.addEventListener("mousedown", function(e){ if (e.button === 2) { e.preventDefault(); }else{ self.mouseClick = true; } }); if(this.options("editTriggerEvent") === "dblclick"){ element.addEventListener("dblclick", function(e){ if(!element.classList.contains("tabulator-editing")){ element.focus({preventScroll: true}); self.edit(cell, e, false); } }); } if(this.options("editTriggerEvent") === "focus" || this.options("editTriggerEvent") === "click"){ element.addEventListener("click", function(e){ if(!element.classList.contains("tabulator-editing")){ element.focus({preventScroll: true}); self.edit(cell, e, false); } }); } if(this.options("editTriggerEvent") === "focus"){ element.addEventListener("focus", function(e){ if(!self.recursionBlock){ self.edit(cell, e, false); } }); } } } focusCellNoEvent(cell, block){ this.recursionBlock = true; if(!(block && this.table.browser === "ie")){ cell.getElement().focus({preventScroll: true}); } this.recursionBlock = false; } editCell(cell, forceEdit){ this.focusCellNoEvent(cell); this.edit(cell, false, forceEdit); } focusScrollAdjust(cell){ if(this.table.rowManager.getRenderMode() == "virtual"){ var topEdge = this.table.rowManager.element.scrollTop, bottomEdge = this.table.rowManager.element.clientHeight + this.table.rowManager.element.scrollTop, rowEl = cell.row.getElement(); if(rowEl.offsetTop < topEdge){ this.table.rowManager.element.scrollTop -= (topEdge - rowEl.offsetTop); }else{ if(rowEl.offsetTop + rowEl.offsetHeight > bottomEdge){ this.table.rowManager.element.scrollTop += (rowEl.offsetTop + rowEl.offsetHeight - bottomEdge); } } var leftEdge = this.table.rowManager.element.scrollLeft, rightEdge = this.table.rowManager.element.clientWidth + this.table.rowManager.element.scrollLeft, cellEl = cell.getElement(); if(this.table.modExists("frozenColumns")){ leftEdge += parseInt(this.table.modules.frozenColumns.leftMargin || 0); rightEdge -= parseInt(this.table.modules.frozenColumns.rightMargin || 0); } if(this.table.options.renderHorizontal === "virtual"){ leftEdge -= parseInt(this.table.columnManager.renderer.vDomPadLeft); rightEdge -= parseInt(this.table.columnManager.renderer.vDomPadLeft); } if(cellEl.offsetLeft < leftEdge){ this.table.rowManager.element.scrollLeft -= (leftEdge - cellEl.offsetLeft); }else{ if(cellEl.offsetLeft + cellEl.offsetWidth > rightEdge){ this.table.rowManager.element.scrollLeft += (cellEl.offsetLeft + cellEl.offsetWidth - rightEdge); } } } } allowEdit(cell) { var check = cell.column.modules.edit ? true : false; if(cell.column.modules.edit){ switch(typeof cell.column.modules.edit.check){ case "function": if(cell.row.initialized){ check = cell.column.modules.edit.check(cell.getComponent()); } break; case "string": check = !!cell.row.data[cell.column.modules.edit.check]; break; case "boolean": check = cell.column.modules.edit.check; break; } } return check; } edit(cell, e, forceEdit){ var self = this, allowEdit = true, rendered = function(){}, element = cell.getElement(), editFinished = false, cellEditor, component, params; //prevent editing if another cell is refusing to leave focus (eg. validation fail) if(this.currentCell){ if(!this.invalidEdit && this.currentCell !== cell){ this.cancelEdit(); } return; } //handle successful value change function success(value){ if(self.currentCell === cell && !editFinished){ var valid = self.chain("edit-success", [cell, value], true, true); if(valid === true || self.table.options.validationMode === "highlight"){ editFinished = true; self.clearEditor(); if(!cell.modules.edit){ cell.modules.edit = {}; } cell.modules.edit.edited = true; if(self.editedCells.indexOf(cell) == -1){ self.editedCells.push(cell); } value = self.transformEmptyValues(value, cell); cell.setValue(value, true); return valid === true; }else{ editFinished = true; self.invalidEdit = true; self.focusCellNoEvent(cell, true); rendered(); setTimeout(() => { editFinished = false; }, 10); return false; } }else{ // console.warn("Edit Success Error - cannot call success on a cell that is no longer being edited"); } } //handle aborted edit function cancel(){ // editFinished = true; if(self.currentCell === cell && !editFinished){ self.cancelEdit(); }else{ // console.warn("Edit Success Error - cannot call cancel on a cell that is no longer being edited"); } } function onRendered(callback){ rendered = callback; } if(!cell.column.modules.edit.blocked){ if(e){ e.stopPropagation(); } allowEdit = this.allowEdit(cell); if(allowEdit || forceEdit){ self.cancelEdit(); self.currentCell = cell; this.focusScrollAdjust(cell); component = cell.getComponent(); if(this.mouseClick){ this.mouseClick = false; if(cell.column.definition.cellClick){ cell.column.definition.cellClick.call(this.table, e, component); } } if(cell.column.definition.cellEditing){ cell.column.definition.cellEditing.call(this.table, component); } this.dispatch("cell-editing", cell); this.dispatchExternal("cellEditing", component); params = typeof cell.column.modules.edit.params === "function" ? cell.column.modules.edit.params(component) : cell.column.modules.edit.params; cellEditor = cell.column.modules.edit.editor.call(self, component, onRendered, success, cancel, params); //if editor returned, add to DOM, if false, abort edit if(this.currentCell && cellEditor !== false){ if(cellEditor instanceof Node){ element.classList.add("tabulator-editing"); cell.row.getElement().classList.add("tabulator-editing"); cell.table.element.classList.add("tabulator-editing"); while(element.firstChild) element.removeChild(element.firstChild); element.appendChild(cellEditor); //trigger onRendered Callback rendered(); //prevent editing from triggering rowClick event var children = element.children; for (var i = 0; i < children.length; i++) { children[i].addEventListener("click", function(e){ e.stopPropagation(); }); } }else{ console.warn("Edit Error - Editor should return an instance of Node, the editor returned:", cellEditor); this.blur(element); return false; } }else{ this.blur(element); return false; } return true; }else{ this.mouseClick = false; this.blur(element); return false; } }else{ this.mouseClick = false; this.blur(element); return false; } } emptyValueCheck(value){ return value === "" || value === null || typeof value === "undefined"; } transformEmptyValues(value, cell){ var mod = cell.column.modules.edit, convert = mod.convertEmptyValues || this.convertEmptyValues, checkFunc; if(convert){ checkFunc = mod.editorEmptyValueFunc || this.options("editorEmptyValueFunc"); if(checkFunc && checkFunc(value)){ value = mod.convertEmptyValues ? mod.editorEmptyValue : this.options("editorEmptyValue"); } } return value; } blur(element){ if(!this.confirm("edit-blur", [element]) ){ element.blur(); } } getEditedCells(){ var output = []; this.editedCells.forEach((cell) => { output.push(cell.getComponent()); }); return output; } clearEdited(cell){ var editIndex; if(cell.modules.edit && cell.modules.edit.edited){ cell.modules.edit.edited = false; this.dispatch("edit-edited-clear", cell); } editIndex = this.editedCells.indexOf(cell); if(editIndex > -1){ this.editedCells.splice(editIndex, 1); } } }