UNPKG

handsontable

Version:

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

1,134 lines (1,093 loc) • 69.4 kB
import "core-js/modules/es.error.cause.js"; import "core-js/modules/es.array.push.js"; function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } import { addClass, removeClass, clearTextSelection, empty, fastInnerHTML, fastInnerText, getScrollbarWidth, hasClass, isChildOf, isInput, isOutsideInput, isVisible, setAttribute, getParentWindow } 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"; import { resolveWithInstance } from "./utils/staticRegister.mjs"; import { A11Y_COLCOUNT, A11Y_MULTISELECTABLE, A11Y_PRESENTATION, A11Y_ROWCOUNT, A11Y_TREEGRID } from "./helpers/a11y.mjs"; /** * @class TableView * @private */ var _columnHeadersCount = /*#__PURE__*/new WeakMap(); var _rowHeadersCount = /*#__PURE__*/new WeakMap(); var _selectionMouseDown = /*#__PURE__*/new WeakMap(); var _mouseDown = /*#__PURE__*/new WeakMap(); var _table = /*#__PURE__*/new WeakMap(); var _lastWidth = /*#__PURE__*/new WeakMap(); var _lastHeight = /*#__PURE__*/new WeakMap(); var _mouseDownLastPos = /*#__PURE__*/new WeakMap(); var _TableView_brand = /*#__PURE__*/new WeakSet(); class TableView { /** * @param {Hanstontable} hotInstance Instance of {@link Handsontable}. */ constructor(hotInstance) { /** * Return the value of the `aria-colcount` attribute. * * @returns {number} The value of the `aria-colcount` attribute. */ _classPrivateMethodInitSpec(this, _TableView_brand); /** * Instance of {@link Handsontable}. * * @private * @type {Handsontable} */ _defineProperty(this, "hot", 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 <THEAD> element. * * @private * @type {HTMLTableSectionElement} */ _defineProperty(this, "THEAD", void 0); /** * Main <TBODY> element. * * @private * @type {HTMLTableSectionElement} */ _defineProperty(this, "TBODY", void 0); /** * Main Walkontable instance. * * @private * @type {Walkontable} */ _defineProperty(this, "_wt", void 0); /** * Main Walkontable instance. * * @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, 0); /** * The total number of the row header renderers applied to the table through the * `afterGetRowHeaderRenderers` hook. * * @type {number} */ _classPrivateFieldInitSpec(this, _rowHeadersCount, 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); /** * Defines if the text should be selected during mousemove. * * @type {boolean} */ _classPrivateFieldInitSpec(this, _selectionMouseDown, false); /** * @type {boolean} */ _classPrivateFieldInitSpec(this, _mouseDown, void 0); /** * Main <TABLE> element. * * @type {HTMLTableElement} */ _classPrivateFieldInitSpec(this, _table, void 0); /** * Cached width of the rootElement. * * @type {number} */ _classPrivateFieldInitSpec(this, _lastWidth, 0); /** * Cached height of the rootElement. * * @type {number} */ _classPrivateFieldInitSpec(this, _lastHeight, 0); /** * The last mouse position of the mousedown event. * * @type {{ x: number, y: number } | null} */ _classPrivateFieldInitSpec(this, _mouseDownLastPos, null); this.hot = hotInstance; this.eventManager = new EventManager(this.hot); this.settings = this.hot.getSettings(); this.createElements(); this.registerEvents(); this.initializeWalkontable(); } /** * Renders WalkontableUI. */ render() { if (!this.hot.isRenderSuspended()) { const isFullRender = this.hot.forceFullRender; this.hot.runHooks('beforeRender', isFullRender); this._wt.draw(!isFullRender); _assertClassBrand(_TableView_brand, this, _updateScrollbarClassNames).call(this); if (this.postponedAdjustElementsSize) { this.postponedAdjustElementsSize = false; this.adjustElementsSize(true); } this.hot.runHooks('afterRender', isFullRender); this.hot.forceFullRender = false; } } /** * Adjust overlays elements size and master table size. By default the internal `adjustElementsSize` * call of the Walkontable is postponed to the next render cycle. If `flush` is set to `true`, the method * will be executed immediately. * * TODO: This method should not exist. It is a workaround for the issue with updating the elements * size after render. It should be calculated and updated automatically in Walkontable. * * @param {boolean} [flush=false] If `true`, the method will be executed immediately. */ adjustElementsSize() { let flush = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (flush) { this._wt.wtOverlays.adjustElementsSize(); } else { this.postponedAdjustElementsSize = true; } } /** * 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 {'auto' | 'start' | 'end'} [horizontalSnap] If `'start'`, viewport is scrolled to show * the cell on the left of the table. If `'end'`, viewport is scrolled to show the cell on the right of * the table. When `'auto'`, the viewport is scrolled only when the column is outside of the viewport. * @param {'auto' | 'top' | 'bottom'} [verticalSnap] If `'top'`, viewport is scrolled to show * the cell on the top of the table. If `'bottom'`, viewport is scrolled to show the cell on the bottom of * the table. When `'auto'`, the viewport is scrolled only when the row is outside of the viewport. * @returns {boolean} */ scrollViewport(coords, horizontalSnap, verticalSnap) { return this._wt.scrollViewport(coords, horizontalSnap, verticalSnap); } /** * Scroll viewport to a column. * * @param {number} column Renderable column index. * @param {'auto' | 'start' | 'end'} [snap] If `'start'`, viewport is scrolled to show * the cell on the left of the table. If `'end'`, viewport is scrolled to show the cell on the right of * the table. When `'auto'`, the viewport is scrolled only when the column is outside of the viewport. * @returns {boolean} */ scrollViewportHorizontally(column, snap) { return this._wt.scrollViewportHorizontally(column, snap); } /** * Scroll viewport to a row. * * @param {number} row Renderable row index. * @param {'auto' | 'top' | 'bottom'} [snap] If `'top'`, viewport is scrolled to show * the cell on the top of the table. If `'bottom'`, viewport is scrolled to show the cell on * the bottom of the table. When `'auto'`, the viewport is scrolled only when the row is outside of * the viewport. * @returns {boolean} */ scrollViewportVertically(row, snap) { return this._wt.scrollViewportVertically(row, snap); } /** * Prepares DOMElements and adds correct className to the root element. * * @private */ createElements() { const { rootElement, rootDocument } = this.hot; 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'); _classPrivateFieldSet(_table, this, rootDocument.createElement('TABLE')); addClass(_classPrivateFieldGet(_table, this), 'htCore'); if (this.hot.getSettings().tableClassName) { addClass(_classPrivateFieldGet(_table, this), this.hot.getSettings().tableClassName); } if (this.settings.ariaTags) { setAttribute(_classPrivateFieldGet(_table, this), [A11Y_PRESENTATION()]); setAttribute(rootElement, [A11Y_TREEGRID(), A11Y_ROWCOUNT(-1), A11Y_COLCOUNT(this.hot.countCols()), A11Y_MULTISELECTABLE()]); } this.THEAD = rootDocument.createElement('THEAD'); _classPrivateFieldGet(_table, this).appendChild(this.THEAD); this.TBODY = rootDocument.createElement('TBODY'); _classPrivateFieldGet(_table, this).appendChild(this.TBODY); this.hot.table = _classPrivateFieldGet(_table, this); this.hot.container.insertBefore(_classPrivateFieldGet(_table, this), this.hot.container.firstChild); } /** * Attaches necessary listeners. * * @private */ registerEvents() { const { rootWrapperElement, rootElement, rootDocument, selection, rootWindow } = this.hot; const documentElement = rootDocument.documentElement; this.eventManager.addEventListener(rootElement, 'mousedown', event => { _classPrivateFieldSet(_selectionMouseDown, this, true); if (!this.isTextSelectionAllowed(event.target)) { 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', () => { _classPrivateFieldSet(_selectionMouseDown, this, false); }); this.eventManager.addEventListener(rootElement, 'mousemove', event => { if (_classPrivateFieldGet(_selectionMouseDown, this) && !this.isTextSelectionAllowed(event.target)) { // Clear selection only when fragmentSelection is enabled, otherwise clearing selection breaks the IME editor. if (this.settings.fragmentSelection) { clearTextSelection(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)) { selection.finish(); } _classPrivateFieldSet(_mouseDown, this, false); const isOutsideInputElement = isOutsideInput(rootDocument.activeElement); if (isInput(rootDocument.activeElement) && !isOutsideInputElement) { return; } if (isOutsideInputElement || !selection.isSelected() && !selection.isSelectedByAnyHeader() && !(rootWrapperElement !== null && rootWrapperElement !== void 0 ? rootWrapperElement : rootElement).contains(event.target) && !isRightClick(event)) { this.hot.unlisten(); } }); this.eventManager.addEventListener(documentElement, 'contextmenu', event => { if (selection.isInProgress() && isRightClick(event)) { selection.finish(); _classPrivateFieldSet(_mouseDown, this, false); } }); this.eventManager.addEventListener(documentElement, 'touchend', () => { if (selection.isInProgress()) { selection.finish(); } _classPrivateFieldSet(_mouseDown, this, 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 (_classPrivateFieldGet(_mouseDown, this) || !rootElement || !this.hot.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._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.hot.deselectCell(); } else { this.hot.destroyEditor(false, false); } }); let parentWindow = getParentWindow(rootWindow); while (parentWindow !== null) { this.eventManager.addEventListener(parentWindow.document.documentElement, 'click', () => { this.hot.unlisten(); }); parentWindow = getParentWindow(parentWindow); } this.eventManager.addEventListener(_classPrivateFieldGet(_table, this), '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.hot._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.hot.rowIndexMapper.getVisualFromRenderableIndex(renderableRow) : renderableRow; let visualColumn = renderableColumn >= 0 ? this.hot.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.hot.columnIndexMapper, this.settings.maxCols); } /** * Returns the number of renderable rows. * * @returns {number} */ countRenderableRows() { return this.countRenderableIndexes(this.hot.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.hot.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.hot.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.hot.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.hot.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.hot.countRows(); const visualFixedRowsBottom = Math.max(countRows - parseInt(this.settings.fixedRowsBottom, 10), 0); return this.countNotHiddenRowIndexes(visualFixedRowsBottom, 1); } /** * The function returns the number of renderable column indexes within the passed range of the visual indexes. * * @param {number} columnStart The column visual start index. * @param {number} columnEnd The column visual end index. * @returns {number} */ countRenderableColumnsInRange(columnStart, columnEnd) { let count = 0; for (let column = columnStart; column <= columnEnd; column++) { if (this.hot.columnIndexMapper.getRenderableFromVisualIndex(column) !== null) { count += 1; } } return count; } /** * The function returns the number of renderable row indexes within the passed range of the visual indexes. * * @param {number} rowStart The row visual start index. * @param {number} rowEnd The row visual end index. * @returns {number} */ countRenderableRowsInRange(rowStart, rowEnd) { let count = 0; for (let row = rowStart; row <= rowEnd; row++) { if (this.hot.rowIndexMapper.getRenderableFromVisualIndex(row) !== null) { count += 1; } } return count; } /** * Add a class name to the license information element. * * @param {string} className The class name to add. */ addClassNameToLicenseElement(className) { var _this$hot$rootElement; const licenseInfoElement = (_this$hot$rootElement = this.hot.rootElement.parentNode) === null || _this$hot$rootElement === void 0 ? void 0 : _this$hot$rootElement.querySelector('.hot-display-license-info'); if (licenseInfoElement) { addClass(licenseInfoElement, className); } } /** * Remove a class name from the license information element. * * @param {string} className The class name to remove. */ removeClassNameFromLicenseElement(className) { var _this$hot$rootElement2; const licenseInfoElement = (_this$hot$rootElement2 = this.hot.rootElement.parentNode) === null || _this$hot$rootElement2 === void 0 ? void 0 : _this$hot$rootElement2.querySelector('.hot-display-license-info'); if (licenseInfoElement) { removeClass(licenseInfoElement, className); } } /** * 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.hot.countRenderedRows() > fixedAllRows && this.hot.countRenderedCols() > fixedAllColumns; } /** * Defines default configuration and initializes WalkOnTable instance. * * @private */ initializeWalkontable() { const walkontableConfig = { ariaTags: this.settings.ariaTags, rtlMode: this.hot.isRtl(), externalRowCalculator: this.hot.getPlugin('autoRowSize') && this.hot.getPlugin('autoRowSize').isEnabled(), table: _classPrivateFieldGet(_table, this), isDataViewInstance: () => isRootInstance(this.hot), preventOverflow: () => this.settings.preventOverflow, preventWheel: () => this.settings.preventWheel, viewportColumnRenderingThreshold: () => this.settings.viewportColumnRenderingThreshold, viewportRowRenderingThreshold: () => this.settings.viewportRowRenderingThreshold, data: (renderableRow, renderableColumn) => { return this.hot.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, renderAllColumns: this.settings.renderAllColumns, rowHeaders: () => { const headerRenderers = []; if (this.hot.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.hot.rowIndexMapper.getVisualFromRenderableIndex(renderableRowIndex) : renderableRowIndex; this.appendRowHeader(visualRowIndex, TH); }); } this.hot.runHooks('afterGetRowHeaderRenderers', headerRenderers); _classPrivateFieldSet(_rowHeadersCount, this, headerRenderers.length); if (this.hot.getSettings().ariaTags) { // Update the aria-colcount attribute. // Only needs to be done once after initialization/data update. if (_assertClassBrand(_TableView_brand, this, _getAriaColcount).call(this) === this.hot.countCols()) { _assertClassBrand(_TableView_brand, this, _updateAriaColcount).call(this, _classPrivateFieldGet(_rowHeadersCount, this)); } } return headerRenderers; }, columnHeaders: () => { const headerRenderers = []; if (this.hot.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.hot.columnIndexMapper.getVisualFromRenderableIndex(renderedColumnIndex) : renderedColumnIndex; this.appendColHeader(visualColumnsIndex, TH); }); } this.hot.runHooks('afterGetColumnHeaderRenderers', headerRenderers); _classPrivateFieldSet(_columnHeadersCount, this, headerRenderers.length); return headerRenderers; }, columnWidth: renderedColumnIndex => { const visualIndex = this.hot.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.hot.getColWidth(visualIndex === null ? renderedColumnIndex : visualIndex); }, rowHeight: renderedRowIndex => { const visualIndex = this.hot.rowIndexMapper.getVisualFromRenderableIndex(renderedRowIndex); return this.hot.getRowHeight(visualIndex === null ? renderedRowIndex : visualIndex); }, rowHeightByOverlayName: (renderedRowIndex, overlayType) => { const visualIndex = this.hot.rowIndexMapper.getVisualFromRenderableIndex(renderedRowIndex); const visualRowIndex = visualIndex === null ? renderedRowIndex : visualIndex; return this.hot.runHooks('modifyRowHeightByOverlayName', this.hot.getRowHeight(visualRowIndex), visualRowIndex, overlayType); }, 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.hot.runHooks('modifyGetCellCoords', visualRowIndex, visualColumnIndex, false, 'meta'); let visualRowToCheck = visualRowIndex; let visualColumnToCheck = visualColumnIndex; if (Array.isArray(modifiedCellCoords)) { [visualRowToCheck, visualColumnToCheck] = modifiedCellCoords; } const cellProperties = this.hot.getCellMeta(visualRowToCheck, visualColumnToCheck); const prop = this.hot.colToProp(visualColumnToCheck); let value = this.hot.getDataAtRowProp(visualRowToCheck, prop); if (this.hot.hasHook('beforeValueRender')) { value = this.hot.runHooks('beforeValueRender', value, cellProperties); } this.hot.runHooks('beforeRenderer', TD, visualRowIndex, visualColumnIndex, prop, value, cellProperties); this.hot.getCellRenderer(cellProperties)(this.hot, TD, visualRowIndex, visualColumnIndex, prop, value, cellProperties); this.hot.runHooks('afterRenderer', TD, visualRowIndex, visualColumnIndex, prop, value, cellProperties); }, selections: this.hot.selection.highlight, hideBorderOnMouseDownOver: () => this.settings.fragmentSelection, onWindowResize: () => { if (this.hot && !this.hot.isDestroyed) { this.hot.refreshDimensions(); } }, onContainerElementResize: () => { if (this.hot && !this.hot.isDestroyed && isVisible(this.hot.rootElement)) { this.hot.refreshDimensions(); } }, onCellMouseDown: (event, coords, TD, wt) => { const visualCoords = this.translateFromRenderableToVisualCoords(coords); const controller = { row: false, column: false, cell: false }; this.hot.listen(); this.activeWt = wt; _classPrivateFieldSet(_mouseDown, this, true); _classPrivateFieldSet(_mouseDownLastPos, this, { x: event.clientX, y: event.clientY }); this.hot.runHooks('beforeOnCellMouseDown', event, visualCoords, TD, controller); if (isImmediatePropagationStopped(event)) { return; } handleMouseEvent(event, { coords: visualCoords, selection: this.hot.selection, controller, cellCoordsFactory: (row, column) => this.hot._createCellCoords(row, column) }); this.hot.runHooks('afterOnCellMouseDown', event, visualCoords, TD); this.activeWt = this._wt; }, onCellContextMenu: (event, coords, TD, wt) => { const visualCoords = this.translateFromRenderableToVisualCoords(coords); this.activeWt = wt; _classPrivateFieldSet(_mouseDown, this, false); if (this.hot.selection.isInProgress()) { this.hot.selection.finish(); } this.hot.runHooks('beforeOnCellContextMenu', event, visualCoords, TD); if (isImmediatePropagationStopped(event)) { return; } this.hot.runHooks('afterOnCellContextMenu', event, visualCoords, TD); this.activeWt = this._wt; }, onCellMouseOut: (event, coords, TD, wt) => { const visualCoords = this.translateFromRenderableToVisualCoords(coords); this.activeWt = wt; this.hot.runHooks('beforeOnCellMouseOut', event, visualCoords, TD); if (isImmediatePropagationStopped(event)) { return; } this.hot.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.hot.runHooks('beforeOnCellMouseOver', event, visualCoords, TD, controller); if (isImmediatePropagationStopped(event)) { return; } // Ignore mouseover events when the mouse has not moved. This solves an issue (#dev-1479) where // column resizing triggered by the long text in the cell causes the mouseover event to be fired, // thus selecting multiple cells with no user intention. if (_classPrivateFieldGet(_mouseDown, this) && (!_classPrivateFieldGet(_mouseDownLastPos, this) || _classPrivateFieldGet(_mouseDownLastPos, this).x !== event.clientX || _classPrivateFieldGet(_mouseDownLastPos, this).y !== event.clientY)) { handleMouseEvent(event, { coords: visualCoords, selection: this.hot.selection, controller, cellCoordsFactory: (row, column) => this.hot._createCellCoords(row, column) }); } this.hot.runHooks('afterOnCellMouseOver', event, visualCoords, TD); this.activeWt = this._wt; _classPrivateFieldSet(_mouseDownLastPos, this, null); }, onCellMouseUp: (event, coords, TD, wt) => { const visualCoords = this.translateFromRenderableToVisualCoords(coords); this.activeWt = wt; this.hot.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.hot.isDestroyed) { return; } handleMouseEvent(event, { coords: visualCoords, selection: this.hot.selection, cellRangeMapper: resolveWithInstance(this.hot, 'cellRangeMapper') }); this.hot.runHooks('afterOnCellMouseUp', event, visualCoords, TD); this.activeWt = this._wt; }, onCellCornerMouseDown: event => { event.preventDefault(); this.hot.runHooks('afterOnCellCornerMouseDown', event); }, onCellCornerDblClick: event => { event.preventDefault(); this.hot.runHooks('afterOnCellCornerDblClick', event); }, beforeDraw: (force, skipRender) => this.beforeRender(force, skipRender), onDraw: force => this.afterRender(force), onBeforeViewportScrollVertically: (renderableRow, snapping) => { const rowMapper = this.hot.rowIndexMapper; const areColumnHeadersSelected = renderableRow < 0; let visualRow = renderableRow; if (!areColumnHeadersSelected) { visualRow = rowMapper.getVisualFromRenderableIndex(renderableRow); // for an empty data return index as is if (visualRow === null) { return renderableRow; } } visualRow = this.hot.runHooks('beforeViewportScrollVertically', visualRow, snapping); this.hot.runHooks('beforeViewportScroll'); if (!areColumnHeadersSelected) { return rowMapper.getRenderableFromVisualIndex(visualRow); } return visualRow; }, onBeforeViewportScrollHorizontally: (renderableColumn, snapping) => { const columnMapper = this.hot.columnIndexMapper; const areRowHeadersSelected = renderableColumn < 0; let visualColumn = renderableColumn; if (!areRowHeadersSelected) { visualColumn = columnMapper.getVisualFromRenderableIndex(renderableColumn); // for an empty data return index as is if (visualColumn === null) { return renderableColumn; } } visualColumn = this.hot.runHooks('beforeViewportScrollHorizontally', visualColumn, snapping); this.hot.runHooks('beforeViewportScroll'); if (!areRowHeadersSelected) { return columnMapper.getRenderableFromVisualIndex(visualColumn); } return visualColumn; }, onScrollVertically: () => { this.hot.runHooks('afterScrollVertically'); this.hot.runHooks('afterScroll'); }, onScrollHorizontally: () => { this.hot.runHooks('afterScrollHorizontally'); this.hot.runHooks('afterScroll'); }, onBeforeRemoveCellClassNames: () => this.hot.runHooks('beforeRemoveCellClassNames'), onBeforeHighlightingRowHeader: (renderableRow, headerLevel, highlightMeta) => { const rowMapper = this.hot.rowIndexMapper; const areColumnHeadersSelected = renderableRow < 0; let visualRow = renderableRow; if (!areColumnHeadersSelected) { visualRow = rowMapper.getVisualFromRenderableIndex(renderableRow); } const newVisualRow = this.hot.runHooks('beforeHighlightingRowHeader', visualRow, headerLevel, highlightMeta); if (!areColumnHeadersSelected) { return rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(newVisualRow, 1)); } return newVisualRow; }, onBeforeHighlightingColumnHeader: (renderableColumn, headerLevel, highlightMeta) => { const columnMapper = this.hot.columnIndexMapper; const areRowHeadersSelected = renderableColumn < 0; let visualColumn = renderableColumn; if (!areRowHeadersSelected) { visualColumn = columnMapper.getVisualFromRenderableIndex(renderableColumn); } const newVisualColumn = this.hot.runHooks('beforeHighlightingColumnHeader', visualColumn, headerLevel, highlightMeta); if (!areRowHeadersSelected) { return columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(newVisualColumn, 1)); } return newVisualColumn; }, onAfterDrawSelection: (currentRow, currentColumn, layerLevel) => { let cornersOfSelection; const [visualRowIndex, visualColumnIndex] = this.translateFromRenderableToVisualIndex(currentRow, currentColumn); const selectedRange = this.hot.selection.getSelectedRange(); const selectionRangeSize = selectedRange.size(); if (selectionRangeSize > 0) { const selectionForLayer = selectedRange.peekByIndex(layerLevel !== null && layerLevel !== void 0 ? layerLevel : 0); cornersOfSelection = [selectionForLayer.from.row, selectionForLayer.from.col, selectionForLayer.to.row, selectionForLayer.to.col]; } return this.hot.runHooks('afterDrawSelection', visualRowIndex, visualColumnIndex, cornersOfSelection, layerLevel); }, onBeforeDrawBorders: (corners, borderClassName) => { const [startRenderableRow, startRenderableColumn, endRenderableRow, endRenderableColumn] = corners; const visualCorners = [this.hot.rowIndexMapper.getVisualFromRenderableIndex(startRenderableRow), this.hot.columnIndexMapper.getVisualFromRenderableIndex(startRenderableColumn), this.hot.rowIndexMapper.getVisualFromRenderableIndex(endRenderableRow), this.hot.columnIndexMapper.getVisualFromRenderableIndex(endRenderableColumn)]; return this.hot.runHooks('beforeDrawBorders', visualCorners, borderClassName); }, onBeforeTouchScroll: () => this.hot.runHooks('beforeTouchScroll'), onAfterMomentumScroll: () => this.hot.runHooks('afterMomentumScroll'), onModifyRowHeaderWidth: rowHeaderWidth => this.hot.runHooks('modifyRowHeaderWidth', rowHeaderWidth), onModifyGetCellCoords: (renderableRowIndex, renderableColumnIndex, topmost, source) => { const rowMapper = this.hot.rowIndexMapper; const columnMapper = this.hot.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.hot.runHooks('modifyGetCellCoords', visualRowIndex, visualColumnIndex, topmost, source); 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]; } }, onModifyGetCoordsElement: (renderableRowIndex, renderableColumnIndex) => { const rowMapper = this.hot.rowIndexMapper; const columnMapper = this.hot.columnIndexMapper; const visualColumnIndex = renderableColumnIndex >= 0 ? columnMapper.getVisualFromRenderableIndex(renderableColumnIndex) : renderableColumnIndex; const visualRowIndex = renderableRowIndex >= 0 ? rowMapper.getVisualFromRenderableIndex(renderableRowIndex) : renderableRowIndex; const visualIndexes = this.hot.runHooks('modifyGetCoordsElement', visualRowIndex, visualColumnIndex); if (Array.isArray(visualIndexes)) { const [visualRow, visualColumn] = visualIndexes; return [visualRow >= 0 ? rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(visualRow, 1)) : visualRow, visualColumn >= 0 ? columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(visualColumn, 1)) : visualColumn]; } }, 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.max(1, Math.ceil(lastRenderedRow / renderableRows * 12)); calc.startRow = Math.max(firstRenderedRow - offset, 0); calc.endRow = Math.min(lastRenderedRow + offset, renderableRows - 1); } } this.hot.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.max(1, Math.ceil(lastRenderedColumn / renderableColumns * 6)); calc.startColumn = Math.max(firstRenderedColumn - offset, 0); calc.endColumn = Math.min(lastRenderedColumn + offset, renderableColumns - 1); } } this.hot.runHooks('afterViewportColumnCalculatorOverride', calc); }, rowHeaderWidth: () => this.settings.rowHeaderWidth, columnHeaderHeight: () => { const columnHeaderHeight = this.hot.runHooks('modifyColumnHeaderHeight'); return this.settings.columnHeaderHeight || columnHeaderHeight; }, stylesHandler: () => { return this.hot.stylesHandler; } }; this.hot.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.hot.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.hot.rootDocument.documentElement, 'click', () => { if (this.settings.observeDOMVisibility) { if (this._wt.drawInterrupted) { this.hot.render(); } } }); } /** * Checks if it's possible to create text selection in element. * * @private * @param {HTMLElement} el The element to check. * @returns {boolean} */ isTextSelectionAllowed(el) { var _this$hot$getSelected, _this$hot$getSelected2; if (isInput(el)) { return true; } const isChildOfTableBody = isChildOf(el, this._wt.wtTable.spreader); if (this.settings.fragmentSelection === true && isChildOfTableBody) { return true; } const isSingleCell = (_this$hot$getSelected = (_this$hot$getSelected2 = this.hot.getSelectedRangeActive()) === null || _this$hot$getSelected2 === void 0 ? void 0 : _this$hot$getSelected2.isSingleCell()) !== null && _this$hot$getSelected !== void 0 ? _this$hot$getSelected : false; if (this.settings.fragmentSelection === 'cell' && isSingleCell && isChildOfTableBody) { return true; } if (!this.settings.fragmentSelection && this.isCellEdited() && isSingleCell) { return true; } return false; } /** * Checks if user's been called mousedown. * * @private * @returns {boolean} */ isMouseDown() { return _classPrivateFieldGet(_mouseDown, this); } /** * Checks if active cell is editing. * * @private * @returns {boolean} */ isCellEdited() { const activeEditor = this.hot.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