UNPKG

tabulator-tables

Version:

Interactive table generation JavaScript library

580 lines (448 loc) 14.2 kB
import Renderer from '../Renderer.js'; import Helpers from '../../tools/Helpers.js'; export default class VirtualDomVertical extends Renderer{ constructor(table){ super(table); this.verticalFillMode = "fill"; this.scrollTop = 0; this.scrollLeft = 0; this.vDomRowHeight = 20; //approximation of row heights for padding this.vDomTop = 0; //hold position for first rendered row in the virtual DOM this.vDomBottom = 0; //hold position for last rendered row in the virtual DOM this.vDomScrollPosTop = 0; //last scroll position of the vDom top; this.vDomScrollPosBottom = 0; //last scroll position of the vDom bottom; this.vDomTopPad = 0; //hold value of padding for top of virtual DOM this.vDomBottomPad = 0; //hold value of padding for bottom of virtual DOM this.vDomMaxRenderChain = 90; //the maximum number of dom elements that can be rendered in 1 go this.vDomWindowBuffer = 0; //window row buffer before removing elements, to smooth scrolling this.vDomWindowMinTotalRows = 20; //minimum number of rows to be generated in virtual dom (prevent buffering issues on tables with tall rows) this.vDomWindowMinMarginRows = 5; //minimum number of rows to be generated in virtual dom margin this.vDomTopNewRows = []; //rows to normalize after appending to optimize render speed this.vDomBottomNewRows = []; //rows to normalize after appending to optimize render speed } ////////////////////////////////////// ///////// Public Functions /////////// ////////////////////////////////////// clearRows(){ var element = this.tableElement; // element.children.detach(); while(element.firstChild) element.removeChild(element.firstChild); element.style.paddingTop = ""; element.style.paddingBottom = ""; // element.style.minWidth = ""; element.style.minHeight = ""; element.style.display = ""; element.style.visibility = ""; this.elementVertical.scrollTop = 0; this.elementVertical.scrollLeft = 0; this.scrollTop = 0; this.scrollLeft = 0; this.vDomTop = 0; this.vDomBottom = 0; this.vDomTopPad = 0; this.vDomBottomPad = 0; this.vDomScrollPosTop = 0; this.vDomScrollPosBottom = 0; } renderRows(){ this._virtualRenderFill(); } rerenderRows(callback){ var scrollTop = this.elementVertical.scrollTop; var topRow = false; var topOffset = false; var left = this.table.rowManager.scrollLeft; var rows = this.rows(); for(var i = this.vDomTop; i <= this.vDomBottom; i++){ if(rows[i]){ var diff = scrollTop - rows[i].getElement().offsetTop; if(topOffset === false || Math.abs(diff) < topOffset){ topOffset = diff; topRow = i; }else{ break; } } } rows.forEach((row) => { row.deinitializeHeight(); }); if(callback){ callback(); } if(this.rows().length){ this._virtualRenderFill((topRow === false ? this.rows.length - 1 : topRow), true, topOffset || 0); }else{ this.clear(); this.table.rowManager._showPlaceholder(); } this.scrollColumns(left); } scrollColumns(left){ this.table.rowManager.scrollHorizontal(left); } scrollRows(top, dir){ var topDiff = top - this.vDomScrollPosTop; var bottomDiff = top - this.vDomScrollPosBottom; var margin = this.vDomWindowBuffer * 2; var rows = this.rows(); this.scrollTop = top; if(-topDiff > margin || bottomDiff > margin){ //if big scroll redraw table; var left = this.table.rowManager.scrollLeft; this._virtualRenderFill(Math.floor((this.elementVertical.scrollTop / this.elementVertical.scrollHeight) * rows.length)); this.scrollColumns(left); }else{ if(dir){ //scrolling up if(topDiff < 0){ this._addTopRow(rows, -topDiff); } if(bottomDiff < 0){ //hide bottom row if needed if(this.vDomScrollHeight - this.scrollTop > this.vDomWindowBuffer){ this._removeBottomRow(rows, -bottomDiff); }else{ this.vDomScrollPosBottom = this.scrollTop; } } }else{ if(bottomDiff >= 0){ this._addBottomRow(rows, bottomDiff); } //scrolling down if(topDiff >= 0){ //hide top row if needed if(this.scrollTop > this.vDomWindowBuffer){ this._removeTopRow(rows, topDiff); }else{ this.vDomScrollPosTop = this.scrollTop; } } } } } resize(){ this.vDomWindowBuffer = this.table.options.renderVerticalBuffer || this.elementVertical.clientHeight; } scrollToRowNearestTop(row){ var rowIndex = this.rows().indexOf(row); return !(Math.abs(this.vDomTop - rowIndex) > Math.abs(this.vDomBottom - rowIndex)); } scrollToRow(row){ var index = this.rows().indexOf(row); if(index > -1){ this._virtualRenderFill(index, true); } } visibleRows(includingBuffer){ var topEdge = this.elementVertical.scrollTop, bottomEdge = this.elementVertical.clientHeight + topEdge, topFound = false, topRow = 0, bottomRow = 0, rows = this.rows(); if(includingBuffer){ topRow = this.vDomTop; bottomRow = this.vDomBottom; }else{ for(var i = this.vDomTop; i <= this.vDomBottom; i++){ if(rows[i]){ if(!topFound){ if((topEdge - rows[i].getElement().offsetTop) >= 0){ topRow = i; }else{ topFound = true; if(bottomEdge - rows[i].getElement().offsetTop >= 0){ bottomRow = i; }else{ break; } } }else{ if(bottomEdge - rows[i].getElement().offsetTop >= 0){ bottomRow = i; }else{ break; } } } } } return rows.slice(topRow, bottomRow + 1); } ////////////////////////////////////// //////// Internal Rendering ////////// ////////////////////////////////////// //full virtual render _virtualRenderFill(position, forceMove, offset){ var element = this.tableElement, holder = this.elementVertical, topPad = 0, rowsHeight = 0, heightOccupied = 0, topPadHeight = 0, i = 0, rows = this.rows(), rowsCount = rows.length, containerHeight = this.elementVertical.clientHeight; position = position || 0; offset = offset || 0; if(!position){ this.clear(); }else{ while(element.firstChild) element.removeChild(element.firstChild); //check if position is too close to bottom of table heightOccupied = (rowsCount - position + 1) * this.vDomRowHeight; if(heightOccupied < containerHeight){ position -= Math.ceil((containerHeight - heightOccupied) / this.vDomRowHeight); if(position < 0){ position = 0; } } //calculate initial pad topPad = Math.min(Math.max(Math.floor(this.vDomWindowBuffer / this.vDomRowHeight), this.vDomWindowMinMarginRows), position); position -= topPad; } if(rowsCount && Helpers.elVisible(this.elementVertical)){ this.vDomTop = position; this.vDomBottom = position -1; while ((rowsHeight <= containerHeight + this.vDomWindowBuffer || i < this.vDomWindowMinTotalRows) && this.vDomBottom < rowsCount -1){ var index = this.vDomBottom + 1, row = rows[index], rowHeight = 0; this.styleRow(row, index); element.appendChild(row.getElement()); row.initialize(); if(!row.heightInitialized){ row.normalizeHeight(true); } rowHeight = row.getHeight(); if(i < topPad){ topPadHeight += rowHeight; }else{ rowsHeight += rowHeight; } if(rowHeight > this.vDomWindowBuffer){ this.vDomWindowBuffer = rowHeight * 2; } this.vDomBottom ++; i++; } if(!position){ this.vDomTopPad = 0; //adjust row height to match average of rendered elements this.vDomRowHeight = Math.floor((rowsHeight + topPadHeight) / i); this.vDomBottomPad = this.vDomRowHeight * (rowsCount - this.vDomBottom -1); this.vDomScrollHeight = topPadHeight + rowsHeight + this.vDomBottomPad - containerHeight; }else{ this.vDomTopPad = !forceMove ? this.scrollTop - topPadHeight : (this.vDomRowHeight * this.vDomTop) + offset; this.vDomBottomPad = this.vDomBottom == rowsCount-1 ? 0 : Math.max(this.vDomScrollHeight - this.vDomTopPad - rowsHeight - topPadHeight, 0); } element.style.paddingTop = this.vDomTopPad + "px"; element.style.paddingBottom = this.vDomBottomPad + "px"; if(forceMove){ this.scrollTop = this.vDomTopPad + (topPadHeight) + offset - (this.elementVertical.scrollWidth > this.elementVertical.clientWidth ? this.elementVertical.offsetHeight - containerHeight : 0); } this.scrollTop = Math.min(this.scrollTop, this.elementVertical.scrollHeight - containerHeight); //adjust for horizontal scrollbar if present (and not at top of table) if(this.elementVertical.scrollWidth > this.elementVertical.offsetWidth && forceMove){ this.scrollTop += this.elementVertical.offsetHeight - containerHeight; } this.vDomScrollPosTop = this.scrollTop; this.vDomScrollPosBottom = this.scrollTop; holder.scrollTop = this.scrollTop; this.dispatch("render-virtual-fill"); } } _addTopRow(rows, fillableSpace){ var table = this.tableElement, addedRows = [], paddingAdjust = 0, index = this.vDomTop -1, i = 0, working = true; while(working){ if(this.vDomTop){ let row = rows[index], rowHeight, initialized; if(row && i < this.vDomMaxRenderChain){ rowHeight = row.getHeight() || this.vDomRowHeight; initialized = row.initialized; if(fillableSpace >= rowHeight){ this.styleRow(row, index); table.insertBefore(row.getElement(), table.firstChild); if(!row.initialized || !row.heightInitialized){ addedRows.push(row); } row.initialize(); if(!initialized){ rowHeight = row.getElement().offsetHeight; if(rowHeight > this.vDomWindowBuffer){ this.vDomWindowBuffer = rowHeight * 2; } } fillableSpace -= rowHeight; paddingAdjust += rowHeight; this.vDomTop--; index--; i++; }else{ working = false; } }else{ working = false; } }else{ working = false; } } for (let row of addedRows){ row.clearCellHeight(); } this._quickNormalizeRowHeight(addedRows); if(paddingAdjust){ this.vDomTopPad -= paddingAdjust; if(this.vDomTopPad < 0){ this.vDomTopPad = index * this.vDomRowHeight; } if(index < 1){ this.vDomTopPad = 0; } table.style.paddingTop = this.vDomTopPad + "px"; this.vDomScrollPosTop -= paddingAdjust; } } _removeTopRow(rows, fillableSpace){ var removableRows = [], paddingAdjust = 0, i = 0, working = true; while(working){ let row = rows[this.vDomTop], rowHeight; if(row && i < this.vDomMaxRenderChain){ rowHeight = row.getHeight() || this.vDomRowHeight; if(fillableSpace >= rowHeight){ this.vDomTop++; fillableSpace -= rowHeight; paddingAdjust += rowHeight; removableRows.push(row); i++; }else{ working = false; } }else{ working = false; } } for (let row of removableRows){ let rowEl = row.getElement(); if(rowEl.parentNode){ rowEl.parentNode.removeChild(rowEl); } } if(paddingAdjust){ this.vDomTopPad += paddingAdjust; this.tableElement.style.paddingTop = this.vDomTopPad + "px"; this.vDomScrollPosTop += this.vDomTop ? paddingAdjust : paddingAdjust + this.vDomWindowBuffer; } } _addBottomRow(rows, fillableSpace){ var table = this.tableElement, addedRows = [], paddingAdjust = 0, index = this.vDomBottom + 1, i = 0, working = true; while(working){ let row = rows[index], rowHeight, initialized; if(row && i < this.vDomMaxRenderChain){ rowHeight = row.getHeight() || this.vDomRowHeight; initialized = row.initialized; if(fillableSpace >= rowHeight){ this.styleRow(row, index); table.appendChild(row.getElement()); if(!row.initialized || !row.heightInitialized){ addedRows.push(row); } row.initialize(); if(!initialized){ rowHeight = row.getElement().offsetHeight; if(rowHeight > this.vDomWindowBuffer){ this.vDomWindowBuffer = rowHeight * 2; } } fillableSpace -= rowHeight; paddingAdjust += rowHeight; this.vDomBottom++; index++; i++; }else{ working = false; } }else{ working = false; } } for (let row of addedRows){ row.clearCellHeight(); } this._quickNormalizeRowHeight(addedRows); if(paddingAdjust){ this.vDomBottomPad -= paddingAdjust; if(this.vDomBottomPad < 0 || index == rows.length -1){ this.vDomBottomPad = 0; } table.style.paddingBottom = this.vDomBottomPad + "px"; this.vDomScrollPosBottom += paddingAdjust; } } _removeBottomRow(rows, fillableSpace){ var removableRows = [], paddingAdjust = 0, i = 0, working = true; while(working){ let row = rows[this.vDomBottom], rowHeight; if(row && i < this.vDomMaxRenderChain){ rowHeight = row.getHeight() || this.vDomRowHeight; if(fillableSpace >= rowHeight){ this.vDomBottom --; fillableSpace -= rowHeight; paddingAdjust += rowHeight; removableRows.push(row); i++; }else{ working = false; } }else{ working = false; } } for (let row of removableRows){ let rowEl = row.getElement(); if(rowEl.parentNode){ rowEl.parentNode.removeChild(rowEl); } } if(paddingAdjust){ this.vDomBottomPad += paddingAdjust; if(this.vDomBottomPad < 0){ this.vDomBottomPad = 0; } this.tableElement.style.paddingBottom = this.vDomBottomPad + "px"; this.vDomScrollPosBottom -= paddingAdjust; } } _quickNormalizeRowHeight(rows){ for(let row of rows){ row.calcHeight(); } for(let row of rows){ row.setCellHeight(); } } }