UNPKG

tabulator-tables

Version:

Interactive table generation JavaScript library

1,059 lines (815 loc) 26.6 kB
import CoreFeature from './CoreFeature.js'; import Row from './row/Row.js'; import RowComponent from './row/RowComponent.js'; import Helpers from './tools/Helpers.js'; import RendererBasicVertical from './rendering/renderers/BasicVertical.js'; import RendererVirtualDomVertical from './rendering/renderers/VirtualDomVertical.js'; export default class RowManager extends CoreFeature{ constructor(table){ super(table); this.element = this.createHolderElement(); //containing element this.tableElement = this.createTableElement(); //table element this.heightFixer = this.createTableElement(); //table element this.placeholder = null; //placeholder element this.placeholderContents = null; //placeholder element this.firstRender = false; //handle first render this.renderMode = "virtual"; //current rendering mode this.fixedHeight = false; //current rendering mode this.rows = []; //hold row data objects this.activeRowsPipeline = []; //hold calculation of active rows this.activeRows = []; //rows currently available to on display in the table this.activeRowsCount = 0; //count of active rows this.displayRows = []; //rows currently on display in the table this.displayRowsCount = 0; //count of display rows this.scrollTop = 0; this.scrollLeft = 0; this.redrawBlock = false; //prevent redraws to allow multiple data manipulations before continuing this.redrawBlockRestoreConfig = false; //store latest redraw function calls for when redraw is needed this.redrawBlockRenderInPosition = false; //store latest redraw function calls for when redraw is needed this.dataPipeline = []; //hold data pipeline tasks this.displayPipeline = []; //hold data display pipeline tasks this.renderer = null; } //////////////// Setup Functions ///////////////// createHolderElement (){ var el = document.createElement("div"); el.classList.add("tabulator-tableholder"); el.setAttribute("tabindex", 0); // el.setAttribute("role", "rowgroup"); return el; } createTableElement (){ var el = document.createElement("div"); el.classList.add("tabulator-table"); el.setAttribute("role", "rowgroup"); return el; } initializePlaceholder(){ //configure placeholder element if(typeof this.table.options.placeholder == "string"){ let el = document.createElement("div"); el.classList.add("tabulator-placeholder"); let contents = document.createElement("div"); contents.classList.add("tabulator-placeholder-contents"); contents.innerHTML = this.table.options.placeholder; el.appendChild(contents); this.placeholderContents = contents; this.placeholder = el; } } //return containing element getElement(){ return this.element; } //return table element getTableElement(){ return this.tableElement; } initialize(){ this.initializePlaceholder(); this.initializeRenderer(); //initialize manager this.element.appendChild(this.tableElement); this.firstRender = true; //scroll header along with table body this.element.addEventListener("scroll", () => { var left = this.element.scrollLeft, leftDir = this.scrollLeft > left, top = this.element.scrollTop, topDir = this.scrollTop > top; //handle horizontal scrolling if(this.scrollLeft != left){ this.scrollLeft = left; this.dispatch("scroll-horizontal", left, leftDir); this.dispatchExternal("scrollHorizontal", left, leftDir); this._positionPlaceholder(); } //handle vertical scrolling if(this.scrollTop != top){ this.scrollTop = top; this.renderer.scrollRows(top, topDir); this.dispatch("scroll-vertical", top, topDir); this.dispatchExternal("scrollVertical", top, topDir); } }); } ////////////////// Row Manipulation ////////////////// findRow(subject){ if(typeof subject == "object"){ if(subject instanceof Row){ //subject is row element return subject; }else if(subject instanceof RowComponent){ //subject is public row component return subject._getSelf() || false; }else if(typeof HTMLElement !== "undefined" && subject instanceof HTMLElement){ //subject is a HTML element of the row let match = this.rows.find((row) => { return row.getElement() === subject; }); return match || false; }else if(subject === null){ return false; } }else if(typeof subject == "undefined"){ return false; }else{ //subject should be treated as the index of the row let match = this.rows.find((row) => { return row.data[this.table.options.index] == subject; }); return match || false; } //catch all for any other type of input return false; } getRowFromDataObject(data){ var match = this.rows.find((row) => { return row.data === data; }); return match || false; } getRowFromPosition(position){ return this.getDisplayRows().find((row) => { return row.getPosition() === position && row.isDisplayed(); }); } scrollToRow(row, position, ifVisible){ return this.renderer.scrollToRowPosition(row, position, ifVisible); } ////////////////// Data Handling ////////////////// setData(data, renderInPosition, columnsChanged){ return new Promise((resolve, reject)=>{ if(renderInPosition && this.getDisplayRows().length){ if(this.table.options.pagination){ this._setDataActual(data, true); }else{ this.reRenderInPosition(() => { this._setDataActual(data); }); } }else{ if(this.table.options.autoColumns && columnsChanged && this.table.initialized){ this.table.columnManager.generateColumnsFromRowData(data); } this.resetScroll(); this._setDataActual(data); } resolve(); }); } _setDataActual(data, renderInPosition){ this.dispatchExternal("dataProcessing", data); this._wipeElements(); if(Array.isArray(data)){ this.dispatch("data-processing", data); data.forEach((def, i) => { if(def && typeof def === "object"){ var row = new Row(def, this); this.rows.push(row); }else{ console.warn("Data Loading Warning - Invalid row data detected and ignored, expecting object but received:", def); } }); this.refreshActiveData(false, false, renderInPosition); this.dispatch("data-processed", data); this.dispatchExternal("dataProcessed", data); }else{ console.error("Data Loading Error - Unable to process data due to invalid data type \nExpecting: array \nReceived: ", typeof data, "\nData: ", data); } } _wipeElements(){ this.dispatch("rows-wipe"); this.rows.forEach((row) => { row.wipe(); }); this.rows = []; this.activeRows = []; this.activeRowsPipeline = []; this.activeRowsCount = 0; this.displayRows = []; this.displayRowsCount = 0; this.adjustTableSize(); } deleteRow(row, blockRedraw){ var allIndex = this.rows.indexOf(row), activeIndex = this.activeRows.indexOf(row); if(activeIndex > -1){ this.activeRows.splice(activeIndex, 1); } if(allIndex > -1){ this.rows.splice(allIndex, 1); } this.setActiveRows(this.activeRows); this.displayRowIterator((rows) => { var displayIndex = rows.indexOf(row); if(displayIndex > -1){ rows.splice(displayIndex, 1); } }); if(!blockRedraw){ this.reRenderInPosition(); } this.regenerateRowPositions(); this.dispatchExternal("rowDeleted", row.getComponent()); if(!this.displayRowsCount){ this._showPlaceholder(); } if(this.subscribedExternal("dataChanged")){ this.dispatchExternal("dataChanged", this.getData()); } } addRow(data, pos, index, blockRedraw){ var row = this.addRowActual(data, pos, index, blockRedraw); return row; } //add multiple rows addRows(data, pos, index){ var rows = []; return new Promise((resolve, reject) => { pos = this.findAddRowPos(pos); if(!Array.isArray(data)){ data = [data]; } if((typeof index == "undefined" && pos) || (typeof index !== "undefined" && !pos)){ data.reverse(); } data.forEach((item, i) => { var row = this.addRow(item, pos, index, true); rows.push(row); this.dispatch("row-added", row, data, pos, index); }); this.refreshActiveData(false, false, true); this.regenerateRowPositions(); if(rows.length){ this._clearPlaceholder(); } resolve(rows); }); } findAddRowPos(pos){ if(typeof pos === "undefined"){ pos = this.table.options.addRowPos; } if(pos === "pos"){ pos = true; } if(pos === "bottom"){ pos = false; } return pos; } addRowActual(data, pos, index, blockRedraw){ var row = data instanceof Row ? data : new Row(data || {}, this), top = this.findAddRowPos(pos), allIndex = -1, activeIndex, chainResult; if(!index){ chainResult = this.chain("row-adding-position", [row, top], null, {index, top}); index = chainResult.index; top = chainResult.top; } if(typeof index !== "undefined"){ index = this.findRow(index); } index = this.chain("row-adding-index", [row, index, top], null, index); if(index){ allIndex = this.rows.indexOf(index); } if(index && allIndex > -1){ activeIndex = this.activeRows.indexOf(index); this.displayRowIterator(function(rows){ var displayIndex = rows.indexOf(index); if(displayIndex > -1){ rows.splice((top ? displayIndex : displayIndex + 1), 0, row); } }); if(activeIndex > -1){ this.activeRows.splice((top ? activeIndex : activeIndex + 1), 0, row); } this.rows.splice((top ? allIndex : allIndex + 1), 0, row); }else{ if(top){ this.displayRowIterator(function(rows){ rows.unshift(row); }); this.activeRows.unshift(row); this.rows.unshift(row); }else{ this.displayRowIterator(function(rows){ rows.push(row); }); this.activeRows.push(row); this.rows.push(row); } } this.setActiveRows(this.activeRows); this.dispatchExternal("rowAdded", row.getComponent()); if(this.subscribedExternal("dataChanged")){ this.dispatchExternal("dataChanged", this.table.rowManager.getData()); } if(!blockRedraw){ this.reRenderInPosition(); } return row; } moveRow(from, to, after){ this.dispatch("row-move", from, to, after); this.moveRowActual(from, to, after); this.regenerateRowPositions(); this.dispatch("row-moved", from, to, after); this.dispatchExternal("rowMoved", from.getComponent()); } moveRowActual(from, to, after){ this.moveRowInArray(this.rows, from, to, after); this.moveRowInArray(this.activeRows, from, to, after); this.displayRowIterator((rows) => { this.moveRowInArray(rows, from, to, after); }); this.dispatch("row-moving", from, to, after); } moveRowInArray(rows, from, to, after){ var fromIndex, toIndex, start, end; if(from !== to){ fromIndex = rows.indexOf(from); if (fromIndex > -1) { rows.splice(fromIndex, 1); toIndex = rows.indexOf(to); if (toIndex > -1) { if(after){ rows.splice(toIndex+1, 0, from); }else{ rows.splice(toIndex, 0, from); } }else{ rows.splice(fromIndex, 0, from); } } //restyle rows if(rows === this.getDisplayRows()){ start = fromIndex < toIndex ? fromIndex : toIndex; end = toIndex > fromIndex ? toIndex : fromIndex +1; for(let i = start; i <= end; i++){ if(rows[i]){ this.styleRow(rows[i], i); } } } } } clearData(){ this.setData([]); } getRowIndex(row){ return this.findRowIndex(row, this.rows); } getDisplayRowIndex(row){ var index = this.getDisplayRows().indexOf(row); return index > -1 ? index : false; } nextDisplayRow(row, rowOnly){ var index = this.getDisplayRowIndex(row), nextRow = false; if(index !== false && index < this.displayRowsCount -1){ nextRow = this.getDisplayRows()[index+1]; } if(nextRow && (!(nextRow instanceof Row) || nextRow.type != "row")){ return this.nextDisplayRow(nextRow, rowOnly); } return nextRow; } prevDisplayRow(row, rowOnly){ var index = this.getDisplayRowIndex(row), prevRow = false; if(index){ prevRow = this.getDisplayRows()[index-1]; } if(rowOnly && prevRow && (!(prevRow instanceof Row) || prevRow.type != "row")){ return this.prevDisplayRow(prevRow, rowOnly); } return prevRow; } findRowIndex(row, list){ var rowIndex; row = this.findRow(row); if(row){ rowIndex = list.indexOf(row); if(rowIndex > -1){ return rowIndex; } } return false; } getData(active, transform){ var output = [], rows = this.getRows(active); rows.forEach(function(row){ if(row.type == "row"){ output.push(row.getData(transform || "data")); } }); return output; } getComponents(active){ var output = [], rows = this.getRows(active); rows.forEach(function(row){ output.push(row.getComponent()); }); return output; } getDataCount(active){ var rows = this.getRows(active); return rows.length; } scrollHorizontal(left){ this.scrollLeft = left; this.element.scrollLeft = left; this.dispatch("scroll-horizontal", left); } registerDataPipelineHandler(handler, priority){ if(typeof priority !== "undefined"){ this.dataPipeline.push({handler, priority}); this.dataPipeline.sort((a, b) => { return a.priority - b.priority; }); }else{ console.error("Data pipeline handlers must have a priority in order to be registered"); } } registerDisplayPipelineHandler(handler, priority){ if(typeof priority !== "undefined"){ this.displayPipeline.push({handler, priority}); this.displayPipeline.sort((a, b) => { return a.priority - b.priority; }); }else{ console.error("Display pipeline handlers must have a priority in order to be registered"); } } //set active data set refreshActiveData(handler, skipStage, renderInPosition){ var table = this.table, stage = "", index = 0, cascadeOrder = ["all", "dataPipeline", "display", "displayPipeline", "end"]; if(!this.table.destroyed){ if(typeof handler === "function"){ index = this.dataPipeline.findIndex((item) => { return item.handler === handler; }); if(index > -1){ stage = "dataPipeline"; if(skipStage){ if(index == this.dataPipeline.length - 1){ stage = "display"; }else{ index++; } } }else{ index = this.displayPipeline.findIndex((item) => { return item.handler === handler; }); if(index > -1){ stage = "displayPipeline"; if(skipStage){ if(index == this.displayPipeline.length - 1){ stage = "end"; }else{ index++; } } }else{ console.error("Unable to refresh data, invalid handler provided", handler); return; } } }else{ stage = handler || "all"; index = 0; } if(this.redrawBlock){ if(!this.redrawBlockRestoreConfig || (this.redrawBlockRestoreConfig && ((this.redrawBlockRestoreConfig.stage === stage && index < this.redrawBlockRestoreConfig.index) || (cascadeOrder.indexOf(stage) < cascadeOrder.indexOf(this.redrawBlockRestoreConfig.stage))))){ this.redrawBlockRestoreConfig = { handler: handler, skipStage: skipStage, renderInPosition: renderInPosition, stage:stage, index:index, }; } return; }else{ if(Helpers.elVisible(this.element)){ if(renderInPosition){ this.reRenderInPosition(this.refreshPipelines.bind(this, handler, stage, index, renderInPosition)); }else{ this.refreshPipelines(handler, stage, index, renderInPosition); if(!handler){ this.table.columnManager.renderer.renderColumns(); } this.renderTable(); if(table.options.layoutColumnsOnNewData){ this.table.columnManager.redraw(true); } } }else{ this.refreshPipelines(handler, stage, index, renderInPosition); } this.dispatch("data-refreshed"); } } } refreshPipelines(handler, stage, index, renderInPosition){ this.dispatch("data-refreshing"); if(!handler){ this.activeRowsPipeline[0] = this.rows.slice(0); } //cascade through data refresh stages switch(stage){ case "all": //handle case where all data needs refreshing case "dataPipeline": for(let i = index; i < this.dataPipeline.length; i++){ let result = this.dataPipeline[i].handler(this.activeRowsPipeline[i].slice(0)); this.activeRowsPipeline[i + 1] = result || this.activeRowsPipeline[i].slice(0); } this.setActiveRows(this.activeRowsPipeline[this.dataPipeline.length]); case "display": index = 0; this.resetDisplayRows(); case "displayPipeline": for(let i = index; i < this.displayPipeline.length; i++){ let result = this.displayPipeline[i].handler((i ? this.getDisplayRows(i - 1) : this.activeRows).slice(0), renderInPosition); this.setDisplayRows(result || this.getDisplayRows(i - 1).slice(0), i); } case "end": //case to handle scenario when trying to skip past end stage this.regenerateRowPositions(); } } //regenerate row positions regenerateRowPositions(){ var rows = this.getDisplayRows(); var index = 1; rows.forEach((row) => { if (row.type === "row"){ row.setPosition(index); index++; } }); } setActiveRows(activeRows){ this.activeRows = activeRows; this.activeRowsCount = this.activeRows.length; } //reset display rows array resetDisplayRows(){ this.displayRows = []; this.displayRows.push(this.activeRows.slice(0)); this.displayRowsCount = this.displayRows[0].length; } getNextDisplayIndex(){ return this.displayRows.length; } //set display row pipeline data setDisplayRows(displayRows, index){ this.displayRows[index] = displayRows; if(index == this.displayRows.length -1){ this.displayRowsCount = this.displayRows[this.displayRows.length -1].length; } } getDisplayRows(index){ if(typeof index == "undefined"){ return this.displayRows.length ? this.displayRows[this.displayRows.length -1] : []; }else{ return this.displayRows[index] || []; } } getVisibleRows(chain, viewable){ var rows = Object.assign([], this.renderer.visibleRows(!viewable)); if(chain){ rows = this.chain("rows-visible", [viewable], rows, rows); } return rows; } //repeat action across display rows displayRowIterator(callback){ this.activeRowsPipeline.forEach(callback); this.displayRows.forEach(callback); this.displayRowsCount = this.displayRows[this.displayRows.length -1].length; } //return only actual rows (not group headers etc) getRows(type){ var rows; switch(type){ case "active": rows = this.activeRows; break; case "display": rows = this.table.rowManager.getDisplayRows(); break; case "visible": rows = this.getVisibleRows(false, true); break; default: rows = this.chain("rows-retrieve", type, null, this.rows) || this.rows; } return rows; } ///////////////// Table Rendering ///////////////// //trigger rerender of table in current position reRenderInPosition(callback){ if(this.redrawBlock){ if(callback){ callback(); }else{ this.redrawBlockRenderInPosition = true; } }else{ this.dispatchExternal("renderStarted"); this.renderer.rerenderRows(callback); if(!this.fixedHeight){ this.adjustTableSize(); } this.dispatchExternal("renderComplete"); } } initializeRenderer(){ var renderClass; var renderers = { "virtual": RendererVirtualDomVertical, "basic": RendererBasicVertical, }; if(typeof this.table.options.renderVertical === "string"){ renderClass = renderers[this.table.options.renderVertical]; }else{ renderClass = this.table.options.renderVertical; } if(renderClass){ this.renderMode = this.table.options.renderVertical; this.renderer = new renderClass(this.table, this.element, this.tableElement); this.renderer.initialize(); if((this.table.element.clientHeight || this.table.options.height)){ this.fixedHeight = true; }else{ this.fixedHeight = false; } }else{ console.error("Unable to find matching renderer:", this.table.options.renderVertical); } } getRenderMode(){ return this.renderMode; } renderTable(){ this.dispatchExternal("renderStarted"); this.element.scrollTop = 0; this._clearTable(); if(this.displayRowsCount){ this.renderer.renderRows(); if(this.firstRender){ this.firstRender = false; this.layoutRefresh(true); } }else{ this.renderEmptyScroll(); } if(!this.fixedHeight){ this.adjustTableSize(); } this.dispatch("table-layout"); if(!this.displayRowsCount){ this._showPlaceholder(); } this.dispatchExternal("renderComplete"); } //show scrollbars on empty table div renderEmptyScroll(){ if(this.placeholder){ this.tableElement.style.display = "none"; }else{ this.tableElement.style.minWidth = this.table.columnManager.getWidth() + "px"; // this.tableElement.style.minHeight = "1px"; // this.tableElement.style.visibility = "hidden"; } } _clearTable(){ this._clearPlaceholder(); this.scrollTop = 0; this.scrollLeft = 0; // clear empty table placeholder min this.tableElement.style.minWidth = ""; this.renderer.clearRows(); } _showPlaceholder(){ if(this.placeholder){ this.placeholder.setAttribute("tabulator-render-mode", this.renderMode); this.getElement().appendChild(this.placeholder); this._positionPlaceholder(); } } _clearPlaceholder(){ if(this.placeholder && this.placeholder.parentNode){ this.placeholder.parentNode.removeChild(this.placeholder); } } _positionPlaceholder(){ if(this.placeholder && this.placeholder.parentNode){ this.placeholder.style.width = this.table.columnManager.getWidth() + "px"; this.placeholderContents.style.width = this.table.rowManager.element.clientWidth + "px"; this.placeholderContents.style.marginLeft = this.scrollLeft + "px"; } } styleRow(row, index){ var rowEl = row.getElement(); if(index % 2){ rowEl.classList.add("tabulator-row-even"); rowEl.classList.remove("tabulator-row-odd"); }else{ rowEl.classList.add("tabulator-row-odd"); rowEl.classList.remove("tabulator-row-even"); } } //normalize height of active rows normalizeHeight(){ this.activeRows.forEach(function(row){ row.normalizeHeight(); }); } //adjust the height of the table holder to fit in the Tabulator element adjustTableSize(){ var initialHeight = this.element.clientHeight, minHeight; if(this.renderer.verticalFillMode === "fill"){ let otherHeight = Math.floor(this.table.columnManager.getElement().getBoundingClientRect().height + (this.table.footerManager && this.table.footerManager.active && !this.table.footerManager.external ? this.table.footerManager.getElement().getBoundingClientRect().height : 0)); if(this.fixedHeight){ minHeight = isNaN(this.table.options.minHeight) ? this.table.options.minHeight : this.table.options.minHeight + "px"; this.element.style.minHeight = minHeight || "calc(100% - " + otherHeight + "px)"; this.element.style.height = "calc(100% - " + otherHeight + "px)"; this.element.style.maxHeight = "calc(100% - " + otherHeight + "px)"; }else{ this.element.style.height = ""; this.element.style.height = (this.table.element.clientHeight - otherHeight) + "px"; this.element.scrollTop = this.scrollTop; } this.renderer.resize(); //check if the table has changed size when dealing with variable height tables if(!this.fixedHeight && initialHeight != this.element.clientHeight){ if(this.subscribed("table-resize")){ this.dispatch("table-resize"); }else{ this.redraw(); } } } this._positionPlaceholder(); } //reinitialize all rows reinitialize(){ this.rows.forEach(function(row){ row.reinitialize(true); }); } //prevent table from being redrawn blockRedraw (){ this.redrawBlock = true; this.redrawBlockRestoreConfig = false; } //restore table redrawing restoreRedraw (){ this.redrawBlock = false; if(this.redrawBlockRestoreConfig){ this.refreshActiveData(this.redrawBlockRestoreConfig.handler, this.redrawBlockRestoreConfig.skipStage, this.redrawBlockRestoreConfig.renderInPosition); this.redrawBlockRestoreConfig = false; }else{ if(this.redrawBlockRenderInPosition){ this.reRenderInPosition(); } } this.redrawBlockRenderInPosition = false; } //redraw table redraw (force){ var left = this.scrollLeft; this.adjustTableSize(); this.table.tableWidth = this.table.element.clientWidth; if(!force){ this.reRenderInPosition(); this.scrollHorizontal(left); }else{ this.renderTable(); } } resetScroll(){ this.element.scrollLeft = 0; this.element.scrollTop = 0; if(this.table.browser === "ie"){ var event = document.createEvent("Event"); event.initEvent("scroll", false, true); this.element.dispatchEvent(event); }else{ this.element.dispatchEvent(new Event('scroll')); } } }