handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
256 lines (252 loc) • 11.4 kB
JavaScript
"use strict";
exports.__esModule = true;
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>`;
}
var _default = exports.default = GhostTable;