tabulator-tables
Version:
Interactive table generation JavaScript library
1,033 lines (811 loc) • 24.9 kB
JavaScript
import maskInput from './inputMask.js';
import urlBuilder from '../Ajax/defaults/urlGenerator.js';
export default class Edit{
constructor(editor, cell, onRendered, success, cancel, editorParams){
this.edit = editor;
this.table = editor.table;
this.cell = cell;
this.params = this._initializeParams(editorParams);
this.data = [];
this.displayItems = [];
this.currentItems = [];
this.focusedItem = null;
this.input = this._createInputElement();
this.listEl = this._createListElement();
this.initialValues = null;
this.isFilter = !cell._getSelf;
this.filterTimeout = null;
this.filtered = false;
this.typing = false;
this.values = [];
this.popup = null;
this.listIteration = 0;
this.lastAction="";
this.blurable = true;
this.actions = {
success:success,
cancel:cancel
};
this._deprecatedOptionsCheck();
this._initializeValue();
onRendered(this._onRendered.bind(this));
}
_deprecatedOptionsCheck(){
if(this.params.listItemFormatter){
this.cell.getTable().deprecationAdvisor.msg("The listItemFormatter editor param has been deprecated, please see the latest editor documentation for updated options");
}
if(this.params.sortValuesList){
this.cell.getTable().deprecationAdvisor.msg("The sortValuesList editor param has been deprecated, please see the latest editor documentation for updated options");
}
if(this.params.searchFunc){
this.cell.getTable().deprecationAdvisor.msg("The searchFunc editor param has been deprecated, please see the latest editor documentation for updated options");
}
if(this.params.searchingPlaceholder){
this.cell.getTable().deprecationAdvisor.msg("The searchingPlaceholder editor param has been deprecated, please see the latest editor documentation for updated options");
}
}
_initializeValue(){
var initialValue = this.cell.getValue();
if(typeof initialValue === "undefined" && typeof this.params.defaultValue !== "undefined"){
initialValue = this.params.defaultValue;
}
this.initialValues = this.params.multiselect ? initialValue : [initialValue];
if(this.isFilter){
this.input.value = this.initialValues ? this.initialValues.join(",") : "";
this.headerFilterInitialListGen();
}
}
_onRendered(){
var cellEl = this.cell.getElement();
function clickStop(e){
e.stopPropagation();
}
this.input.style.height = "100%";
this.input.focus({preventScroll: true});
cellEl.addEventListener("click", clickStop);
setTimeout(() => {
cellEl.removeEventListener("click", clickStop);
}, 1000);
this.input.addEventListener("mousedown", this._preventPopupBlur.bind(this));
}
_createListElement(){
var listEl = document.createElement("div");
listEl.classList.add("tabulator-edit-list");
listEl.addEventListener("mousedown", this._preventBlur.bind(this));
listEl.addEventListener("keydown", this._inputKeyDown.bind(this));
return listEl;
}
_setListWidth(){
var element = this.isFilter ? this.input : this.cell.getElement();
this.listEl.style.minWidth = element.offsetWidth + "px";
if(this.params.maxWidth){
if(this.params.maxWidth === true){
this.listEl.style.maxWidth = element.offsetWidth + "px";
}else if(typeof this.params.maxWidth === "number"){
this.listEl.style.maxWidth = this.params.maxWidth + "px";
}else{
this.listEl.style.maxWidth = this.params.maxWidth;
}
}
}
_createInputElement(){
var attribs = this.params.elementAttributes;
var input = document.createElement("input");
input.setAttribute("type", this.params.clearable ? "search" : "text");
input.style.padding = "4px";
input.style.width = "100%";
input.style.boxSizing = "border-box";
if(!this.params.autocomplete){
input.style.cursor = "default";
input.style.caretColor = "transparent";
// input.readOnly = (this.edit.currentCell != false);
}
if(attribs && typeof attribs == "object"){
for (let key in attribs){
if(key.charAt(0) == "+"){
key = key.slice(1);
input.setAttribute(key, input.getAttribute(key) + attribs["+" + key]);
}else{
input.setAttribute(key, attribs[key]);
}
}
}
if(this.params.mask){
maskInput(input, this.params);
}
this._bindInputEvents(input);
return input;
}
_initializeParams(params){
var valueKeys = ["values", "valuesURL", "valuesLookup"],
valueCheck;
params = Object.assign({}, params);
params.verticalNavigation = params.verticalNavigation || "editor";
params.placeholderLoading = typeof params.placeholderLoading === "undefined" ? "Searching ..." : params.placeholderLoading;
params.placeholderEmpty = typeof params.placeholderEmpty === "undefined" ? "No Results Found" : params.placeholderEmpty;
params.filterDelay = typeof params.filterDelay === "undefined" ? 300 : params.filterDelay;
params.emptyValue = Object.keys(params).includes("emptyValue") ? params.emptyValue : "";
valueCheck = Object.keys(params).filter(key => valueKeys.includes(key)).length;
if(!valueCheck){
console.warn("list editor config error - either the values, valuesURL, or valuesLookup option must be set");
}else if(valueCheck > 1){
console.warn("list editor config error - only one of the values, valuesURL, or valuesLookup options can be set on the same editor");
}
if(params.autocomplete){
if(params.multiselect){
params.multiselect = false;
console.warn("list editor config error - multiselect option is not available when autocomplete is enabled");
}
}else{
if(params.freetext){
params.freetext = false;
console.warn("list editor config error - freetext option is only available when autocomplete is enabled");
}
if(params.filterFunc){
params.filterFunc = false;
console.warn("list editor config error - filterFunc option is only available when autocomplete is enabled");
}
if(params.filterRemote){
params.filterRemote = false;
console.warn("list editor config error - filterRemote option is only available when autocomplete is enabled");
}
if(params.mask){
params.mask = false;
console.warn("list editor config error - mask option is only available when autocomplete is enabled");
}
if(params.allowEmpty){
params.allowEmpty = false;
console.warn("list editor config error - allowEmpty option is only available when autocomplete is enabled");
}
if(params.listOnEmpty){
params.listOnEmpty = false;
console.warn("list editor config error - listOnEmpty option is only available when autocomplete is enabled");
}
}
if(params.filterRemote && !(typeof params.valuesLookup === "function" || params.valuesURL)){
params.filterRemote = false;
console.warn("list editor config error - filterRemote option should only be used when values list is populated from a remote source");
}
return params;
}
//////////////////////////////////////
////////// Event Handling ////////////
//////////////////////////////////////
_bindInputEvents(input){
input.addEventListener("focus", this._inputFocus.bind(this));
input.addEventListener("click", this._inputClick.bind(this));
input.addEventListener("blur", this._inputBlur.bind(this));
input.addEventListener("keydown", this._inputKeyDown.bind(this));
input.addEventListener("search", this._inputSearch.bind(this));
if(this.params.autocomplete){
input.addEventListener("keyup", this._inputKeyUp.bind(this));
}
}
_inputFocus(e){
this.rebuildOptionsList();
}
_filter(){
if(this.params.filterRemote){
clearTimeout(this.filterTimeout);
this.filterTimeout = setTimeout(() => {
this.rebuildOptionsList();
}, this.params.filterDelay);
}else{
this._filterList();
}
}
_inputClick(e){
e.stopPropagation();
}
_inputBlur(e){
if(this.blurable){
if(this.popup){
this.popup.hide();
}else{
this._resolveValue(true);
}
}
}
_inputSearch(){
this._clearChoices();
}
_inputKeyDown(e){
switch(e.keyCode){
case 38: //up arrow
this._keyUp(e);
break;
case 40: //down arrow
this._keyDown(e);
break;
case 37: //left arrow
case 39: //right arrow
this._keySide(e);
break;
case 13: //enter
this._keyEnter();
break;
case 27: //escape
this._keyEsc();
break;
case 36: //home
case 35: //end
this._keyHomeEnd(e);
break;
case 9: //tab
break;
default:
this._keySelectLetter(e);
}
}
_inputKeyUp(e){
switch(e.keyCode){
case 38: //up arrow
case 37: //left arrow
case 39: //up arrow
case 40: //right arrow
case 13: //enter
case 27: //escape
break;
default:
this._keyAutoCompLetter(e);
}
}
_preventPopupBlur(){
if(this.popup){
this.popup.blockHide();
}
setTimeout(() =>{
if(this.popup){
this.popup.restoreHide();
}
}, 10);
}
_preventBlur(){
this.blurable = false;
setTimeout(() =>{
this.blurable = true;
}, 10);
}
//////////////////////////////////////
//////// Keyboard Navigation /////////
//////////////////////////////////////
_keyUp(e){
var index = this.displayItems.indexOf(this.focusedItem);
if(this.params.verticalNavigation == "editor" || (this.params.verticalNavigation == "hybrid" && index)){
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
if(index > 0){
this._focusItem(this.displayItems[index - 1]);
}
}
}
_keyDown(e){
var index = this.displayItems.indexOf(this.focusedItem);
if(this.params.verticalNavigation == "editor" || (this.params.verticalNavigation == "hybrid" && index < this.displayItems.length - 1)){
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
if(index < this.displayItems.length - 1){
if(index == -1){
this._focusItem(this.displayItems[0]);
}else{
this._focusItem(this.displayItems[index + 1]);
}
}
}
}
_keySide(e){
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
}
_keyEnter(e){
if(this.params.autocomplete && this.lastAction === "typing"){
this._resolveValue(true);
}else{
if(this.focusedItem){
this._chooseItem(this.focusedItem);
}
}
}
_keyEsc(e){
this._cancel();
}
_keyHomeEnd(e){
if(this.params.autocomplete){
//prevent table navigation while using input element
e.stopImmediatePropagation();
}
}
_keySelectLetter(e){
if(!this.params.autocomplete){
// if(this.edit.currentCell === false){
e.preventDefault();
// }
if(e.keyCode >= 38 && e.keyCode <= 90){
this._scrollToValue(e.keyCode);
}
}
}
_keyAutoCompLetter(e){
this._filter();
this.lastAction = "typing";
this.typing = true;
}
_scrollToValue(char){
clearTimeout(this.filterTimeout);
var character = String.fromCharCode(char).toLowerCase();
this.filterTerm += character.toLowerCase();
var match = this.displayItems.find((item) => {
return typeof item.label !== "undefined" && item.label.toLowerCase().startsWith(this.filterTerm);
});
if(match){
this._focusItem(match);
}
this.filterTimeout = setTimeout(() => {
this.filterTerm = "";
}, 800);
}
_focusItem(item){
this.lastAction = "focus";
if(this.focusedItem && this.focusedItem.element){
this.focusedItem.element.classList.remove("focused");
}
this.focusedItem = item;
if(item && item.element){
item.element.classList.add("focused");
item.element.scrollIntoView({behavior: 'smooth', block: 'nearest', inline: 'start'});
}
}
//////////////////////////////////////
/////// Data List Generation /////////
//////////////////////////////////////
headerFilterInitialListGen(){
this._generateOptions(true);
}
rebuildOptionsList(){
this._generateOptions()
.then(this._sortOptions.bind(this))
.then(this._buildList.bind(this))
.then(this._showList.bind(this))
.catch((e) => {
if(!Number.isInteger(e)){
console.error("List generation error", e);
}
});
}
_filterList(){
this._buildList(this._filterOptions());
this._showList();
}
_generateOptions(silent){
var values = [];
var iteration = ++ this.listIteration;
this.filtered = false;
if(this.params.values){
values = this.params.values;
}else if (this.params.valuesURL){
values = this._ajaxRequest(this.params.valuesURL, this.input.value);
}else{
if(typeof this.params.valuesLookup === "function"){
values = this.params.valuesLookup(this.cell, this.input.value);
}else if(this.params.valuesLookup){
values = this._uniqueColumnValues(this.params.valuesLookupField);
}
}
if(values instanceof Promise){
if(!silent){
this._addPlaceholder(this.params.placeholderLoading);
}
return values.then()
.then((responseValues) => {
if(this.listIteration === iteration){
return this._parseList(responseValues);
}else{
return Promise.reject(iteration);
}
});
}else{
return Promise.resolve(this._parseList(values));
}
}
_addPlaceholder(contents){
var placeholder = document.createElement("div");
if(typeof contents === "function"){
contents = contents(this.cell.getComponent(), this.listEl);
}
if(contents){
this._clearList();
if(contents instanceof HTMLElement){
placeholder = contents;
}else{
placeholder.classList.add("tabulator-edit-list-placeholder");
placeholder.innerHTML = contents;
}
this.listEl.appendChild(placeholder);
this._showList();
}
}
_ajaxRequest(url, term){
var params = this.params.filterRemote ? {term:term} : {};
url = urlBuilder(url, {}, params);
return fetch(url)
.then((response)=>{
if(response.ok) {
return response.json()
.catch((error)=>{
console.warn("List Ajax Load Error - Invalid JSON returned", error);
return Promise.reject(error);
});
}else{
console.error("List Ajax Load Error - Connection Error: " + response.status, response.statusText);
return Promise.reject(response);
}
})
.catch((error)=>{
console.error("List Ajax Load Error - Connection Error: ", error);
return Promise.reject(error);
});
}
_uniqueColumnValues(field){
var output = {},
data = this.table.getData(this.params.valuesLookup),
column;
if(field){
column = this.table.columnManager.getColumnByField(field);
}else{
column = this.cell.getColumn()._getSelf();
}
if(column){
data.forEach((row) => {
var val = column.getFieldValue(row);
if(val !== null && typeof val !== "undefined" && val !== ""){
output[val] = true;
}
});
}else{
console.warn("unable to find matching column to create select lookup list:", field);
output = [];
}
return Object.keys(output);
}
_parseList(inputValues){
var data = [];
if(!Array.isArray(inputValues)){
inputValues = Object.entries(inputValues).map(([key, value]) => {
return {
label:value,
value:key,
};
});
}
inputValues.forEach((value) => {
if(typeof value !== "object"){
value = {
label:value,
value:value,
};
}
this._parseListItem(value, data, 0);
});
if(!this.currentItems.length && this.params.freetext){
this.input.value = this.initialValues;
this.typing = true;
this.lastAction = "typing";
}
this.data = data;
return data;
}
_parseListItem(option, data, level){
var item = {};
if(option.options){
item = this._parseListGroup(option, level + 1);
}else{
item = {
label:option.label,
value:option.value,
itemParams:option.itemParams,
elementAttributes: option.elementAttributes,
element:false,
selected:false,
visible:true,
level:level,
original:option,
};
if(this.initialValues && this.initialValues.indexOf(option.value) > -1){
this._chooseItem(item, true);
}
}
data.push(item);
}
_parseListGroup(option, level){
var item = {
label:option.label,
group:true,
itemParams:option.itemParams,
elementAttributes:option.elementAttributes,
element:false,
visible:true,
level:level,
options:[],
original:option,
};
option.options.forEach((child) => {
this._parseListItem(child, item.options, level);
});
return item;
}
_sortOptions(options){
var sorter;
if(this.params.sort){
sorter = typeof this.params.sort === "function" ? this.params.sort : this._defaultSortFunction.bind(this);
this._sortGroup(sorter, options);
}
return options;
}
_sortGroup(sorter, options){
options.sort((a,b) => {
return sorter(a.label, b.label, a.value, b.value, a.original, b.original);
});
options.forEach((option) => {
if(option.group){
this._sortGroup(sorter, option.options);
}
});
}
_defaultSortFunction(as, bs){
var a, b, a1, b1, i= 0, L, rx = /(\d+)|(\D+)/g, rd = /\d/;
var emptyAlign = 0;
if(this.params.sort === "desc"){
[as, bs] = [bs, as];
}
//handle empty values
if(!as && as!== 0){
emptyAlign = !bs && bs!== 0 ? 0 : -1;
}else if(!bs && bs!== 0){
emptyAlign = 1;
}else{
if(isFinite(as) && isFinite(bs)) return as - bs;
a = String(as).toLowerCase();
b = String(bs).toLowerCase();
if(a === b) return 0;
if(!(rd.test(a) && rd.test(b))) return a > b ? 1 : -1;
a = a.match(rx);
b = b.match(rx);
L = a.length > b.length ? b.length : a.length;
while(i < L){
a1= a[i];
b1= b[i++];
if(a1 !== b1){
if(isFinite(a1) && isFinite(b1)){
if(a1.charAt(0) === "0") a1 = "." + a1;
if(b1.charAt(0) === "0") b1 = "." + b1;
return a1 - b1;
}
else return a1 > b1 ? 1 : -1;
}
}
return a.length > b.length;
}
return emptyAlign;
}
_filterOptions(){
var filterFunc = this.params.filterFunc || this._defaultFilterFunc,
term = this.input.value;
if(term){
this.filtered = true;
this.data.forEach((item) => {
this._filterItem(filterFunc, term, item);
});
}else{
this.filtered = false;
}
return this.data;
}
_filterItem(func, term, item){
var matches = false;
if(!item.group){
item.visible = func(term, item.label, item.value, item.original);
}else{
item.options.forEach((option) => {
if(this._filterItem(func, term, option)){
matches = true;
}
});
item.visible = matches;
}
return item.visible;
}
_defaultFilterFunc(term, label, value, item){
term = String(term).toLowerCase();
if(label !== null && typeof label !== "undefined"){
if(String(label).toLowerCase().indexOf(term) > -1 || String(value).toLowerCase().indexOf(term) > -1){
return true;
}
}
return false;
}
//////////////////////////////////////
/////////// Display List /////////////
//////////////////////////////////////
_clearList(){
while(this.listEl.firstChild) this.listEl.removeChild(this.listEl.firstChild);
this.displayItems = [];
}
_buildList(data){
this._clearList();
data.forEach((option) => {
this._buildItem(option);
});
if(!this.displayItems.length){
this._addPlaceholder(this.params.placeholderEmpty);
}
}
_buildItem(item){
var el = item.element,
contents;
if(!this.filtered || item.visible){
if(!el){
el = document.createElement("div");
el.tabIndex = 0;
contents = this.params.itemFormatter ? this.params.itemFormatter(item.label, item.value, item.original, el) : item.label;
if(contents instanceof HTMLElement){
el.appendChild(contents);
}else{
el.innerHTML = contents;
}
if(item.group){
el.classList.add("tabulator-edit-list-group");
}else{
el.classList.add("tabulator-edit-list-item");
}
el.classList.add("tabulator-edit-list-group-level-" + item.level);
if(item.elementAttributes && typeof item.elementAttributes == "object"){
for (let key in item.elementAttributes){
if(key.charAt(0) == "+"){
key = key.slice(1);
el.setAttribute(key, this.input.getAttribute(key) + item.elementAttributes["+" + key]);
}else{
el.setAttribute(key, item.elementAttributes[key]);
}
}
}
if(item.group){
el.addEventListener("click", this._groupClick.bind(this, item));
}else{
el.addEventListener("click", this._itemClick.bind(this, item));
}
el.addEventListener("mousedown", this._preventBlur.bind(this));
item.element = el;
}
this._styleItem(item);
this.listEl.appendChild(el);
if(item.group){
item.options.forEach((option) => {
this._buildItem(option);
});
}else{
this.displayItems.push(item);
}
}
}
_showList(){
var startVis = this.popup && this.popup.isVisible();
if(this.input.parentNode){
if(this.params.autocomplete && this.input.value === "" && !this.params.listOnEmpty){
if(this.popup){
this.popup.hide(true);
}
return;
}
this._setListWidth();
if(!this.popup){
this.popup = this.edit.popup(this.listEl);
}
this.popup.show(this.cell.getElement(), "bottom");
if(!startVis){
setTimeout(() => {
this.popup.hideOnBlur(this._resolveValue.bind(this, true));
}, 10);
}
}
}
_styleItem(item){
if(item && item.element){
if(item.selected){
item.element.classList.add("active");
}else{
item.element.classList.remove("active");
}
}
}
//////////////////////////////////////
///////// User Interaction ///////////
//////////////////////////////////////
_itemClick(item, e){
e.stopPropagation();
this._chooseItem(item);
}
_groupClick(item, e){
e.stopPropagation();
}
//////////////////////////////////////
////// Current Item Management ///////
//////////////////////////////////////
_cancel(){
this.popup.hide(true);
this.actions.cancel();
}
_clearChoices(){
this.typing = true;
this.currentItems.forEach((item) => {
item.selected = false;
this._styleItem(item);
});
this.currentItems = [];
this.focusedItem = null;
}
_chooseItem(item, silent){
var index;
this.typing = false;
if(this.params.multiselect){
index = this.currentItems.indexOf(item);
if(index > -1){
this.currentItems.splice(index, 1);
item.selected = false;
}else{
this.currentItems.push(item);
item.selected = true;
}
this.input.value = this.currentItems.map(item => item.label).join(",");
this._styleItem(item);
}else{
this.currentItems = [item];
item.selected = true;
this.input.value = item.label;
this._styleItem(item);
if(!silent){
this._resolveValue();
}
}
this._focusItem(item);
}
_resolveValue(blur){
var output, initialValue;
if(this.popup){
this.popup.hide(true);
}
if(this.params.multiselect){
output = this.currentItems.map(item => item.value);
}else{
if(blur && this.params.autocomplete && this.typing){
if(this.params.freetext || (this.params.allowEmpty && this.input.value === "")){
output = this.input.value;
}else{
this.actions.cancel();
return;
}
}else{
if(this.currentItems[0]){
output = this.currentItems[0].value;
}else{
initialValue = this.initialValues[0];
if(initialValue === null || typeof initialValue === "undefined" || initialValue === ""){
output = initialValue;
}else{
output = this.params.emptyValue;
}
}
}
}
if(output === ""){
output = this.params.emptyValue;
}
this.actions.success(output);
if(this.isFilter){
this.initialValues = output && !Array.isArray(output) ? [output] : output;
this.currentItems = [];
}
}
}