UNPKG

handsontable

Version:

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

253 lines (250 loc) • 11.3 kB
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } 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 _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"); } var _GhostTable_brand = /*#__PURE__*/new WeakSet(); /** * The class generates the nested headers structure in the DOM and reads the column width for * each column. The hierarchy is built only for visible, non-hidden columns. Each time the * column is shown or hidden, the structure is rebuilt, and the width of the columns in the * map updated. * * @private */ class GhostTable { constructor(_ref) { let { hot, headersStateManager } = _ref; /** * Measure widths from the full (uncollapsed) ghost table. * * @returns {Map<number, number>} Map of physical column index to width. */ _classPrivateMethodInitSpec(this, _GhostTable_brand); /** * Reference to the Handsontable instance. * * @private * @type {Handsontable} */ _defineProperty(this, "hot", void 0); /** * The state manager for the nested headers. * * @private * @type {StateManager} */ _defineProperty(this, "headersStateManager", void 0); /** * The value that holds information about the number of the nested header layers (header rows). * * @private * @type {number} */ _defineProperty(this, "layersCount", 0); /** * Temporary element created to get minimal headers widths. * * @private * @type {*} */ _defineProperty(this, "container", void 0); /** * PhysicalIndexToValueMap to keep and track of the columns' widths (as rendered in the main table). * * @private * @type {PhysicalIndexToValueMap} */ _defineProperty(this, "widthsMap", void 0); this.hot = hot; this.headersStateManager = headersStateManager; this.widthsMap = this.hot.columnIndexMapper.createAndRegisterIndexMap('nestedHeaders.widthsMap', 'physicalIndexToValue'); } /** * Sets the number of nested headers layers count. * * @param {number} layersCount Total number of headers levels. * @returns {GhostTable} */ setLayersCount(layersCount) { this.layersCount = layersCount; return this; } /** * Gets the column width based on the visual column index (as rendered in the main table). * * @param {number} visualColumn Visual column index. * @returns {number|null} */ getWidth(visualColumn) { return this.widthsMap.getValueAtIndex(this.hot.toPhysicalColumn(visualColumn)); } /** * Build cache of the headers widths. */ buildWidthsMap() { const collapsedPhysicalColumns = _assertClassBrand(_GhostTable_brand, this, _getCollapsedPhysicalColumns).call(this); const hasCollapsedGroups = collapsedPhysicalColumns.size > 0; const currentThemeName = this.hot.getCurrentThemeName(); this.container = this.hot.rootDocument.createElement('div'); this.container.classList.add('handsontable', 'htGhostTable', 'htAutoSize', 'htNestedHeaders'); if (currentThemeName) { this.container.classList.add(currentThemeName); } _assertClassBrand(_GhostTable_brand, this, _buildGhostTable).call(this, this.container, hasCollapsedGroups); this.hot.rootDocument.body.appendChild(this.container); const fullWidthByPhysical = hasCollapsedGroups ? _assertClassBrand(_GhostTable_brand, this, _measureFullTable).call(this) : null; const renderedTable = this.container.querySelector('[data-ghost-table="rendered"]'); const renderedColumns = renderedTable.querySelectorAll('tr:last-of-type th'); this.widthsMap.clear(); for (let column = 0; column < renderedColumns.length; column++) { const visualColumnIndex = Number.parseInt(renderedColumns[column].dataset.column, 10); const physicalColumnIndex = this.hot.toPhysicalColumn(visualColumnIndex); if (this.hot.columnIndexMapper.isHidden(physicalColumnIndex)) { continue; } let width; if (hasCollapsedGroups && collapsedPhysicalColumns.has(physicalColumnIndex)) { const fullWidth = fullWidthByPhysical.get(physicalColumnIndex); if (fullWidth !== undefined) { width = fullWidth; } } if (width === undefined) { width = renderedColumns[column].getBoundingClientRect().width; } this.widthsMap.setValueAtIndex(physicalColumnIndex, width); } this.container.remove(); this.container = null; } /** * Clear the widths cache. */ clear() { this.widthsMap.clear(); this.container = null; } } function _measureFullTable() { const fullTable = this.container.querySelector('[data-ghost-table="full"]'); const fullColumns = fullTable.querySelectorAll('tr:last-of-type th'); const fullWidthByPhysical = new Map(); for (let column = 0; column < fullColumns.length; column++) { const visualColumnIndex = Number.parseInt(fullColumns[column].dataset.column, 10); const physicalColumnIndex = this.hot.toPhysicalColumn(visualColumnIndex); fullWidthByPhysical.set(physicalColumnIndex, fullColumns[column].getBoundingClientRect().width); } return fullWidthByPhysical; } /** * Pre-compute the set of physical column indexes that have any collapsed ancestor. * This avoids repeated walkUp() calls per column during measurement. * * @returns {Set<number>} Set of physical column indexes under collapsed ancestors. */ function _getCollapsedPhysicalColumns() { const collapsedPhysicals = new Set(); const maxColumnsCount = this.hot.countCols(); for (let col = 0; col < maxColumnsCount; col++) { const treeNode = this.headersStateManager.getHeaderTreeNode(-1, col); if (!treeNode) { continue; } let found = false; treeNode.walkUp(node => { if (node.data.isCollapsed === true) { found = true; return false; } }); if (found) { collapsedPhysicals.add(this.hot.toPhysicalColumn(col)); } } return collapsedPhysicals; } /** * Build temporary tables for getting minimal columns widths. Builds two tables: * - Full: one TH per column (no collapse/hidden), to store width of the first column of a collapsed * group and fix jump on collapse/uncollapse. Only built when collapsed groups exist. * - Rendered: same structure as the main table (colspans, only visible roots), for width when a * column has children and one is hidden (parent gets the width of the visible one). * * @param {HTMLElement} container The element where the DOM nodes are injected. * @param {boolean} hasCollapsedGroups Whether any collapsed groups exist. */ function _buildGhostTable(container, hasCollapsedGroups) { const isDropdownEnabled = !!this.hot.getSettings().dropdownMenu; const maxColumnsCount = this.hot.countCols(); const sanitizer = this.hot.getSettings().sanitizer; if (hasCollapsedGroups) { container.innerHTML = _assertClassBrand(_GhostTable_brand, this, _buildFullColumnsTableHTML).call(this, maxColumnsCount, isDropdownEnabled, sanitizer) + _assertClassBrand(_GhostTable_brand, this, _buildRenderedTableHTML).call(this, maxColumnsCount, isDropdownEnabled, sanitizer); } else { container.innerHTML = _assertClassBrand(_GhostTable_brand, this, _buildRenderedTableHTML).call(this, maxColumnsCount, isDropdownEnabled, sanitizer); } } /** * Build HTML string for a table with one TH per column (colspan = origColspan), * regardless of hidden/collapsed state. * * @param {number} maxColumnsCount Total column count. * @param {boolean} isDropdownEnabled Whether dropdown menu is enabled. * @param {Function|undefined} sanitizer The sanitizer function. * @returns {string} HTML string for the full table. */ function _buildFullColumnsTableHTML(maxColumnsCount, isDropdownEnabled, sanitizer) { let rowsHTML = ''; for (let row = 0; row < this.layersCount; row++) { let cellsHTML = ''; for (let col = 0; col < maxColumnsCount; col++) { const headerSettings = this.headersStateManager.getHeaderTreeNodeData(row, col); if (headerSettings && headerSettings.isRoot) { cellsHTML += `<th data-column="${col}" colspan="${headerSettings.origColspan}">${_assertClassBrand(_GhostTable_brand, this, _buildHeaderLabelHTML).call(this, headerSettings, isDropdownEnabled, sanitizer)}</th>`; } } rowsHTML += `<tr>${cellsHTML}</tr>`; } return `<table data-ghost-table="full"><thead>${rowsHTML}</thead></table>`; } /** * Build HTML string for a table that mirrors the main table (colspans, only visible roots). * * @param {number} maxColumnsCount Total column count. * @param {boolean} isDropdownEnabled Whether dropdown menu is enabled. * @param {Function|undefined} sanitizer The sanitizer function. * @returns {string} HTML string for the rendered table. */ function _buildRenderedTableHTML(maxColumnsCount, isDropdownEnabled, sanitizer) { let rowsHTML = ''; for (let row = 0; row < this.layersCount; row++) { let cellsHTML = ''; for (let col = 0; col < maxColumnsCount; col++) { const headerSettings = this.headersStateManager.getHeaderSettings(row, col); if (headerSettings && !headerSettings.isPlaceholder && !headerSettings.isHidden) { cellsHTML += `<th data-column="${col}" colspan="${headerSettings.colspan}">${_assertClassBrand(_GhostTable_brand, this, _buildHeaderLabelHTML).call(this, headerSettings, isDropdownEnabled, sanitizer)}</th>`; } } rowsHTML += `<tr>${cellsHTML}</tr>`; } return `<table data-ghost-table="rendered"><thead>${rowsHTML}</thead></table>`; } /** * Build header cell content HTML string. * * @param {object} headerSettings Header settings (label, colspan, etc). * @param {boolean} isDropdownEnabled Whether dropdown menu is enabled. * @param {Function|undefined} sanitizer The sanitizer function. * @returns {string} HTML string for the header label. */ function _buildHeaderLabelHTML(headerSettings, isDropdownEnabled, sanitizer) { const label = typeof sanitizer === 'function' ? sanitizer(headerSettings.label, 'innerHTML') : headerSettings.label; const dropdownHtml = isDropdownEnabled ? '<button class="changeType"></button>' : ''; const indicatorHtml = headerSettings.collapsible ? '<div class="collapsibleIndicator expanded">-</div>' : ''; return `<div class="relative"><span class="colHeader">${label}</span>${dropdownHtml}${indicatorHtml}</div>`; } export default GhostTable;