UNPKG

tabulator-tables

Version:

Interactive table generation JavaScript library

1,033 lines (811 loc) 24.9 kB
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 = []; } } }