|
|
@@ -0,0 +1,490 @@
|
|
|
+import {
|
|
|
+ getScrollbarWidth,
|
|
|
+ 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();
|
|
|
+ });
|
|
|
+ this.offset = offset(this.wot.wtTable.TABLE);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @returns {number}
|
|
|
+ */
|
|
|
+ getWorkspaceHeight() {
|
|
|
+ const 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;
|
|
|
+ const totalColumns = this.wot.getSetting('totalColumns');
|
|
|
+ const trimmingContainer = this.instance.wtOverlays.leftOverlay.trimmingContainer;
|
|
|
+ let overflow;
|
|
|
+ const stretchSetting = this.wot.getSetting('stretchH');
|
|
|
+ const docOffsetWidth = window.innerWidth; // document.documentElement.offsetWidth;
|
|
|
+ const 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 = 'hidden';// 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, this.instance.wtOverlays.leftOverlay.trimmingContainerClientWidth);// 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) {
|
|
|
+ const wtTable = this.wot.wtTable;
|
|
|
+ let sum = 0;
|
|
|
+ let column = from;
|
|
|
+
|
|
|
+ while (column < length) {
|
|
|
+ sum += wtTable.getColumnWidth(column);
|
|
|
+ column += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ return sum;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @returns {Number}
|
|
|
+ */
|
|
|
+ getContainerFillWidth() {
|
|
|
+ if (this.containerWidth) {
|
|
|
+ return this.containerWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ const mainContainer = this.instance.wtTable.holder;
|
|
|
+ const dummyElement = document.createElement('div');
|
|
|
+
|
|
|
+ dummyElement.style.width = '100%';
|
|
|
+ dummyElement.style.height = '1px';
|
|
|
+ mainContainer.appendChild(dummyElement);
|
|
|
+
|
|
|
+ const fillWidth = dummyElement.offsetWidth;
|
|
|
+
|
|
|
+ this.containerWidth = fillWidth;
|
|
|
+ mainContainer.removeChild(dummyElement);
|
|
|
+
|
|
|
+ return fillWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @returns {Number}
|
|
|
+ */
|
|
|
+ getWorkspaceOffset() {
|
|
|
+ return this.offset;// 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 <table> 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();
|
|
|
+
|
|
|
+ if (containerHeight === Infinity) {
|
|
|
+ return containerHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ const columnHeaderHeight = this.getColumnHeaderHeight();
|
|
|
+
|
|
|
+ if (columnHeaderHeight > 0) {
|
|
|
+ containerHeight -= columnHeaderHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ return containerHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @returns {Number}
|
|
|
+ */
|
|
|
+ getRowHeaderWidth() {
|
|
|
+ const rowHeadersHeightSetting = this.instance.getSetting('rowHeaderWidth');
|
|
|
+ const 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() {
|
|
|
+ const containerWidth = this.getWorkspaceWidth();
|
|
|
+
|
|
|
+ if (containerWidth === Infinity) {
|
|
|
+ return containerWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ const 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 scrollbarHeight;
|
|
|
+ let fixedRowsHeight;
|
|
|
+
|
|
|
+ this.rowHeaderWidth = NaN;
|
|
|
+
|
|
|
+ if (this.wot.wtSettings.settings.renderAllRows && !visible) {
|
|
|
+ height = Infinity;
|
|
|
+ } else {
|
|
|
+ height = this.getViewportHeight();
|
|
|
+ }
|
|
|
+
|
|
|
+ let pos = this.wot.wtOverlays.topOverlay.getScrollPosition() - this.wot.wtOverlays.topOverlay.getTableParentOffset();
|
|
|
+
|
|
|
+ if (pos < 0) {
|
|
|
+ pos = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ const fixedRowsTop = this.wot.getSetting('fixedRowsTop');
|
|
|
+ const fixedRowsBottom = this.wot.getSetting('fixedRowsBottom');
|
|
|
+ const 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 = this.wot.wtOverlays.leftOverlay.getScrollPosition() - this.wot.wtOverlays.leftOverlay.getTableParentOffset();
|
|
|
+
|
|
|
+ this.columnHeaderHeight = NaN;
|
|
|
+
|
|
|
+ if (pos < 0) {
|
|
|
+ pos = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ const fixedColumnsLeft = this.wot.getSetting('fixedColumnsLeft');
|
|
|
+
|
|
|
+ if (fixedColumnsLeft) {
|
|
|
+ const 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) {
|
|
|
+ let runFastDraw = fastDraw;
|
|
|
+
|
|
|
+ if (runFastDraw) {
|
|
|
+ const proposedRowsVisibleCalculator = this.createRowsCalculator(true);
|
|
|
+ const proposedColumnsVisibleCalculator = this.createColumnsCalculator(true);
|
|
|
+
|
|
|
+ if (!(this.areAllProposedVisibleRowsAlreadyRendered(proposedRowsVisibleCalculator) &&
|
|
|
+ this.areAllProposedVisibleColumnsAlreadyRendered(proposedColumnsVisibleCalculator))) {
|
|
|
+ runFastDraw = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!runFastDraw) {
|
|
|
+ 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 runFastDraw;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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;
|