tabulator-tables
Version:
Interactive table generation JavaScript library
726 lines (560 loc) • 18.7 kB
JavaScript
import Module from '../../core/Module.js';
import Helpers from '../../core/tools/Helpers.js';
import defaultEditors from './defaults/editors.js';
class Edit extends Module{
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.editors = Edit.editors;
this.registerColumnOption("editable");
this.registerColumnOption("editor");
this.registerColumnOption("editorParams");
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("data-refreshing", this.cancelEdit.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));
}
///////////////////////////////////
////// 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(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.findNextEditableCell(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();
}
}
//initialize column editor
initializeColumn(column){
var config = {
editor:false,
blocked:false,
check:column.definition.editable,
params:column.definition.editorParams || {}
};
//set column editor
switch(typeof column.definition.editor){
case "string":
if(this.editors[column.definition.editor]){
config.editor = this.editors[column.definition.editor];
}else{
console.warn("Editor Error - No such editor found: ", column.definition.editor);
}
break;
case "function":
config.editor = column.definition.editor;
break;
case "boolean":
if(column.definition.editor === true){
if(typeof column.definition.formatter !== "function"){
if(this.editors[column.definition.formatter]){
config.editor = this.editors[column.definition.formatter];
}else{
config.editor = this.editors["input"];
}
}else{
console.warn("Editor Error - Cannot auto lookup editor for a custom formatter: ", column.definition.formatter);
}
}
break;
}
if(config.editor){
column.modules.edit = config;
}
}
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("click", function(e){
if(!element.classList.contains("tabulator-editing")){
element.focus({preventScroll: true});
}
});
element.addEventListener("mousedown", function(e){
if (e.button === 2) {
e.preventDefault();
}else{
self.mouseClick = true;
}
});
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);
rightEdge -= parseInt(this.table.modules.frozenColumns.rightMargin);
}
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":
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(),
cellEditor, component, params;
//prevent editing if another cell is refusing to leave focus (eg. validation fail)
if(this.currentCell){
if(!this.invalidEdit){
this.cancelEdit();
}
return;
}
//handle successful value change
function success(value){
if(self.currentCell === cell){
var valid = self.chain("edit-success", [cell, value], true, true);
if(valid === true || self.table.options.validationMode === "highlight"){
self.clearEditor();
if(!cell.modules.edit){
cell.modules.edit = {};
}
cell.modules.edit.edited = true;
if(self.editedCells.indexOf(cell) == -1){
self.editedCells.push(cell);
}
cell.setValue(value, true);
return valid === true;
}else{
self.invalidEdit = true;
self.focusCellNoEvent(cell, true);
rendered();
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(){
if(self.currentCell === cell){
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(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);
element.blur();
return false;
}
}else{
element.blur();
return false;
}
return true;
}else{
this.mouseClick = false;
element.blur();
return false;
}
}else{
this.mouseClick = false;
element.blur();
return false;
}
}
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);
}
}
}
Edit.moduleName = "edit";
//load defaults
Edit.editors = defaultEditors;
export default Edit;