"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 _eventManager = _interopRequireDefault(require("../../eventManager")); var _event = require("../../helpers/dom/event"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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 _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 _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); } function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; } function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; } 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 _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; } } const PLUGIN_KEY = 'collapsibleColumns'; exports.PLUGIN_KEY = PLUGIN_KEY; const PLUGIN_PRIORITY = 290; exports.PLUGIN_PRIORITY = PLUGIN_PRIORITY; const SETTING_KEYS = ['nestedHeaders']; const COLLAPSIBLE_ELEMENT_CLASS = 'collapsibleIndicator'; 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 * * * // or * * ``` * ::: */ var _collapsedColumnsMap = /*#__PURE__*/new WeakMap(); class CollapsibleColumns extends _base.BasePlugin { constructor() { super(...arguments); /** * Cached reference to the NestedHeaders plugin. * * @private * @type {NestedHeaders} */ _defineProperty(this, "nestedHeadersPlugin", null); /** * Event manager instance reference. * * @private * @type {EventManager} */ _defineProperty(this, "eventManager", new _eventManager.default(this)); /** * 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, { writable: true, value: 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(this, _collapsedColumnsMap, this.hot.columnIndexMapper.createAndRegisterIndexMap(this.pluginName, 'hiding')); this.nestedHeadersPlugin = this.hot.getPlugin('nestedHeaders'); this.headerStateManager = this.nestedHeadersPlugin.getStateManager(); this.addHook('init', () => this.onInit()); this.addHook('afterLoadData', function () { return _this.onAfterLoadData(...arguments); }); this.addHook('afterGetColHeader', function () { return _this.onAfterGetColHeader(...arguments); }); this.addHook('beforeOnCellMouseDown', (event, coords, TD) => this.onBeforeOnCellMouseDown(event, coords, TD)); 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(this, _collapsedColumnsMap, null); this.nestedHeadersPlugin = null; this.clearButtons(); super.disablePlugin(); } /** * 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$headerStateMana; let { row, col: column } = _ref2; const { collapsible, isCollapsed } = (_this$headerStateMana = this.headerStateManager.getHeaderSettings(row, column)) !== null && _this$headerStateMana !== void 0 ? _this$headerStateMana : {}; 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(this, _collapsedColumnsMap).setValueAtIndex(this.hot.toPhysicalColumn(visualColumn), actionTranslator.hideColumn); }); }, true); const isActionPerformed = this.getCollapsedColumns().length !== currentCollapsedColumns.length; this.hot.runHooks(actionTranslator.afterHook, currentCollapsedColumns, destinationCollapsedColumns, isActionPossible, isActionPerformed); this.hot.render(); this.hot.view.adjustElementsSize(true); } /** * Gets an array of physical indexes of collapsed columns. * * @private * @returns {number[]} */ getCollapsedColumns() { return _classPrivateFieldGet(this, _collapsedColumnsMap).getHiddenIndexes(); } /** * Adds the indicator to the headers. * * @private * @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). */ onAfterGetColHeader(column, TH, headerLevel) { var _this$headerStateMana2; const { collapsible, origColspan, isCollapsed } = (_this$headerStateMana2 = this.headerStateManager.getHeaderSettings(headerLevel, column)) !== null && _this$headerStateMana2 !== void 0 ? _this$headerStateMana2 : {}; const isNodeCollapsible = collapsible && origColspan > 1 && column >= this.hot.getSettings().fixedColumnsStart; let collapsibleElement = TH.querySelector(`.${COLLAPSIBLE_ELEMENT_CLASS}`); 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, '+'); } else { (0, _element.addClass)(collapsibleElement, 'expanded'); (0, _element.fastInnerText)(collapsibleElement, '-'); } } else { var _collapsibleElement; (_collapsibleElement = collapsibleElement) === null || _collapsibleElement === void 0 || _collapsibleElement.remove(); } } /** * Indicator mouse event callback. * * @private * @param {object} event Mouse event. * @param {object} coords Event coordinates. */ 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. * * @private */ onInit() { // @TODO: Workaround for broken plugin initialization abstraction (#6806). 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, _collapsedColumnsMap, null); super.destroy(); } } exports.CollapsibleColumns = CollapsibleColumns;