function _check_private_redeclaration(obj, privateCollection) {
    if (privateCollection.has(obj)) {
        throw new TypeError("Cannot initialize the same private elements twice on an object");
    }
}
function _class_apply_descriptor_get(receiver, descriptor) {
    if (descriptor.get) {
        return descriptor.get.call(receiver);
    }
    return descriptor.value;
}
function _class_apply_descriptor_set(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 _class_extract_field_descriptor(receiver, privateMap, action) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to " + action + " private field on non-instance");
    }
    return privateMap.get(receiver);
}
function _class_private_field_get(receiver, privateMap) {
    var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get");
    return _class_apply_descriptor_get(receiver, descriptor);
}
function _class_private_field_init(obj, privateMap, value) {
    _check_private_redeclaration(obj, privateMap);
    privateMap.set(obj, value);
}
function _class_private_field_set(receiver, privateMap, value) {
    var descriptor = _class_extract_field_descriptor(receiver, privateMap, "set");
    _class_apply_descriptor_set(receiver, descriptor, value);
    return value;
}
function _class_private_method_get(receiver, privateSet, fn) {
    if (!privateSet.has(receiver)) {
        throw new TypeError("attempted to get private field on non-instance");
    }
    return fn;
}
function _class_private_method_init(obj, privateSet) {
    _check_private_redeclaration(obj, privateSet);
    privateSet.add(obj);
}
import { BasePlugin } from '../base/index.mjs';
import { throwWithCause } from '../../helpers/errors.mjs';
import { arrayEach, arrayFilter, arrayUnique } from '../../helpers/array.mjs';
import { rangeEach } from '../../helpers/number.mjs';
import { warn } from '../../helpers/console.mjs';
import { addClass, hasClass, removeClass, fastInnerText, removeAttribute, setAttribute } from '../../helpers/dom/element.mjs';
import { stopImmediatePropagation } from '../../helpers/dom/event.mjs';
import { EDITOR_EDIT_GROUP as SHORTCUTS_GROUP_EDITOR } from '../../shortcutContexts/index.mjs';
import { A11Y_EXPANDED, A11Y_HIDDEN } from '../../helpers/a11y.mjs';
export const PLUGIN_KEY = 'collapsibleColumns';
export const 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'
        }
    ]
]);
var /**
   * Map of collapsed columns by the plugin.
   *
   * @private
   * @type {HidingMap|null}
   */ _collapsedColumnsMap = /*#__PURE__*/ new WeakMap(), /**
   * 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).
   */ _onAfterGetColHeader = /*#__PURE__*/ new WeakSet(), /**
   * Indicator mouse event callback.
   *
   * @param {object} event Mouse event.
   * @param {object} coords Event coordinates.
   */ _onBeforeOnCellMouseDown = /*#__PURE__*/ new WeakSet(), /**
   * Updates the plugin state after HoT initialization.
   */ _onInit = /*#__PURE__*/ new WeakSet(), /**
   * 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.
   */ _onAfterLoadData = /*#__PURE__*/ new WeakSet();
