UNPKG

handsontable

Version:

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

1,031 lines (1,008 loc) • 45.7 kB
import "core-js/modules/es.error.cause.js"; import "core-js/modules/es.array.push.js"; import "core-js/modules/esnext.iterator.constructor.js"; import "core-js/modules/esnext.iterator.for-each.js"; function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } 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 _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 _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } 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 } from "../../helpers/dom/element.mjs"; import { isNumeric, clamp } from "../../helpers/number.mjs"; import { toSingleLine } from "../../helpers/templateLiteralTag.mjs"; import { isLeftClick, isRightClick, isTouchEvent } from "../../helpers/dom/event.mjs"; import { warn } from "../../helpers/console.mjs"; import { ACTIVE_HEADER_TYPE, HEADER_TYPE } from "../../selection/index.mjs"; import { BasePlugin } from "../base/index.mjs"; import StateManager from "./stateManager/index.mjs"; import GhostTable from "./utils/ghostTable.mjs"; export const PLUGIN_KEY = 'nestedHeaders'; export const PLUGIN_PRIORITY = 280; /* eslint-disable jsdoc/require-description-complete-sentence */ /** * @plugin NestedHeaders * @class NestedHeaders * * @description * The plugin allows to create a nested header structure, using the HTML's colspan attribute. * * To make any header wider (covering multiple table columns), it's corresponding configuration array element should be * provided as an object with `label` and `colspan` properties. The `label` property defines the header's label, * while the `colspan` property defines a number of columns that the header should cover. * You can also set custom class names to any of the headers by providing the `headerClassName` property. * * __Note__ that the plugin supports a *nested* structure, which means, any header cannot be wider than it's "parent". In * other words, headers cannot overlap each other. * @example * * ::: only-for javascript * ```js * const container = document.getElementById('example'); * const hot = new Handsontable(container, { * data: getData(), * nestedHeaders: [ * ['A', {label: 'B', colspan: 8, headerClassName: 'htRight'}, 'C'], * ['D', {label: 'E', colspan: 4}, {label: 'F', colspan: 4}, 'G'], * ['H', {label: 'I', colspan: 2}, {label: 'J', colspan: 2}, {label: 'K', colspan: 2}, {label: 'L', colspan: 2}, 'M'], * ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'] * ], * ``` * ::: * * ::: only-for react * ```jsx * <HotTable * data={getData()} * nestedHeaders={[ * ['A', {label: 'B', colspan: 8, headerClassName: 'htRight'}, 'C'], * ['D', {label: 'E', colspan: 4}, {label: 'F', colspan: 4}, 'G'], * ['H', {label: 'I', colspan: 2}, {label: 'J', colspan: 2}, {label: 'K', colspan: 2}, {label: 'L', colspan: 2}, 'M'], * ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'] * ]} * /> * ``` * ::: * * ::: only-for angular * ```ts * settings = { * data: getData(), * nestedHeaders: [ * ["A", { label: "B", colspan: 8, headerClassName: "htRight" }, "C"], * ["D", { label: "E", colspan: 4 }, { label: "F", colspan: 4 }, "G"], * [ * "H", * { label: "I", colspan: 2 }, * { label: "J", colspan: 2 }, * { label: "K", colspan: 2 }, * { label: "L", colspan: 2 }, * "M", * ], * ["N", "O", "P", "Q", "R", "S", "T", "U", "V", "W"], * ], * }; * ``` * * ```html * <hot-table [settings]="settings"></hot-table> * ``` * ::: */ var _stateManager = /*#__PURE__*/new WeakMap(); var _hidingIndexMapObserver = /*#__PURE__*/new WeakMap(); var _focusInitialCoords = /*#__PURE__*/new WeakMap(); var _isColumnsSelectionInProgress = /*#__PURE__*/new WeakMap(); var _recentlyHighlightCoords = /*#__PURE__*/new WeakMap(); var _updateFocusHighlightPosition = /*#__PURE__*/new WeakMap(); var _NestedHeaders_brand = /*#__PURE__*/new WeakSet(); export class NestedHeaders extends BasePlugin { constructor() { super(...arguments); /** * Allows to control to which column index the viewport will be scrolled. To ensure that the viewport * is scrolled to the correct column for the nested header the most left and the most right visual column * indexes are used. * * @param {number} visualColumn A visual column index to which the viewport will be scrolled. * @param {{ value: 'auto' | 'start' | 'end' }} snapping 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 {number} */ _classPrivateMethodInitSpec(this, _NestedHeaders_brand); /** * The state manager for the nested headers. * * @type {StateManager} */ _classPrivateFieldInitSpec(this, _stateManager, new StateManager()); /** * The instance of the ChangesObservable class that allows track the changes that happens in the * column indexes. * * @type {ChangesObservable} */ _classPrivateFieldInitSpec(this, _hidingIndexMapObserver, null); /** * Holds the coords that points to the place where the column selection starts. * * @type {number|null} */ _classPrivateFieldInitSpec(this, _focusInitialCoords, null); /** * Determines if there is performed the column selection. * * @type {boolean} */ _classPrivateFieldInitSpec(this, _isColumnsSelectionInProgress, false); /** * Keeps the last highlight position made by column selection. The coords are necessary to scroll * the viewport to the correct position when the nested header is clicked when the `navigableHeaders` * option is disabled. * * @type {CellCoords | null} */ _classPrivateFieldInitSpec(this, _recentlyHighlightCoords, null); /** * Custom helper for getting widths of the nested headers. * * @private * @type {GhostTable} */ // @TODO This should be changed after refactor handsontable/utils/ghostTable. _defineProperty(this, "ghostTable", new GhostTable(this.hot, (row, column) => this.getHeaderSettings(row, column))); /** * The flag which determines that the nested header settings contains overlapping headers * configuration. * * @type {boolean} */ _defineProperty(this, "detectedOverlappedHeaders", false); /** * Updates the selection focus highlight position to point to the nested header root element (TH) * even when the logical coordinates point in-between the header. * * The method uses arrow function to keep the reference to the class method. Necessary for * the `removeLocalHook` method of the row and column index mapper. */ _classPrivateFieldInitSpec(this, _updateFocusHighlightPosition, () => { var _this$hot; const selection = (_this$hot = this.hot) === null || _this$hot === void 0 ? void 0 : _this$hot.getSelectedRangeActive(); if (!selection) { return; } const { highlight } = selection; const isNestedHeadersRange = highlight.isHeader() && highlight.col >= 0; if (isNestedHeadersRange) { const columnIndex = _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(highlight.row, highlight.col); const focusHighlight = this.hot.selection.highlight.getFocus(); // Correct the highlight/focus selection to highlight the correct TH element focusHighlight.visualCellRange.highlight.col = columnIndex; focusHighlight.visualCellRange.from.col = columnIndex; focusHighlight.visualCellRange.to.col = columnIndex; focusHighlight.commit(); } }); } static get PLUGIN_KEY() { return PLUGIN_KEY; } static get PLUGIN_PRIORITY() { return PLUGIN_PRIORITY; } /** * Check if plugin is enabled. * * @returns {boolean} */ isEnabled() { return !!this.hot.getSettings()[PLUGIN_KEY]; } /** * Enables the plugin functionality for this Handsontable instance. */ enablePlugin() { var _this = this; if (this.enabled) { return; } const { nestedHeaders } = this.hot.getSettings(); if (!Array.isArray(nestedHeaders) || !Array.isArray(nestedHeaders[0])) { warn(toSingleLine`Your Nested Headers plugin configuration is invalid. The settings has to be\x20 passed as an array of arrays e.q. [['A1', { label: 'A2', colspan: 2 }]]`); } this.addHook('init', () => _assertClassBrand(_NestedHeaders_brand, this, _onInit).call(this)); this.addHook('afterLoadData', function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onAfterLoadData).call(_this, ...args); }); this.addHook('beforeOnCellMouseDown', function () { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeOnCellMouseDown).call(_this, ...args); }); this.addHook('afterOnCellMouseDown', function () { for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onAfterOnCellMouseDown).call(_this, ...args); }); this.addHook('beforeOnCellMouseOver', function () { for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { args[_key4] = arguments[_key4]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeOnCellMouseOver).call(_this, ...args); }); this.addHook('beforeOnCellMouseUp', function () { for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { args[_key5] = arguments[_key5]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeOnCellMouseUp).call(_this, ...args); }); this.addHook('beforeSelectionHighlightSet', function () { for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) { args[_key6] = arguments[_key6]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeSelectionHighlightSet).call(_this, ...args); }); this.addHook('modifyTransformStart', function () { for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) { args[_key7] = arguments[_key7]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onModifyTransformStart).call(_this, ...args); }); this.addHook('afterSelection', () => _classPrivateFieldGet(_updateFocusHighlightPosition, this).call(this)); this.addHook('afterSelectionFocusSet', () => _classPrivateFieldGet(_updateFocusHighlightPosition, this).call(this)); this.addHook('beforeViewportScrollHorizontally', function () { for (var _len8 = arguments.length, args = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) { args[_key8] = arguments[_key8]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeViewportScrollHorizontally).call(_this, ...args); }); this.addHook('afterGetColumnHeaderRenderers', array => _assertClassBrand(_NestedHeaders_brand, this, _onAfterGetColumnHeaderRenderers).call(this, array)); this.addHook('modifyColWidth', function () { for (var _len9 = arguments.length, args = new Array(_len9), _key9 = 0; _key9 < _len9; _key9++) { args[_key9] = arguments[_key9]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onModifyColWidth).call(_this, ...args); }); this.addHook('modifyColumnHeaderValue', function () { for (var _len0 = arguments.length, args = new Array(_len0), _key0 = 0; _key0 < _len0; _key0++) { args[_key0] = arguments[_key0]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onModifyColumnHeaderValue).call(_this, ...args); }); this.addHook('beforeHighlightingColumnHeader', function () { for (var _len1 = arguments.length, args = new Array(_len1), _key1 = 0; _key1 < _len1; _key1++) { args[_key1] = arguments[_key1]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeHighlightingColumnHeader).call(_this, ...args); }); this.addHook('beforeCopy', function () { for (var _len10 = arguments.length, args = new Array(_len10), _key10 = 0; _key10 < _len10; _key10++) { args[_key10] = arguments[_key10]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeCopy).call(_this, ...args); }); this.addHook('beforeSelectColumns', function () { for (var _len11 = arguments.length, args = new Array(_len11), _key11 = 0; _key11 < _len11; _key11++) { args[_key11] = arguments[_key11]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onBeforeSelectColumns).call(_this, ...args); }); this.addHook('afterViewportColumnCalculatorOverride', function () { for (var _len12 = arguments.length, args = new Array(_len12), _key12 = 0; _key12 < _len12; _key12++) { args[_key12] = arguments[_key12]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onAfterViewportColumnCalculatorOverride).call(_this, ...args); }); this.addHook('modifyFocusedElement', function () { for (var _len13 = arguments.length, args = new Array(_len13), _key13 = 0; _key13 < _len13; _key13++) { args[_key13] = arguments[_key13]; } return _assertClassBrand(_NestedHeaders_brand, _this, _onModifyFocusedElement).call(_this, ...args); }); this.hot.columnIndexMapper.addLocalHook('cacheUpdated', _classPrivateFieldGet(_updateFocusHighlightPosition, this)); this.hot.rowIndexMapper.addLocalHook('cacheUpdated', _classPrivateFieldGet(_updateFocusHighlightPosition, this)); super.enablePlugin(); this.updatePlugin(); // @TODO: Workaround for broken plugin initialization abstraction. } /** * Updates the plugin's state. * * This method is executed when [`updateSettings()`](@/api/core.md#updatesettings) is invoked with any of the following configuration options: * - [`nestedHeaders`](@/api/options.md#nestedheaders) */ updatePlugin() { if (!this.hot.view) { // @TODO: Workaround for broken plugin initialization abstraction. return; } const { nestedHeaders } = this.hot.getSettings(); _classPrivateFieldGet(_stateManager, this).setColumnsLimit(this.hot.countCols()); if (Array.isArray(nestedHeaders)) { this.detectedOverlappedHeaders = _classPrivateFieldGet(_stateManager, this).setState(nestedHeaders); } if (this.detectedOverlappedHeaders) { warn(toSingleLine`Your Nested Headers plugin setup contains overlapping headers. This kind of configuration\x20 is currently not supported.`); } if (this.enabled) { // This line covers the case when a developer uses the external hiding maps to manipulate // the columns' visibility. The tree state built from the settings - which is always built // as if all the columns are visible, needs to be modified to be in sync with a dataset. this.hot.columnIndexMapper.hidingMapsCollection.getMergedValues().forEach((isColumnHidden, physicalColumnIndex) => { const actionName = isColumnHidden === true ? 'hide-column' : 'show-column'; _classPrivateFieldGet(_stateManager, this).triggerColumnModification(actionName, physicalColumnIndex); }); } if (!_classPrivateFieldGet(_hidingIndexMapObserver, this) && this.enabled) { _classPrivateFieldSet(_hidingIndexMapObserver, this, this.hot.columnIndexMapper.createChangesObserver('hiding').subscribe(changes => { changes.forEach(_ref => { let { op, index: columnIndex, newValue } = _ref; if (op === 'replace') { const actionName = newValue === true ? 'hide-column' : 'show-column'; _classPrivateFieldGet(_stateManager, this).triggerColumnModification(actionName, columnIndex); } }); this.ghostTable.buildWidthsMap(); })); } this.ghostTable.setLayersCount(this.getLayersCount()).buildWidthsMap(); super.updatePlugin(); } /** * Disables the plugin functionality for this Handsontable instance. */ disablePlugin() { this.hot.rowIndexMapper.removeLocalHook('cacheUpdated', _classPrivateFieldGet(_updateFocusHighlightPosition, this)); this.hot.columnIndexMapper.removeLocalHook('cacheUpdated', _classPrivateFieldGet(_updateFocusHighlightPosition, this)); this.clearColspans(); _classPrivateFieldGet(_stateManager, this).clear(); _classPrivateFieldGet(_hidingIndexMapObserver, this).unsubscribe(); _classPrivateFieldSet(_hidingIndexMapObserver, this, null); this.ghostTable.clear(); super.disablePlugin(); } /** * Returns an instance of the internal state manager of the plugin. * * @private * @returns {StateManager} */ getStateManager() { return _classPrivateFieldGet(_stateManager, this); } /** * Gets a total number of headers levels. * * @private * @returns {number} */ getLayersCount() { return _classPrivateFieldGet(_stateManager, this).getLayersCount(); } /** * Gets column settings for a specified header. The returned object contains * information about the header label, its colspan length, or if it is hidden * in the header renderers. * * @private * @param {number} headerLevel Header level (0 = most distant to the table). * @param {number} columnIndex A visual column index. * @returns {object} */ getHeaderSettings(headerLevel, columnIndex) { return _classPrivateFieldGet(_stateManager, this).getHeaderSettings(headerLevel, columnIndex); } /** * Clear the colspans remaining after plugin usage. * * @private */ clearColspans() { if (!this.hot.view) { return; } const { _wt: wt } = this.hot.view; const headerLevels = wt.getSetting('columnHeaders').length; const mainHeaders = wt.wtTable.THEAD; const topHeaders = wt.wtOverlays.topOverlay.clone.wtTable.THEAD; const topLeftCornerHeaders = wt.wtOverlays.topInlineStartCornerOverlay ? wt.wtOverlays.topInlineStartCornerOverlay.clone.wtTable.THEAD : null; for (let i = 0; i < headerLevels; i++) { const masterLevel = mainHeaders.childNodes[i]; if (!masterLevel) { break; } const topLevel = topHeaders.childNodes[i]; const topLeftCornerLevel = topLeftCornerHeaders ? topLeftCornerHeaders.childNodes[i] : null; for (let j = 0, masterNodes = masterLevel.childNodes.length; j < masterNodes; j++) { masterLevel.childNodes[j].removeAttribute('colspan'); removeClass(masterLevel.childNodes[j], 'hiddenHeader'); if (topLevel && topLevel.childNodes[j]) { topLevel.childNodes[j].removeAttribute('colspan'); removeClass(topLevel.childNodes[j], 'hiddenHeader'); } if (topLeftCornerHeaders && topLeftCornerLevel && topLeftCornerLevel.childNodes[j]) { topLeftCornerLevel.childNodes[j].removeAttribute('colspan'); removeClass(topLeftCornerLevel.childNodes[j], 'hiddenHeader'); } } } } /** * Generates the appropriate header renderer for a header row. * * @private * @param {number} headerLevel The index of header level counting from the top (positive * values counting from 0 to N). * @returns {Function} * @fires Hooks#afterGetColHeader */ headerRendererFactory(headerLevel) { var _this2 = this; const fixedColumnsStart = this.hot.view._wt.getSetting('fixedColumnsStart'); return (renderedColumnIndex, TH) => { var _classPrivateFieldGet2; const { columnIndexMapper, view } = this.hot; let visualColumnIndex = columnIndexMapper.getVisualFromRenderableIndex(renderedColumnIndex); if (visualColumnIndex === null) { visualColumnIndex = renderedColumnIndex; } TH.removeAttribute('colspan'); removeClass(TH, 'hiddenHeader'); removeClass(TH, 'hiddenHeaderText'); const { colspan, isHidden, isPlaceholder, headerClassNames } = (_classPrivateFieldGet2 = _classPrivateFieldGet(_stateManager, this).getHeaderSettings(headerLevel, visualColumnIndex)) !== null && _classPrivateFieldGet2 !== void 0 ? _classPrivateFieldGet2 : { label: '' }; if (isPlaceholder || isHidden) { addClass(TH, 'hiddenHeader'); } else if (colspan > 1) { var _wtOverlays$topInline, _wtOverlays$inlineSta, _wtOverlays$topOverla; const { wtOverlays } = view._wt; const isTopInlineStartOverlay = (_wtOverlays$topInline = wtOverlays.topInlineStartCornerOverlay) === null || _wtOverlays$topInline === void 0 ? void 0 : _wtOverlays$topInline.clone.wtTable.THEAD.contains(TH); const isInlineStartOverlay = (_wtOverlays$inlineSta = wtOverlays.inlineStartOverlay) === null || _wtOverlays$inlineSta === void 0 ? void 0 : _wtOverlays$inlineSta.clone.wtTable.THEAD.contains(TH); const isTopOverlay = (_wtOverlays$topOverla = wtOverlays.topOverlay) === null || _wtOverlays$topOverla === void 0 ? void 0 : _wtOverlays$topOverla.clone.wtTable.THEAD.contains(TH); if (isTopOverlay && visualColumnIndex < fixedColumnsStart) { addClass(TH, 'hiddenHeaderText'); } // Check if there is a fixed column enabled, if so then reduce colspan to fixed column width. const correctedColspan = isTopInlineStartOverlay || isInlineStartOverlay ? Math.min(colspan, fixedColumnsStart - renderedColumnIndex) : colspan; if (correctedColspan > 1) { TH.setAttribute('colspan', correctedColspan); } } this.hot.view.appendColHeader(visualColumnIndex, TH, function () { return _this2.getColumnHeaderValue(...arguments); }, headerLevel); // Replace the higher-order `headerClassName`s with the one provided in the plugin config, if it was provided. if (!isPlaceholder && !isHidden) { const innerHeaderDiv = TH.querySelector('div.relative'); if (innerHeaderDiv && headerClassNames && headerClassNames.length > 0) { removeClass(innerHeaderDiv, this.hot.getColumnMeta(visualColumnIndex).headerClassName); addClass(innerHeaderDiv, headerClassNames); } } }; } /** * Returns the column header value for specified column and header level index. * * @private * @param {number} visualColumnIndex Visual column index. * @param {number} headerLevel The index of header level. The header level accepts positive (0 to N) * and negative (-1 to -N) values. For positive values, 0 points to the * top most header, and for negative direction, -1 points to the most bottom * header (the header closest to the cells). * @returns {string} Returns the column header value to update. */ getColumnHeaderValue(visualColumnIndex, headerLevel) { var _classPrivateFieldGet3; const { isHidden, isPlaceholder } = (_classPrivateFieldGet3 = _classPrivateFieldGet(_stateManager, this).getHeaderSettings(headerLevel, visualColumnIndex)) !== null && _classPrivateFieldGet3 !== void 0 ? _classPrivateFieldGet3 : {}; if (isPlaceholder || isHidden) { return ''; } return this.hot.getColHeader(visualColumnIndex, headerLevel); } /** * Destroys the plugin instance. */ destroy() { _classPrivateFieldSet(_stateManager, this, null); if (_classPrivateFieldGet(_hidingIndexMapObserver, this) !== null) { _classPrivateFieldGet(_hidingIndexMapObserver, this).unsubscribe(); _classPrivateFieldSet(_hidingIndexMapObserver, this, null); } super.destroy(); } /** * Gets the tree data that belongs to the column headers pointed by the passed coordinates. * * @private * @param {CellCoords} coords The CellCoords instance. * @returns {object|undefined} */ _getHeaderTreeNodeDataByCoords(coords) { if (coords.row >= 0 || coords.col < 0) { return; } return _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(coords.row, coords.col); } } function _onBeforeViewportScrollHorizontally(visualColumn, snapping) { var _classPrivateFieldGet4; const selection = this.hot.getSelectedRangeActive(); if (!selection) { return visualColumn; } const { highlight } = selection; const { navigableHeaders } = this.hot.getSettings(); const isSelectedByColumnHeader = this.hot.selection.isSelectedByColumnHeader(); const highlightRow = navigableHeaders ? highlight.row : (_classPrivateFieldGet4 = _classPrivateFieldGet(_recentlyHighlightCoords, this)) === null || _classPrivateFieldGet4 === void 0 ? void 0 : _classPrivateFieldGet4.row; const highlightColumn = isSelectedByColumnHeader ? visualColumn : highlight.col; const isNestedHeadersRange = highlightRow < 0 && highlightColumn >= 0; _classPrivateFieldSet(_recentlyHighlightCoords, this, null); if (!isNestedHeadersRange) { return visualColumn; } const firstVisibleColumn = this.hot.getFirstFullyVisibleColumn(); const lastVisibleColumn = this.hot.getLastFullyVisibleColumn(); const viewportWidth = lastVisibleColumn - firstVisibleColumn + 1; const mostLeftColumnIndex = _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(highlightRow, highlightColumn); const mostRightColumnIndex = _classPrivateFieldGet(_stateManager, this).findRightMostColumnIndex(highlightRow, highlightColumn); const headerWidth = mostRightColumnIndex - mostLeftColumnIndex + 1; // scroll the viewport always to the left when the header is wider than the viewport if (mostLeftColumnIndex < firstVisibleColumn && mostRightColumnIndex > lastVisibleColumn) { return mostLeftColumnIndex; } if (isSelectedByColumnHeader) { let scrollColumnIndex = null; if (mostLeftColumnIndex >= firstVisibleColumn && mostRightColumnIndex > lastVisibleColumn) { if (headerWidth > viewportWidth) { snapping.value = 'start'; scrollColumnIndex = mostLeftColumnIndex; } else { snapping.value = 'end'; scrollColumnIndex = mostRightColumnIndex; } } else if (mostLeftColumnIndex < firstVisibleColumn && mostRightColumnIndex <= lastVisibleColumn) { if (headerWidth > viewportWidth) { snapping.value = 'end'; scrollColumnIndex = mostRightColumnIndex; } else { snapping.value = 'start'; scrollColumnIndex = mostLeftColumnIndex; } } return scrollColumnIndex; } return mostLeftColumnIndex <= firstVisibleColumn ? mostLeftColumnIndex : mostRightColumnIndex; } /** * Allows to control which header DOM element will be used to highlight. * * @param {number} visualColumn A visual column index of the highlighted row header. * @param {number} headerLevel A row header level that is currently highlighted. * @param {object} highlightMeta An object with meta data that describes the highlight state. * @returns {number} */ function _onBeforeHighlightingColumnHeader(visualColumn, headerLevel, highlightMeta) { const headerNodeData = _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(headerLevel, visualColumn); if (!headerNodeData) { return visualColumn; } const { columnCursor, selectionType, selectionWidth } = highlightMeta; const { isRoot, colspan } = _classPrivateFieldGet(_stateManager, this).getHeaderSettings(headerLevel, visualColumn); if (selectionType === HEADER_TYPE) { if (!isRoot) { return headerNodeData.columnIndex; } } else if (selectionType === ACTIVE_HEADER_TYPE) { if (colspan > selectionWidth - columnCursor || !isRoot) { // Prevents adding any CSS class names to the TH element return null; } } return visualColumn; } /** * Listens the `beforeCopy` hook that allows processing the copied column headers so that the * merged column headers do not propagate the value for each column but only once at the beginning * of the column. * * @private * @param {Array[]} data An array of arrays which contains data to copied. * @param {object[]} copyableRanges An array of objects with ranges of the visual indexes (`startRow`, `startCol`, `endRow`, `endCol`) * which will copied. * @param {{ columnHeadersCount: number }} copiedHeadersCount An object with keys that holds information with * the number of copied headers. */ function _onBeforeCopy(data, copyableRanges, _ref2) { let { columnHeadersCount } = _ref2; if (columnHeadersCount === 0) { return; } for (let rangeIndex = 0; rangeIndex < copyableRanges.length; rangeIndex++) { const { startRow, startCol, endRow, endCol } = copyableRanges[rangeIndex]; const rowsCount = endRow - startRow + 1; const columnsCount = startCol - endCol + 1; // do not process dataset ranges and column headers where only one column is copied if (startRow >= 0 || columnsCount === 1) { break; } for (let column = startCol; column <= endCol; column++) { for (let row = startRow; row <= endRow; row++) { var _classPrivateFieldGet5; const zeroBasedColumnHeaderLevel = rowsCount + row; const zeroBasedColumnIndex = column - startCol; if (zeroBasedColumnIndex === 0) { continue; // eslint-disable-line no-continue } const isRoot = (_classPrivateFieldGet5 = _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(row, column)) === null || _classPrivateFieldGet5 === void 0 ? void 0 : _classPrivateFieldGet5.isRoot; if (isRoot === false) { data[zeroBasedColumnHeaderLevel][zeroBasedColumnIndex] = ''; } } } } } /** * Allows blocking the column selection that is controlled by the core Selection module. * * @param {MouseEvent} event Mouse event. * @param {CellCoords} coords Cell coords object containing the visual coordinates of the clicked cell. * @param {CellCoords} TD The table cell or header element. * @param {object} controller An object with properties `row`, `column` and `cell`. Each property contains * a boolean value that allows or disallows changing the selection for that particular area. */ function _onBeforeOnCellMouseDown(event, coords, TD, controller) { const headerNodeData = this._getHeaderTreeNodeDataByCoords(coords); if (headerNodeData) { // Block the Selection module in controlling how the columns are selected. Pass the // responsibility of the column selection to this plugin (see "onAfterOnCellMouseDown" hook). controller.column = true; } } /** * Allows to control how the column selection based on the coordinates and the nested headers is made. * * @param {MouseEvent} event Mouse event. * @param {CellCoords} coords Cell coords object containing the visual coordinates of the clicked cell. */ function _onAfterOnCellMouseDown(event, coords) { const headerNodeData = this._getHeaderTreeNodeDataByCoords(coords); if (!headerNodeData) { return; } _classPrivateFieldSet(_focusInitialCoords, this, coords.clone()); _classPrivateFieldSet(_isColumnsSelectionInProgress, this, true); const { selection } = this.hot; const currentSelection = selection.isSelected() ? selection.getSelectedRange().current() : null; const columnsToSelect = []; const { columnIndex, origColspan } = headerNodeData; // The Selection module doesn't allow it to extend its behavior easily. That's why here we need // to re-implement the "click" and "shift" behavior. As a workaround, the logic for the nested // headers must implement a similar logic as in the original Selection handler // (see src/selection/mouseEventHandler.js). const allowRightClickSelection = !selection.inInSelection(coords); if (event.shiftKey && currentSelection) { if (coords.col < currentSelection.from.col) { columnsToSelect.push(currentSelection.getTopEndCorner().col, columnIndex, coords.row); } else if (coords.col > currentSelection.from.col) { columnsToSelect.push(currentSelection.getTopStartCorner().col, columnIndex + origColspan - 1, coords.row); } else { columnsToSelect.push(columnIndex, columnIndex + origColspan - 1, coords.row); } } else if (isLeftClick(event) || isRightClick(event) && allowRightClickSelection || isTouchEvent(event)) { columnsToSelect.push(columnIndex, columnIndex + origColspan - 1, coords.row); } // The plugin takes control of how the columns are selected. selection.selectColumns(...columnsToSelect); } /** * Makes the header-selection properly select the nested headers. * * @param {MouseEvent} event Mouse event. * @param {CellCoords} coords Cell coords object containing the visual coordinates of the clicked cell. * @param {HTMLElement} TD The cell element. * @param {object} controller An object with properties `row`, `column` and `cell`. Each property contains * a boolean value that allows or disallows changing the selection for that particular area. */ function _onBeforeOnCellMouseOver(event, coords, TD, controller) { if (!this.hot.view.isMouseDown() || controller.column) { return; } const headerNodeData = this._getHeaderTreeNodeDataByCoords(coords); if (!headerNodeData) { return; } const { columnIndex, origColspan } = headerNodeData; const selectedRange = this.hot.getSelectedRangeActive(); const topStartCoords = selectedRange.getTopStartCorner(); const bottomEndCoords = selectedRange.getBottomEndCorner(); const { from } = selectedRange; // Block the Selection module in controlling how the columns and cells are selected. // From now on, the plugin is responsible for the selection. controller.column = true; controller.cell = true; const columnsToSelect = []; const headerLevel = clamp(coords.row, -Infinity, -1); if (coords.col < from.col) { columnsToSelect.push(bottomEndCoords.col, columnIndex, headerLevel); } else if (coords.col > from.col) { columnsToSelect.push(topStartCoords.col, columnIndex + origColspan - 1, headerLevel); } else { columnsToSelect.push(columnIndex, columnIndex + origColspan - 1, headerLevel); } this.hot.selection.selectColumns(...columnsToSelect); } /** * Switches internal flag about selection progress to `false`. */ function _onBeforeOnCellMouseUp() { _classPrivateFieldSet(_isColumnsSelectionInProgress, this, false); } /** * The hook checks and ensures that the focus position that depends on the selected columns * range is always positioned within the range. */ function _onBeforeSelectionHighlightSet() { const { navigableHeaders } = this.hot.getSettings(); if (!this.hot.view.isMouseDown() || !_classPrivateFieldGet(_isColumnsSelectionInProgress, this) || !navigableHeaders) { return; } const selectedRange = this.hot.getSelectedRangeLast(); const columnStart = selectedRange.getTopStartCorner().col; const columnEnd = selectedRange.getBottomEndCorner().col; const { columnIndex, origColspan } = _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(_classPrivateFieldGet(_focusInitialCoords, this).row, _classPrivateFieldGet(_focusInitialCoords, this).col); selectedRange.setHighlight(_classPrivateFieldGet(_focusInitialCoords, this)); if (origColspan > selectedRange.getWidth() || columnIndex < columnStart || columnIndex + origColspan - 1 > columnEnd) { const headerLevel = _classPrivateFieldGet(_stateManager, this).findTopMostEntireHeaderLevel(clamp(columnStart, columnIndex, columnIndex + origColspan - 1), clamp(columnEnd, columnIndex, columnIndex + origColspan - 1)); selectedRange.highlight.row = headerLevel; selectedRange.highlight.col = selectedRange.from.col; } } /** * `modifyTransformStart` hook is called every time the keyboard navigation is used. * * @param {object} delta The transformation delta. */ function _onModifyTransformStart(delta) { const { highlight } = this.hot.getSelectedRangeActive(); const nextCoords = this.hot._createCellCoords(highlight.row + delta.row, highlight.col + delta.col); const isNestedHeadersRange = nextCoords.isHeader() && nextCoords.col >= 0; if (!isNestedHeadersRange) { return; } const visualColumnIndexStart = _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(nextCoords.row, nextCoords.col); const visualColumnIndexEnd = _classPrivateFieldGet(_stateManager, this).findRightMostColumnIndex(nextCoords.row, nextCoords.col); if (delta.col < 0) { const nextColumn = highlight.col >= visualColumnIndexStart && highlight.col <= visualColumnIndexEnd ? visualColumnIndexStart - 1 : visualColumnIndexEnd; const notHiddenColumnIndex = this.hot.columnIndexMapper.getNearestNotHiddenIndex(nextColumn, -1); if (notHiddenColumnIndex === null) { // There are no visible columns anymore, so move the selection out of the table edge. This will // be processed by the selection Transformer class as a move selection to the previous row (if autoWrapRow is enabled). delta.col = -this.hot.view.countRenderableColumnsInRange(0, highlight.col); } else { delta.col = -Math.max(this.hot.view.countRenderableColumnsInRange(notHiddenColumnIndex, highlight.col) - 1, 1); } } else if (delta.col > 0) { const nextColumn = highlight.col >= visualColumnIndexStart && highlight.col <= visualColumnIndexEnd ? visualColumnIndexEnd + 1 : visualColumnIndexStart; const notHiddenColumnIndex = this.hot.columnIndexMapper.getNearestNotHiddenIndex(nextColumn, 1); if (notHiddenColumnIndex === null) { // There are no visible columns anymore, so move the selection out of the table edge. This will // be processed by the selection Transformer class as a move selection to the next row (if autoWrapRow is enabled). delta.col = this.hot.view.countRenderableColumnsInRange(highlight.col, this.hot.countCols()); } else { delta.col = Math.max(this.hot.view.countRenderableColumnsInRange(highlight.col, notHiddenColumnIndex) - 1, 1); } } } /** * The hook observes the column selection from the Selection API and modifies the column range to * ensure that the whole nested column will be covered. * * @param {CellCoords} from The coords object where the selection starts. * @param {CellCoords} to The coords object where the selection ends. * @param {CellCoords} highlight The coords object where the focus is. */ function _onBeforeSelectColumns(from, to, highlight) { const headerLevel = from.row; const startNodeData = this._getHeaderTreeNodeDataByCoords({ row: headerLevel, col: from.col }); const endNodeData = this._getHeaderTreeNodeDataByCoords({ row: headerLevel, col: to.col }); _classPrivateFieldSet(_recentlyHighlightCoords, this, highlight.clone()); if (to.col < from.col) { // Column selection from right to left if (startNodeData) { from.col = startNodeData.columnIndex + startNodeData.origColspan - 1; } if (endNodeData) { to.col = endNodeData.columnIndex; } } else if (to.col >= from.col) { // Column selection from left to right or a single column selection if (startNodeData) { from.col = startNodeData.columnIndex; } if (endNodeData) { to.col = endNodeData.columnIndex + endNodeData.origColspan - 1; } } } /** * `afterGetColumnHeader` hook callback - prepares the header structure. * * @param {Array} renderersArray Array of renderers. */ function _onAfterGetColumnHeaderRenderers(renderersArray) { if (_classPrivateFieldGet(_stateManager, this).getLayersCount() > 0) { renderersArray.length = 0; for (let headerLayer = 0; headerLayer < _classPrivateFieldGet(_stateManager, this).getLayersCount(); headerLayer++) { renderersArray.push(this.headerRendererFactory(headerLayer)); } } } /** * Make the renderer render the first nested column in its entirety. * * @param {object} calc Viewport column calculator. */ function _onAfterViewportColumnCalculatorOverride(calc) { const headerLayersCount = _classPrivateFieldGet(_stateManager, this).getLayersCount(); let newStartColumn = calc.startColumn; let nonRenderable = !!headerLayersCount; for (let headerLayer = 0; headerLayer < headerLayersCount; headerLayer++) { const startColumn = _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(headerLayer, calc.startColumn); const renderedStartColumn = this.hot.columnIndexMapper.getRenderableFromVisualIndex(startColumn); // If any of the headers for that column index is rendered, all of them should be rendered properly, see // comment below. if (startColumn >= 0) { nonRenderable = false; } // `renderedStartColumn` can be `null` if the leftmost columns are hidden. In that case -> ignore that header // level, as it should be handled by the "parent" header if (isNumeric(renderedStartColumn) && renderedStartColumn < calc.startColumn) { newStartColumn = renderedStartColumn; break; } } // If no headers for the provided column index are renderable, start rendering from the beginning of the upmost // header for that position. calc.startColumn = nonRenderable ? _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(0, newStartColumn).columnIndex : newStartColumn; } /** * `modifyColWidth` hook callback - returns width from cache, when is greater than incoming from hook. * * @param {number} width Width from hook. * @param {number} column Visual index of an column. * @returns {number} */ function _onModifyColWidth(width, column) { const cachedWidth = this.ghostTable.getWidth(column); return width > cachedWidth ? width : cachedWidth; } /** * Listens the `modifyColumnHeaderValue` hook that overwrites the column headers values based on * the internal state and settings of the plugin. * * @param {string} value The column header value. * @param {number} visualColumnIndex The visual column index. * @param {number} headerLevel The index of header level. The header level accepts positive (0 to N) * and negative (-1 to -N) values. For positive values, 0 points to the * top most header, and for negative direction, -1 points to the most bottom * header (the header closest to the cells). * @returns {string} Returns the column header value to update. */ function _onModifyColumnHeaderValue(value, visualColumnIndex, headerLevel) { var _classPrivateFieldGet6; const { label } = (_classPrivateFieldGet6 = _classPrivateFieldGet(_stateManager, this).getHeaderTreeNodeData(headerLevel, visualColumnIndex)) !== null && _classPrivateFieldGet6 !== void 0 ? _classPrivateFieldGet6 : { label: '' }; return label; } /** * `modifyFocusedElement` hook callback. * * @param {number} row Row index. * @param {number} column Column index. * @returns {HTMLTableCellElement} The `TH` element to be focused. */ function _onModifyFocusedElement(row, column) { if (row < 0) { return this.hot.getCell(row, _classPrivateFieldGet(_stateManager, this).findLeftMostColumnIndex(row, column), true); } } /** * Updates the plugin state after HoT initialization. */ function _onInit() { // @TODO: Workaround for broken plugin initialization abstraction. this.updatePlugin(); } /** * Updates the plugin state after new dataset load. * * @param {Array[]} sourceData Array of arrays or array of objects containing data. * @param {boolean} initialLoad Flag that determines whether the data has been loaded * during the initialization. */ function _onAfterLoadData(sourceData, initialLoad) { if (!initialLoad) { this.updatePlugin(); } }