import { getScrollbarWidth, getScrollTop, getStyle, offset, outerHeight, outerWidth, } from './../../../helpers/dom/element'; import {objectEach} from './../../../helpers/object'; import EventManager from './../../../eventManager'; import ViewportColumnsCalculator from './calculator/viewportColumns'; import ViewportRowsCalculator from './calculator/viewportRows'; /** * @class Viewport */ class Viewport { /** * @param wotInstance */ constructor(wotInstance) { this.wot = wotInstance; // legacy support this.instance = this.wot; this.oversizedRows = []; this.oversizedColumnHeaders = []; this.hasOversizedColumnHeadersMarked = {}; this.clientHeight = 0; this.containerWidth = NaN; this.rowHeaderWidth = NaN; this.rowsVisibleCalculator = null; this.columnsVisibleCalculator = null; this.eventManager = new EventManager(this.wot); this.eventManager.addEventListener(window, 'resize', () => { this.clientHeight = this.getWorkspaceHeight(); }); } /** * @returns {number} */ getWorkspaceHeight() { let trimmingContainer = this.instance.wtOverlays.topOverlay.trimmingContainer; let elemHeight; let height = 0; if (trimmingContainer === window) { height = document.documentElement.clientHeight; } else { elemHeight = outerHeight(trimmingContainer); // returns height without DIV scrollbar height = (elemHeight > 0 && trimmingContainer.clientHeight > 0) ? trimmingContainer.clientHeight : Infinity; } return height; } getWorkspaceWidth() { let width; let totalColumns = this.wot.getSetting('totalColumns'); let trimmingContainer = this.instance.wtOverlays.leftOverlay.trimmingContainer; let overflow; let stretchSetting = this.wot.getSetting('stretchH'); let docOffsetWidth = document.documentElement.offsetWidth; let preventOverflow = this.wot.getSetting('preventOverflow'); if (preventOverflow) { return outerWidth(this.instance.wtTable.wtRootElement); } if (this.wot.getSetting('freezeOverlays')) { width = Math.min(docOffsetWidth - this.getWorkspaceOffset().left, docOffsetWidth); } else { width = Math.min(this.getContainerFillWidth(), docOffsetWidth - this.getWorkspaceOffset().left, docOffsetWidth); } if (trimmingContainer === window && totalColumns > 0 && this.sumColumnWidths(0, totalColumns - 1) > width) { // in case sum of column widths is higher than available stylesheet width, let's assume using the whole window // otherwise continue below, which will allow stretching // this is used in `scroll_window.html` // TODO test me return document.documentElement.clientWidth; } if (trimmingContainer !== window) { overflow = getStyle(this.instance.wtOverlays.leftOverlay.trimmingContainer, 'overflow'); if (overflow == 'scroll' || overflow == 'hidden' || overflow == 'auto') { // this is used in `scroll.html` // TODO test me return Math.max(width, trimmingContainer.clientWidth); } } if (stretchSetting === 'none' || !stretchSetting) { // if no stretching is used, return the maximum used workspace width return Math.max(width, outerWidth(this.instance.wtTable.TABLE)); } // if stretching is used, return the actual container width, so the columns can fit inside it return width; } /** * Checks if viewport has vertical scroll * * @returns {Boolean} */ hasVerticalScroll() { return this.getWorkspaceActualHeight() > this.getWorkspaceHeight(); } /** * Checks if viewport has horizontal scroll * * @returns {Boolean} */ hasHorizontalScroll() { return this.getWorkspaceActualWidth() > this.getWorkspaceWidth(); } /** * @param from * @param length * @returns {Number} */ sumColumnWidths(from, length) { let sum = 0; while (from < length) { sum += this.wot.wtTable.getColumnWidth(from); from++; } return sum; } /** * @returns {Number} */ getContainerFillWidth() { if (this.containerWidth) { return this.containerWidth; } let mainContainer = this.instance.wtTable.holder; let fillWidth; let dummyElement; dummyElement = document.createElement('div'); dummyElement.style.width = '100%'; dummyElement.style.height = '1px'; mainContainer.appendChild(dummyElement); fillWidth = dummyElement.offsetWidth; this.containerWidth = fillWidth; mainContainer.removeChild(dummyElement); return fillWidth; } /** * @returns {Number} */ getWorkspaceOffset() { return offset(this.wot.wtTable.TABLE); } /** * @returns {Number} */ getWorkspaceActualHeight() { return outerHeight(this.wot.wtTable.TABLE); } /** * @returns {Number} */ getWorkspaceActualWidth() { return outerWidth(this.wot.wtTable.TABLE) || outerWidth(this.wot.wtTable.TBODY) || outerWidth(this.wot.wtTable.THEAD); // IE8 reports 0 as offsetWidth; } /** * @returns {Number} */ getColumnHeaderHeight() { if (isNaN(this.columnHeaderHeight)) { this.columnHeaderHeight = outerHeight(this.wot.wtTable.THEAD); } return this.columnHeaderHeight; } /** * @returns {Number} */ getViewportHeight() { let containerHeight = this.getWorkspaceHeight(); let columnHeaderHeight; if (containerHeight === Infinity) { return containerHeight; } columnHeaderHeight = this.getColumnHeaderHeight(); if (columnHeaderHeight > 0) { containerHeight -= columnHeaderHeight; } return containerHeight; } /** * @returns {Number} */ getRowHeaderWidth() { let rowHeadersHeightSetting = this.instance.getSetting('rowHeaderWidth'); let rowHeaders = this.instance.getSetting('rowHeaders'); if (rowHeadersHeightSetting) { this.rowHeaderWidth = 0; for (let i = 0, len = rowHeaders.length; i < len; i++) { this.rowHeaderWidth += rowHeadersHeightSetting[i] || rowHeadersHeightSetting; } } if (this.wot.cloneSource) { return this.wot.cloneSource.wtViewport.getRowHeaderWidth(); } if (isNaN(this.rowHeaderWidth)) { if (rowHeaders.length) { let TH = this.instance.wtTable.TABLE.querySelector('TH'); this.rowHeaderWidth = 0; for (let i = 0, len = rowHeaders.length; i < len; i++) { if (TH) { this.rowHeaderWidth += 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.instance.getSetting('onModifyRowHeaderWidth', this.rowHeaderWidth) || this.rowHeaderWidth; return this.rowHeaderWidth; } /** * @returns {Number} */ getViewportWidth() { let containerWidth = this.getWorkspaceWidth(); let rowHeaderWidth; if (containerWidth === Infinity) { return containerWidth; } rowHeaderWidth = this.getRowHeaderWidth(); if (rowHeaderWidth > 0) { return containerWidth - rowHeaderWidth; } return containerWidth; } /** * Creates: * - rowsRenderCalculator (before draw, to qualify rows for rendering) * - rowsVisibleCalculator (after draw, to measure which rows are actually visible) * * @returns {ViewportRowsCalculator} */ createRowsCalculator(visible = false) { let height; let pos; let fixedRowsTop; let scrollbarHeight; let fixedRowsBottom; let fixedRowsHeight; let totalRows; this.rowHeaderWidth = NaN; if (this.wot.wtSettings.settings.renderAllRows) { height = Infinity; } else { height = this.getViewportHeight(); } pos = this.wot.wtOverlays.topOverlay.getScrollPosition() - this.wot.wtOverlays.topOverlay.getTableParentOffset(); if (pos < 0) { pos = 0; } fixedRowsTop = this.wot.getSetting('fixedRowsTop'); fixedRowsBottom = this.wot.getSetting('fixedRowsBottom'); totalRows = this.wot.getSetting('totalRows'); if (fixedRowsTop) { fixedRowsHeight = this.wot.wtOverlays.topOverlay.sumCellSizes(0, fixedRowsTop); pos += fixedRowsHeight; height -= fixedRowsHeight; } if (fixedRowsBottom && this.wot.wtOverlays.bottomOverlay.clone) { fixedRowsHeight = this.wot.wtOverlays.bottomOverlay.sumCellSizes(totalRows - fixedRowsBottom, totalRows); height -= fixedRowsHeight; } if (this.wot.wtTable.holder.clientHeight === this.wot.wtTable.holder.offsetHeight) { scrollbarHeight = 0; } else { scrollbarHeight = getScrollbarWidth(); } return new ViewportRowsCalculator( height, pos, this.wot.getSetting('totalRows'), (sourceRow) => this.wot.wtTable.getRowHeight(sourceRow), visible ? null : this.wot.wtSettings.settings.viewportRowCalculatorOverride, visible, scrollbarHeight ); } /** * Creates: * - columnsRenderCalculator (before draw, to qualify columns for rendering) * - columnsVisibleCalculator (after draw, to measure which columns are actually visible) * * @returns {ViewportRowsCalculator} */ createColumnsCalculator(visible = false) { let width = this.getViewportWidth(); let pos; let fixedColumnsLeft; this.columnHeaderHeight = NaN; pos = this.wot.wtOverlays.leftOverlay.getScrollPosition() - this.wot.wtOverlays.leftOverlay.getTableParentOffset(); if (pos < 0) { pos = 0; } fixedColumnsLeft = this.wot.getSetting('fixedColumnsLeft'); if (fixedColumnsLeft) { let fixedColumnsWidth = this.wot.wtOverlays.leftOverlay.sumCellSizes(0, fixedColumnsLeft); pos += fixedColumnsWidth; width -= fixedColumnsWidth; } if (this.wot.wtTable.holder.clientWidth !== this.wot.wtTable.holder.offsetWidth) { width -= getScrollbarWidth(); } return new ViewportColumnsCalculator( width, pos, this.wot.getSetting('totalColumns'), (sourceCol) => this.wot.wtTable.getColumnWidth(sourceCol), visible ? null : this.wot.wtSettings.settings.viewportColumnCalculatorOverride, visible, this.wot.getSetting('stretchH'), (stretchedWidth, column) => this.wot.getSetting('onBeforeStretchingColumnWidth', stretchedWidth, column) ); } /** * Creates rowsRenderCalculator and columnsRenderCalculator (before draw, to determine what rows and * cols should be rendered) * * @param fastDraw {Boolean} If `true`, will try to avoid full redraw and only update the border positions. * If `false` or `undefined`, will perform a full redraw * @returns fastDraw {Boolean} The fastDraw value, possibly modified */ createRenderCalculators(fastDraw = false) { if (fastDraw) { let proposedRowsVisibleCalculator = this.createRowsCalculator(true); let proposedColumnsVisibleCalculator = this.createColumnsCalculator(true); if (!(this.areAllProposedVisibleRowsAlreadyRendered(proposedRowsVisibleCalculator) && this.areAllProposedVisibleColumnsAlreadyRendered(proposedColumnsVisibleCalculator))) { fastDraw = false; } } if (!fastDraw) { this.rowsRenderCalculator = this.createRowsCalculator(); this.columnsRenderCalculator = this.createColumnsCalculator(); } // delete temporarily to make sure that renderers always use rowsRenderCalculator, not rowsVisibleCalculator this.rowsVisibleCalculator = null; this.columnsVisibleCalculator = null; return fastDraw; } /** * Creates rowsVisibleCalculator and columnsVisibleCalculator (after draw, to determine what are * the actually visible rows and columns) */ createVisibleCalculators() { this.rowsVisibleCalculator = this.createRowsCalculator(true); this.columnsVisibleCalculator = this.createColumnsCalculator(true); } /** * Returns information whether proposedRowsVisibleCalculator viewport * is contained inside rows rendered in previous draw (cached in rowsRenderCalculator) * * @param {Object} proposedRowsVisibleCalculator * @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(proposedRowsVisibleCalculator) { if (this.rowsVisibleCalculator) { if (proposedRowsVisibleCalculator.startRow < this.rowsRenderCalculator.startRow || (proposedRowsVisibleCalculator.startRow === this.rowsRenderCalculator.startRow && proposedRowsVisibleCalculator.startRow > 0)) { return false; } else if (proposedRowsVisibleCalculator.endRow > this.rowsRenderCalculator.endRow || (proposedRowsVisibleCalculator.endRow === this.rowsRenderCalculator.endRow && proposedRowsVisibleCalculator.endRow < this.wot.getSetting('totalRows') - 1)) { return false; } return true; } return false; } /** * Returns information whether proposedColumnsVisibleCalculator viewport * is contained inside column rendered in previous draw (cached in columnsRenderCalculator) * * @param {Object} proposedColumnsVisibleCalculator * @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(proposedColumnsVisibleCalculator) { if (this.columnsVisibleCalculator) { if (proposedColumnsVisibleCalculator.startColumn < this.columnsRenderCalculator.startColumn || (proposedColumnsVisibleCalculator.startColumn === this.columnsRenderCalculator.startColumn && proposedColumnsVisibleCalculator.startColumn > 0)) { return false; } else if (proposedColumnsVisibleCalculator.endColumn > this.columnsRenderCalculator.endColumn || (proposedColumnsVisibleCalculator.endColumn === this.columnsRenderCalculator.endColumn && proposedColumnsVisibleCalculator.endColumn < this.wot.getSetting('totalColumns') - 1)) { return false; } return true; } return false; } /** * Resets values in keys of the hasOversizedColumnHeadersMarked object after updateSettings. */ resetHasOversizedColumnHeadersMarked() { objectEach(this.hasOversizedColumnHeadersMarked, (value, key, object) => { object[key] = void 0; }); } } export default Viewport;