UNPKG

handsontable

Version:

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

630 lines (610 loc) • 22.5 kB
"use strict"; exports.__esModule = true; require("core-js/modules/es.error.cause.js"); require("core-js/modules/es.array.push.js"); var _base = require("../base"); var _array = require("../../helpers/array"); var _number = require("../../helpers/number"); var _console = require("../../helpers/console"); var _element = require("../../helpers/dom/element"); var _event = require("../../helpers/dom/event"); var _shortcutContexts = require("../../shortcutContexts"); var _a11y = require("../../helpers/a11y"); 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"); } const PLUGIN_KEY = exports.PLUGIN_KEY = 'collapsibleColumns'; const PLUGIN_PRIORITY = exports.PLUGIN_PRIORITY = 290; const SETTING_KEYS = ['nestedHeaders']; const COLLAPSIBLE_ELEMENT_CLASS = 'collapsibleIndicator'; const SHORTCUTS_GROUP = PLUGIN_KEY; const actionDictionary = new Map([['collapse', { hideColumn: true, beforeHook: 'beforeColumnCollapse', afterHook: 'afterColumnCollapse' }], ['expand', { hideColumn: false, beforeHook: 'beforeColumnExpand', afterHook: 'afterColumnExpand' }]]); /* eslint-disable jsdoc/require-description-complete-sentence */ /** * @plugin CollapsibleColumns * @class CollapsibleColumns * * @description * The _CollapsibleColumns_ plugin allows collapsing of columns, covered by a header with the `colspan` property defined. * * Clicking the "collapse/expand" button collapses (or expands) all "child" headers except the first one. * * Setting the {@link Options#collapsiblecolumns} property to `true` will display a "collapse/expand" button in every header * with a defined `colspan` property. * * To limit this functionality to a smaller group of headers, define the `collapsibleColumns` property as an array * of objects, as in the example below. * * @example * ::: only-for javascript * ```js * const container = document.getElementById('example'); * const hot = new Handsontable(container, { * data: generateDataObj(), * colHeaders: true, * rowHeaders: true, * nestedHeaders: true, * // enable plugin * collapsibleColumns: true, * }); * * // or * const hot = new Handsontable(container, { * data: generateDataObj(), * colHeaders: true, * rowHeaders: true, * nestedHeaders: true, * // enable and configure which columns can be collapsed * collapsibleColumns: [ * {row: -4, col: 1, collapsible: true}, * {row: -3, col: 5, collapsible: true} * ], * }); * ``` * ::: * * ::: only-for react * ```jsx * <HotTable * data={generateDataObj()} * colHeaders={true} * rowHeaders={true} * nestedHeaders={true} * // enable plugin * collapsibleColumns={true} * /> * * // or * <HotTable * data={generateDataObj()} * colHeaders={true} * rowHeaders={true} * nestedHeaders={true} * // enable and configure which columns can be collapsed * collapsibleColumns={[ * {row: -4, col: 1, collapsible: true}, * {row: -3, col: 5, collapsible: true} * ]} * /> * ``` * ::: * * ::: only-for angular * ```ts * // Enable the collapsibleColumns plugin * settings = { * data: generateDataObj(), * colHeaders: true, * rowHeaders: true, * nestedHeaders: true, * // enable plugin * collapsibleColumns: true, * }; * * // Or enable and configure specific collapsible columns * settings = { * data: generateDataObj(), * colHeaders: true, * rowHeaders: true, * nestedHeaders: true, * // enable and configure which columns can be collapsed * collapsibleColumns: [ * { row: -4, col: 1, collapsible: true }, * { row: -3, col: 5, collapsible: true }, * ], * }; * ``` * * ```html * <hot-table [settings]="settings"></hot-table> * ``` * ::: */ var _collapsedColumnsMap = /*#__PURE__*/new WeakMap(); var _CollapsibleColumns_brand = /*#__PURE__*/new WeakSet(); class CollapsibleColumns extends _base.BasePlugin { constructor() { super(...arguments); /** * Adds the indicator to the headers. * * @param {number} column Column index. * @param {HTMLElement} TH TH element. * @param {number} headerLevel The index of header level counting from the top (positive * values counting from 0 to N). */ _classPrivateMethodInitSpec(this, _CollapsibleColumns_brand); /** * Cached reference to the NestedHeaders plugin. * * @private * @type {NestedHeaders} */ _defineProperty(this, "nestedHeadersPlugin", null); /** * The NestedHeaders plugin StateManager instance. * * @private * @type {StateManager} */ _defineProperty(this, "headerStateManager", null); /** * Map of collapsed columns by the plugin. * * @private * @type {HidingMap|null} */ _classPrivateFieldInitSpec(this, _collapsedColumnsMap, null); } static get PLUGIN_KEY() { return PLUGIN_KEY; } static get PLUGIN_PRIORITY() { return PLUGIN_PRIORITY; } static get PLUGIN_DEPS() { return ['plugin:NestedHeaders']; } static get SETTING_KEYS() { return [PLUGIN_KEY, ...SETTING_KEYS]; } /** * Checks if the plugin is enabled in the handsontable settings. This method is executed in {@link Hooks#beforeInit} * hook and if it returns `true` then the {@link CollapsibleColumns#enablePlugin} method is called. * * @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 (!nestedHeaders) { (0, _console.warn)('You need to configure the Nested Headers plugin in order to use collapsible headers.'); } _classPrivateFieldSet(_collapsedColumnsMap, this, this.hot.columnIndexMapper.createAndRegisterIndexMap(this.pluginName, 'hiding')); this.nestedHeadersPlugin = this.hot.getPlugin('nestedHeaders'); this.headerStateManager = this.nestedHeadersPlugin.getStateManager(); this.addHook('init', () => _assertClassBrand(_CollapsibleColumns_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(_CollapsibleColumns_brand, _this, _onAfterLoadData).call(_this, ...args); }); this.addHook('afterGetColHeader', function () { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return _assertClassBrand(_CollapsibleColumns_brand, _this, _onAfterGetColHeader).call(_this, ...args); }); this.addHook('beforeOnCellMouseDown', (event, coords, TD) => _assertClassBrand(_CollapsibleColumns_brand, this, _onBeforeOnCellMouseDown).call(this, event, coords, TD)); this.registerShortcuts(); super.enablePlugin(); // @TODO: Workaround for broken plugin initialization abstraction (#6806). this.updatePlugin(); } /** * Updates the plugin's state. * * This method is executed when [`updateSettings()`](@/api/core.md#updatesettings) is invoked with any of the following configuration options: * - [`collapsibleColumns`](@/api/options.md#collapsiblecolumns) * - [`nestedHeaders`](@/api/options.md#nestedheaders) */ updatePlugin() { // @TODO: Workaround for broken plugin initialization abstraction (#6806). if (!this.hot.view) { return; } if (!this.nestedHeadersPlugin.detectedOverlappedHeaders) { const { collapsibleColumns } = this.hot.getSettings(); if (typeof collapsibleColumns === 'boolean') { // Add `collapsible: true` attribute to all headers with colspan higher than 1. this.headerStateManager.mapState(headerSettings => { return { collapsible: headerSettings.origColspan > 1 }; }); } else if (Array.isArray(collapsibleColumns)) { this.headerStateManager.mapState(() => { return { collapsible: false }; }); this.headerStateManager.mergeStateWith(collapsibleColumns); } } super.updatePlugin(); } /** * Disables the plugin functionality for this Handsontable instance. */ disablePlugin() { this.hot.columnIndexMapper.unregisterMap(this.pluginName); _classPrivateFieldSet(_collapsedColumnsMap, this, null); this.nestedHeadersPlugin = null; this.unregisterShortcuts(); this.clearButtons(); super.disablePlugin(); } /** * Register shortcuts responsible for toggling collapsible columns. * * @private */ registerShortcuts() { this.hot.getShortcutManager().getContext('grid').addShortcut({ keys: [['Enter']], callback: () => { var _this$headerStateMana; const { row, col } = this.hot.getSelectedRangeActive().highlight; const { collapsible, isCollapsed, columnIndex } = (_this$headerStateMana = this.headerStateManager.getHeaderTreeNodeData(row, col)) !== null && _this$headerStateMana !== void 0 ? _this$headerStateMana : {}; if (!collapsible) { return; } if (isCollapsed) { this.expandSection({ row, col: columnIndex }); } else { this.collapseSection({ row, col: columnIndex }); } // prevent default Enter behavior (move to the next row within a selection range) return false; }, runOnlyIf: () => { var _this$hot$getSelected, _this$hot$getSelected2; return ((_this$hot$getSelected = this.hot.getSelectedRangeActive()) === null || _this$hot$getSelected === void 0 ? void 0 : _this$hot$getSelected.isSingle()) && ((_this$hot$getSelected2 = this.hot.getSelectedRangeActive()) === null || _this$hot$getSelected2 === void 0 ? void 0 : _this$hot$getSelected2.highlight.isHeader()); }, group: SHORTCUTS_GROUP, relativeToGroup: _shortcutContexts.EDITOR_EDIT_GROUP, position: 'before' }); } /** * Unregister shortcuts responsible for toggling collapsible columns. * * @private */ unregisterShortcuts() { this.hot.getShortcutManager().getContext('grid').removeShortcutsByGroup(SHORTCUTS_GROUP); } /** * Clears the expand/collapse buttons. * * @private */ clearButtons() { if (!this.hot.view) { return; } const headerLevels = this.hot.view._wt.getSetting('columnHeaders').length; const mainHeaders = this.hot.view._wt.wtTable.THEAD; const topHeaders = this.hot.view._wt.wtOverlays.topOverlay.clone.wtTable.THEAD; const topLeftCornerHeaders = this.hot.view._wt.wtOverlays.topInlineStartCornerOverlay ? this.hot.view._wt.wtOverlays.topInlineStartCornerOverlay.clone.wtTable.THEAD : null; const removeButton = function (button) { if (button) { button.parentNode.removeChild(button); } }; (0, _number.rangeEach)(0, headerLevels - 1, i => { const masterLevel = mainHeaders.childNodes[i]; const topLevel = topHeaders.childNodes[i]; const topLeftCornerLevel = topLeftCornerHeaders ? topLeftCornerHeaders.childNodes[i] : null; (0, _number.rangeEach)(0, masterLevel.childNodes.length - 1, j => { let button = masterLevel.childNodes[j].querySelector(`.${COLLAPSIBLE_ELEMENT_CLASS}`); removeButton(button); if (topLevel && topLevel.childNodes[j]) { button = topLevel.childNodes[j].querySelector(`.${COLLAPSIBLE_ELEMENT_CLASS}`); removeButton(button); } if (topLeftCornerHeaders && topLeftCornerLevel && topLeftCornerLevel.childNodes[j]) { button = topLeftCornerLevel.childNodes[j].querySelector(`.${COLLAPSIBLE_ELEMENT_CLASS}`); removeButton(button); } }); }, true); } /** * Expands section at the provided coords. * * @param {object} coords Contains coordinates information. (`coords.row`, `coords.col`). */ expandSection(coords) { this.toggleCollapsibleSection([coords], 'expand'); } /** * Collapses section at the provided coords. * * @param {object} coords Contains coordinates information. (`coords.row`, `coords.col`). */ collapseSection(coords) { this.toggleCollapsibleSection([coords], 'collapse'); } /** * Collapses or expand all collapsible sections, depending on the action parameter. * * @param {string} action 'collapse' or 'expand'. */ toggleAllCollapsibleSections(action) { const coords = this.headerStateManager.mapNodes(headerSettings => { const { collapsible, origColspan, headerLevel, columnIndex, isCollapsed } = headerSettings; if (collapsible === true && origColspan > 1 && (isCollapsed && action === 'expand' || !isCollapsed && action === 'collapse')) { return { row: this.headerStateManager.levelToRowCoords(headerLevel), col: columnIndex }; } }); this.toggleCollapsibleSection(coords, action); } /** * Collapses all collapsible sections. */ collapseAll() { this.toggleAllCollapsibleSections('collapse'); } /** * Expands all collapsible sections. */ expandAll() { this.toggleAllCollapsibleSections('expand'); } /** * Collapses/Expands a section. * * @param {Array} coords Array of coords - section coordinates. * @param {string} [action] Action definition ('collapse' or 'expand'). * @fires Hooks#beforeColumnCollapse * @fires Hooks#beforeColumnExpand * @fires Hooks#afterColumnCollapse * @fires Hooks#afterColumnExpand */ toggleCollapsibleSection(coords, action) { if (!actionDictionary.has(action)) { throw new Error(`Unsupported action is passed (${action}).`); } if (!Array.isArray(coords)) { return; } // Ignore coordinates which points to the cells range. const filteredCoords = (0, _array.arrayFilter)(coords, _ref => { let { row } = _ref; return row < 0; }); let isActionPossible = filteredCoords.length > 0; (0, _array.arrayEach)(filteredCoords, _ref2 => { var _this$headerStateMana2; let { row, col: column } = _ref2; const { collapsible, isCollapsed } = (_this$headerStateMana2 = this.headerStateManager.getHeaderSettings(row, column)) !== null && _this$headerStateMana2 !== void 0 ? _this$headerStateMana2 : {}; if (!collapsible || isCollapsed && action === 'collapse' || !isCollapsed && action === 'expand') { isActionPossible = false; return false; } }); const nodeModRollbacks = []; const affectedColumnsIndexes = []; if (isActionPossible) { (0, _array.arrayEach)(filteredCoords, _ref3 => { let { row, col: column } = _ref3; const { colspanCompensation, affectedColumns, rollbackModification } = this.headerStateManager.triggerNodeModification(action, row, column); if (colspanCompensation > 0) { affectedColumnsIndexes.push(...affectedColumns); nodeModRollbacks.push(rollbackModification); } }); } const currentCollapsedColumns = this.getCollapsedColumns(); let destinationCollapsedColumns = []; if (action === 'collapse') { destinationCollapsedColumns = (0, _array.arrayUnique)([...currentCollapsedColumns, ...affectedColumnsIndexes]); } else if (action === 'expand') { destinationCollapsedColumns = (0, _array.arrayFilter)(currentCollapsedColumns, index => !affectedColumnsIndexes.includes(index)); } const actionTranslator = actionDictionary.get(action); const isActionAllowed = this.hot.runHooks(actionTranslator.beforeHook, currentCollapsedColumns, destinationCollapsedColumns, isActionPossible); if (isActionAllowed === false) { // Rollback all header nodes modification (collapse or expand). (0, _array.arrayEach)(nodeModRollbacks, nodeModRollback => { nodeModRollback(); }); return; } this.hot.batchExecution(() => { (0, _array.arrayEach)(affectedColumnsIndexes, visualColumn => { _classPrivateFieldGet(_collapsedColumnsMap, this).setValueAtIndex(this.hot.toPhysicalColumn(visualColumn), actionTranslator.hideColumn); }); }, true); const isActionPerformed = this.getCollapsedColumns().length !== currentCollapsedColumns.length; const selectionRange = this.hot.getSelectedRangeActive(); if (action === 'collapse' && isActionPerformed && selectionRange) { const { row, col } = selectionRange.highlight; const isHidden = this.hot.rowIndexMapper.isHidden(row) || this.hot.columnIndexMapper.isHidden(col); if (isHidden && affectedColumnsIndexes.includes(col)) { const nextRow = row >= 0 ? this.hot.rowIndexMapper.getNearestNotHiddenIndex(row, 1, true) : row; const nextColumn = col >= 0 ? this.hot.columnIndexMapper.getNearestNotHiddenIndex(col, 1, true) : col; if (nextRow !== null && nextColumn !== null) { this.hot.selectCell(nextRow, nextColumn); } } } this.hot.runHooks(actionTranslator.afterHook, currentCollapsedColumns, destinationCollapsedColumns, isActionPossible, isActionPerformed); this.hot.view.adjustElementsSize(); this.hot.render(); } /** * Gets an array of physical indexes of collapsed columns. * * @private * @returns {number[]} */ getCollapsedColumns() { return _classPrivateFieldGet(_collapsedColumnsMap, this).getHiddenIndexes(); } /** * Destroys the plugin instance. */ destroy() { _classPrivateFieldSet(_collapsedColumnsMap, this, null); super.destroy(); } } exports.CollapsibleColumns = CollapsibleColumns; function _onAfterGetColHeader(column, TH, headerLevel) { var _this$headerStateMana3; const { collapsible, origColspan, isCollapsed } = (_this$headerStateMana3 = this.headerStateManager.getHeaderSettings(headerLevel, column)) !== null && _this$headerStateMana3 !== void 0 ? _this$headerStateMana3 : {}; const isNodeCollapsible = collapsible && origColspan > 1 && column >= this.hot.getSettings().fixedColumnsStart; const isAriaTagsEnabled = this.hot.getSettings().ariaTags; let collapsibleElement = TH.querySelector(`.${COLLAPSIBLE_ELEMENT_CLASS}`); (0, _element.removeAttribute)(TH, [(0, _a11y.A11Y_EXPANDED)('')[0]]); if (isNodeCollapsible) { if (!collapsibleElement) { collapsibleElement = this.hot.rootDocument.createElement('div'); (0, _element.addClass)(collapsibleElement, COLLAPSIBLE_ELEMENT_CLASS); TH.querySelector('div:first-child').appendChild(collapsibleElement); } (0, _element.removeClass)(collapsibleElement, ['collapsed', 'expanded']); if (isCollapsed) { (0, _element.addClass)(collapsibleElement, 'collapsed'); (0, _element.fastInnerText)(collapsibleElement, '+'); // Add ARIA tags if (isAriaTagsEnabled) { (0, _element.setAttribute)(TH, ...(0, _a11y.A11Y_EXPANDED)(false)); } } else { (0, _element.addClass)(collapsibleElement, 'expanded'); (0, _element.fastInnerText)(collapsibleElement, '-'); // Add ARIA tags if (isAriaTagsEnabled) { (0, _element.setAttribute)(TH, ...(0, _a11y.A11Y_EXPANDED)(true)); } } if (isAriaTagsEnabled) { (0, _element.setAttribute)(collapsibleElement, ...(0, _a11y.A11Y_HIDDEN)()); } } else { var _collapsibleElement; (_collapsibleElement = collapsibleElement) === null || _collapsibleElement === void 0 || _collapsibleElement.remove(); } } /** * Indicator mouse event callback. * * @param {object} event Mouse event. * @param {object} coords Event coordinates. */ function _onBeforeOnCellMouseDown(event, coords) { if ((0, _element.hasClass)(event.target, COLLAPSIBLE_ELEMENT_CLASS)) { if ((0, _element.hasClass)(event.target, 'expanded')) { this.eventManager.fireEvent(event.target, 'mouseup'); this.toggleCollapsibleSection([coords], 'collapse'); } else if ((0, _element.hasClass)(event.target, 'collapsed')) { this.eventManager.fireEvent(event.target, 'mouseup'); this.toggleCollapsibleSection([coords], 'expand'); } (0, _event.stopImmediatePropagation)(event); } } /** * Updates the plugin state after HoT initialization. */ function _onInit() { // @TODO: Workaround for broken plugin initialization abstraction (#6806). 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(); } }