import "core-js/modules/es.array.push.js"; import "core-js/modules/es.error.cause.js"; function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); } function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); } function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; } function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; } function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); } function _classApplyDescriptorSet(receiver, descriptor, value) { if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } } import { addClass, clearTextSelection, empty, fastInnerHTML, fastInnerText, getScrollbarWidth, hasClass, isChildOf, isInput, isOutsideInput } from "./helpers/dom/element.mjs"; import EventManager from "./eventManager.mjs"; import { isImmediatePropagationStopped, isRightClick, isLeftClick } from "./helpers/dom/event.mjs"; import Walkontable from "./3rdparty/walkontable/src/index.mjs"; import { handleMouseEvent } from "./selection/mouseEventHandler.mjs"; import { isRootInstance } from "./utils/rootInstance.mjs"; const privatePool = new WeakMap(); /** * @class TableView * @private */ var _columnHeadersCount = /*#__PURE__*/new WeakMap(); var _rowHeadersCount = /*#__PURE__*/new WeakMap(); class TableView { /** * @param {Hanstontable} instance Instance of {@link Handsontable}. */ constructor(instance) { /** * Instance of {@link Handsontable}. * * @private * @type {Handsontable} */ _defineProperty(this, "instance", void 0); /** * Instance of {@link EventManager}. * * @private * @type {EventManager} */ _defineProperty(this, "eventManager", void 0); /** * Current Handsontable's GridSettings object. * * @private * @type {GridSettings} */ _defineProperty(this, "settings", void 0); /** * Main element. * * @private * @type {HTMLTableSectionElement} */ _defineProperty(this, "THEAD", void 0); /** * Main element. * * @private * @type {HTMLTableSectionElement} */ _defineProperty(this, "TBODY", void 0); /** * Main Walkontable instance. * * @private * @type {Walkontable} */ _defineProperty(this, "_wt", void 0); /** * Main Walkontable instance. * * @private * @type {Walkontable} */ _defineProperty(this, "activeWt", void 0); /** * The total number of the column header renderers applied to the table through the * `afterGetColumnHeaderRenderers` hook. * * @type {number} */ _classPrivateFieldInitSpec(this, _columnHeadersCount, { writable: true, value: 0 }); /** * The total number of the row header renderers applied to the table through the * `afterGetRowHeaderRenderers` hook. * * @type {number} */ _classPrivateFieldInitSpec(this, _rowHeadersCount, { writable: true, value: 0 }); /** * The flag determines if the `adjustElementsSize` method call was made during * the render suspending. If true, the method has to be triggered once after render * resuming. * * @private * @type {boolean} */ _defineProperty(this, "postponedAdjustElementsSize", false); this.instance = instance; this.eventManager = new EventManager(this.instance); this.settings = this.instance.getSettings(); privatePool.set(this, { /** * Defines if the text should be selected during mousemove. * * @private * @type {boolean} */ selectionMouseDown: false, /** * @private * @type {boolean} */ mouseDown: void 0, /** * Main element. * * @private * @type {HTMLTableElement} */ table: void 0, /** * Cached width of the rootElement. * * @type {number} */ lastWidth: 0, /** * Cached height of the rootElement. * * @type {number} */ lastHeight: 0 }); this.createElements(); this.registerEvents(); this.initializeWalkontable(); } /** * Renders WalkontableUI. */ render() { if (!this.instance.isRenderSuspended()) { this.instance.runHooks('beforeRender', this.instance.forceFullRender); if (this.postponedAdjustElementsSize) { this.postponedAdjustElementsSize = false; this.adjustElementsSize(true); } this._wt.draw(!this.instance.forceFullRender); this.instance.runHooks('afterRender', this.instance.forceFullRender); this.instance.forceFullRender = false; this.instance.renderCall = false; } } /** * Adjust overlays elements size and master table size. * * @param {boolean} [force=false] When `true`, it adjust the DOM nodes sizes for all overlays. */ adjustElementsSize() { let force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (this.instance.isRenderSuspended()) { this.postponedAdjustElementsSize = true; } else { this._wt.wtOverlays.adjustElementsSize(force); } } /** * Returns td object given coordinates. * * @param {CellCoords} coords Renderable cell coordinates. * @param {boolean} topmost Indicates whether the cell should be calculated from the topmost. * @returns {HTMLTableCellElement|null} */ getCellAtCoords(coords, topmost) { const td = this._wt.getCell(coords, topmost); if (td < 0) { // there was an exit code (cell is out of bounds) return null; } return td; } /** * Scroll viewport to a cell. * * @param {CellCoords} coords Renderable cell coordinates. * @param {boolean} [snapToTop] If `true`, viewport is scrolled to show the cell on the top of the table. * @param {boolean} [snapToRight] If `true`, viewport is scrolled to show the cell on the right side of the table. * @param {boolean} [snapToBottom] If `true`, viewport is scrolled to show the cell on the bottom side of the table. * @param {boolean} [snapToLeft] If `true`, viewport is scrolled to show the cell on the left side of the table. * @returns {boolean} */ scrollViewport(coords, snapToTop, snapToRight, snapToBottom, snapToLeft) { return this._wt.scrollViewport(coords, snapToTop, snapToRight, snapToBottom, snapToLeft); } /** * Scroll viewport to a column. * * @param {number} column Renderable column index. * @param {boolean} [snapToRight] If `true`, viewport is scrolled to show the cell on the right side of the table. * @param {boolean} [snapToLeft] If `true`, viewport is scrolled to show the cell on the left side of the table. * @returns {boolean} */ scrollViewportHorizontally(column, snapToRight, snapToLeft) { return this._wt.scrollViewportHorizontally(column, snapToRight, snapToLeft); } /** * Scroll viewport to a row. * * @param {number} row Renderable row index. * @param {boolean} [snapToTop] If `true`, viewport is scrolled to show the cell on the top of the table. * @param {boolean} [snapToBottom] If `true`, viewport is scrolled to show the cell on the bottom side of the table. * @returns {boolean} */ scrollViewportVertically(row, snapToTop, snapToBottom) { return this._wt.scrollViewportVertically(row, snapToTop, snapToBottom); } /** * Prepares DOMElements and adds correct className to the root element. * * @private */ createElements() { const priv = privatePool.get(this); const { rootElement, rootDocument } = this.instance; const originalStyle = rootElement.getAttribute('style'); if (originalStyle) { rootElement.setAttribute('data-originalstyle', originalStyle); // needed to retrieve original style in jsFiddle link generator in HT examples. may be removed in future versions } addClass(rootElement, 'handsontable'); priv.table = rootDocument.createElement('TABLE'); addClass(priv.table, 'htCore'); if (this.instance.getSettings().tableClassName) { addClass(priv.table, this.instance.getSettings().tableClassName); } this.THEAD = rootDocument.createElement('THEAD'); priv.table.appendChild(this.THEAD); this.TBODY = rootDocument.createElement('TBODY'); priv.table.appendChild(this.TBODY); this.instance.table = priv.table; this.instance.container.insertBefore(priv.table, this.instance.container.firstChild); } /** * Attaches necessary listeners. * * @private */ registerEvents() { const priv = privatePool.get(this); const { rootElement, rootDocument, selection } = this.instance; const documentElement = rootDocument.documentElement; this.eventManager.addEventListener(rootElement, 'mousedown', event => { priv.selectionMouseDown = true; if (!this.isTextSelectionAllowed(event.target)) { const { rootWindow } = this.instance; clearTextSelection(rootWindow); event.preventDefault(); rootWindow.focus(); // make sure that window that contains HOT is active. Important when HOT is in iframe. } }); this.eventManager.addEventListener(rootElement, 'mouseup', () => { priv.selectionMouseDown = false; }); this.eventManager.addEventListener(rootElement, 'mousemove', event => { if (priv.selectionMouseDown && !this.isTextSelectionAllowed(event.target)) { // Clear selection only when fragmentSelection is enabled, otherwise clearing selection breaks the IME editor. if (this.settings.fragmentSelection) { clearTextSelection(this.instance.rootWindow); } event.preventDefault(); } }); this.eventManager.addEventListener(documentElement, 'keyup', event => { // TODO: is it the best place and way to finish cell selection? if (selection.isInProgress() && !event.shiftKey) { selection.finish(); } }); this.eventManager.addEventListener(documentElement, 'mouseup', event => { if (selection.isInProgress() && isLeftClick(event)) { // is left mouse button selection.finish(); } priv.mouseDown = false; if (isOutsideInput(rootDocument.activeElement) || !selection.isSelected() && !selection.isSelectedByAnyHeader() && !rootElement.contains(event.target) && !isRightClick(event)) { this.instance.unlisten(); } }); this.eventManager.addEventListener(documentElement, 'contextmenu', event => { if (selection.isInProgress() && isRightClick(event)) { selection.finish(); priv.mouseDown = false; } }); this.eventManager.addEventListener(documentElement, 'touchend', () => { if (selection.isInProgress()) { selection.finish(); } priv.mouseDown = false; }); this.eventManager.addEventListener(documentElement, 'mousedown', event => { const originalTarget = event.target; const eventX = event.x || event.clientX; const eventY = event.y || event.clientY; let next = event.target; if (priv.mouseDown || !rootElement || !this.instance.view) { return; // it must have been started in a cell } // immediate click on "holder" means click on the right side of vertical scrollbar const { holder } = this.instance.view._wt.wtTable; if (next === holder) { const scrollbarWidth = getScrollbarWidth(rootDocument); if (rootDocument.elementFromPoint(eventX + scrollbarWidth, eventY) !== holder || rootDocument.elementFromPoint(eventX, eventY + scrollbarWidth) !== holder) { return; } } else { while (next !== documentElement) { if (next === null) { if (event.isTargetWebComponent) { break; } // click on something that was a row but now is detached (possibly because your click triggered a rerender) return; } if (next === rootElement) { // click inside container return; } next = next.parentNode; } } // function did not return until here, we have an outside click! const outsideClickDeselects = typeof this.settings.outsideClickDeselects === 'function' ? this.settings.outsideClickDeselects(originalTarget) : this.settings.outsideClickDeselects; if (outsideClickDeselects) { this.instance.deselectCell(); } else { this.instance.destroyEditor(false, false); } }); this.eventManager.addEventListener(priv.table, 'selectstart', event => { if (this.settings.fragmentSelection || isInput(event.target)) { return; } // https://github.com/handsontable/handsontable/issues/160 // Prevent text from being selected when performing drag down. event.preventDefault(); }); } /** * Translate renderable cell coordinates to visual coordinates. * * @param {CellCoords} coords The cell coordinates. * @returns {CellCoords} */ translateFromRenderableToVisualCoords(_ref) { let { row, col } = _ref; // TODO: To consider an idea to reusing the CellCoords instance instead creating new one. return this.instance._createCellCoords(...this.translateFromRenderableToVisualIndex(row, col)); } /** * Translate renderable row and column indexes to visual row and column indexes. * * @param {number} renderableRow Renderable row index. * @param {number} renderableColumn Renderable columnIndex. * @returns {number[]} */ translateFromRenderableToVisualIndex(renderableRow, renderableColumn) { // TODO: Some helper may be needed. // We perform translation for indexes (without headers). let visualRow = renderableRow >= 0 ? this.instance.rowIndexMapper.getVisualFromRenderableIndex(renderableRow) : renderableRow; let visualColumn = renderableColumn >= 0 ? this.instance.columnIndexMapper.getVisualFromRenderableIndex(renderableColumn) : renderableColumn; if (visualRow === null) { visualRow = renderableRow; } if (visualColumn === null) { visualColumn = renderableColumn; } return [visualRow, visualColumn]; } /** * Returns the number of renderable indexes. * * @private * @param {IndexMapper} indexMapper The IndexMapper instance for specific axis. * @param {number} maxElements Maximum number of elements (rows or columns). * * @returns {number|*} */ countRenderableIndexes(indexMapper, maxElements) { const consideredElements = Math.min(indexMapper.getNotTrimmedIndexesLength(), maxElements); // Don't take hidden indexes into account. We are looking just for renderable indexes. const firstNotHiddenIndex = indexMapper.getNearestNotHiddenIndex(consideredElements - 1, -1); // There are no renderable indexes. if (firstNotHiddenIndex === null) { return 0; } return indexMapper.getRenderableFromVisualIndex(firstNotHiddenIndex) + 1; } /** * Returns the number of renderable columns. * * @returns {number} */ countRenderableColumns() { return this.countRenderableIndexes(this.instance.columnIndexMapper, this.settings.maxCols); } /** * Returns the number of renderable rows. * * @returns {number} */ countRenderableRows() { return this.countRenderableIndexes(this.instance.rowIndexMapper, this.settings.maxRows); } /** * Returns number of not hidden row indexes counting from the passed starting index. * The counting direction can be controlled by `incrementBy` argument. * * @param {number} visualIndex The visual index from which the counting begins. * @param {number} incrementBy If `-1` then counting is backwards or forward when `1`. * @returns {number} */ countNotHiddenRowIndexes(visualIndex, incrementBy) { return this.countNotHiddenIndexes(visualIndex, incrementBy, this.instance.rowIndexMapper, this.countRenderableRows()); } /** * Returns number of not hidden column indexes counting from the passed starting index. * The counting direction can be controlled by `incrementBy` argument. * * @param {number} visualIndex The visual index from which the counting begins. * @param {number} incrementBy If `-1` then counting is backwards or forward when `1`. * @returns {number} */ countNotHiddenColumnIndexes(visualIndex, incrementBy) { return this.countNotHiddenIndexes(visualIndex, incrementBy, this.instance.columnIndexMapper, this.countRenderableColumns()); } /** * Returns number of not hidden indexes counting from the passed starting index. * The counting direction can be controlled by `incrementBy` argument. * * @param {number} visualIndex The visual index from which the counting begins. * @param {number} incrementBy If `-1` then counting is backwards or forward when `1`. * @param {IndexMapper} indexMapper The IndexMapper instance for specific axis. * @param {number} renderableIndexesCount Total count of renderable indexes for specific axis. * @returns {number} */ countNotHiddenIndexes(visualIndex, incrementBy, indexMapper, renderableIndexesCount) { if (isNaN(visualIndex) || visualIndex < 0) { return 0; } const firstVisibleIndex = indexMapper.getNearestNotHiddenIndex(visualIndex, incrementBy); const renderableIndex = indexMapper.getRenderableFromVisualIndex(firstVisibleIndex); if (!Number.isInteger(renderableIndex)) { return 0; } let notHiddenIndexes = 0; if (incrementBy < 0) { // Zero-based numbering for renderable indexes corresponds to a number of not hidden indexes. notHiddenIndexes = renderableIndex + 1; } else if (incrementBy > 0) { notHiddenIndexes = renderableIndexesCount - renderableIndex; } return notHiddenIndexes; } /** * The function returns the number of not hidden column indexes that fit between the first and * last fixed column in the left (or right in RTL mode) overlay. * * @returns {number} */ countNotHiddenFixedColumnsStart() { const countCols = this.instance.countCols(); const visualFixedColumnsStart = Math.min(parseInt(this.settings.fixedColumnsStart, 10), countCols) - 1; return this.countNotHiddenColumnIndexes(visualFixedColumnsStart, -1); } /** * The function returns the number of not hidden row indexes that fit between the first and * last fixed row in the top overlay. * * @returns {number} */ countNotHiddenFixedRowsTop() { const countRows = this.instance.countRows(); const visualFixedRowsTop = Math.min(parseInt(this.settings.fixedRowsTop, 10), countRows) - 1; return this.countNotHiddenRowIndexes(visualFixedRowsTop, -1); } /** * The function returns the number of not hidden row indexes that fit between the first and * last fixed row in the bottom overlay. * * @returns {number} */ countNotHiddenFixedRowsBottom() { const countRows = this.instance.countRows(); const visualFixedRowsBottom = Math.max(countRows - parseInt(this.settings.fixedRowsBottom, 10), 0); return this.countNotHiddenRowIndexes(visualFixedRowsBottom, 1); } /** * Checks if at least one cell than belongs to the main table is not covered by the top, left or * bottom overlay. * * @returns {boolean} */ isMainTableNotFullyCoveredByOverlays() { const fixedAllRows = this.countNotHiddenFixedRowsTop() + this.countNotHiddenFixedRowsBottom(); const fixedAllColumns = this.countNotHiddenFixedColumnsStart(); return this.instance.countRenderedRows() > fixedAllRows && this.instance.countRenderedCols() > fixedAllColumns; } /** * Defines default configuration and initializes WalkOnTable instance. * * @private */ initializeWalkontable() { const priv = privatePool.get(this); const walkontableConfig = { rtlMode: this.instance.isRtl(), externalRowCalculator: this.instance.getPlugin('autoRowSize') && this.instance.getPlugin('autoRowSize').isEnabled(), table: priv.table, isDataViewInstance: () => isRootInstance(this.instance), preventOverflow: () => this.settings.preventOverflow, preventWheel: () => this.settings.preventWheel, stretchH: () => this.settings.stretchH, data: (renderableRow, renderableColumn) => { return this.instance.getDataAtCell(...this.translateFromRenderableToVisualIndex(renderableRow, renderableColumn)); }, totalRows: () => this.countRenderableRows(), totalColumns: () => this.countRenderableColumns(), // Number of renderable columns for the left overlay. fixedColumnsStart: () => this.countNotHiddenFixedColumnsStart(), // Number of renderable rows for the top overlay. fixedRowsTop: () => this.countNotHiddenFixedRowsTop(), // Number of renderable rows for the bottom overlay. fixedRowsBottom: () => this.countNotHiddenFixedRowsBottom(), // Enable the inline start overlay when conditions are met. shouldRenderInlineStartOverlay: () => { return this.settings.fixedColumnsStart > 0 || walkontableConfig.rowHeaders().length > 0; }, // Enable the top overlay when conditions are met. shouldRenderTopOverlay: () => { return this.settings.fixedRowsTop > 0 || walkontableConfig.columnHeaders().length > 0; }, // Enable the bottom overlay when conditions are met. shouldRenderBottomOverlay: () => { return this.settings.fixedRowsBottom > 0; }, minSpareRows: () => this.settings.minSpareRows, renderAllRows: this.settings.renderAllRows, rowHeaders: () => { const headerRenderers = []; if (this.instance.hasRowHeaders()) { headerRenderers.push((renderableRowIndex, TH) => { // TODO: Some helper may be needed. // We perform translation for row indexes (without row headers). const visualRowIndex = renderableRowIndex >= 0 ? this.instance.rowIndexMapper.getVisualFromRenderableIndex(renderableRowIndex) : renderableRowIndex; this.appendRowHeader(visualRowIndex, TH); }); } this.instance.runHooks('afterGetRowHeaderRenderers', headerRenderers); _classPrivateFieldSet(this, _rowHeadersCount, headerRenderers.length); return headerRenderers; }, columnHeaders: () => { const headerRenderers = []; if (this.instance.hasColHeaders()) { headerRenderers.push((renderedColumnIndex, TH) => { // TODO: Some helper may be needed. // We perform translation for columns indexes (without column headers). const visualColumnsIndex = renderedColumnIndex >= 0 ? this.instance.columnIndexMapper.getVisualFromRenderableIndex(renderedColumnIndex) : renderedColumnIndex; this.appendColHeader(visualColumnsIndex, TH); }); } this.instance.runHooks('afterGetColumnHeaderRenderers', headerRenderers); _classPrivateFieldSet(this, _columnHeadersCount, headerRenderers.length); return headerRenderers; }, columnWidth: renderedColumnIndex => { const visualIndex = this.instance.columnIndexMapper.getVisualFromRenderableIndex(renderedColumnIndex); // It's not a bug that we can't find visual index for some handled by method indexes. The function is called also // for indexes that are not displayed (indexes that are beyond the grid's boundaries), i.e. when `fixedColumnsStart` > `startCols` (wrong config?) or // scrolling and dataset is empty (scroll should handle that?). return this.instance.getColWidth(visualIndex === null ? renderedColumnIndex : visualIndex); }, rowHeight: renderedRowIndex => { const visualIndex = this.instance.rowIndexMapper.getVisualFromRenderableIndex(renderedRowIndex); return this.instance.getRowHeight(visualIndex === null ? renderedRowIndex : visualIndex); }, cellRenderer: (renderedRowIndex, renderedColumnIndex, TD) => { const [visualRowIndex, visualColumnIndex] = this.translateFromRenderableToVisualIndex(renderedRowIndex, renderedColumnIndex); // Coords may be modified. For example, by the `MergeCells` plugin. It should affect cell value and cell meta. const modifiedCellCoords = this.instance.runHooks('modifyGetCellCoords', visualRowIndex, visualColumnIndex); let visualRowToCheck = visualRowIndex; let visualColumnToCheck = visualColumnIndex; if (Array.isArray(modifiedCellCoords)) { [visualRowToCheck, visualColumnToCheck] = modifiedCellCoords; } const cellProperties = this.instance.getCellMeta(visualRowToCheck, visualColumnToCheck); const prop = this.instance.colToProp(visualColumnToCheck); let value = this.instance.getDataAtRowProp(visualRowToCheck, prop); if (this.instance.hasHook('beforeValueRender')) { value = this.instance.runHooks('beforeValueRender', value, cellProperties); } this.instance.runHooks('beforeRenderer', TD, visualRowIndex, visualColumnIndex, prop, value, cellProperties); this.instance.getCellRenderer(cellProperties)(this.instance, TD, visualRowIndex, visualColumnIndex, prop, value, cellProperties); this.instance.runHooks('afterRenderer', TD, visualRowIndex, visualColumnIndex, prop, value, cellProperties); }, selections: this.instance.selection.highlight, hideBorderOnMouseDownOver: () => this.settings.fragmentSelection, onWindowResize: () => { if (this.instance && !this.instance.isDestroyed) { this.instance.refreshDimensions(); } }, onContainerElementResize: () => { if (this.instance && !this.instance.isDestroyed) { this.instance.refreshDimensions(); } }, onCellMouseDown: (event, coords, TD, wt) => { const visualCoords = this.translateFromRenderableToVisualCoords(coords); const controller = { row: false, column: false, cell: false }; this.instance.listen(); this.activeWt = wt; priv.mouseDown = true; this.instance.runHooks('beforeOnCellMouseDown', event, visualCoords, TD, controller); if (isImmediatePropagationStopped(event)) { return; } handleMouseEvent(event, { coords: visualCoords, selection: this.instance.selection, controller, cellCoordsFactory: (row, column) => this.instance._createCellCoords(row, column) }); this.instance.runHooks('afterOnCellMouseDown', event, visualCoords, TD); this.activeWt = this._wt; }, onCellContextMenu: (event, coords, TD, wt) => { const visualCoords = this.translateFromRenderableToVisualCoords(coords); this.activeWt = wt; priv.mouseDown = false; if (this.instance.selection.isInProgress()) { this.instance.selection.finish(); } this.instance.runHooks('beforeOnCellContextMenu', event, visualCoords, TD); if (isImmediatePropagationStopped(event)) { return; } this.instance.runHooks('afterOnCellContextMenu', event, visualCoords, TD); this.activeWt = this._wt; }, onCellMouseOut: (event, coords, TD, wt) => { const visualCoords = this.translateFromRenderableToVisualCoords(coords); this.activeWt = wt; this.instance.runHooks('beforeOnCellMouseOut', event, visualCoords, TD); if (isImmediatePropagationStopped(event)) { return; } this.instance.runHooks('afterOnCellMouseOut', event, visualCoords, TD); this.activeWt = this._wt; }, onCellMouseOver: (event, coords, TD, wt) => { const visualCoords = this.translateFromRenderableToVisualCoords(coords); const controller = { row: false, column: false, cell: false }; this.activeWt = wt; this.instance.runHooks('beforeOnCellMouseOver', event, visualCoords, TD, controller); if (isImmediatePropagationStopped(event)) { return; } if (priv.mouseDown) { handleMouseEvent(event, { coords: visualCoords, selection: this.instance.selection, controller, cellCoordsFactory: (row, column) => this.instance._createCellCoords(row, column) }); } this.instance.runHooks('afterOnCellMouseOver', event, visualCoords, TD); this.activeWt = this._wt; }, onCellMouseUp: (event, coords, TD, wt) => { const visualCoords = this.translateFromRenderableToVisualCoords(coords); this.activeWt = wt; this.instance.runHooks('beforeOnCellMouseUp', event, visualCoords, TD); // TODO: The second condition check is a workaround. Callback corresponding the method `updateSettings` // disable plugin and enable it again. Disabling plugin closes the menu. Thus, calling the // `updateSettings` in a body of any callback executed right after some context-menu action // breaks the table (#7231). if (isImmediatePropagationStopped(event) || this.instance.isDestroyed) { return; } this.instance.runHooks('afterOnCellMouseUp', event, visualCoords, TD); this.activeWt = this._wt; }, onCellCornerMouseDown: event => { event.preventDefault(); this.instance.runHooks('afterOnCellCornerMouseDown', event); }, onCellCornerDblClick: event => { event.preventDefault(); this.instance.runHooks('afterOnCellCornerDblClick', event); }, beforeDraw: (force, skipRender) => this.beforeRender(force, skipRender), onDraw: force => this.afterRender(force), onScrollVertically: () => this.instance.runHooks('afterScrollVertically'), onScrollHorizontally: () => this.instance.runHooks('afterScrollHorizontally'), onBeforeRemoveCellClassNames: () => this.instance.runHooks('beforeRemoveCellClassNames'), onBeforeHighlightingRowHeader: (renderableRow, headerLevel, highlightMeta) => { const rowMapper = this.instance.rowIndexMapper; const visualRow = rowMapper.getVisualFromRenderableIndex(renderableRow); const newVisualRow = this.instance.runHooks('beforeHighlightingRowHeader', visualRow, headerLevel, highlightMeta); return rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(newVisualRow, 1)); }, onBeforeHighlightingColumnHeader: (renderableColumn, headerLevel, highlightMeta) => { const columnMapper = this.instance.columnIndexMapper; const visualColumn = columnMapper.getVisualFromRenderableIndex(renderableColumn); const newVisualColumn = this.instance.runHooks('beforeHighlightingColumnHeader', visualColumn, headerLevel, highlightMeta); return columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(newVisualColumn, 1)); }, onAfterDrawSelection: (currentRow, currentColumn, layerLevel) => { let cornersOfSelection; const [visualRowIndex, visualColumnIndex] = this.translateFromRenderableToVisualIndex(currentRow, currentColumn); const selectedRange = this.instance.selection.getSelectedRange(); const selectionRangeSize = selectedRange.size(); if (selectionRangeSize > 0) { // Selection layers are stored from the "oldest" to the "newest". We should calculate the offset. // Please look at the `SelectedRange` class and it's method for getting selection's layer for more information. const selectionOffset = (layerLevel !== null && layerLevel !== void 0 ? layerLevel : 0) + 1 - selectionRangeSize; const selectionForLayer = selectedRange.peekByIndex(selectionOffset); cornersOfSelection = [selectionForLayer.from.row, selectionForLayer.from.col, selectionForLayer.to.row, selectionForLayer.to.col]; } return this.instance.runHooks('afterDrawSelection', visualRowIndex, visualColumnIndex, cornersOfSelection, layerLevel); }, onBeforeDrawBorders: (corners, borderClassName) => { const [startRenderableRow, startRenderableColumn, endRenderableRow, endRenderableColumn] = corners; const visualCorners = [this.instance.rowIndexMapper.getVisualFromRenderableIndex(startRenderableRow), this.instance.columnIndexMapper.getVisualFromRenderableIndex(startRenderableColumn), this.instance.rowIndexMapper.getVisualFromRenderableIndex(endRenderableRow), this.instance.columnIndexMapper.getVisualFromRenderableIndex(endRenderableColumn)]; return this.instance.runHooks('beforeDrawBorders', visualCorners, borderClassName); }, onBeforeTouchScroll: () => this.instance.runHooks('beforeTouchScroll'), onAfterMomentumScroll: () => this.instance.runHooks('afterMomentumScroll'), onBeforeStretchingColumnWidth: (stretchedWidth, renderedColumnIndex) => { const visualColumnIndex = this.instance.columnIndexMapper.getVisualFromRenderableIndex(renderedColumnIndex); return this.instance.runHooks('beforeStretchingColumnWidth', stretchedWidth, visualColumnIndex); }, onModifyRowHeaderWidth: rowHeaderWidth => this.instance.runHooks('modifyRowHeaderWidth', rowHeaderWidth), onModifyGetCellCoords: (renderableRowIndex, renderableColumnIndex, topmost) => { const rowMapper = this.instance.rowIndexMapper; const columnMapper = this.instance.columnIndexMapper; // Callback handle also headers. We shouldn't translate them. const visualColumnIndex = renderableColumnIndex >= 0 ? columnMapper.getVisualFromRenderableIndex(renderableColumnIndex) : renderableColumnIndex; const visualRowIndex = renderableRowIndex >= 0 ? rowMapper.getVisualFromRenderableIndex(renderableRowIndex) : renderableRowIndex; const visualIndexes = this.instance.runHooks('modifyGetCellCoords', visualRowIndex, visualColumnIndex, topmost); if (Array.isArray(visualIndexes)) { const [visualRowFrom, visualColumnFrom, visualRowTo, visualColumnTo] = visualIndexes; // Result of the hook is handled by the Walkontable (renderable indexes). return [visualRowFrom >= 0 ? rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(visualRowFrom, 1)) : visualRowFrom, visualColumnFrom >= 0 ? columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(visualColumnFrom, 1)) : visualColumnFrom, visualRowTo >= 0 ? rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(visualRowTo, -1)) : visualRowTo, visualColumnTo >= 0 ? columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(visualColumnTo, -1)) : visualColumnTo]; } }, viewportRowCalculatorOverride: calc => { let viewportOffset = this.settings.viewportRowRenderingOffset; if (viewportOffset === 'auto' && this.settings.fixedRowsTop) { viewportOffset = 10; } if (viewportOffset > 0 || viewportOffset === 'auto') { const renderableRows = this.countRenderableRows(); const firstRenderedRow = calc.startRow; const lastRenderedRow = calc.endRow; if (typeof viewportOffset === 'number') { calc.startRow = Math.max(firstRenderedRow - viewportOffset, 0); calc.endRow = Math.min(lastRenderedRow + viewportOffset, renderableRows - 1); } else if (viewportOffset === 'auto') { const offset = Math.ceil(lastRenderedRow / renderableRows * 12); calc.startRow = Math.max(firstRenderedRow - offset, 0); calc.endRow = Math.min(lastRenderedRow + offset, renderableRows - 1); } } this.instance.runHooks('afterViewportRowCalculatorOverride', calc); }, viewportColumnCalculatorOverride: calc => { let viewportOffset = this.settings.viewportColumnRenderingOffset; if (viewportOffset === 'auto' && this.settings.fixedColumnsStart) { viewportOffset = 10; } if (viewportOffset > 0 || viewportOffset === 'auto') { const renderableColumns = this.countRenderableColumns(); const firstRenderedColumn = calc.startColumn; const lastRenderedColumn = calc.endColumn; if (typeof viewportOffset === 'number') { calc.startColumn = Math.max(firstRenderedColumn - viewportOffset, 0); calc.endColumn = Math.min(lastRenderedColumn + viewportOffset, renderableColumns - 1); } if (viewportOffset === 'auto') { const offset = Math.ceil(lastRenderedColumn / renderableColumns * 6); calc.startColumn = Math.max(firstRenderedColumn - offset, 0); calc.endColumn = Math.min(lastRenderedColumn + offset, renderableColumns - 1); } } this.instance.runHooks('afterViewportColumnCalculatorOverride', calc); }, rowHeaderWidth: () => this.settings.rowHeaderWidth, columnHeaderHeight: () => { const columnHeaderHeight = this.instance.runHooks('modifyColumnHeaderHeight'); return this.settings.columnHeaderHeight || columnHeaderHeight; } }; this.instance.runHooks('beforeInitWalkontable', walkontableConfig); this._wt = new Walkontable(walkontableConfig); this.activeWt = this._wt; const spreader = this._wt.wtTable.spreader; // We have to cache width and height after Walkontable initialization. const { width, height } = this.instance.rootElement.getBoundingClientRect(); this.setLastSize(width, height); this.eventManager.addEventListener(spreader, 'mousedown', event => { // right mouse button exactly on spreader means right click on the right hand side of vertical scrollbar if (event.target === spreader && event.which === 3) { event.stopPropagation(); } }); this.eventManager.addEventListener(spreader, 'contextmenu', event => { // right mouse button exactly on spreader means right click on the right hand side of vertical scrollbar if (event.target === spreader && event.which === 3) { event.stopPropagation(); } }); this.eventManager.addEventListener(this.instance.rootDocument.documentElement, 'click', () => { if (this.settings.observeDOMVisibility) { if (this._wt.drawInterrupted) { this.instance.forceFullRender = true; this.render(); } } }); } /** * Checks if it's possible to create text selection in element. * * @private * @param {HTMLElement} el The element to check. * @returns {boolean} */ isTextSelectionAllowed(el) { if (isInput(el)) { return true; } const isChildOfTableBody = isChildOf(el, this.instance.view._wt.wtTable.spreader); if (this.settings.fragmentSelection === true && isChildOfTableBody) { return true; } if (this.settings.fragmentSelection === 'cell' && this.isSelectedOnlyCell() && isChildOfTableBody) { return true; } if (!this.settings.fragmentSelection && this.isCellEdited() && this.isSelectedOnlyCell()) { return true; } return false; } /** * Checks if user's been called mousedown. * * @private * @returns {boolean} */ isMouseDown() { return privatePool.get(this).mouseDown; } /** * Check if selected only one cell. * * @private * @returns {boolean} */ isSelectedOnlyCell() { var _this$instance$getSel, _this$instance$getSel2; return (_this$instance$getSel = (_this$instance$getSel2 = this.instance.getSelectedRangeLast()) === null || _this$instance$getSel2 === void 0 ? void 0 : _this$instance$getSel2.isSingle()) !== null && _this$instance$getSel !== void 0 ? _this$instance$getSel : false; } /** * Checks if active cell is editing. * * @private * @returns {boolean} */ isCellEdited() { const activeEditor = this.instance.getActiveEditor(); return activeEditor && activeEditor.isOpened(); } /** * `beforeDraw` callback. * * @private * @param {boolean} force If `true` rendering was triggered by a change of settings or data or `false` if * rendering was triggered by scrolling or moving selection. * @param {object} skipRender Object with `skipRender` property, if it is set to `true ` the next rendering * cycle will be skipped. */ beforeRender(force, skipRender) { if (force) { // this.instance.forceFullRender = did Handsontable request full render? this.instance.runHooks('beforeViewRender', this.instance.forceFullRender, skipRender); } } /** * `afterRender` callback. * * @private * @param {boolean} force If `true` rendering was triggered by a change of settings or data or `false` if * rendering was triggered by scrolling or moving selection. */ afterRender(force) { if (force) { // this.instance.forceFullRender = did Handsontable request full render? this.instance.runHooks('afterViewRender', this.instance.forceFullRender); } } /** * Append row header to a TH element. * * @private * @param {number} visualRowIndex The visual row index. * @param {HTMLTableHeaderCellElement} TH The table header element. */ appendRowHeader(visualRowIndex, TH) { if (TH.firstChild) { const container = TH.firstChild; if (!hasClass(container, 'relative')) { empty(TH); this.appendRowHeader(visualRowIndex, TH); return; } this.updateCellHeader(container.querySelector('.rowHeader'), visualRowIndex, this.instance.getRowHeader); } else { const { rootDocument, getRowHeader } = this.instance; const div = rootDocument.createElement('div'); const span = rootDocument.createElement('span'); div.className = 'relative'; span.className = 'rowHeader'; this.updateCellHeader(span, visualRowIndex, getRowHeader); div.appendChild(span); TH.appendChild(div); } this.instance.runHooks('afterGetRowHeader', visualRowIndex, TH); } /** * Append column header to a TH element. * * @private * @param {number} visualColumnIndex Visual column index. * @param {HTMLTableCellElement} TH The table header element. * @param {Function} [label] The function that returns the header label. * @param {number} [headerLevel=0] The index of header level counting from the top (positive * values counting from 0 to N). */ appendColHeader(visualColumnIndex, TH) { let label = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.instance.getColHeader; let headerLevel = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; if (TH.firstChild) { const container = TH.firstChild; if (hasClass(container, 'relative')) { this.updateCellHeader(container.querySelector('.colHeader'), visualColumnIndex, label, headerLevel); } else { empty(TH); this.appendColHeader(visualColumnIndex, TH, label, headerLevel); } } else { const { rootDocument } = this.instance; const div = rootDocument.createElement('div'); const span = rootDocument.createElement('span'); div.className = 'relative'; span.className = 'colHeader'; this.updateCellHeader(span, visualColumnIndex, label, headerLevel); div.appendChild(span); TH.appendChild(div); } this.instance.runHooks('afterGetColHeader', visualColumnIndex, TH, headerLevel); } /** * Updates header cell content. * * @private * @param {HTMLElement} element Element to update. * @param {number} index Row index or column index. * @param {Function} content Function which should be returns content for this cell. * @param {number} [headerLevel=0] The index of header level counting from the top (positive * values counting from 0 to N). */ updateCellHeader(element, index, content) { let headerLevel = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; let renderedIndex = index; const parentOverlay = this._wt.wtOverlays.getParentOverlay(element) || this._wt; // prevent wrong calculations from SampleGenerator if (element.parentNode) { if (hasClass(element, 'colHeader')) { renderedIndex = parentOverlay.wtTable.columnFilter.sourceToRendered(index); } else if (hasClass(element, 'rowHeader')) { renderedIndex = parentOverlay.wtTable.rowFilter.sourceToRendered(index); } } if (renderedIndex > -1) { fastInnerHTML(element, content(index, headerLevel)); } else { // workaround for https://github.com/handsontable/handsontable/issues/1946 fastInnerText(element, String.fromCharCode(160)); addClass(element, 'cornerHeader'); } } /** * Given a element's left (or right in RTL mode) position relative to the viewport, returns maximum * element width until the right (or left) edge of the viewport (before scrollbar). * * @private * @param {number} inlineOffset The left (or right in RTL mode) offset. * @returns {number} */ maximumVisibleElementWidth(inlineOffset) { const workspaceWidth = this._wt.wtViewport.getWorkspaceWidth(); const maxWidth = workspaceWidth - inlineOffset; return maxWidth > 0 ? maxWidth : 0; } /** * Given a element's top position relative to the viewport, returns maximum element height until the bottom * edge of the viewport (before scrollbar). * * @private * @param {number} topOffset The top offset. * @returns {number} */ maximumVisibleElementHeight(topOffset) { const workspaceHeight = this._wt.wtViewport.getWorkspaceHeight(); const maxHeight = workspaceHeight - topOffset; return maxHeight > 0 ? maxHeight : 0; } /** * Sets new dimensions of the container. * * @param {number} width The table width. * @param {number} height The table height. */ setLastSize(width, height) { const priv = privatePool.get(this); [priv.lastWidth, priv.lastHeight] = [width, height]; } /** * Returns cached dimensions. * * @returns {object} */ getLastSize() { const priv = privatePool.get(this); return { width: priv.lastWidth, height: priv.lastHeight }; } /** * Returns the first fully visible row in the table viewport. * * @returns {number} */ getFirstFullyVisibleRow() { return this.instance.rowIndexMapper.getVisualFromRenderableIndex(this.instance.view._wt.wtScroll.getFirstVisibleRow()); } /** * Returns the last fully visible row in the table viewport. * * @returns {number} */ getLastFullyVisibleRow() { return this.instance.rowIndexMapper.getVisualFromRenderableIndex(this.instance.view._wt.wtScroll.getLastVisibleRow()); } /** * Returns the first fully visible column in the table viewport. * * @returns {number} */ getFirstFullyVisibleColumn() { return this.instance.columnIndexMapper.getVisualFromRenderableIndex(this.instance.view._wt.wtScroll.getFirstVisibleColumn()); } /** * Returns the last fully visible column in the table viewport. * * @returns {number} */ getLastFullyVisibleColumn() { return this.instance.columnIndexMapper.getVisualFromRenderableIndex(this.instance.view._wt.wtScroll.getLastVisibleColumn()); } /** * Returns the total count of the rendered column headers. * * @returns {number} */ getColumnHeadersCount() { return _classPrivateFieldGet(this, _columnHeadersCount); } /** * Returns the total count of the rendered row headers. * * @returns {number} */ getRowHeadersCount() { return _classPrivateFieldGet(this, _rowHeadersCount); } /** * Destroys internal WalkOnTable's instance. Detaches all of the bonded listeners. * * @private */ destroy() { this._wt.destroy(); this.eventManager.destroy(); } } export default TableView;