711 lines
29 KiB
JavaScript
711 lines
29 KiB
JavaScript
import "core-js/modules/es.array.push.js";
|
|
import "core-js/modules/es.error.cause.js";
|
|
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
|
|
function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
|
|
function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
|
|
function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; }
|
|
function _classApplyDescriptorSet(receiver, descriptor, value) { if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } }
|
|
function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
|
|
function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }
|
|
function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
|
|
import { addClass, removeClass } from "../../helpers/dom/element.mjs";
|
|
import { isNumeric } from "../../helpers/number.mjs";
|
|
import { isLeftClick, isRightClick } from "../../helpers/dom/event.mjs";
|
|
import { toSingleLine } from "../../helpers/templateLiteralTag.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.
|
|
*
|
|
* __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}, '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}, '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']
|
|
* ]}
|
|
* />
|
|
* ```
|
|
* :::
|
|
*/
|
|
var _stateManager = /*#__PURE__*/new WeakMap();
|
|
var _hidingIndexMapObserver = /*#__PURE__*/new WeakMap();
|
|
export class NestedHeaders extends BasePlugin {
|
|
constructor() {
|
|
super(...arguments);
|
|
/**
|
|
* The state manager for the nested headers.
|
|
*
|
|
* @private
|
|
* @type {StateManager}
|
|
*/
|
|
_classPrivateFieldInitSpec(this, _stateManager, {
|
|
writable: true,
|
|
value: new StateManager()
|
|
});
|
|
/**
|
|
* The instance of the ChangesObservable class that allows track the changes that happens in the
|
|
* column indexes.
|
|
*
|
|
* @private
|
|
* @type {ChangesObservable}
|
|
*/
|
|
_classPrivateFieldInitSpec(this, _hidingIndexMapObserver, {
|
|
writable: true,
|
|
value: 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);
|
|
}
|
|
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', () => this.onInit());
|
|
this.addHook('afterLoadData', function () {
|
|
return _this.onAfterLoadData(...arguments);
|
|
});
|
|
this.addHook('beforeOnCellMouseDown', function () {
|
|
return _this.onBeforeOnCellMouseDown(...arguments);
|
|
});
|
|
this.addHook('afterOnCellMouseDown', function () {
|
|
return _this.onAfterOnCellMouseDown(...arguments);
|
|
});
|
|
this.addHook('beforeOnCellMouseOver', function () {
|
|
return _this.onBeforeOnCellMouseOver(...arguments);
|
|
});
|
|
this.addHook('afterGetColumnHeaderRenderers', array => this.onAfterGetColumnHeaderRenderers(array));
|
|
this.addHook('modifyColWidth', function () {
|
|
return _this.onModifyColWidth(...arguments);
|
|
});
|
|
this.addHook('modifyColumnHeaderValue', function () {
|
|
return _this.onModifyColumnHeaderValue(...arguments);
|
|
});
|
|
this.addHook('beforeHighlightingColumnHeader', function () {
|
|
return _this.onBeforeHighlightingColumnHeader(...arguments);
|
|
});
|
|
this.addHook('beforeCopy', function () {
|
|
return _this.onBeforeCopy(...arguments);
|
|
});
|
|
this.addHook('afterViewportColumnCalculatorOverride', function () {
|
|
return _this.onAfterViewportColumnCalculatorOverride(...arguments);
|
|
});
|
|
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(this, _stateManager).setColumnsLimit(this.hot.countCols());
|
|
if (Array.isArray(nestedHeaders)) {
|
|
this.detectedOverlappedHeaders = _classPrivateFieldGet(this, _stateManager).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(this, _stateManager).triggerColumnModification(actionName, physicalColumnIndex);
|
|
});
|
|
}
|
|
if (!_classPrivateFieldGet(this, _hidingIndexMapObserver) && this.enabled) {
|
|
_classPrivateFieldSet(this, _hidingIndexMapObserver, 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(this, _stateManager).triggerColumnModification(actionName, columnIndex);
|
|
}
|
|
});
|
|
this.ghostTable.buildWidthsMap();
|
|
}));
|
|
}
|
|
this.ghostTable.setLayersCount(this.getLayersCount()).buildWidthsMap();
|
|
super.updatePlugin();
|
|
}
|
|
|
|
/**
|
|
* Disables the plugin functionality for this Handsontable instance.
|
|
*/
|
|
disablePlugin() {
|
|
this.clearColspans();
|
|
_classPrivateFieldGet(this, _stateManager).clear();
|
|
_classPrivateFieldGet(this, _hidingIndexMapObserver).unsubscribe();
|
|
_classPrivateFieldSet(this, _hidingIndexMapObserver, null);
|
|
this.ghostTable.clear();
|
|
super.disablePlugin();
|
|
}
|
|
|
|
/**
|
|
* Returns an instance of the internal state manager of the plugin.
|
|
*
|
|
* @private
|
|
* @returns {StateManager}
|
|
*/
|
|
getStateManager() {
|
|
return _classPrivateFieldGet(this, _stateManager);
|
|
}
|
|
|
|
/**
|
|
* Gets a total number of headers levels.
|
|
*
|
|
* @private
|
|
* @returns {number}
|
|
*/
|
|
getLayersCount() {
|
|
return _classPrivateFieldGet(this, _stateManager).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(this, _stateManager).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');
|
|
const {
|
|
colspan,
|
|
isHidden,
|
|
isPlaceholder
|
|
} = (_classPrivateFieldGet2 = _classPrivateFieldGet(this, _stateManager).getHeaderSettings(headerLevel, visualColumnIndex)) !== null && _classPrivateFieldGet2 !== void 0 ? _classPrivateFieldGet2 : {
|
|
label: ''
|
|
};
|
|
if (isPlaceholder || isHidden) {
|
|
addClass(TH, 'hiddenHeader');
|
|
} else if (colspan > 1) {
|
|
var _wtOverlays$topInline, _wtOverlays$inlineSta;
|
|
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);
|
|
|
|
// 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);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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(this, _stateManager).getHeaderSettings(headerLevel, visualColumnIndex)) !== null && _classPrivateFieldGet3 !== void 0 ? _classPrivateFieldGet3 : {};
|
|
if (isPlaceholder || isHidden) {
|
|
return '';
|
|
}
|
|
return this.hot.getColHeader(visualColumnIndex, headerLevel);
|
|
}
|
|
|
|
/**
|
|
* Allows to control which header DOM element will be used to highlight.
|
|
*
|
|
* @private
|
|
* @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}
|
|
*/
|
|
onBeforeHighlightingColumnHeader(visualColumn, headerLevel, highlightMeta) {
|
|
const headerNodeData = _classPrivateFieldGet(this, _stateManager).getHeaderTreeNodeData(headerLevel, visualColumn);
|
|
if (!headerNodeData) {
|
|
return visualColumn;
|
|
}
|
|
const {
|
|
classNames,
|
|
columnCursor,
|
|
selectionType,
|
|
selectionWidth
|
|
} = highlightMeta;
|
|
const {
|
|
isRoot,
|
|
colspan
|
|
} = _classPrivateFieldGet(this, _stateManager).getHeaderSettings(headerLevel, visualColumn);
|
|
if (selectionType === HEADER_TYPE) {
|
|
if (!isRoot) {
|
|
return headerNodeData.columnIndex;
|
|
}
|
|
} else if (selectionType === ACTIVE_HEADER_TYPE) {
|
|
if (colspan > selectionWidth - columnCursor || !isRoot) {
|
|
// Reset the class names array so the generated TH element won't be modified.
|
|
classNames.length = 0;
|
|
}
|
|
}
|
|
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.
|
|
*/
|
|
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 _classPrivateFieldGet4;
|
|
const zeroBasedColumnHeaderLevel = rowsCount + row;
|
|
const zeroBasedColumnIndex = column - startCol;
|
|
if (zeroBasedColumnIndex === 0) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
const isRoot = (_classPrivateFieldGet4 = _classPrivateFieldGet(this, _stateManager).getHeaderTreeNodeData(row, column)) === null || _classPrivateFieldGet4 === void 0 ? void 0 : _classPrivateFieldGet4.isRoot;
|
|
if (isRoot === false) {
|
|
data[zeroBasedColumnHeaderLevel][zeroBasedColumnIndex] = '';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allows blocking the column selection that is controlled by the core Selection module.
|
|
*
|
|
* @private
|
|
* @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.
|
|
*/
|
|
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.
|
|
*
|
|
* @private
|
|
* @param {MouseEvent} event Mouse event.
|
|
* @param {CellCoords} coords Cell coords object containing the visual coordinates of the clicked cell.
|
|
*/
|
|
onAfterOnCellMouseDown(event, coords) {
|
|
const headerNodeData = this._getHeaderTreeNodeDataByCoords(coords);
|
|
if (!headerNodeData) {
|
|
return;
|
|
}
|
|
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) {
|
|
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.
|
|
*
|
|
* @private
|
|
* @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.
|
|
*/
|
|
onBeforeOnCellMouseOver(event, coords, TD, controller) {
|
|
if (!this.hot.view.isMouseDown()) {
|
|
return;
|
|
}
|
|
const headerNodeData = this._getHeaderTreeNodeDataByCoords(coords);
|
|
if (!headerNodeData) {
|
|
return;
|
|
}
|
|
const {
|
|
columnIndex,
|
|
origColspan
|
|
} = headerNodeData;
|
|
const selectedRange = this.hot.getSelectedRangeLast();
|
|
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 = [];
|
|
if (coords.col < from.col) {
|
|
columnsToSelect.push(bottomEndCoords.col, columnIndex);
|
|
} else if (coords.col > from.col) {
|
|
columnsToSelect.push(topStartCoords.col, columnIndex + origColspan - 1);
|
|
} else {
|
|
columnsToSelect.push(columnIndex, columnIndex + origColspan - 1);
|
|
}
|
|
this.hot.selectColumns(...columnsToSelect);
|
|
}
|
|
|
|
/**
|
|
* `afterGetColumnHeader` hook callback - prepares the header structure.
|
|
*
|
|
* @private
|
|
* @param {Array} renderersArray Array of renderers.
|
|
*/
|
|
onAfterGetColumnHeaderRenderers(renderersArray) {
|
|
renderersArray.length = 0;
|
|
for (let headerLayer = 0; headerLayer < _classPrivateFieldGet(this, _stateManager).getLayersCount(); headerLayer++) {
|
|
renderersArray.push(this.headerRendererFactory(headerLayer));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make the renderer render the first nested column in its entirety.
|
|
*
|
|
* @private
|
|
* @param {object} calc Viewport column calculator.
|
|
*/
|
|
onAfterViewportColumnCalculatorOverride(calc) {
|
|
const headerLayersCount = _classPrivateFieldGet(this, _stateManager).getLayersCount();
|
|
let newStartColumn = calc.startColumn;
|
|
let nonRenderable = !!headerLayersCount;
|
|
for (let headerLayer = 0; headerLayer < headerLayersCount; headerLayer++) {
|
|
const startColumn = _classPrivateFieldGet(this, _stateManager).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(this, _stateManager).getHeaderTreeNodeData(0, newStartColumn).columnIndex : newStartColumn;
|
|
}
|
|
|
|
/**
|
|
* `modifyColWidth` hook callback - returns width from cache, when is greater than incoming from hook.
|
|
*
|
|
* @private
|
|
* @param {number} width Width from hook.
|
|
* @param {number} column Visual index of an column.
|
|
* @returns {number}
|
|
*/
|
|
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.
|
|
*
|
|
* @private
|
|
* @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.
|
|
*/
|
|
onModifyColumnHeaderValue(value, visualColumnIndex, headerLevel) {
|
|
var _classPrivateFieldGet5;
|
|
const {
|
|
label
|
|
} = (_classPrivateFieldGet5 = _classPrivateFieldGet(this, _stateManager).getHeaderTreeNodeData(headerLevel, visualColumnIndex)) !== null && _classPrivateFieldGet5 !== void 0 ? _classPrivateFieldGet5 : {
|
|
label: ''
|
|
};
|
|
return label;
|
|
}
|
|
|
|
/**
|
|
* Updates the plugin state after HoT initialization.
|
|
*
|
|
* @private
|
|
*/
|
|
onInit() {
|
|
// @TODO: Workaround for broken plugin initialization abstraction.
|
|
this.updatePlugin();
|
|
}
|
|
|
|
/**
|
|
* Updates the plugin state after new dataset load.
|
|
*
|
|
* @private
|
|
* @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.
|
|
*/
|
|
onAfterLoadData(sourceData, initialLoad) {
|
|
if (!initialLoad) {
|
|
this.updatePlugin();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroys the plugin instance.
|
|
*/
|
|
destroy() {
|
|
_classPrivateFieldSet(this, _stateManager, null);
|
|
if (_classPrivateFieldGet(this, _hidingIndexMapObserver) !== null) {
|
|
_classPrivateFieldGet(this, _hidingIndexMapObserver).unsubscribe();
|
|
_classPrivateFieldSet(this, _hidingIndexMapObserver, 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(this, _stateManager).getHeaderTreeNodeData(coords.row, coords.col);
|
|
}
|
|
} |