| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- import {
- addClass,
- empty,
- getScrollbarWidth,
- hasClass,
- innerHeight,
- outerWidth
- } from './../../../helpers/dom/element';
- import Overlay from './overlay/_base';
- let performanceWarningAppeared = false;
- /**
- * @class TableRenderer
- */
- class TableRenderer {
- /**
- * @param {WalkontableTable} wtTable
- */
- constructor(wtTable) {
- this.wtTable = wtTable;
- this.wot = wtTable.instance;
- // legacy support
- this.instance = wtTable.instance;
- this.rowFilter = wtTable.rowFilter;
- this.columnFilter = wtTable.columnFilter;
- this.TABLE = wtTable.TABLE;
- this.THEAD = wtTable.THEAD;
- this.TBODY = wtTable.TBODY;
- this.COLGROUP = wtTable.COLGROUP;
- this.rowHeaders = [];
- this.rowHeaderCount = 0;
- this.columnHeaders = [];
- this.columnHeaderCount = 0;
- this.fixedRowsTop = 0;
- this.fixedRowsBottom = 0;
- }
- /**
- *
- */
- render() {
- if (!this.wtTable.isWorkingOnClone()) {
- const skipRender = {};
- this.wot.getSetting('beforeDraw', true, skipRender);
- if (skipRender.skipRender === true) {
- return;
- }
- }
- this.rowHeaders = this.wot.getSetting('rowHeaders');
- this.rowHeaderCount = this.rowHeaders.length;
- this.fixedRowsTop = this.wot.getSetting('fixedRowsTop');
- this.fixedRowsBottom = this.wot.getSetting('fixedRowsBottom');
- this.columnHeaders = this.wot.getSetting('columnHeaders');
- this.columnHeaderCount = this.columnHeaders.length;
- let columnsToRender = this.wtTable.getRenderedColumnsCount();
- let rowsToRender = this.wtTable.getRenderedRowsCount();
- let totalColumns = this.wot.getSetting('totalColumns');
- let totalRows = this.wot.getSetting('totalRows');
- let workspaceWidth;
- let adjusted = false;
- if (Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_BOTTOM) ||
- Overlay.isOverlayTypeOf(this.wot.cloneOverlay, Overlay.CLONE_BOTTOM_LEFT_CORNER)) {
- // do NOT render headers on the bottom or bottom-left corner overlay
- this.columnHeaders = [];
- this.columnHeaderCount = 0;
- }
- if (totalColumns >= 0) {
- // prepare COL and TH elements for rendering
- this.adjustAvailableNodes();
- adjusted = true;
- // adjust column widths according to user widths settings
- this.renderColumnHeaders();
- // Render table rows
- this.renderRows(totalRows, rowsToRender, columnsToRender);
- if (!this.wtTable.isWorkingOnClone()) {
- workspaceWidth = this.wot.wtViewport.getWorkspaceWidth();
- this.wot.wtViewport.containerWidth = null;
- }
- this.adjustColumnWidths(columnsToRender);
- this.markOversizedColumnHeaders();
- this.adjustColumnHeaderHeights();
- }
- if (!adjusted) {
- this.adjustAvailableNodes();
- }
- this.removeRedundantRows(rowsToRender);
- if (!this.wtTable.isWorkingOnClone() || this.wot.isOverlayName(Overlay.CLONE_BOTTOM)) {
- this.markOversizedRows();
- }
- if (!this.wtTable.isWorkingOnClone()) {
- this.wot.wtViewport.createVisibleCalculators();
- this.wot.wtOverlays.refresh(false);
- this.wot.wtOverlays.applyToDOM();
- let hiderWidth = outerWidth(this.wtTable.hider);
- let tableWidth = outerWidth(this.wtTable.TABLE);
- if (hiderWidth !== 0 && (tableWidth !== hiderWidth)) {
- // Recalculate the column widths, if width changes made in the overlays removed the scrollbar, thus changing the viewport width.
- this.adjustColumnWidths(columnsToRender);
- }
- if (workspaceWidth !== this.wot.wtViewport.getWorkspaceWidth()) {
- // workspace width changed though to shown/hidden vertical scrollbar. Let's reapply stretching
- this.wot.wtViewport.containerWidth = null;
- let firstRendered = this.wtTable.getFirstRenderedColumn();
- let lastRendered = this.wtTable.getLastRenderedColumn();
- let defaultColumnWidth = this.wot.getSetting('defaultColumnWidth');
- let rowHeaderWidthSetting = this.wot.getSetting('rowHeaderWidth');
- rowHeaderWidthSetting = this.instance.getSetting('onModifyRowHeaderWidth', rowHeaderWidthSetting);
- if (rowHeaderWidthSetting != null) {
- for (let i = 0; i < this.rowHeaderCount; i++) {
- let width = Array.isArray(rowHeaderWidthSetting) ? rowHeaderWidthSetting[i] : rowHeaderWidthSetting;
- width = width == null ? defaultColumnWidth : width;
- this.COLGROUP.childNodes[i].style.width = `${width}px`;
- }
- }
- for (let i = firstRendered; i < lastRendered; i++) {
- let width = this.wtTable.getStretchedColumnWidth(i);
- let renderedIndex = this.columnFilter.sourceToRendered(i);
- this.COLGROUP.childNodes[renderedIndex + this.rowHeaderCount].style.width = `${width}px`;
- }
- }
- this.wot.getSetting('onDraw', true);
- } else if (this.wot.isOverlayName(Overlay.CLONE_BOTTOM)) {
- this.wot.cloneSource.wtOverlays.adjustElementsSize();
- }
- }
- /**
- * @param {Number} renderedRowsCount
- */
- removeRedundantRows(renderedRowsCount) {
- while (this.wtTable.tbodyChildrenLength > renderedRowsCount) {
- this.TBODY.removeChild(this.TBODY.lastChild);
- this.wtTable.tbodyChildrenLength--;
- }
- }
- /**
- * @param {Number} totalRows
- * @param {Number} rowsToRender
- * @param {Number} columnsToRender
- */
- renderRows(totalRows, rowsToRender, columnsToRender) {
- let lastTD,
- TR;
- let visibleRowIndex = 0;
- let sourceRowIndex = this.rowFilter.renderedToSource(visibleRowIndex);
- let isWorkingOnClone = this.wtTable.isWorkingOnClone();
- while (sourceRowIndex < totalRows && sourceRowIndex >= 0) {
- if (!performanceWarningAppeared && visibleRowIndex > 1000) {
- performanceWarningAppeared = true;
- console.warn('Performance tip: Handsontable rendered more than 1000 visible rows. Consider limiting the number ' +
- 'of rendered rows by specifying the table height and/or turning off the "renderAllRows" option.');
- }
- if (rowsToRender !== void 0 && visibleRowIndex === rowsToRender) {
- // We have as much rows as needed for this clone
- break;
- }
- TR = this.getOrCreateTrForRow(visibleRowIndex, TR);
- // Render row headers
- this.renderRowHeaders(sourceRowIndex, TR);
- // Add and/or remove TDs to TR to match the desired number
- this.adjustColumns(TR, columnsToRender + this.rowHeaderCount);
- lastTD = this.renderCells(sourceRowIndex, TR, columnsToRender);
- if (!isWorkingOnClone ||
- // Necessary to refresh oversized row heights after editing cell in overlays
- this.wot.isOverlayName(Overlay.CLONE_BOTTOM)) {
- // Reset the oversized row cache for this row
- this.resetOversizedRow(sourceRowIndex);
- }
- if (TR.firstChild) {
- // if I have 2 fixed columns with one-line content and the 3rd column has a multiline content, this is
- // the way to make sure that the overlay will has same row height
- let height = this.wot.wtTable.getRowHeight(sourceRowIndex);
- if (height) {
- // Decrease height. 1 pixel will be "replaced" by 1px border top
- height--;
- TR.firstChild.style.height = `${height}px`;
- } else {
- TR.firstChild.style.height = '';
- }
- }
- visibleRowIndex++;
- sourceRowIndex = this.rowFilter.renderedToSource(visibleRowIndex);
- }
- }
- /**
- * Reset the oversized row cache for the provided index
- *
- * @param {Number} sourceRow Row index
- */
- resetOversizedRow(sourceRow) {
- if (this.wot.getSetting('externalRowCalculator')) {
- return;
- }
- if (this.wot.wtViewport.oversizedRows && this.wot.wtViewport.oversizedRows[sourceRow]) {
- this.wot.wtViewport.oversizedRows[sourceRow] = void 0;
- }
- }
- /**
- * Check if any of the rendered rows is higher than expected, and if so, cache them
- */
- markOversizedRows() {
- if (this.wot.getSetting('externalRowCalculator')) {
- return;
- }
- let rowCount = this.instance.wtTable.TBODY.childNodes.length;
- let expectedTableHeight = rowCount * this.instance.wtSettings.settings.defaultRowHeight;
- let actualTableHeight = innerHeight(this.instance.wtTable.TBODY) - 1;
- let previousRowHeight;
- let rowInnerHeight;
- let sourceRowIndex;
- let currentTr;
- let rowHeader;
- let totalRows = this.instance.getSetting('totalRows');
- if (expectedTableHeight === actualTableHeight && !this.instance.getSetting('fixedRowsBottom')) {
- // If the actual table height equals rowCount * default single row height, no row is oversized -> no need to iterate over them
- return;
- }
- while (rowCount) {
- rowCount--;
- sourceRowIndex = this.instance.wtTable.rowFilter.renderedToSource(rowCount);
- previousRowHeight = this.instance.wtTable.getRowHeight(sourceRowIndex);
- currentTr = this.instance.wtTable.getTrForRow(sourceRowIndex);
- rowHeader = currentTr.querySelector('th');
- if (rowHeader) {
- rowInnerHeight = innerHeight(rowHeader);
- } else {
- rowInnerHeight = innerHeight(currentTr) - 1;
- }
- if ((!previousRowHeight && this.instance.wtSettings.settings.defaultRowHeight < rowInnerHeight ||
- previousRowHeight < rowInnerHeight)) {
- this.instance.wtViewport.oversizedRows[sourceRowIndex] = ++rowInnerHeight;
- }
- }
- }
- /**
- * Check if any of the rendered columns is higher than expected, and if so, cache them.
- */
- markOversizedColumnHeaders() {
- let overlayName = this.wot.getOverlayName();
- if (!this.columnHeaderCount || this.wot.wtViewport.hasOversizedColumnHeadersMarked[overlayName] || this.wtTable.isWorkingOnClone()) {
- return;
- }
- let columnCount = this.wtTable.getRenderedColumnsCount();
- for (let i = 0; i < this.columnHeaderCount; i++) {
- for (let renderedColumnIndex = (-1) * this.rowHeaderCount; renderedColumnIndex < columnCount; renderedColumnIndex++) {
- this.markIfOversizedColumnHeader(renderedColumnIndex);
- }
- }
- this.wot.wtViewport.hasOversizedColumnHeadersMarked[overlayName] = true;
- }
- /**
- *
- */
- adjustColumnHeaderHeights() {
- let columnHeaders = this.wot.getSetting('columnHeaders');
- let children = this.wot.wtTable.THEAD.childNodes;
- let oversizedColumnHeaders = this.wot.wtViewport.oversizedColumnHeaders;
- for (let i = 0, len = columnHeaders.length; i < len; i++) {
- if (oversizedColumnHeaders[i]) {
- if (!children[i] || children[i].childNodes.length === 0) {
- return;
- }
- children[i].childNodes[0].style.height = `${oversizedColumnHeaders[i]}px`;
- }
- }
- }
- /**
- * Check if column header for the specified column is higher than expected, and if so, cache it
- *
- * @param {Number} col Index of column
- */
- markIfOversizedColumnHeader(col) {
- let sourceColIndex = this.wot.wtTable.columnFilter.renderedToSource(col);
- let level = this.columnHeaderCount;
- let defaultRowHeight = this.wot.wtSettings.settings.defaultRowHeight;
- let previousColHeaderHeight;
- let currentHeader;
- let currentHeaderHeight;
- let columnHeaderHeightSetting = this.wot.getSetting('columnHeaderHeight') || [];
- while (level) {
- level--;
- previousColHeaderHeight = this.wot.wtTable.getColumnHeaderHeight(level);
- currentHeader = this.wot.wtTable.getColumnHeader(sourceColIndex, level);
- if (!currentHeader) {
- /* eslint-disable no-continue */
- continue;
- }
- currentHeaderHeight = innerHeight(currentHeader);
- if (!previousColHeaderHeight && defaultRowHeight < currentHeaderHeight || previousColHeaderHeight < currentHeaderHeight) {
- this.wot.wtViewport.oversizedColumnHeaders[level] = currentHeaderHeight;
- }
- if (Array.isArray(columnHeaderHeightSetting)) {
- if (columnHeaderHeightSetting[level] != null) {
- this.wot.wtViewport.oversizedColumnHeaders[level] = columnHeaderHeightSetting[level];
- }
- } else if (!isNaN(columnHeaderHeightSetting)) {
- this.wot.wtViewport.oversizedColumnHeaders[level] = columnHeaderHeightSetting;
- }
- if (this.wot.wtViewport.oversizedColumnHeaders[level] < (columnHeaderHeightSetting[level] || columnHeaderHeightSetting)) {
- this.wot.wtViewport.oversizedColumnHeaders[level] = (columnHeaderHeightSetting[level] || columnHeaderHeightSetting);
- }
- }
- }
- /**
- * @param {Number} sourceRowIndex
- * @param {HTMLTableRowElement} TR
- * @param {Number} columnsToRender
- * @returns {HTMLTableCellElement}
- */
- renderCells(sourceRowIndex, TR, columnsToRender) {
- let TD;
- let sourceColIndex;
- for (let visibleColIndex = 0; visibleColIndex < columnsToRender; visibleColIndex++) {
- sourceColIndex = this.columnFilter.renderedToSource(visibleColIndex);
- if (visibleColIndex === 0) {
- TD = TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(sourceColIndex)];
- } else {
- TD = TD.nextSibling; // http://jsperf.com/nextsibling-vs-indexed-childnodes
- }
- // If the number of headers has been reduced, we need to replace excess TH with TD
- if (TD.nodeName == 'TH') {
- TD = replaceThWithTd(TD, TR);
- }
- if (!hasClass(TD, 'hide')) {
- TD.className = '';
- }
- TD.removeAttribute('style');
- this.wot.wtSettings.settings.cellRenderer(sourceRowIndex, sourceColIndex, TD);
- }
- return TD;
- }
- /**
- * @param {Number} columnsToRender Number of columns to render.
- */
- adjustColumnWidths(columnsToRender) {
- let scrollbarCompensation = 0;
- let sourceInstance = this.wot.cloneSource ? this.wot.cloneSource : this.wot;
- let mainHolder = sourceInstance.wtTable.holder;
- let defaultColumnWidth = this.wot.getSetting('defaultColumnWidth');
- let rowHeaderWidthSetting = this.wot.getSetting('rowHeaderWidth');
- if (mainHolder.offsetHeight < mainHolder.scrollHeight) {
- scrollbarCompensation = getScrollbarWidth();
- }
- this.wot.wtViewport.columnsRenderCalculator.refreshStretching(this.wot.wtViewport.getViewportWidth() - scrollbarCompensation);
- rowHeaderWidthSetting = this.instance.getSetting('onModifyRowHeaderWidth', rowHeaderWidthSetting);
- if (rowHeaderWidthSetting != null) {
- for (let i = 0; i < this.rowHeaderCount; i++) {
- let width = Array.isArray(rowHeaderWidthSetting) ? rowHeaderWidthSetting[i] : rowHeaderWidthSetting;
- width = width == null ? defaultColumnWidth : width;
- this.COLGROUP.childNodes[i].style.width = `${width}px`;
- }
- }
- for (let renderedColIndex = 0; renderedColIndex < columnsToRender; renderedColIndex++) {
- let width = this.wtTable.getStretchedColumnWidth(this.columnFilter.renderedToSource(renderedColIndex));
- this.COLGROUP.childNodes[renderedColIndex + this.rowHeaderCount].style.width = `${width}px`;
- }
- }
- /**
- * @param {HTMLTableCellElement} TR
- */
- appendToTbody(TR) {
- this.TBODY.appendChild(TR);
- this.wtTable.tbodyChildrenLength++;
- }
- /**
- * @param {Number} rowIndex
- * @param {HTMLTableRowElement} currentTr
- * @returns {HTMLTableCellElement}
- */
- getOrCreateTrForRow(rowIndex, currentTr) {
- let TR;
- if (rowIndex >= this.wtTable.tbodyChildrenLength) {
- TR = this.createRow();
- this.appendToTbody(TR);
- } else if (rowIndex === 0) {
- TR = this.TBODY.firstChild;
- } else {
- // http://jsperf.com/nextsibling-vs-indexed-childnodes
- TR = currentTr.nextSibling;
- }
- if (TR.className) {
- TR.removeAttribute('class');
- }
- return TR;
- }
- /**
- * @returns {HTMLTableCellElement}
- */
- createRow() {
- let TR = document.createElement('TR');
- for (let visibleColIndex = 0; visibleColIndex < this.rowHeaderCount; visibleColIndex++) {
- TR.appendChild(document.createElement('TH'));
- }
- return TR;
- }
- /**
- * @param {Number} row
- * @param {Number} col
- * @param {HTMLTableCellElement} TH
- */
- renderRowHeader(row, col, TH) {
- TH.className = '';
- TH.removeAttribute('style');
- this.rowHeaders[col](row, TH, col);
- }
- /**
- * @param {Number} row
- * @param {HTMLTableCellElement} TR
- */
- renderRowHeaders(row, TR) {
- for (let TH = TR.firstChild, visibleColIndex = 0; visibleColIndex < this.rowHeaderCount; visibleColIndex++) {
- // If the number of row headers increased we need to create TH or replace an existing TD node with TH
- if (!TH) {
- TH = document.createElement('TH');
- TR.appendChild(TH);
- } else if (TH.nodeName == 'TD') {
- TH = replaceTdWithTh(TH, TR);
- }
- this.renderRowHeader(row, visibleColIndex, TH);
- // http://jsperf.com/nextsibling-vs-indexed-childnodes
- TH = TH.nextSibling;
- }
- }
- /**
- * Adjust the number of COL and TH elements to match the number of columns and headers that need to be rendered
- */
- adjustAvailableNodes() {
- this.adjustColGroups();
- this.adjustThead();
- }
- /**
- * Renders the column headers
- */
- renderColumnHeaders() {
- if (!this.columnHeaderCount) {
- return;
- }
- let columnCount = this.wtTable.getRenderedColumnsCount();
- for (let i = 0; i < this.columnHeaderCount; i++) {
- let TR = this.getTrForColumnHeaders(i);
- for (let renderedColumnIndex = (-1) * this.rowHeaderCount; renderedColumnIndex < columnCount; renderedColumnIndex++) {
- let sourceCol = this.columnFilter.renderedToSource(renderedColumnIndex);
- this.renderColumnHeader(i, sourceCol, TR.childNodes[renderedColumnIndex + this.rowHeaderCount]);
- }
- }
- }
- /**
- * Adjusts the number of COL elements to match the number of columns that need to be rendered
- */
- adjustColGroups() {
- let columnCount = this.wtTable.getRenderedColumnsCount();
- while (this.wtTable.colgroupChildrenLength < columnCount + this.rowHeaderCount) {
- this.COLGROUP.appendChild(document.createElement('COL'));
- this.wtTable.colgroupChildrenLength++;
- }
- while (this.wtTable.colgroupChildrenLength > columnCount + this.rowHeaderCount) {
- this.COLGROUP.removeChild(this.COLGROUP.lastChild);
- this.wtTable.colgroupChildrenLength--;
- }
- if (this.rowHeaderCount) {
- addClass(this.COLGROUP.childNodes[0], 'rowHeader');
- }
- }
- /**
- * Adjusts the number of TH elements in THEAD to match the number of headers and columns that need to be rendered
- */
- adjustThead() {
- let columnCount = this.wtTable.getRenderedColumnsCount();
- let TR = this.THEAD.firstChild;
- if (this.columnHeaders.length) {
- for (let i = 0, len = this.columnHeaders.length; i < len; i++) {
- TR = this.THEAD.childNodes[i];
- if (!TR) {
- TR = document.createElement('TR');
- this.THEAD.appendChild(TR);
- }
- this.theadChildrenLength = TR.childNodes.length;
- while (this.theadChildrenLength < columnCount + this.rowHeaderCount) {
- TR.appendChild(document.createElement('TH'));
- this.theadChildrenLength++;
- }
- while (this.theadChildrenLength > columnCount + this.rowHeaderCount) {
- TR.removeChild(TR.lastChild);
- this.theadChildrenLength--;
- }
- }
- let theadChildrenLength = this.THEAD.childNodes.length;
- if (theadChildrenLength > this.columnHeaders.length) {
- for (let i = this.columnHeaders.length; i < theadChildrenLength; i++) {
- this.THEAD.removeChild(this.THEAD.lastChild);
- }
- }
- } else if (TR) {
- empty(TR);
- }
- }
- /**
- * @param {Number} index
- * @returns {HTMLTableCellElement}
- */
- getTrForColumnHeaders(index) {
- return this.THEAD.childNodes[index];
- }
- /**
- * @param {Number} row
- * @param {Number} col
- * @param {HTMLTableCellElement} TH
- * @returns {*}
- */
- renderColumnHeader(row, col, TH) {
- TH.className = '';
- TH.removeAttribute('style');
- return this.columnHeaders[row](col, TH, row);
- }
- /**
- * Add and/or remove the TDs to match the desired number
- *
- * @param {HTMLTableCellElement} TR Table row in question
- * @param {Number} desiredCount The desired number of TDs in the TR
- */
- adjustColumns(TR, desiredCount) {
- let count = TR.childNodes.length;
- while (count < desiredCount) {
- let TD = document.createElement('TD');
- TR.appendChild(TD);
- count++;
- }
- while (count > desiredCount) {
- TR.removeChild(TR.lastChild);
- count--;
- }
- }
- /**
- * @param {Number} columnsToRender
- */
- removeRedundantColumns(columnsToRender) {
- while (this.wtTable.tbodyChildrenLength > columnsToRender) {
- this.TBODY.removeChild(this.TBODY.lastChild);
- this.wtTable.tbodyChildrenLength--;
- }
- }
- }
- function replaceTdWithTh(TD, TR) {
- let TH = document.createElement('TH');
- TR.insertBefore(TH, TD);
- TR.removeChild(TD);
- return TH;
- }
- function replaceThWithTd(TH, TR) {
- let TD = document.createElement('TD');
- TR.insertBefore(TD, TH);
- TR.removeChild(TH);
- return TD;
- }
- export default TableRenderer;
|