"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
function _export(target, all) {
    for(var name in all)Object.defineProperty(target, name, {
        enumerable: true,
        get: Object.getOwnPropertyDescriptor(all, name).get
    });
}
_export(exports, {
    get DropdownMenu () {
        return DropdownMenu;
    },
    get PLUGIN_KEY () {
        return PLUGIN_KEY;
    },
    get PLUGIN_PRIORITY () {
        return PLUGIN_PRIORITY;
    }
});
const _base = require("../base");
const _array = require("../../helpers/array");
const _object = require("../../helpers/object");
const _commandExecutor = require("../contextMenu/commandExecutor");
const _utils = require("../contextMenu/utils");
const _element = require("../../helpers/dom/element");
const _itemsFactory = require("../contextMenu/itemsFactory");
const _menu = require("../contextMenu/menu");
const _hooks = require("../../core/hooks");
const _predefinedItems = require("../contextMenu/predefinedItems");
const _a11y = require("../../helpers/a11y");
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);
}
_hooks.Hooks.getSingleton().register('afterDropdownMenuDefaultOptions');
_hooks.Hooks.getSingleton().register('beforeDropdownMenuShow');
_hooks.Hooks.getSingleton().register('afterDropdownMenuShow');
_hooks.Hooks.getSingleton().register('afterDropdownMenuHide');
_hooks.Hooks.getSingleton().register('afterDropdownMenuExecute');
const PLUGIN_KEY = 'dropdownMenu';
const PLUGIN_PRIORITY = 230;
const BUTTON_CLASS_NAME = 'changeType';
const SHORTCUTS_GROUP = PLUGIN_KEY;
var /**
   * Flag which determines if the button that opens the menu was clicked.
   *
   * @type {boolean}
   */ _isButtonClicked = /*#__PURE__*/ new WeakMap(), /**
   * Add custom shortcuts to the provided menu instance.
   *
   * @param {Menu} menuInstance The menu instance.
   */ _addCustomShortcuts = /*#__PURE__*/ new WeakSet(), /**
   * Table click listener.
   *
   * @private
   * @param {Event} event The mouse event object.
   */ _onTableClick = /*#__PURE__*/ new WeakSet(), /**
   * Returns the bounding rect of the button's ignoring the hit area box.
   *
   * @param {HTMLElement} button The change-type button element.
   * @returns {{ top: number, left: number, right: number, bottom: number, width: number, height: number }}
   */ _getButtonRect = /*#__PURE__*/ new WeakSet(), /**
   * On after get column header listener.
   *
   * @private
   * @param {number} col Visual column index.
   * @param {HTMLTableCellElement} TH Header's TH element.
   */ _onAfterGetColHeader = /*#__PURE__*/ new WeakSet(), /**
   * On menu before open listener.
   *
   * @private
   * @fires Hooks#beforeDropdownMenuShow
   */ _onMenuBeforeOpen = /*#__PURE__*/ new WeakSet(), /**
   * On menu after open listener.
   *
   * @private
   * @fires Hooks#afterDropdownMenuShow
   */ _onMenuAfterOpen = /*#__PURE__*/ new WeakSet(), /**
   * Listener for the `afterSubmenuOpen` hook.
   *
   * @private
   * @param {Menu} subMenuInstance The opened sub menu instance.
   */ _onSubMenuAfterOpen = /*#__PURE__*/ new WeakSet(), /**
   * On menu after close listener.
   *
   * @private
   * @fires Hooks#afterDropdownMenuHide
   */ _onMenuAfterClose = /*#__PURE__*/ new WeakSet(), /**
   * Hook allows blocking horizontal scroll when the menu is opened by clicking on
   * the column header button. This prevents from scrolling the viewport (jump effect) when
   * the button is clicked.
   *
   * @param {number} visualColumn Visual column index.
   * @returns {number | null}
   */ _onBeforeViewportScrollHorizontally = /*#__PURE__*/ new WeakSet(), /**
   * Hook sets the internal flag to `true` when the button is clicked.
   *
   * @param {MouseEvent} event The mouse event object.
   */ _onBeforeOnCellMouseDown = /*#__PURE__*/ new WeakSet();