/* 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>
 * ```
 * :::
 */ export class CollapsibleColumns extends BasePlugin {
    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() {
        if (this.enabled) {
            return;
        }
        const { nestedHeaders } = this.hot.getSettings();
        if (!nestedHeaders) {
            warn('You need to configure the Nested Headers plugin in order to use collapsible headers.');
        }
        _class_private_field_set(this, _collapsedColumnsMap, this.hot.columnIndexMapper.createAndRegisterIndexMap(this.pluginName, 'hiding'));
        this.nestedHeadersPlugin = this.hot.getPlugin('nestedHeaders');
        this.headerStateManager = this.nestedHeadersPlugin.getStateManager();
        this.addHook('init', ()=>_class_private_method_get(this, _onInit, onInit).call(this));
        this.addHook('afterLoadData', (...args)=>_class_private_method_get(this, _onAfterLoadData, onAfterLoadData).call(this, ...args));
        this.addHook('afterGetColHeader', (...args)=>_class_private_method_get(this, _onAfterGetColHeader, onAfterGetColHeader).call(this, ...args));
        this.addHook('beforeOnCellMouseDown', (event, coords, TD)=>_class_private_method_get(this, _onBeforeOnCellMouseDown, 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);
        _class_private_field_set(this, _collapsedColumnsMap, 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: ()=>{
                const { row, col } = this.hot.getSelectedRangeActive().highlight;
                const { collapsible, isCollapsed, columnIndex } = this.headerStateManager.getHeaderTreeNodeData(row, col) ?? {};
                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: ()=>this.hot.getSelectedRangeActive()?.isSingle() && this.hot.getSelectedRangeActive()?.highlight.isHeader(),
            group: SHORTCUTS_GROUP,
            relativeToGroup: SHORTCUTS_GROUP_EDITOR,
            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);
            }
        };
        rangeEach(0, headerLevels - 1, (i)=>{
            const masterLevel = mainHeaders.childNodes[i];
            const topLevel = topHeaders.childNodes[i];
            const topLeftCornerLevel = topLeftCornerHeaders ? topLeftCornerHeaders.childNodes[i] : null;
            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)) {
            throwWithCause(`Unsupported action is passed (${action}).`);
        }
        if (!Array.isArray(coords)) {
            return;
        }
        // Ignore coordinates which points to the cells range.
        const filteredCoords = arrayFilter(coords, ({ row })=>row < 0);
        let isActionPossible = filteredCoords.length > 0;
        arrayEach(filteredCoords, ({ row, col: column })=>{
            const { collapsible, isCollapsed } = this.headerStateManager.getHeaderSettings(row, column) ?? {};
            if (!collapsible || isCollapsed && action === 'collapse' || !isCollapsed && action === 'expand') {
                isActionPossible = false;
                return false;
            }
        });
        const nodeModRollbacks = [];
        const affectedColumnsIndexes = [];
        if (isActionPossible) {
            arrayEach(filteredCoords, ({ row, col: column })=>{
                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 = arrayUnique([
                ...currentCollapsedColumns,
                ...affectedColumnsIndexes
            ]);
        } else if (action === 'expand') {
            destinationCollapsedColumns = 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).
            arrayEach(nodeModRollbacks, (nodeModRollback)=>{
                nodeModRollback();
            });
            return;
        }
        this.hot.batchExecution(()=>{
            arrayEach(affectedColumnsIndexes, (visualColumn)=>{
                _class_private_field_get(this, _collapsedColumnsMap).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 _class_private_field_get(this, _collapsedColumnsMap).getHiddenIndexes();
    }
    /**
   * Destroys the plugin instance.
   */ destroy() {
        _class_private_field_set(this, _collapsedColumnsMap, null);
        super.destroy();
    }
    constructor(...args){
        super(...args), _class_private_method_init(this, _onAfterGetColHeader), _class_private_method_init(this, _onBeforeOnCellMouseDown), _class_private_method_init(this, _onInit), _class_private_method_init(this, _onAfterLoadData), _class_private_field_init(this, _collapsedColumnsMap, {
            writable: true,
            value: void 0
        }), /**
   * Cached reference to the NestedHeaders plugin.
   *
   * @private
   * @type {NestedHeaders}
   */ this.nestedHeadersPlugin = null, /**
   * The NestedHeaders plugin StateManager instance.
   *
   * @private
   * @type {StateManager}
   */ this.headerStateManager = null, _class_private_field_set(this, _collapsedColumnsMap, null);
    }
}
function onAfterGetColHeader(column, TH, headerLevel) {
    const { collapsible, origColspan, isCollapsed } = this.headerStateManager.getHeaderSettings(headerLevel, column) ?? {};
    const isNodeCollapsible = collapsible && origColspan > 1 && column >= this.hot.getSettings().fixedColumnsStart;
    const isAriaTagsEnabled = this.hot.getSettings().ariaTags;
    let collapsibleElement = TH.querySelector(`.${COLLAPSIBLE_ELEMENT_CLASS}`);
    removeAttribute(TH, [
        A11Y_EXPANDED('')[0]
    ]);
    if (isNodeCollapsible) {
        if (!collapsibleElement) {
            collapsibleElement = this.hot.rootDocument.createElement('div');
            addClass(collapsibleElement, COLLAPSIBLE_ELEMENT_CLASS);
            TH.querySelector('div:first-child').appendChild(collapsibleElement);
        }
        removeClass(collapsibleElement, [
            'collapsed',
            'expanded'
        ]);
        if (isCollapsed) {
            addClass(collapsibleElement, 'collapsed');
            fastInnerText(collapsibleElement, '+');
            // Add ARIA tags
            if (isAriaTagsEnabled) {
                setAttribute(TH, ...A11Y_EXPANDED(false));
            }
        } else {
            addClass(collapsibleElement, 'expanded');
            fastInnerText(collapsibleElement, '-');
            // Add ARIA tags
            if (isAriaTagsEnabled) {
                setAttribute(TH, ...A11Y_EXPANDED(true));
            }
        }
        if (isAriaTagsEnabled) {
            setAttribute(collapsibleElement, ...A11Y_HIDDEN());
        }
    } else {
        collapsibleElement?.remove();
    }
}
function onBeforeOnCellMouseDown(event, coords) {
    if (hasClass(event.target, COLLAPSIBLE_ELEMENT_CLASS)) {
        if (hasClass(event.target, 'expanded')) {
            this.eventManager.fireEvent(event.target, 'mouseup');
            this.toggleCollapsibleSection([
                coords
            ], 'collapse');
        } else if (hasClass(event.target, 'collapsed')) {
            this.eventManager.fireEvent(event.target, 'mouseup');
            this.toggleCollapsibleSection([
                coords
            ], 'expand');
        }
        stopImmediatePropagation(event);
    }
}
function onInit() {
    // @TODO: Workaround for broken plugin initialization abstraction (#6806).
    this.updatePlugin();
}
function onAfterLoadData(sourceData, initialLoad) {
    if (!initialLoad) {
        this.updatePlugin();
    }
}
