import { BindingEventService } from '@slickgrid-universal/binding';
import { calculateAvailableSpace, classNameToList, createDomElement, emptyElement, getOffset, getOffsetRelativeToParent, isDefined, titleCase, } from '@slickgrid-universal/utils';
import { SlickEventHandler } from '../core/index.js';
import { applyHtmlToElement } from '../core/utils.js';
import { wireMenuKeyboardNavigation } from './keyboardNavigation.js';
export class MenuBaseClass {
    /** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */
    constructor(extensionUtility, pubSubService, sharedService) {
        this.extensionUtility = extensionUtility;
        this.pubSubService = pubSubService;
        this.sharedService = sharedService;
        this._addonOptions = {};
        this._areVisibleColumnDifferent = false;
        this._camelPluginName = '';
        this._columnCheckboxes = [];
        this._columns = [];
        this._gridUid = '';
        this._menuCssPrefix = '';
        this._menuPluginCssPrefix = '';
        this.pluginName = '';
        this._bindEventService = new BindingEventService();
        this._eventHandler = new SlickEventHandler();
    }
    get addonOptions() {
        return this._addonOptions;
    }
    set addonOptions(newOptions) {
        this._addonOptions = newOptions;
    }
    get eventHandler() {
        return this._eventHandler;
    }
    get grid() {
        return this.sharedService.slickGrid;
    }
    get gridOptions() {
        return this.sharedService.gridOptions ?? {};
    }
    /** Getter for the grid uid */
    get gridUid() {
        return this._gridUid || (this.grid?.getUID() ?? '');
    }
    get gridUidSelector() {
        return this.gridUid ? `.${this.gridUid}` : '';
    }
    get menuCssClass() {
        return this._menuPluginCssPrefix || this._menuCssPrefix;
    }
    get menuElement() {
        return this._menuElm || document.querySelector(`.${this.menuCssClass}${this.gridUidSelector}`);
    }
    /** Dispose (destroy) of the plugin */
    dispose() {
        clearTimeout(this._timer);
        this._eventHandler?.unsubscribeAll();
        this._bindEventService.unbindAll();
        this.pubSubService.unsubscribeAll();
        this._commandTitleElm?.remove();
        this._optionTitleElm?.remove();
        this.disposeAllMenus();
        emptyElement(this._menuElm);
        this.menuElement?.remove();
        this._menuElm?.remove();
    }
    /** Remove/dispose all parent menus and any sub-menu(s) */
    disposeAllMenus() {
        this.disposeSubMenus();
        // remove all parent menu listeners before removing them from the DOM
        this._bindEventService.unbindAll('parent-menu');
        this._bindEventService.unbindAll('keyboard-navigation'); // Clean up keyboard/mouse event bindings
        document.querySelectorAll(`.${this.menuCssClass}${this.gridUidSelector}`).forEach((subElm) => subElm.remove());
        // Restore focus to the trigger element if it exists
        this._menuTriggerElement?.focus();
    }
    /**
     * Remove/dispose all previously opened sub-menu(s),
     * it will first remove all sub-menu listeners then remove sub-menus from the DOM
     */
    disposeSubMenus() {
        this._bindEventService.unbindAll('sub-menu');
        document.querySelectorAll(`.${this.menuCssClass}.slick-submenu${this.gridUidSelector}`).forEach((subElm) => subElm.remove());
    }
    setOptions(newOptions) {
        this._addonOptions = { ...this._addonOptions, ...newOptions };
    }
    get columnPickerMenuAddonOptions() {
        return this.addonOptions;
    }
    /** Create a Close button element and add it to the Menu element */
    addCloseButtomElement(menuElm) {
        const dismiss = this._camelPluginName === 'gridMenu' ? 'slick-grid-menu' : 'slick-column-picker';
        const closePickerButtonElm = createDomElement('button', {
            type: 'button',
            className: 'close',
            ariaLabel: 'Close',
            textContent: '×',
            dataset: { dismiss },
        });
        menuElm.appendChild(closePickerButtonElm);
    }
    /** When columnTitle is provided, create a title element for the columns list. */
    addColumnTitleElementWhenDefined(menuElm) {
        if (this.columnPickerMenuAddonOptions?.columnTitle) {
            this._columnTitleElm = createDomElement('div', { className: 'slick-menu-title', textContent: this.columnPickerMenuAddonOptions.columnTitle }, menuElm);
        }
    }
    /**
     * Handle column picker item click for both SlickColumnPicker and SlickGridMenu controls.
     * @param event - input checkbox event
     */
    handleColumnPickerItemClick(event) {
        const controlType = this._camelPluginName || 'columnPicker';
        const iconContainerElm = event.target?.closest('.icon-checkbox-container');
        const iconElm = iconContainerElm?.querySelector('.sgi');
        const isChecked = !!event.target.checked;
        event.target.ariaChecked = String(isChecked);
        this.togglePickerCheckbox(iconElm, isChecked);
        // when calling setOptions, it will resize with ALL Columns (even the hidden ones)
        // we can avoid this problem by keeping a reference to the visibleColumns before setOptions and then setColumns after
        if (event.target.dataset.option === 'autoresize') {
            this.grid.setOptions({ forceFitColumns: isChecked });
            this.grid.updateColumns();
            return;
        }
        if (event.target.dataset.option === 'syncresize') {
            this.grid.setOptions({ syncColumnCellResize: isChecked });
            return;
        }
        if (event.target.type === 'checkbox') {
            this._areVisibleColumnDifferent = true;
            const columnId = event.target.dataset.columnid || '';
            // validate that the checkbox changes is allowed before going any further
            const isFrozenAllowed = this.grid.validateColumnFreeze(columnId, true);
            let visibleColumns = this.grid.getVisibleColumns();
            if (!isFrozenAllowed || (visibleColumns.length - 1 < 1 && !isChecked)) {
                event.target.checked = true;
                this.togglePickerCheckbox(iconElm, true);
                return;
            }
            this.grid.updateColumnById(columnId, { hidden: !isChecked });
            if (!isChecked && this.gridOptions.enableCellRowSpan) {
                this.grid.remapAllColumnsRowSpan();
            }
            this.grid.updateColumns();
            visibleColumns = this.grid.getVisibleColumns();
            // when using row selection, SlickGrid will only apply the "selected" CSS class on the visible columns only
            // and if the row selection was done prior to the column being shown then that column that was previously hidden (at the time of the row selection)
            // will not have the "selected" CSS class because it wasn't visible at the time.
            // To bypass this problem we can simply recall the row selection with the same selection and that will trigger a re-apply of the CSS class
            // on all columns including the column we just made visible
            if (this.gridOptions.enableSelection && isChecked) {
                const rowSelection = this.grid.getSelectedRows();
                this.grid.setSelectedRows(rowSelection);
            }
            const callbackArgs = {
                columnId,
                showing: isChecked,
                allColumns: this.grid.getColumns(),
                visibleColumns,
                columns: visibleColumns,
                grid: this.grid,
            };
            // Restore focus to the triggering <li> if possible (for keyboard accessibility)
            // Only do this if the event was triggered by keyboard or programmatically (not mouse)
            // We check for type 'keydown' or if the activeElement is not the li
            this.pubSubService.publish(`on${titleCase(controlType)}ColumnsChanged`, callbackArgs);
            if (typeof this.columnPickerMenuAddonOptions.onColumnsChanged === 'function') {
                // Use a timeout to allow DOM updates before restoring focus
                this.columnPickerMenuAddonOptions.onColumnsChanged(event, callbackArgs);
            }
            this.onColumnsChanged.notify(callbackArgs, null, this);
        }
        const liElm = event.target?.closest('li');
        if (liElm && typeof liElm.focus === 'function') {
            setTimeout(() => liElm?.focus(), 0);
        }
    }
    /**
     * Because columns can be reordered, we have to update the `columns` to reflect the new order, however we can't just take `grid.getColumns()`,
     * as it does not include columns currently hidden by the picker. We create a new `columns` structure by leaving currently-hidden
     * columns in their original ordinal position and interleaving the results of the current column sort.
     */
    updateColumnPickerOrder() {
        const current = this.grid.getColumns().slice(0);
        const ordered = new Array(this._columns.length);
        for (let i = 0, ln = ordered.length; i < ln; i++) {
            const columnIdx = this.grid.getColumnIndex(this._columns[i].id);
            ordered[i] = columnIdx === undefined ? this._columns[i] : current.shift();
        }
        // the new set of ordered columns becomes the new set of column picker columns
        this._columns = ordered;
    }
    populateColumnPicker(addonOptions, defaultExtractor) {
        const isGridMenu = this._camelPluginName === 'gridMenu';
        const menuPrefix = isGridMenu ? 'gridmenu-' : '';
        let processedColumns = this._columns;
        if (typeof addonOptions?.columnSort === 'function') {
            // create a sorted copy of the columns array based on the "name" property
            processedColumns = [...this._columns].sort(addonOptions.columnSort);
        }
        // execute column list builder to allow user to filter/sort columns
        if (typeof addonOptions.columnListBuilder === 'function') {
            processedColumns = addonOptions.columnListBuilder(processedColumns);
        }
        for (const column of processedColumns) {
            const columnId = column.id;
            const columnLiElm = createDomElement('li', { tabIndex: -1 });
            // deprecated, let's filter out the exclude columns before `columnListBuilder` above (let's do that in next major v11)
            if ((column.excludeFromColumnPicker && !isGridMenu) || (column.excludeFromGridMenu && isGridMenu)) {
                columnLiElm.className = 'hidden';
            }
            const inputId = `${this._gridUid}-${menuPrefix}colpicker-${columnId}`;
            const isChecked = this.grid.getVisibleColumns().some((col) => col.id === columnId && !col.hidden);
            const { inputElm, labelElm, labelSpanElm } = this.generatePickerCheckbox(columnLiElm, inputId, { columnid: `${columnId}` }, isChecked);
            this._columnCheckboxes.push(inputElm);
            const headerColumnValueExtractorFn = typeof addonOptions?.headerColumnValueExtractor === 'function' ? addonOptions.headerColumnValueExtractor : defaultExtractor;
            const columnLabel = headerColumnValueExtractorFn(column, this.gridOptions);
            applyHtmlToElement(labelSpanElm, columnLabel, this.gridOptions);
            columnLiElm.appendChild(labelElm);
            this._listElm.appendChild(columnLiElm);
        }
        if (!addonOptions.hideForceFitButton || !addonOptions.hideSyncResizeButton) {
            this._listElm.appendChild(document.createElement('hr'));
        }
        if (!addonOptions?.hideForceFitButton) {
            const fitLiElm = createDomElement('li', { tabIndex: -1 });
            const inputId = `${this._gridUid}-${menuPrefix}colpicker-forcefit`;
            const { labelSpanElm } = this.generatePickerCheckbox(fitLiElm, inputId, { option: 'autoresize' }, this.gridOptions.forceFitColumns);
            labelSpanElm.textContent = addonOptions?.forceFitTitle ?? '';
            this._listElm.appendChild(fitLiElm);
        }
        if (!addonOptions?.hideSyncResizeButton) {
            const syncLiElm = createDomElement('li', { tabIndex: -1 });
            const inputId = `${this._gridUid}-${menuPrefix}colpicker-syncresize`;
            const { labelSpanElm } = this.generatePickerCheckbox(syncLiElm, inputId, { option: 'syncresize' }, this.gridOptions.forceFitColumns);
            labelSpanElm.textContent = addonOptions?.syncResizeTitle ?? '';
            this._listElm.appendChild(syncLiElm);
        }
    }
    // --
    // protected functions
    // ------------------
    /**
     * add Menu Item Command when not found and also make sure that we have an `action` callback
     * (could be missing when provided by user) if not use built-in `action` callback when missing
     * @param originalMenuItems
     * @param builtInMenuItem
     * @param showCommand - is command hidden from menu option (deprecated)
     * @returns - returns true when added to the commands array
     */
    addMissingCommandOrAction(builtInMenuItem, hideCommands = [], targetMenuItems, originalMenuItems) {
        // remove any commands that the user doesn't want
        let skip = false;
        if (hideCommands.length) {
            skip = builtInMenuItem !== 'divider' && new Set(hideCommands).has(builtInMenuItem.command);
        }
        if (builtInMenuItem !== 'divider' && !skip) {
            const cmdName = builtInMenuItem.command;
            const cmd = (originalMenuItems ?? targetMenuItems).find((item) => item !== 'divider' && item.command === cmdName);
            if (!cmd) {
                targetMenuItems.push(builtInMenuItem);
            }
            else if (!cmd.action) {
                // action might be missing (custom menu items), if so copy over from built-in
                cmd.action = builtInMenuItem.action;
            }
        }
    }
    /** Focus the first focusable menu item after menu is opened */
    focusFirstMenuItem(menuElm) {
        // Find the first menu list (direct child with role="menu")
        const menuList = menuElm.querySelector('[role="menu"]');
        if (menuList) {
            // Get all menu items and find the first one that's not disabled/divider/hidden
            const menuItems = Array.from(menuList.querySelectorAll('[role="menuitem"]'));
            const firstMenuItem = menuItems.find((item) => {
                // Skip dividers, disabled, and hidden items
                return (!item.classList.contains('disabled') &&
                    !item.classList.contains('slick-menu-item-disabled') &&
                    !item.classList.contains('slick-menu-item-divider') &&
                    !item.classList.contains('slick-menu-item-hidden') &&
                    item.offsetParent !== null);
            });
            firstMenuItem?.focus();
        }
    }
    /** Set the menu trigger element for focus restoration when menu closes */
    setMenuTriggerElement(triggerElement) {
        this._menuTriggerElement = triggerElement;
    }
    togglePickerCheckbox(iconElm, checked = false) {
        if (iconElm) {
            iconElm.className = `sgi ${checked ? 'sgi-icon-picker-check' : 'sgi-icon-picker-uncheck'}`;
        }
    }
    generatePickerCheckbox(columnLiElm, inputId, inputData, checked = false) {
        const labelElm = createDomElement('label', { className: 'checkbox-picker-label', htmlFor: inputId });
        const divElm = createDomElement('div', { className: 'icon-checkbox-container' });
        const inputElm = createDomElement('input', {
            id: inputId,
            type: 'checkbox',
            dataset: inputData,
            tabIndex: -1,
        });
        const colInputDivElm = createDomElement('div', { className: `sgi ${checked ? 'sgi-icon-picker-check' : 'sgi-icon-picker-uncheck'}` });
        const labelSpanElm = createDomElement('span', { className: 'checkbox-label' });
        divElm.appendChild(inputElm);
        divElm.appendChild(colInputDivElm);
        labelElm.appendChild(divElm);
        labelElm.appendChild(labelSpanElm);
        columnLiElm.appendChild(labelElm);
        if (checked) {
            inputElm.ariaChecked = 'true';
            inputElm.checked = true;
        }
        return { inputElm, labelElm, labelSpanElm };
    }
    /**
     * Render slot content using a renderer callback.
     * The renderer receives the menu item and args for full context access.
     * @param parentElm - The parent element (LI) to insert the slot content into
     * @param slotRenderer - A callback function that receives (item, args) and returns string or HTMLElement
     * @param item - The menu item object (passed to callback)
     * @param args - The callback args providing access to grid, column, dataContext, etc.
     */
    renderSlotRenderer(parentElm, slotRenderer, item, args) {
        const result = slotRenderer(item, args);
        if (typeof result === 'string') {
            parentElm.innerHTML = this.grid.sanitizeHtmlString(result);
        }
        else if (result instanceof HTMLElement) {
            parentElm.appendChild(result);
        }
    }
    addSubMenuTitleWhenExists(item, commandOrOptionMenu) {
        if (item !== 'divider' && item?.subMenuTitle) {
            const subMenuTitleElm = document.createElement('div');
            subMenuTitleElm.className = 'slick-menu-title';
            subMenuTitleElm.textContent = item.subMenuTitle;
            const subMenuTitleClass = item.subMenuTitleCssClass;
            if (subMenuTitleClass) {
                subMenuTitleElm.classList.add(...classNameToList(subMenuTitleClass));
            }
            commandOrOptionMenu.appendChild(subMenuTitleElm);
        }
    }
    /**
     * @deprecated
     * not entirely deprecated, but I just want to rename the function since we'll be deprecating/removing `optionItems` in v11
     * Construct the Command/Options Items section.
     */
    populateCommandOrOptionItems(itemType, menuOptions, commandOrOptionMenuElm, commandOrOptionItems, args, triggeredByElm, itemClickCallback, itemMouseoverCallback, keyboardNavOptions) {
        if (args && commandOrOptionItems && menuOptions) {
            for (const item of commandOrOptionItems) {
                const li = this.populateSingleCommandOrOptionItem(itemType, menuOptions, commandOrOptionMenuElm, item, args, triggeredByElm, itemClickCallback, itemMouseoverCallback);
                if (li && (item?.commandItems || item?.optionItems)) {
                    // Use command for MenuCommandItem, option for MenuOptionItem
                    const isCommand = typeof item.command === 'string';
                    const key = isCommand ? item.command : item.option;
                    const keyStr = typeof key === 'string' ? key.replace(/\s/g, '') : String(key);
                    if (keyStr) {
                        const selector = `.slick-submenu[data-sub-menu-parent="${keyStr}"]`;
                        const subMenuElm = document.body.querySelector(selector);
                        if (subMenuElm && !subMenuElm.dataset.keyboardNavBound) {
                            this.wireMenuKeyboardNavigation(subMenuElm, keyboardNavOptions);
                        }
                    }
                }
            }
        }
    }
    /** @deprecated (same as above and rename the function in v11) Add the Command/Options Title when necessary. */
    populateCommandOrOptionTitle(itemType, menuOptions, commandOrOptionMenuElm, level) {
        if (menuOptions) {
            const isSubMenu = level > 0;
            // return or create a title container
            const menuHeaderElm = this._menuElm?.querySelector(`.slick-${itemType}-header`) ?? createDomElement('div', { className: `slick-${itemType}-header` });
            // 'optionTitle' is deprecated, user could pass a title on top of the Commands/Options section
            const titleProp = `${itemType}Title`;
            if (!isSubMenu) {
                if (menuOptions?.[titleProp]) {
                    emptyElement(menuHeaderElm); // make sure title container is empty before adding anything inside it
                    this[`_${itemType}TitleElm`] = createDomElement('span', {
                        className: 'slick-menu-title',
                        textContent: menuOptions[titleProp],
                    });
                    menuHeaderElm.appendChild(this[`_${itemType}TitleElm`]);
                    menuHeaderElm.classList.add('with-title');
                }
                else {
                    menuHeaderElm.classList.add('no-title');
                }
                commandOrOptionMenuElm.appendChild(menuHeaderElm);
            }
        }
    }
    /** @deprecated (same as above and rename the function in v11) Construct the Command/Options Items section. */
    populateSingleCommandOrOptionItem(itemType, menuOptions, commandOrOptionMenuElm, item, args, triggeredByElm, itemClickCallback, itemMouseoverCallback) {
        let commandLiElm = null;
        const isHeaderButton = this._camelPluginName === 'headerButtons';
        if (args && item && menuOptions) {
            const level = args?.level || 0;
            const pluginMiddleName = isHeaderButton ? '' : '-item';
            const menuCssPrefix = `${this._menuCssPrefix}${pluginMiddleName}`;
            // run each override functions to know if the item is visible and usable
            let isItemVisible = true;
            let isItemUsable = true;
            if (typeof item === 'object') {
                isItemVisible = this.extensionUtility.runOverrideFunctionWhenExists(item.itemVisibilityOverride, args);
                isItemUsable = this.extensionUtility.runOverrideFunctionWhenExists(item.itemUsabilityOverride, args);
            }
            // if the result is not visible then there's no need to go further
            if (!isItemVisible) {
                return null;
            }
            // when the override is defined (and previously executed), we need to use its result to update the disabled property
            // so that "handleMenuItemCommandClick" has the correct flag and won't trigger a command/option clicked event
            if (typeof item === 'object' && item.itemUsabilityOverride) {
                item.disabled = isItemUsable ? false : true;
            }
            commandLiElm = createDomElement('li', { className: menuCssPrefix, role: 'menuitem' });
            if (item !== 'divider' && !item.divider) {
                commandLiElm.tabIndex = -1;
            }
            if (typeof item === 'object' && isDefined(item[itemType])) {
                commandLiElm.dataset[itemType] = item?.[itemType];
            }
            if (commandOrOptionMenuElm) {
                commandOrOptionMenuElm.appendChild(commandLiElm);
            }
            if ((typeof item === 'object' && item.divider) || item === 'divider') {
                commandLiElm.classList.add(`${menuCssPrefix}-divider`);
                commandLiElm.setAttribute('role', 'separator');
                return commandLiElm;
            }
            if (item.disabled) {
                commandLiElm.classList.add(`${menuCssPrefix}-disabled`);
                commandLiElm.ariaDisabled = 'true';
            }
            if (item.hidden || item.showOnHover) {
                commandLiElm.classList.add(`${menuCssPrefix}-hidden`);
            }
            if (item.cssClass) {
                if (isHeaderButton) {
                    commandLiElm.appendChild(createDomElement('span', { className: item.cssClass }));
                }
                else {
                    commandLiElm.classList.add(...classNameToList(item.cssClass));
                }
            }
            if (item.tooltip) {
                commandLiElm.title = item.tooltip;
            }
            if (!isHeaderButton) {
                // Check if we have slot renderer on the menu item or a default item renderer
                const slotRenderer = item.slotRenderer || this._addonOptions.defaultMenuItemRenderer;
                if (slotRenderer) {
                    this.renderSlotRenderer(commandLiElm, slotRenderer, item, args);
                }
                // Default rendering: icon + content
                else {
                    const iconElm = createDomElement('div', { className: `${this._menuCssPrefix}-icon` });
                    commandLiElm.appendChild(iconElm);
                    if (item.iconCssClass) {
                        iconElm.classList.add(...classNameToList(item.iconCssClass));
                    }
                    else if (!item.commandItems && !item.optionItems) {
                        iconElm.textContent = '◦';
                    }
                    const textElm = createDomElement('span', {
                        className: `${this._menuCssPrefix}-content`,
                        textContent: (typeof item === 'object' && item.title) || '',
                    }, commandLiElm);
                    if (item.textCssClass) {
                        textElm.classList.add(...classNameToList(item.textCssClass));
                    }
                }
            }
            // execute command callback on menu item clicked
            const eventGroupName = level > 0 ? 'sub-menu' : 'parent-menu';
            if (commandLiElm) {
                this._bindEventService.bind(commandLiElm, 'click', ((e) => {
                    // if there's a slot renderer, call it with the event
                    const slotRenderer = item.slotRenderer || this._addonOptions.defaultMenuItemRenderer;
                    if (slotRenderer) {
                        slotRenderer(item, args, e);
                    }
                    // if the click was stopped by an interactive element handler, don't trigger the menu action
                    if (e.defaultPrevented) {
                        return;
                    }
                    itemClickCallback.call(this, e, itemType, item, level, args?.column);
                    triggeredByElm?.focus(); // Restore focus to the triggering element after click
                    // CellMenu commands can re-render the row/cell and detach the original trigger node.
                    // If focus is no longer on a grid cell after command execution, restore cell navigation focus.
                    if (this.pluginName === 'CellMenu' && !document.activeElement?.closest('.slick-cell')) {
                        this.grid.focus('cell');
                    }
                }), undefined, eventGroupName);
            }
            // optionally open sub-menu(s) by mouseover
            if (this._addonOptions?.subMenuOpenByEvent === 'mouseover' && typeof itemMouseoverCallback === 'function') {
                this._bindEventService.bind(commandLiElm, 'mouseover', ((e) => itemMouseoverCallback.call(this, e, itemType, item, level, args?.column)), undefined, eventGroupName);
            }
            // the option/command item could be a sub-menu if it has another list of commands/options
            if (item.commandItems || item.optionItems) {
                const chevronElm = document.createElement('span');
                chevronElm.className = 'sub-item-chevron';
                if (this._addonOptions.subItemChevronClass) {
                    chevronElm.classList.add(...classNameToList(this._addonOptions.subItemChevronClass));
                }
                else {
                    chevronElm.textContent = '⮞'; // ⮞ or ▸
                }
                commandLiElm.classList.add('slick-submenu-item');
                commandLiElm.ariaHasPopup = 'true';
                commandLiElm.ariaExpanded = 'false';
                commandLiElm.appendChild(chevronElm);
            }
        }
        return commandLiElm;
    }
    /**
     * Wire up keyboard navigation for the menu container using shared utility.
     * Should be called after menu DOM is created for all non-GridMenu plugins.
     * Handles sub-menu open/close and focus transfer for a11y.
     */
    wireMenuKeyboardNavigation(menuElm, options) {
        wireMenuKeyboardNavigation(menuElm, this._bindEventService, {
            ...options,
            onActivate: options?.onActivate ??
                ((focusedItem) => {
                    // Default: trigger click on menu item
                    if (focusedItem) {
                        focusedItem.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
                    }
                }),
            onEscape: options?.onEscape ?? (() => this.disposeAllMenus()),
            onTab: options?.onTab ??
                ((evt) => {
                    evt.preventDefault();
                    evt.stopPropagation();
                }),
            allItemsSelector: options?.allItemsSelector,
            focusedItemSelector: options?.focusedItemSelector,
            onOpenSubMenu: (focusedItem) => {
                // Try to open sub-menu (simulate mouseover/click if needed)
                const command = focusedItem.dataset.command;
                let subMenuSelector = '';
                if (command) {
                    const cmdStr = typeof command === 'string' ? command.replace(/\s/g, '') : String(command);
                    subMenuSelector = `.slick-submenu[data-sub-menu-parent="${cmdStr}"]`;
                }
                // Simulate mouseover/click to open sub-menu if not present
                let subMenuElm = subMenuSelector ? document.body.querySelector(subMenuSelector) : null;
                if (!subMenuElm) {
                    focusedItem.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));
                    // Try again after event
                    if (subMenuSelector) {
                        subMenuElm = document.body.querySelector(subMenuSelector);
                    }
                }
                // If sub-menu is now present, wire keyboard navigation and focus first item
                if (subMenuElm) {
                    if (!subMenuElm.dataset.keyboardNavBound) {
                        this.wireMenuKeyboardNavigation(subMenuElm, options);
                    }
                    focusedItem.ariaExpanded = 'true';
                    this.focusFirstMenuItem(subMenuElm);
                }
            },
            onCloseSubMenu: (focusedItem) => {
                // Close the current sub-menu, then focus the trigger in the previous menu
                const currentSubMenu = focusedItem.closest('.slick-submenu');
                if (currentSubMenu) {
                    // Remove the current sub-menu from the DOM
                    currentSubMenu.remove();
                    // Find the parent menu (the previous menu level)
                    const parentMenu = document.body.querySelector(`.slick-menu-level-${parseInt(currentSubMenu.className.match(/slick-menu-level-(\d+)/)?.[1] || '1', 10) - 1}`);
                    // Find the submenu trigger in the parent menu that matches the data-sub-menu-parent
                    let triggerSelector = '';
                    const subMenuParentId = currentSubMenu.dataset.subMenuParent;
                    if (subMenuParentId) {
                        triggerSelector = `.slick-submenu-item[data-command="${subMenuParentId}"], .slick-submenu-item[data-option="${subMenuParentId}"]`;
                    }
                    let triggerItem = parentMenu && triggerSelector ? parentMenu.querySelector(triggerSelector) : null;
                    // Fallback: focus first menu item if trigger not found
                    if (!triggerItem && parentMenu) {
                        triggerItem = parentMenu.querySelector('.slick-submenu-item, [role="menuitem"]');
                    }
                    if (triggerItem) {
                        triggerItem.ariaExpanded = 'false';
                        triggerItem.focus();
                    }
                }
            },
        });
    }
    /**
     * Reposition any of the menu plugins (CellMenu, ContextMenu, GridMenu, HeaderMenu) to where the user clicked,
     * it will calculate the best position depending on available space in the viewport and the menu type.
     */
    repositionMenu(e, menuElm, buttonElm, addonOptions) {
        const targetElm = e.target; // get header button createElement
        const targetEvent = e?.touches?.[0] ?? e;
        const isSubMenu = menuElm.classList.contains('slick-submenu');
        const rowHeight = this.gridOptions.rowHeight || 0;
        const parentElm = isSubMenu
            ? e.target.closest('.slick-menu-item')
            : this.pluginName === 'CellMenu' || this.pluginName === 'ContextMenu'
                ? e.target.closest('.slick-cell')
                : targetEvent.target;
        if (menuElm && parentElm) {
            // for Cell/Context Menus we should move to (0,0) coordinates before calculating height/width
            // since it could end up being cropped width values when element is outside browser viewport.
            if (this.pluginName === 'CellMenu' || this.pluginName === 'ContextMenu') {
                menuElm.style.top = `0px`;
                menuElm.style.left = `0px`;
            }
            const containerElm = this.sharedService.gridContainerElement.classList.contains('slickgrid-container')
                ? this.sharedService.gridContainerElement
                : (this.sharedService.gridContainerElement.querySelector('.slickgrid-container') ?? this.sharedService.gridContainerElement);
            const relativePos = getOffsetRelativeToParent(containerElm, targetElm);
            const menuWidth = menuElm.offsetWidth;
            const parentOffset = getOffset(parentElm);
            let menuOffsetLeft = 0;
            let menuOffsetTop = 0;
            let dropOffset = 0;
            let sideOffset = 0;
            let availableSpaceBottom = 0;
            let availableSpaceTop = 0;
            const { bottom: parentSpaceBottom, top: parentSpaceTop } = calculateAvailableSpace(parentElm);
            if (this.pluginName === 'GridMenu' && buttonElm) {
                if (!isSubMenu) {
                    const buttonComptStyle = getComputedStyle(buttonElm);
                    const buttonWidth = parseInt(buttonComptStyle?.width ?? addonOptions?.menuWidth, 10);
                    const contentMinWidth = addonOptions?.contentMinWidth ?? 0;
                    const currentMenuWidth = (contentMinWidth > menuWidth ? contentMinWidth : menuWidth) || 0;
                    if (contentMinWidth > 0) {
                        menuElm.style.minWidth = `${contentMinWidth}px`;
                    }
                    const menuIconOffset = getOffset(buttonElm); // get button offset position
                    const nextPositionLeft = menuIconOffset.right;
                    menuOffsetTop = menuIconOffset.top + buttonElm.offsetHeight; // top position has to include button height so the menu is placed just below it
                    menuOffsetLeft =
                        addonOptions?.dropSide === 'right' ? nextPositionLeft - buttonWidth : nextPositionLeft - currentMenuWidth;
                }
            }
            else if (this.pluginName === 'CellMenu' || this.pluginName === 'ContextMenu') {
                menuOffsetLeft = parentElm && this.pluginName === 'CellMenu' ? parentOffset.left : targetEvent.pageX;
                menuOffsetTop = parentElm && this.pluginName === 'CellMenu' ? parentOffset.top : targetEvent.pageY;
                dropOffset = Number(addonOptions?.autoAdjustDropOffset || 0);
                sideOffset = Number(addonOptions?.autoAlignSideOffset || 0);
            }
            else {
                menuOffsetLeft = isSubMenu ? parentOffset.left : relativePos.left;
                menuOffsetTop = isSubMenu
                    ? parentOffset.top
                    : relativePos.top + (addonOptions?.menuOffsetTop ?? 0) + targetElm.clientHeight;
            }
            if ((this.pluginName === 'ContextMenu' || this.pluginName === 'GridMenu') && isSubMenu) {
                menuOffsetLeft = parentOffset.left;
                menuOffsetTop = parentOffset.top;
            }
            // for sub-menus only, auto-adjust drop position (up/down)
            // we first need to see what position the drop will be located (defaults to bottom)
            // since we reposition menu below slick cell, we need to take it in consideration and do our calculation from that element
            const menuHeight = menuElm?.offsetHeight || 0;
            if ((this.pluginName === 'GridMenu' || this.pluginName === 'HeaderMenu') && isSubMenu) {
                availableSpaceBottom = parentSpaceBottom;
                availableSpaceTop = parentSpaceTop;
            }
            else if (this.pluginName === 'CellMenu' ||
                this.pluginName === 'ContextMenu' ||
                addonOptions?.autoAdjustDrop ||
                addonOptions?.dropDirection) {
                availableSpaceBottom = parentSpaceBottom + dropOffset - rowHeight;
                availableSpaceTop = parentSpaceTop - dropOffset + rowHeight;
            }
            const dropPosition = availableSpaceBottom < menuHeight && availableSpaceTop > availableSpaceBottom ? 'top' : 'bottom';
            if (dropPosition === 'top' || addonOptions?.dropDirection === 'top') {
                menuElm.classList.remove('dropdown');
                menuElm.classList.add('dropup');
                if (isSubMenu) {
                    menuOffsetTop -= menuHeight - dropOffset - parentElm.clientHeight;
                }
                else {
                    menuOffsetTop -= menuHeight - dropOffset;
                }
            }
            else {
                menuElm.classList.remove('dropup');
                menuElm.classList.add('dropdown');
                if (this.pluginName === 'CellMenu' || this.pluginName === 'ContextMenu') {
                    menuOffsetTop = menuOffsetTop + dropOffset;
                    if (this.pluginName === 'CellMenu') {
                        if (isSubMenu) {
                            menuOffsetTop += dropOffset;
                        }
                        else {
                            menuOffsetTop += rowHeight + dropOffset;
                        }
                    }
                }
            }
            // when auto-align is set, it will calculate whether it has enough space in the viewport to show the drop menu on the right (default)
            // if there isn't enough space on the right, it will automatically align the drop menu to the left
            // to simulate an align left, we actually need to know the width of the drop menu
            if (addonOptions?.autoAlign ||
                addonOptions?.autoAlignSide ||
                addonOptions?.dropSide === 'left') {
                let subMenuPosCalc = menuOffsetLeft + Number(menuWidth); // calculate coordinate at caller element far right
                if (isSubMenu) {
                    subMenuPosCalc += parentElm.clientWidth;
                }
                const gridPos = this.grid.getGridPosition();
                const browserWidth = document.documentElement.clientWidth;
                const dropSide = subMenuPosCalc >= gridPos.width || subMenuPosCalc >= browserWidth ? 'left' : 'right';
                let needHeaderMenuOffsetLeftRecalc = false;
                if (dropSide === 'left' || (!isSubMenu && addonOptions?.dropSide === 'left')) {
                    menuElm.classList.remove('dropright');
                    menuElm.classList.add('dropleft');
                    if (this.pluginName === 'HeaderMenu') {
                        if (isSubMenu) {
                            menuOffsetLeft -= menuWidth;
                        }
                        else {
                            needHeaderMenuOffsetLeftRecalc = true;
                        }
                    }
                    else if (this.pluginName === 'CellMenu' && !isSubMenu) {
                        const parentCellWidth = parentElm.offsetWidth || 0;
                        menuOffsetLeft -= Number(menuWidth) - parentCellWidth - sideOffset;
                    }
                    else if (this.pluginName !== 'GridMenu' || (this.pluginName === 'GridMenu' && isSubMenu)) {
                        menuOffsetLeft -= Number(menuWidth) - sideOffset;
                    }
                }
                else {
                    menuElm.classList.remove('dropleft');
                    menuElm.classList.add('dropright');
                    if (isSubMenu) {
                        menuOffsetLeft += sideOffset + parentElm.offsetWidth;
                    }
                    else {
                        if (this.pluginName === 'HeaderMenu') {
                            needHeaderMenuOffsetLeftRecalc = true;
                        }
                        else {
                            menuOffsetLeft += sideOffset;
                        }
                    }
                }
                if (needHeaderMenuOffsetLeftRecalc) {
                    menuOffsetLeft = relativePos?.left ?? 0;
                    if (addonOptions?.autoAlign && gridPos?.width && menuOffsetLeft + (menuElm.clientWidth ?? 0) >= gridPos.width) {
                        menuOffsetLeft =
                            menuOffsetLeft + targetElm.clientWidth - menuElm.clientWidth + (addonOptions?.autoAlignOffset || 0);
                    }
                }
            }
            // ready to reposition the menu
            menuElm.style.top = `${menuOffsetTop}px`;
            menuElm.style.left = `${menuOffsetLeft}px`;
            if (this.pluginName === 'GridMenu') {
                menuElm.style.opacity = '1';
                menuElm.style.display = 'block';
            }
        }
    }
}
//# sourceMappingURL=menuBaseClass.js.map