class DropdownMenu extends _base.BasePlugin {
    static get PLUGIN_KEY() {
        return PLUGIN_KEY;
    }
    static get PLUGIN_PRIORITY() {
        return PLUGIN_PRIORITY;
    }
    static get PLUGIN_DEPS() {
        return [
            'plugin:AutoColumnSize'
        ];
    }
    /**
   * Default menu items order when `dropdownMenu` is enabled by setting the config item to `true`.
   *
   * @returns {Array}
   */ static get DEFAULT_ITEMS() {
        return [
            _predefinedItems.COLUMN_LEFT,
            _predefinedItems.COLUMN_RIGHT,
            _predefinedItems.SEPARATOR,
            _predefinedItems.REMOVE_COLUMN,
            _predefinedItems.SEPARATOR,
            _predefinedItems.CLEAR_COLUMN,
            _predefinedItems.SEPARATOR,
            _predefinedItems.READ_ONLY,
            _predefinedItems.SEPARATOR,
            _predefinedItems.ALIGNMENT
        ];
    }
    /**
   * 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 DropdownMenu#enablePlugin} method is called.
   *
   * @returns {boolean}
   */ isEnabled() {
        return this.hot.getSettings()[PLUGIN_KEY];
    }
    /**
   * Enables the plugin functionality for this Handsontable instance.
   *
   * @fires Hooks#afterDropdownMenuDefaultOptions
   * @fires Hooks#beforeDropdownMenuSetItems
   */ enablePlugin() {
        if (this.enabled) {
            return;
        }
        this.itemsFactory = new _itemsFactory.ItemsFactory(this.hot, DropdownMenu.DEFAULT_ITEMS);
        this.addHook('beforeOnCellMouseDown', (...args)=>_class_private_method_get(this, _onBeforeOnCellMouseDown, onBeforeOnCellMouseDown).call(this, ...args));
        this.addHook('beforeViewportScrollHorizontally', (...args)=>_class_private_method_get(this, _onBeforeViewportScrollHorizontally, onBeforeViewportScrollHorizontally).call(this, ...args));
        this.addHook('beforeDialogShow', ()=>this.close());
        const settings = this.hot.getSettings()[PLUGIN_KEY];
        const predefinedItems = {
            items: this.itemsFactory.getItems(settings)
        };
        this.registerEvents();
        if (typeof settings.callback === 'function') {
            this.commandExecutor.setCommonCallback(settings.callback);
        }
        this.registerShortcuts();
        super.enablePlugin();
        this.callOnPluginsReady(()=>{
            this.hot.runHooks('afterDropdownMenuDefaultOptions', predefinedItems);
            this.itemsFactory.setPredefinedItems(predefinedItems.items);
            const menuItems = this.itemsFactory.getItems(settings);
            if (this.menu) {
                this.menu.destroy();
            }
            this.menu = new _menu.Menu(this.hot, {
                className: 'htDropdownMenu',
                keepInViewport: true,
                container: settings.uiContainer || this.hot.rootPortalElement
            });
            this.hot.runHooks('beforeDropdownMenuSetItems', menuItems);
            this.menu.setMenuItems(menuItems);
            this.menu.addLocalHook('beforeOpen', ()=>_class_private_method_get(this, _onMenuBeforeOpen, onMenuBeforeOpen).call(this));
            this.menu.addLocalHook('afterOpen', ()=>_class_private_method_get(this, _onMenuAfterOpen, onMenuAfterOpen).call(this));
            this.menu.addLocalHook('afterSubmenuOpen', (subMenuInstance)=>_class_private_method_get(this, _onSubMenuAfterOpen, onSubMenuAfterOpen).call(this, subMenuInstance));
            this.menu.addLocalHook('afterClose', ()=>_class_private_method_get(this, _onMenuAfterClose, onMenuAfterClose).call(this));
            this.menu.addLocalHook('executeCommand', (...params)=>this.executeCommand.call(this, ...params));
            // Register all commands. Predefined and added by user or by plugins
            (0, _array.arrayEach)(menuItems, (command)=>this.commandExecutor.registerCommand(command.key, command));
        });
    }
    /**
   * Updates the plugin's state.
   *
   * This method is executed when [`updateSettings()`](@/api/core.md#updatesettings) is invoked with any of the following configuration options:
   *  - [`dropdownMenu`](@/api/options.md#dropdownmenu)
   */ updatePlugin() {
        this.disablePlugin();
        this.enablePlugin();
        super.updatePlugin();
    }
    /**
   * Disables the plugin functionality for this Handsontable instance.
   */ disablePlugin() {
        this.close();
        if (this.menu) {
            this.menu.destroy();
        }
        this.unregisterShortcuts();
        super.disablePlugin();
    }
    /**
   * Register shortcuts responsible for toggling dropdown menu.
   *
   * @private
   */ registerShortcuts() {
        const gridContext = this.hot.getShortcutManager().getContext('grid');
        const callback = ()=>{
            const { highlight } = this.hot.getSelectedRangeActive();
            const highlightedHeaderElement = highlight.isHeader() ? this.hot.getCell(highlight.row, highlight.col, true) : null;
            const isBottomMostHeaderSelected = highlight.isHeader() && highlight.col >= 0 && (0, _element.isBottomMostColumnHeader)(highlightedHeaderElement);
            const isHiddenNestedPlaceholderSelected = highlight.isHeader() && highlight.col >= 0 && highlightedHeaderElement && (0, _element.hasClass)(highlightedHeaderElement, 'hiddenHeader');
            const isValidSelection = isBottomMostHeaderSelected || isHiddenNestedPlaceholderSelected || highlight.isCell();
            if (isValidSelection && highlight.col >= 0) {
                let headerRow = isBottomMostHeaderSelected || isHiddenNestedPlaceholderSelected ? highlight.row : -1;
                this.hot.selectColumns(highlight.col, highlight.col, headerRow);
                const { from } = this.hot.getSelectedRangeActive();
                const offset = (0, _utils.getDocumentOffsetByElement)(this.menu.container, this.hot.rootDocument);
                let target = this.hot.getCell(headerRow, from.col, true)?.querySelector(`.${BUTTON_CLASS_NAME}`);
                if (!target) {
                    for(let row = -this.hot.view.getColumnHeadersCount(); row <= -1; row++){
                        const candidate = this.hot.getCell(row, from.col, true)?.querySelector(`.${BUTTON_CLASS_NAME}`);
                        if (candidate) {
                            target = candidate;
                            headerRow = row;
                            this.hot.selectColumns(highlight.col, highlight.col, headerRow);
                            break;
                        }
                    }
                }
                if (!target && isHiddenNestedPlaceholderSelected) {
                    for(let column = from.col - 1; column >= 0; column--){
                        const candidateHeader = this.hot.getCell(headerRow, column, true);
                        if (!candidateHeader || (0, _element.hasClass)(candidateHeader, 'hiddenHeader')) {
                            continue; // eslint-disable-line no-continue
                        }
                        const candidateButton = candidateHeader?.querySelector(`.${BUTTON_CLASS_NAME}`);
                        if (candidateButton) {
                            target = candidateButton;
                            this.hot.selectColumns(column, column, headerRow);
                        }
                        break;
                    }
                }
                if (!target) {
                    return;
                }
                const buttonRect = _class_private_method_get(this, _getButtonRect, getButtonRect).call(this, target);
                this.open({
                    left: buttonRect.left + offset.left,
                    top: buttonRect.bottom + offset.top
                }, {
                    left: buttonRect.width,
                    right: 0,
                    above: 0,
                    below: 3
                });
                // Make sure the first item is selected (role=menuitem). Otherwise, screen readers
                // will block the Esc key for the whole menu.
                this.menu.getNavigator().toFirstItem();
            }
        };
        gridContext.addShortcuts([
            {
                keys: [
                    [
                        'Shift',
                        'Alt',
                        'ArrowDown'
                    ],
                    [
                        'Control/Meta',
                        'Enter'
                    ]
                ],
                callback,
                runOnlyIf: ()=>{
                    const highlight = this.hot.getSelectedRangeActive()?.highlight;
                    const highlightedHeaderElement = highlight?.isHeader() ? this.hot.getCell(highlight.row, highlight.col, true) : null;
                    const isHiddenNestedPlaceholderSelected = highlight?.isHeader() && highlight.col >= 0 && highlightedHeaderElement && (0, _element.hasClass)(highlightedHeaderElement, 'hiddenHeader');
                    return highlight && !this.menu.isOpened() && highlight.isHeader() && (this.hot.selection.isCellVisible(highlight) || isHiddenNestedPlaceholderSelected);
                },
                captureCtrl: true,
                group: SHORTCUTS_GROUP
            },
            {
                keys: [
                    [
                        'Shift',
                        'Alt',
                        'ArrowDown'
                    ]
                ],
                callback,
                runOnlyIf: ()=>{
                    const highlight = this.hot.getSelectedRangeActive()?.highlight;
                    return highlight && this.hot.selection.isCellVisible(highlight) && highlight.isCell() && !this.menu.isOpened();
                },
                group: SHORTCUTS_GROUP
            }
        ]);
    }
    /**
   * Unregister shortcuts responsible for toggling dropdown menu.
   *
   * @private
   */ unregisterShortcuts() {
        this.hot.getShortcutManager().getContext('grid').removeShortcutsByGroup(SHORTCUTS_GROUP);
    }
    /**
   * Registers the DOM listeners.
   *
   * @private
   */ registerEvents() {
        this.eventManager.addEventListener(this.hot.rootElement, 'click', (event)=>_class_private_method_get(this, _onTableClick, onTableClick).call(this, event));
    }
    /**
   * Opens menu and re-position it based on the passed coordinates.
   *
   * @param {{ top: number, left: number }|Event} position An object with `top` and `left` properties
   * which contains coordinates relative to the browsers viewport (without included scroll offsets).
   * Or if the native event is passed the menu will be positioned based on the `pageX` and `pageY`
   * coordinates.
   * @param {{ above: number, below: number, left: number, right: number }} offset An object allows applying
   * the offset to the menu position.
   * @fires Hooks#beforeDropdownMenuShow
   * @fires Hooks#afterDropdownMenuShow
   * @example
   * ```js
   * const menu = hot.getPlugin('dropdownMenu');
   *
   * hot.selectCell(0, 0);
   * menu.open({ top: 50, left: 50 });
   * ```
   */ open(position, offset = {
        above: 0,
        below: 0,
        left: 0,
        right: 0
    }) {
        if (this.menu?.isOpened()) {
            return;
        }
        this.menu.open();
        (0, _object.objectEach)(offset, (value, key)=>{
            this.menu.setOffset(key, value);
        });
        this.menu.setPosition(position);
    }
    /**
   * Closes dropdown menu.
   */ close() {
        this.menu?.close();
    }
    /**
   * Executes context menu command.
   *
   * The `executeCommand()` method works only for selected cells.
   *
   * When no cells are selected, `executeCommand()` doesn't do anything.
   *
   * You can execute all predefined commands:
   *  * `'col_left'` - Insert column left
   *  * `'col_right'` - Insert column right
   *  * `'clear_column'` - Clear selected column
   *  * `'remove_col'` - Remove column
   *  * `'undo'` - Undo last action
   *  * `'redo'` - Redo last action
   *  * `'make_read_only'` - Make cell read only
   *  * `'alignment:left'` - Alignment to the left
   *  * `'alignment:top'` - Alignment to the top
   *  * `'alignment:right'` - Alignment to the right
   *  * `'alignment:bottom'` - Alignment to the bottom
   *  * `'alignment:middle'` - Alignment to the middle
   *  * `'alignment:center'` - Alignment to the center (justify).
   *
   * Or you can execute command registered in settings where `key` is your command name.
   *
   * @param {string} commandName Command name to execute.
   * @param {*} params Additional parameters passed to the command executor.
   */ executeCommand(commandName, ...params) {
        this.commandExecutor.execute(commandName, ...params);
    }
    /**
   * Turns on / off listening on dropdown menu.
   *
   * @private
   * @param {boolean} listen Turn on listening when value is set to true, otherwise turn it off.
   */ setListening(listen = true) {
        if (this.menu.isOpened()) {
            if (listen) {
                this.menu.hotMenu.listen();
            } else {
                this.menu.hotMenu.unlisten();
            }
        }
    }
    /**
   * Destroys the plugin instance.
   */ destroy() {
        this.close();
        if (this.menu) {
            this.menu.destroy();
        }
        super.destroy();
    }
    constructor(hotInstance){
        super(hotInstance), _class_private_method_init(this, _addCustomShortcuts), _class_private_method_init(this, _onTableClick), _class_private_method_init(this, _getButtonRect), _class_private_method_init(this, _onAfterGetColHeader), _class_private_method_init(this, _onMenuBeforeOpen), _class_private_method_init(this, _onMenuAfterOpen), _class_private_method_init(this, _onSubMenuAfterOpen), _class_private_method_init(this, _onMenuAfterClose), _class_private_method_init(this, _onBeforeViewportScrollHorizontally), _class_private_method_init(this, _onBeforeOnCellMouseDown), _class_private_field_init(this, _isButtonClicked, {
            writable: true,
            value: void 0
        }), /**
   * Instance of {@link CommandExecutor}.
   *
   * @private
   * @type {CommandExecutor}
   */ this.commandExecutor = new _commandExecutor.CommandExecutor(this.hot), /**
   * Instance of {@link ItemsFactory}.
   *
   * @private
   * @type {ItemsFactory}
   */ this.itemsFactory = null, /**
   * Instance of {@link Menu}.
   *
   * @private
   * @type {Menu}
   */ this.menu = null, _class_private_field_set(this, _isButtonClicked, false);
        // One listener for enable/disable functionality
        this.hot.addHook('afterGetColHeader', (col, TH)=>_class_private_method_get(this, _onAfterGetColHeader, onAfterGetColHeader).call(this, col, TH));
    }
}
function addCustomShortcuts(menuInstance) {
    menuInstance.getKeyboardShortcutsCtrl().addCustomShortcuts([
        {
            keys: [
                [
                    'Control/Meta',
                    'A'
                ]
            ],
            callback: ()=>false
        }
    ]);
}
function onTableClick(event) {
    if ((0, _element.hasClass)(event.target, BUTTON_CLASS_NAME)) {
        const offset = (0, _utils.getDocumentOffsetByElement)(this.menu.container, this.hot.rootDocument);
        const buttonRect = _class_private_method_get(this, _getButtonRect, getButtonRect).call(this, event.target);
        event.stopPropagation();
        _class_private_field_set(this, _isButtonClicked, false);
        this.open({
            left: buttonRect.left + offset.left,
            top: buttonRect.bottom + offset.top
        }, {
            left: buttonRect.width,
            right: 0,
            above: 0,
            below: 3
        });
    }
}
function getButtonRect(button) {
    const rect = button.getBoundingClientRect();
    const beforeStyle = this.hot.rootWindow.getComputedStyle(button, '::before');
    const iconSize = Number.parseFloat(beforeStyle.width);
    if (Number.isFinite(iconSize) && rect.width >= iconSize && rect.height >= iconSize) {
        const left = rect.left + (rect.width - iconSize) / 2;
        const top = rect.top + (rect.height - iconSize) / 2;
        return {
            top,
            left,
            right: left + iconSize,
            bottom: top + iconSize,
            width: iconSize,
            height: iconSize
        };
    }
    return {
        top: rect.top,
        left: rect.left,
        right: rect.right,
        bottom: rect.bottom,
        width: rect.width,
        height: rect.height
    };
}
function onAfterGetColHeader(col, TH) {
    if (col < 0 || !(0, _element.isBottomMostColumnHeader)(TH)) {
        return;
    }
    const existingButton = TH.querySelector(`.${BUTTON_CLASS_NAME}`);
    // Plugin enabled and buttons already exists, return.
    if (this.enabled && existingButton) {
        return;
    }
    // Plugin disabled and buttons still exists, so remove them.
    if (!this.enabled) {
        if (existingButton) {
            existingButton.parentNode.removeChild(existingButton);
        }
        return;
    }
    const button = this.hot.rootDocument.createElement('button');
    button.className = BUTTON_CLASS_NAME;
    button.type = 'button';
    button.tabIndex = -1;
    if (this.hot.getSettings().ariaTags) {
        (0, _element.setAttribute)(button, [
            (0, _a11y.A11Y_HIDDEN)(),
            (0, _a11y.A11Y_LABEL)(' ')
        ]);
        (0, _element.setAttribute)(TH, [
            (0, _a11y.A11Y_HASPOPUP)('menu')
        ]);
    }
    // prevent page reload on button click
    button.onclick = function() {
        return false;
    };
    const relativeContainer = TH.firstChild;
    if (!relativeContainer) {
        return;
    }
    const colHeaderSpan = relativeContainer.querySelector('.colHeader');
    if (colHeaderSpan) {
        relativeContainer.insertBefore(button, colHeaderSpan.nextSibling);
    } else {
        relativeContainer.appendChild(button);
    }
}
function onMenuBeforeOpen() {
    this.hot.runHooks('beforeDropdownMenuShow', this);
}
function onMenuAfterOpen() {
    this.hot.runHooks('afterDropdownMenuShow', this);
    _class_private_method_get(this, _addCustomShortcuts, addCustomShortcuts).call(this, this.menu);
}
function onSubMenuAfterOpen(subMenuInstance) {
    _class_private_method_get(this, _addCustomShortcuts, addCustomShortcuts).call(this, subMenuInstance);
}
function onMenuAfterClose() {
    this.hot.listen();
    this.hot.runHooks('afterDropdownMenuHide', this);
}
function onBeforeViewportScrollHorizontally(visualColumn) {
    return _class_private_field_get(this, _isButtonClicked) ? null : visualColumn;
}
function onBeforeOnCellMouseDown(event) {
    if ((0, _element.hasClass)(event.target, BUTTON_CLASS_NAME)) {
        _class_private_field_set(this, _isButtonClicked, true);
    }
}
DropdownMenu.SEPARATOR = {
    name: _predefinedItems.SEPARATOR
};
