UNPKG

handsontable

Version:

Handsontable is a JavaScript Data Grid available for React, Angular and Vue.

501 lines (478 loc) • 21.6 kB
"use strict"; exports.__esModule = true; require("core-js/modules/esnext.iterator.constructor.js"); require("core-js/modules/esnext.iterator.map.js"); var _element = require("../../../helpers/dom/element"); var _object = require("../../../helpers/object"); var _calculator = require("./calculator"); /** * @class Viewport */ class Viewport { /** * @param {ViewportDao} dataAccessObject The Walkontable instance. * @param {DomBindings} domBindings Bindings into DOM. * @param {Settings} wtSettings The Walkontable settings. * @param {EventManager} eventManager The instance event manager. * @param {Table} wtTable The table. */ constructor(dataAccessObject, domBindings, wtSettings, eventManager, wtTable) { this.dataAccessObject = dataAccessObject; // legacy support this.wot = dataAccessObject.wot; this.instance = this.wot; this.domBindings = domBindings; this.wtSettings = wtSettings; this.wtTable = wtTable; this.oversizedRows = []; this.oversizedColumnHeaders = []; this.hasOversizedColumnHeadersMarked = {}; this.clientHeight = 0; this.rowHeaderWidth = NaN; this.rowsVisibleCalculator = null; this.columnsVisibleCalculator = null; this.rowsCalculatorTypes = new Map([['rendered', () => this.wtSettings.getSetting('renderAllRows') ? new _calculator.RenderedAllRowsCalculationType() : new _calculator.RenderedRowsCalculationType()], ['fullyVisible', () => new _calculator.FullyVisibleRowsCalculationType()], ['partiallyVisible', () => new _calculator.PartiallyVisibleRowsCalculationType()]]); this.columnsCalculatorTypes = new Map([['rendered', () => this.wtSettings.getSetting('renderAllColumns') ? new _calculator.RenderedAllColumnsCalculationType() : new _calculator.RenderedColumnsCalculationType()], ['fullyVisible', () => new _calculator.FullyVisibleColumnsCalculationType()], ['partiallyVisible', () => new _calculator.PartiallyVisibleColumnsCalculationType()]]); this.eventManager = eventManager; this.eventManager.addEventListener(this.domBindings.rootWindow, 'resize', () => { this.clientHeight = this.getWorkspaceHeight(); }); } /** * @returns {number} */ getWorkspaceHeight() { const currentDocument = this.domBindings.rootDocument; const trimmingContainer = this.dataAccessObject.topOverlayTrimmingContainer; let height = 0; if (trimmingContainer === this.domBindings.rootWindow) { height = currentDocument.documentElement.clientHeight; } else { const elemHeight = (0, _element.outerHeight)(trimmingContainer); // returns height without DIV scrollbar height = elemHeight > 0 && trimmingContainer.clientHeight > 0 ? trimmingContainer.clientHeight : Infinity; } return height; } /** * @returns {number} */ getViewportHeight() { let containerHeight = this.getWorkspaceHeight(); if (containerHeight === Infinity) { return containerHeight; } const columnHeaderHeight = this.getColumnHeaderHeight(); if (columnHeaderHeight > 0) { containerHeight -= columnHeaderHeight; } return containerHeight; } /** * Gets the width of the table workspace (in pixels). The workspace size in the current * implementation returns the width of the table holder element including scrollbar width when * the table has defined size and the width of the window excluding scrollbar width when * the table has no defined size (the window is a scrollable container). * * This is a bug, as the method should always return stable values, always without scrollbar width. * Changing this behavior would break the column calculators, which would also need to be adjusted. * * @returns {number} */ getWorkspaceWidth() { const { rootDocument, rootWindow } = this.domBindings; const trimmingContainer = this.dataAccessObject.inlineStartOverlayTrimmingContainer; let width; if (trimmingContainer === rootWindow) { const totalColumns = this.wtSettings.getSetting('totalColumns'); width = this.wtTable.holder.offsetWidth; if (this.getRowHeaderWidth() + this.sumColumnWidths(0, totalColumns) > width) { width = rootDocument.documentElement.clientWidth; } } else { width = trimmingContainer.clientWidth; } return width; } /** * @returns {number} */ getViewportWidth() { const containerWidth = this.getWorkspaceWidth(); if (containerWidth === Infinity) { return containerWidth; } const rowHeaderWidth = this.getRowHeaderWidth(); if (rowHeaderWidth > 0) { return containerWidth - rowHeaderWidth; } return containerWidth; } /** * Checks if viewport has vertical scroll. * * @returns {boolean} */ hasVerticalScroll() { if (this.isVerticallyScrollableByWindow()) { const documentElement = this.domBindings.rootDocument.documentElement; return documentElement.scrollHeight > documentElement.clientHeight; } const { holder, hider } = this.wtTable; const holderHeight = holder.clientHeight; const hiderOffsetHeight = hider.offsetHeight; if (holderHeight < hiderOffsetHeight) { return true; } return hiderOffsetHeight > this.getWorkspaceHeight(); } /** * Checks if viewport has horizontal scroll. * * @returns {boolean} */ hasHorizontalScroll() { if (this.isVerticallyScrollableByWindow()) { const documentElement = this.domBindings.rootDocument.documentElement; return documentElement.scrollWidth > documentElement.clientWidth; } const { hider } = this.wtTable; const hiderOffsetWidth = hider.offsetWidth; const scrollbarWidth = this.hasVerticalScroll() ? (0, _element.getScrollbarWidth)() : 0; return hiderOffsetWidth > this.getWorkspaceWidth() - scrollbarWidth; } /** * Checks if the table uses the window as a viewport and if there is a vertical scrollbar. * * @returns {boolean} */ isVerticallyScrollableByWindow() { return this.dataAccessObject.topOverlayTrimmingContainer === this.domBindings.rootWindow; } /** * Checks if the table uses the window as a viewport and if there is a horizontal scrollbar. * * @returns {boolean} */ isHorizontallyScrollableByWindow() { return this.dataAccessObject.inlineStartOverlayTrimmingContainer === this.domBindings.rootWindow; } /** * @param {number} from The visual column index from the width sum is start calculated. * @param {number} length The length of the column to traverse. * @returns {number} */ sumColumnWidths(from, length) { let sum = 0; let column = from; while (column < length) { sum += this.wtTable.getColumnWidth(column); column += 1; } return sum; } /** * @returns {number} */ getWorkspaceOffset() { return (0, _element.offset)(this.wtTable.holder); } /** * @returns {number} */ getColumnHeaderHeight() { const columnHeaders = this.wtSettings.getSetting('columnHeaders'); if (!columnHeaders.length) { this.columnHeaderHeight = 0; } else if (isNaN(this.columnHeaderHeight)) { this.columnHeaderHeight = (0, _element.outerHeight)(this.wtTable.THEAD); } return this.columnHeaderHeight; } /** * @returns {number} */ getRowHeaderWidth() { const rowHeadersWidthSetting = this.wtSettings.getSetting('rowHeaderWidth'); const rowHeaders = this.wtSettings.getSetting('rowHeaders'); if (rowHeadersWidthSetting) { this.rowHeaderWidth = 0; for (let i = 0, len = rowHeaders.length; i < len; i++) { this.rowHeaderWidth += rowHeadersWidthSetting[i] || rowHeadersWidthSetting; } } if (isNaN(this.rowHeaderWidth)) { if (rowHeaders.length) { let TH = this.wtTable.TABLE.querySelector('TH'); this.rowHeaderWidth = 0; for (let i = 0, len = rowHeaders.length; i < len; i++) { if (TH) { this.rowHeaderWidth += (0, _element.outerWidth)(TH); TH = TH.nextSibling; } else { // yes this is a cheat but it worked like that before, just taking assumption from CSS instead of measuring. // TODO: proper fix this.rowHeaderWidth += 50; } } } else { this.rowHeaderWidth = 0; } } this.rowHeaderWidth = this.wtSettings.getSetting('onModifyRowHeaderWidth', this.rowHeaderWidth) || this.rowHeaderWidth; return this.rowHeaderWidth; } /** * Creates rows calculators. The type of the calculations can be chosen from the list: * - 'rendered' Calculates rows that should be rendered within the current table's viewport; * - 'fullyVisible' Calculates rows that are fully visible (used mostly for scrolling purposes); * - 'partiallyVisible' Calculates rows that are partially visible (used mostly for scrolling purposes). * * @param {'rendered' | 'fullyVisible' | 'partiallyVisible'} calculatorTypes The list of the calculation types. * @returns {ViewportRowsCalculator} */ createRowsCalculator() { let calculatorTypes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['rendered', 'fullyVisible', 'partiallyVisible']; const { wtSettings, wtTable } = this; let height = this.getViewportHeight(); let scrollbarHeight; let fixedRowsHeight; this.rowHeaderWidth = NaN; let pos = this.dataAccessObject.topScrollPosition - this.dataAccessObject.topParentOffset; const fixedRowsTop = wtSettings.getSetting('fixedRowsTop'); const fixedRowsBottom = wtSettings.getSetting('fixedRowsBottom'); const totalRows = wtSettings.getSetting('totalRows'); if (fixedRowsTop && pos >= 0) { fixedRowsHeight = this.dataAccessObject.topOverlay.sumCellSizes(0, fixedRowsTop); pos += fixedRowsHeight; height -= fixedRowsHeight; } if (fixedRowsBottom && this.dataAccessObject.bottomOverlay.clone) { fixedRowsHeight = this.dataAccessObject.bottomOverlay.sumCellSizes(totalRows - fixedRowsBottom, totalRows); height -= fixedRowsHeight; } if (wtTable.holder.clientHeight === wtTable.holder.offsetHeight) { scrollbarHeight = 0; } else { scrollbarHeight = (0, _element.getScrollbarWidth)(this.domBindings.rootDocument); } return new _calculator.ViewportRowsCalculator({ calculationTypes: calculatorTypes.map(type => [type, this.rowsCalculatorTypes.get(type)()]), viewportHeight: height, scrollOffset: pos, totalRows: wtSettings.getSetting('totalRows'), defaultRowHeight: wtSettings.getSetting('stylesHandler').getDefaultRowHeight(), rowHeightFn: sourceRow => wtTable.getRowHeight(sourceRow), overrideFn: wtSettings.getSettingPure('viewportRowCalculatorOverride'), horizontalScrollbarHeight: scrollbarHeight }); } /** * Creates columns calculators. The type of the calculations can be chosen from the list: * - 'rendered' Calculates columns that should be rendered within the current table's viewport; * - 'fullyVisible' Calculates columns that are fully visible (used mostly for scrolling purposes); * - 'partiallyVisible' Calculates columns that are partially visible (used mostly for scrolling purposes). * * @param {'rendered' | 'fullyVisible' | 'partiallyVisible'} calculatorTypes The list of the calculation types. * @returns {ViewportColumnsCalculator} */ createColumnsCalculator() { let calculatorTypes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['rendered', 'fullyVisible', 'partiallyVisible']; const { wtSettings, wtTable } = this; let width = this.getViewportWidth(); let pos = Math.abs(this.dataAccessObject.inlineStartScrollPosition) - this.dataAccessObject.inlineStartParentOffset; this.columnHeaderHeight = NaN; const fixedColumnsStart = wtSettings.getSetting('fixedColumnsStart'); if (fixedColumnsStart && pos >= 0) { const fixedColumnsWidth = this.dataAccessObject.inlineStartOverlay.sumCellSizes(0, fixedColumnsStart); pos += fixedColumnsWidth; width -= fixedColumnsWidth; } if (wtTable.holder.clientWidth !== wtTable.holder.offsetWidth) { width -= (0, _element.getScrollbarWidth)(this.domBindings.rootDocument); } return new _calculator.ViewportColumnsCalculator({ calculationTypes: calculatorTypes.map(type => [type, this.columnsCalculatorTypes.get(type)()]), viewportWidth: width, scrollOffset: pos, totalColumns: wtSettings.getSetting('totalColumns'), columnWidthFn: sourceCol => wtTable.getColumnWidth(sourceCol), overrideFn: wtSettings.getSettingPure('viewportColumnCalculatorOverride'), inlineStartOffset: this.dataAccessObject.inlineStartParentOffset }); } /** * Creates rowsRenderCalculator and columnsRenderCalculator (before draw, to determine what rows and * cols should be rendered). * * @param {boolean} fastDraw If `true`, will try to avoid full redraw and only update the border positions. * If `false` or `undefined`, will perform a full redraw. * @returns {boolean} The fastDraw value, possibly modified. */ createCalculators() { let fastDraw = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; const { wtSettings } = this; const rowsCalculator = this.createRowsCalculator(); const columnsCalculator = this.createColumnsCalculator(); if (fastDraw && !wtSettings.getSetting('renderAllRows')) { const proposedFullyVisibleRowsCalculator = rowsCalculator.getResultsFor('fullyVisible'); const proposedPartiallyVisibleRowsCalculator = rowsCalculator.getResultsFor('partiallyVisible'); fastDraw = this.areAllProposedVisibleRowsAlreadyRendered(proposedFullyVisibleRowsCalculator, proposedPartiallyVisibleRowsCalculator); } if (fastDraw && !wtSettings.getSetting('renderAllColumns')) { const proposedFullyVisibleColumnsCalculator = columnsCalculator.getResultsFor('fullyVisible'); const proposedPartiallyVisibleColumnsCalculator = columnsCalculator.getResultsFor('partiallyVisible'); fastDraw = this.areAllProposedVisibleColumnsAlreadyRendered(proposedFullyVisibleColumnsCalculator, proposedPartiallyVisibleColumnsCalculator); } if (!fastDraw) { this.rowsRenderCalculator = rowsCalculator.getResultsFor('rendered'); this.columnsRenderCalculator = columnsCalculator.getResultsFor('rendered'); } this.rowsVisibleCalculator = rowsCalculator.getResultsFor('fullyVisible'); this.columnsVisibleCalculator = columnsCalculator.getResultsFor('fullyVisible'); this.rowsPartiallyVisibleCalculator = rowsCalculator.getResultsFor('partiallyVisible'); this.columnsPartiallyVisibleCalculator = columnsCalculator.getResultsFor('partiallyVisible'); return fastDraw; } /** * Creates rows and columns calculators (after draw, to determine what are * the actually fully visible and partially visible rows and columns). */ createVisibleCalculators() { const rowsCalculator = this.createRowsCalculator(['fullyVisible', 'partiallyVisible']); const columnsCalculator = this.createColumnsCalculator(['fullyVisible', 'partiallyVisible']); this.rowsVisibleCalculator = rowsCalculator.getResultsFor('fullyVisible'); this.columnsVisibleCalculator = columnsCalculator.getResultsFor('fullyVisible'); this.rowsPartiallyVisibleCalculator = rowsCalculator.getResultsFor('partiallyVisible'); this.columnsPartiallyVisibleCalculator = columnsCalculator.getResultsFor('partiallyVisible'); } /** * Returns information whether proposedFullyVisibleRowsCalculator viewport * is contained inside rows rendered in previous draw (cached in rowsRenderCalculator). * * @param {ViewportRowsCalculator} proposedFullyVisibleRowsCalculator The instance of the fully visible rows viewport calculator to compare with. * @param {ViewportRowsCalculator} proposedPartiallyVisibleRowsCalculator The instance of the partially visible rows viewport calculator to compare with. * @returns {boolean} Returns `true` if all proposed visible rows are already rendered (meaning: redraw is not needed). * Returns `false` if at least one proposed visible row is not already rendered (meaning: redraw is needed). */ areAllProposedVisibleRowsAlreadyRendered(proposedFullyVisibleRowsCalculator, proposedPartiallyVisibleRowsCalculator) { if (!this.rowsVisibleCalculator) { return false; } let { startRow, endRow } = proposedFullyVisibleRowsCalculator; const { startRow: partiallyVisibleStartRow, endRow: partiallyVisibleEndRow } = proposedPartiallyVisibleRowsCalculator; // if there are no fully visible rows at all... if (startRow === null && endRow === null) { if (!proposedFullyVisibleRowsCalculator.isVisibleInTrimmingContainer && !this.wtTable.isRowBeforeRenderedRows(partiallyVisibleStartRow) && !this.wtTable.isRowAfterRenderedRows(partiallyVisibleEndRow)) { return true; } // ...use partially visible rows calculator to determine what render type is needed startRow = partiallyVisibleStartRow; endRow = partiallyVisibleEndRow; } const { startRow: renderedStartRow, endRow: renderedEndRow, rowStartOffset, rowEndOffset } = this.rowsRenderCalculator; const totalRows = this.wtSettings.getSetting('totalRows') - 1; const renderingThreshold = this.wtSettings.getSetting('viewportRowRenderingThreshold'); if (Number.isInteger(renderingThreshold) && renderingThreshold > 0) { startRow = Math.max(0, startRow - Math.min(rowStartOffset, renderingThreshold)); endRow = Math.min(totalRows, endRow + Math.min(rowEndOffset, renderingThreshold)); } else if (renderingThreshold === 'auto') { startRow = Math.max(0, startRow - Math.ceil(rowStartOffset / 2)); endRow = Math.min(totalRows, endRow + Math.ceil(rowEndOffset / 2)); } if (startRow < renderedStartRow || startRow === renderedStartRow && startRow > 0) { return false; } else if (endRow > renderedEndRow || endRow === renderedEndRow && endRow < totalRows) { return false; } return true; } /** * Returns information whether proposedFullyVisibleColumnsCalculator viewport * is contained inside column rendered in previous draw (cached in columnsRenderCalculator). * * @param {ViewportRowsCalculator} proposedFullyVisibleColumnsCalculator The instance of the fully visible columns viewport calculator to compare with. * @param {ViewportRowsCalculator} proposedPartiallyVisibleColumnsCalculator The instance of the partially visible columns viewport calculator to compare with. * @returns {boolean} Returns `true` if all proposed visible columns are already rendered (meaning: redraw is not needed). * Returns `false` if at least one proposed visible column is not already rendered (meaning: redraw is needed). */ areAllProposedVisibleColumnsAlreadyRendered(proposedFullyVisibleColumnsCalculator, proposedPartiallyVisibleColumnsCalculator) { if (!this.columnsVisibleCalculator) { return false; } let { startColumn, endColumn } = proposedFullyVisibleColumnsCalculator; const { startColumn: partiallyVisibleStartColumn, endColumn: partiallyVisibleEndColumn } = proposedPartiallyVisibleColumnsCalculator; // if there are no fully visible columns at all... if (startColumn === null && endColumn === null) { if (!proposedFullyVisibleColumnsCalculator.isVisibleInTrimmingContainer && !this.wtTable.isColumnBeforeRenderedColumns(partiallyVisibleStartColumn) && !this.wtTable.isColumnAfterRenderedColumns(partiallyVisibleEndColumn)) { return true; } // ...use partially visible columns calculator to determine what render type is needed startColumn = partiallyVisibleStartColumn; endColumn = partiallyVisibleEndColumn; } const { startColumn: renderedStartColumn, endColumn: renderedEndColumn, columnStartOffset, columnEndOffset } = this.columnsRenderCalculator; const totalColumns = this.wtSettings.getSetting('totalColumns') - 1; const renderingThreshold = this.wtSettings.getSetting('viewportColumnRenderingThreshold'); if (Number.isInteger(renderingThreshold) && renderingThreshold > 0) { startColumn = Math.max(0, startColumn - Math.min(columnStartOffset, renderingThreshold)); endColumn = Math.min(totalColumns, endColumn + Math.min(columnEndOffset, renderingThreshold)); } else if (renderingThreshold === 'auto') { startColumn = Math.max(0, startColumn - Math.ceil(columnStartOffset / 2)); endColumn = Math.min(totalColumns, endColumn + Math.ceil(columnEndOffset / 2)); } if (startColumn < renderedStartColumn || startColumn === renderedStartColumn && startColumn > 0) { return false; } else if (endColumn > renderedEndColumn || endColumn === renderedEndColumn && endColumn < totalColumns) { return false; } return true; } /** * Resets values in keys of the hasOversizedColumnHeadersMarked object after updateSettings. */ resetHasOversizedColumnHeadersMarked() { (0, _object.objectEach)(this.hasOversizedColumnHeadersMarked, (value, key, object) => { object[key] = undefined; }); } } var _default = exports.default = Viewport;