import { createDomElement, emptyElement, extend, findWidthOrDefault, getHtmlStringOutput } from '@slickgrid-universal/utils';
import { SlickEvent, Utils as SlickUtils } from '../core/index.js';
import { addCloseButtomElement, addColumnTitleElementWhenDefined, handleColumnPickerItemClick, populateColumnPicker, updateColumnPickerOrder, } from '../extensions/extensionCommonUtils.js';
import { MenuBaseClass } from '../extensions/menuBaseClass.js';
import { getTranslationPrefix } from '../services/index.js';
/**
 * A control to add a Grid Menu with Extra Commands & Column Picker (hambuger menu on top-right of the grid)
 * To specify a custom button in a column header, extend the column definition like so:
 *   this.gridOptions = {
 *     enableGridMenu: true,
 *     gridMenu: {
 *       ... grid menu options ...
 *       commandItems: [{ ...command... }, { ...command... }]
 *     }
 *   }];
 * @class GridMenuControl
 * @constructor
 */
export class SlickGridMenu extends MenuBaseClass {
    /** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */
    constructor(extensionUtility, filterService, pubSubService, sharedService, sortService) {
        super(extensionUtility, pubSubService, sharedService);
        this.extensionUtility = extensionUtility;
        this.filterService = filterService;
        this.pubSubService = pubSubService;
        this.sharedService = sharedService;
        this.sortService = sortService;
        this._areVisibleColumnDifferent = false;
        this._columns = [];
        this._columnCheckboxes = [];
        this._commandMenuElm = null;
        this._headerElm = null;
        this._isMenuOpen = false;
        this._subMenuParentId = '';
        this._defaults = {
            autoAlignSide: true,
            dropSide: 'left',
            showButton: true,
            hideForceFitButton: false,
            hideSyncResizeButton: false,
            forceFitTitle: 'Force fit columns',
            marginBottom: 15,
            menuWidth: 12,
            minHeight: 150,
            contentMinWidth: 0,
            resizeOnShowHeaderRow: false,
            syncResizeTitle: 'Synchronous resize',
            subMenuOpenByEvent: 'mouseover',
            headerColumnValueExtractor: (columnDef) => getHtmlStringOutput(columnDef.columnPickerLabel || columnDef.name || '', 'innerHTML'),
        };
        this.pluginName = 'GridMenu';
        this._menuCssPrefix = 'slick-menu';
        this._menuPluginCssPrefix = 'slick-grid-menu';
        this._camelPluginName = 'gridMenu';
        this._columns = this.grid?.getColumns() ?? [];
        this._gridUid = this.grid?.getUID() ?? '';
        this.onAfterMenuShow = new SlickEvent('onAfterMenuShow');
        this.onBeforeMenuShow = new SlickEvent('onBeforeMenuShow');
        this.onMenuClose = new SlickEvent('onMenuClose');
        this.onCommand = new SlickEvent('onCommand');
        this.onColumnsChanged = new SlickEvent('onColumnsChanged');
        this.initEventHandlers();
        this.init();
    }
    get addonOptions() {
        return this._addonOptions || {};
    }
    get columns() {
        return this._columns;
    }
    set columns(newColumns) {
        this._columns = newColumns;
    }
    get gridOptions() {
        return this.grid?.getOptions() || {};
    }
    get gridUidSelector() {
        return this.gridUid ? `.${this.gridUid}` : '';
    }
    initEventHandlers() {
        // when grid columns are reordered then we also need to update/resync our picker column in the same order
        this._eventHandler.subscribe(this.grid.onColumnsReordered, updateColumnPickerOrder.bind(this));
        this._eventHandler.subscribe(this.grid.onClick, (e) => this.hideMenu(e));
        // subscribe to the grid, when it's destroyed, we should also destroy the Grid Menu
        this._eventHandler.subscribe(this.grid.onBeforeDestroy, this.dispose.bind(this));
        // when a grid optionally changes from a regular grid to a frozen grid, we need to destroy & recreate the grid menu
        // we do this change because the Grid Menu is on the left container for a regular grid, it should however be displayed on the right container for a frozen grid
        this._eventHandler.subscribe(this.grid.onSetOptions, (_e, args) => {
            if (args && args.optionsBefore && args.optionsAfter) {
                const switchedFromRegularToFrozen = args.optionsBefore.frozenColumn >= 0 && args.optionsAfter.frozenColumn === -1;
                const switchedFromFrozenToRegular = args.optionsBefore.frozenColumn === -1 && args.optionsAfter.frozenColumn >= 0;
                if (switchedFromRegularToFrozen || switchedFromFrozenToRegular) {
                    this.recreateGridMenu();
                }
            }
        });
    }
    /** Initialize plugin. */
    init(isFirstLoad = true) {
        this._gridUid = this.grid.getUID() ?? '';
        // add PubSub instance to all SlickEvent
        SlickUtils.addSlickEventPubSubWhenDefined(this.pubSubService, this);
        // keep original user grid menu, useful when switching locale to translate
        if (isFirstLoad) {
            this._originalGridMenu = extend(true, {}, this.sharedService.gridOptions.gridMenu);
        }
        this._userOriginalGridMenu = { ...this.sharedService.gridOptions.gridMenu };
        this._addonOptions = { ...this._defaults, ...this.getDefaultGridMenuOptions(), ...this.sharedService.gridOptions.gridMenu };
        this.sharedService.gridOptions.gridMenu = this._addonOptions;
        // merge original user grid menu items with internal items
        // then sort all Grid Menu command items (sorted by pointer, no need to use the return)
        const gridMenuCommandItems = this._userOriginalGridMenu.commandItems;
        const originalCommandItems = this._userOriginalGridMenu && Array.isArray(gridMenuCommandItems) ? gridMenuCommandItems : [];
        // merge the original commands with the built-in internal commands
        const initialCommandItems = [...originalCommandItems, ...this.addGridMenuCustomCommands(originalCommandItems)];
        this._addonOptions.commandItems =
            this._addonOptions.commandListBuilder?.(initialCommandItems) ?? initialCommandItems;
        this.extensionUtility.translateMenuItemsFromTitleKey(this._addonOptions.commandItems || [], 'commandItems');
        this.extensionUtility.sortItems(this._addonOptions.commandItems, 'positionOrder');
        // create the Grid Menu DOM element
        this.createGridMenu();
    }
    /** Dispose (destroy) the SlickGrid 3rd party plugin */
    dispose() {
        this.deleteMenu();
        super.dispose();
    }
    deleteMenu() {
        this._bindEventService.unbindAll();
        this._menuElm?.remove();
        this._menuElm = null;
        this._gridMenuButtonElm?.remove();
        if (this._headerElm) {
            // put back grid header original width (fixes width and frozen+gridMenu on left header)
            this._headerElm.style.width = '100%';
            this._headerElm.parentElement?.querySelector('.slick-grid-menu-container')?.remove();
        }
    }
    createColumnPickerContainer() {
        if (this._menuElm) {
            // user could pass a title on top of the columns list
            addColumnTitleElementWhenDefined.call(this, this._menuElm);
            this._listElm = createDomElement('div', { className: 'slick-column-picker-list', role: 'menu' });
            // update all columns on any of the column title button click from column picker
            this._bindEventService.bind(this._menuElm, 'click', handleColumnPickerItemClick.bind(this), undefined, 'parent-menu');
        }
    }
    /** Create parent grid menu container */
    createGridMenu() {
        const gridMenuWidth = (this._addonOptions?.menuWidth || this._defaults.menuWidth);
        const gridContainer = this.grid.getContainerNode();
        const headerSide = this.gridOptions.hasOwnProperty('frozenColumn') && this.gridOptions.frozenColumn >= 0 ? 'right' : 'left';
        // find the header or pre-header element where to insert the grid menu button
        this._headerElm =
            this._addonOptions?.iconButtonContainer === 'preheader'
                ? gridContainer.querySelector('.slick-preheader-panel')
                : gridContainer.querySelector(`.slick-header-${headerSide}`);
        if (this._headerElm?.parentElement && this._addonOptions) {
            if (this._addonOptions.showButton ?? this._defaults.showButton) {
                const buttonPadding = 2; // add small padding around the button to make it more usable
                const gridMenuContainerElm = createDomElement('div', {
                    className: 'slick-grid-menu-container',
                    style: { width: `${gridMenuWidth + buttonPadding}px` },
                });
                // Create button
                this._gridMenuButtonElm = createDomElement('button', { className: 'slick-grid-menu-button', ariaLabel: 'Grid Menu', tabIndex: 0 });
                // Create icon span inside button
                const iconSpan = createDomElement('span', { className: this._addonOptions?.iconCssClass || 'mdi mdi-menu' });
                this._gridMenuButtonElm.appendChild(iconSpan);
                // add the grid menu button to the header container
                gridMenuContainerElm.appendChild(this._gridMenuButtonElm);
                this._headerElm.parentElement.appendChild(gridMenuContainerElm);
                // show the Grid Menu when hamburger menu is clicked or activated with keyboard
                this._bindEventService.bind(gridMenuContainerElm, 'click', this.showGridMenu.bind(this));
                this._bindEventService.bind(gridMenuContainerElm, 'keydown', this.handleKeyDown.bind(this));
            }
            this.sharedService.gridOptions.gridMenu = { ...this._defaults, ...this._addonOptions };
            // localization support for the picker
            this.translateTitleLabels(this._addonOptions);
            this.translateTitleLabels(this.sharedService.gridOptions.gridMenu);
            // hide the menu on outside click.
            this._bindEventService.bind(document.body, 'mousedown', this.handleBodyMouseDown.bind(this));
            // destroy the picker if user leaves the page
            this._bindEventService.bind(document.body, 'beforeunload', this.dispose.bind(this));
        }
    }
    /** Create the menu or sub-menu(s) but without the column picker which is a separate single process */
    createCommandMenu(triggeredByElm, commandItems, level = 0, item) {
        // Always use the triggering item's command (whitespace removed) as the sub-menu parent id for every level
        const subMenuCommand = item?.command;
        const subMenuId = subMenuCommand ? subMenuCommand.replace(/\s/g, '') : '';
        const menuClasses = `${this.menuCssClass} slick-menu-level-${level} ${this._gridUid}`;
        const bodyMenuElm = document.body.querySelector(`.${this.menuCssClass}.slick-menu-level-${level}${this.gridUidSelector}`);
        // return menu/sub-menu if it's already opened unless we are on different sub-menu tree if so close them all
        if (bodyMenuElm) {
            if (bodyMenuElm.dataset.subMenuParent === subMenuId) {
                return bodyMenuElm;
            }
            this.disposeSubMenus();
        }
        const menuElm = createDomElement('div', {
            role: 'menu',
            className: menuClasses,
            ariaLabel: level > 1 ? 'SubMenu' : 'Grid Menu',
        });
        if (level > 0) {
            menuElm.classList.add('slick-submenu');
            if (subMenuId) {
                menuElm.dataset.subMenuParent = subMenuId;
            }
        }
        const callbackArgs = {
            grid: this.grid,
            menu: this._menuElm,
            columns: this.columns,
            allColumns: this.getAllColumns(),
            visibleColumns: this.getVisibleColumns(),
            level,
        };
        this._commandMenuElm = this.recreateCommandList(triggeredByElm, commandItems, menuElm, callbackArgs, item);
        // increment level for possible next sub-menus if exists
        level++;
        return menuElm;
    }
    /**
     * Get all columns including hidden columns.
     * @returns {Array<Object>} - all columns array
     */
    getAllColumns() {
        return this._columns;
    }
    /**
     * Get only the visible columns.
     * @returns {Array<Object>} - only the visible columns array
     */
    getVisibleColumns() {
        return this.grid.getVisibleColumns();
    }
    /**
     * Hide the Grid Menu but only if it does detect as open prior to executing anything.
     * @param event
     * @returns
     */
    hideMenu(event) {
        const callbackArgs = {
            grid: this.grid,
            menu: this._menuElm,
            allColumns: this.columns,
            visibleColumns: this.getVisibleColumns(),
        };
        // execute optional callback method defined by the user, if it returns false then we won't go further neither close the menu
        this.pubSubService.publish('onGridMenuMenuClose', callbackArgs);
        if ((typeof this._addonOptions?.onMenuClose === 'function' && this._addonOptions.onMenuClose(event, callbackArgs) === false) ||
            this.onMenuClose.notify(callbackArgs, null, this).getReturnValue() === false) {
            return;
        }
        this._isMenuOpen = false;
        // we also want to resize the columns if the user decided to hide certain column(s)
        if (typeof this.grid?.autosizeColumns === 'function' && this._addonOptions?.autoResizeColumns !== false) {
            // make sure that the grid still exist (by looking if the Grid UID is found in the DOM tree)
            const gridUid = this.grid.getUID() || '';
            if (this._areVisibleColumnDifferent && gridUid && document.querySelector(`.${gridUid}`) !== null) {
                if (this.gridOptions.enableAutoSizeColumns) {
                    this.grid.autosizeColumns();
                }
                this._areVisibleColumnDifferent = false;
            }
        }
        // dispose of all sub-menus from the DOM and unbind all listeners
        this.disposeSubMenus();
        this._menuElm?.remove();
        this._menuElm = null;
    }
    /** destroy and recreate the Grid Menu in the DOM */
    recreateGridMenu() {
        this.deleteMenu();
        this.init(false);
    }
    /** Open the Grid Menu */
    openGridMenu() {
        const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, composed: false });
        Object.defineProperty(clickEvent, 'target', {
            writable: true,
            configurable: true,
            value: createDomElement('button', { className: 'slick-grid-menu-button' }),
        });
        this.showGridMenu(clickEvent);
    }
    /** show Grid Menu from the click event, which in theory will recreate the grid menu in the DOM */
    showGridMenu(e, options) {
        const targetEvent = e?.touches?.[0] ?? e;
        this.stopFullBubbling(e);
        // empty the entire menu so that it's recreated every time it opens
        emptyElement(this._menuElm);
        this._menuElm?.remove();
        if (this._addonOptions) {
            const callbackArgs = {
                grid: this.grid,
                menu: this._menuElm,
                columns: this.columns,
                allColumns: this.getAllColumns(),
                visibleColumns: this.getVisibleColumns(),
            };
            const addonOptions = { ...this._addonOptions, ...options }; // merge optional picker option
            // run the override function (when defined), if the result is false then we won't go further
            if (addonOptions &&
                !this.extensionUtility.runOverrideFunctionWhenExists(addonOptions.menuUsabilityOverride, callbackArgs)) {
                return;
            }
            // execute optional callback method defined by the user, if it returns false then we won't go further and not open the grid menu
            if (typeof e.stopPropagation === 'function') {
                this.pubSubService.publish('onGridMenuBeforeMenuShow', callbackArgs);
                if ((typeof addonOptions?.onBeforeMenuShow === 'function' && addonOptions.onBeforeMenuShow(e, callbackArgs) === false) ||
                    this.onBeforeMenuShow.notify(callbackArgs, null, this).getReturnValue() === false) {
                    return;
                }
            }
            this._menuElm = this.createCommandMenu(e.target, this._addonOptions?.commandItems ?? []);
            this.createColumnPickerContainer();
            updateColumnPickerOrder.call(this);
            this._columnCheckboxes = [];
            // load the column & create column picker list
            populateColumnPicker.call(this, addonOptions);
            document.body.appendChild(this._menuElm);
            // add dark mode CSS class when enabled
            if (this.gridOptions.darkMode) {
                this._menuElm.classList.add('slick-dark-mode');
            }
            // calculate the necessary menu height/width and reposition twice because if we do it only once and the grid menu is wider than the original width,
            // it will be offset the 1st time we open the menu but if we do it twice then it will be at the correct position every time
            this._menuElm.style.opacity = '0';
            const menuMarginBottom = (addonOptions?.marginBottom !== undefined ? addonOptions.marginBottom : this._defaults.marginBottom) || 0;
            // set 'height' when defined OR ELSE use the 'max-height' with available window size and optional margin bottom
            this._menuElm.style.minHeight = findWidthOrDefault(addonOptions?.minHeight, '');
            if (addonOptions?.height !== undefined) {
                this._menuElm.style.height = findWidthOrDefault(addonOptions.height, '');
            }
            else {
                this._menuElm.style.maxHeight = findWidthOrDefault(addonOptions?.maxHeight, `${window.innerHeight - targetEvent.clientY - menuMarginBottom}px`);
            }
            let buttonElm = e.target.nodeName === 'BUTTON'
                ? e.target
                : e.target.querySelector('button'); // get button element
            if (!buttonElm) {
                buttonElm = e.target.parentElement; // external grid menu might fall in this last case if wrapped in a span/div
            }
            this._menuElm.ariaExpanded = 'true';
            this._menuElm.appendChild(this._listElm);
            // once we have both lists (commandItems + columnPicker), we are ready to reposition the menu since its height/width should be calculated by then
            this.repositionMenu(e, this._menuElm, buttonElm, addonOptions);
            // Track the element that triggered the menu (for focus restoration)
            if (buttonElm) {
                this.setMenuTriggerElement(buttonElm);
            }
            // Focus the first available item in either command or column picker section
            const allItems = this.getAllFocusableMenuItems();
            if (allItems.length > 0) {
                allItems[0].focus();
            }
            // Use base class method to wire up keyboard navigation
            const itemSelector = '.slick-grid-menu li[tabindex]:not(.disabled, .hidden, .slick-menu-item-disabled, .slick-menu-item-divider, .slick-menu-item-hidden)';
            this.wireMenuKeyboardNavigation(this._menuElm, {
                allItemsSelector: itemSelector,
                focusedItemSelector: itemSelector,
                onActivate: (focusedItem) => {
                    if (focusedItem) {
                        // If it's a column picker item, trigger the checkbox click
                        if (focusedItem.closest('.slick-column-picker-list')) {
                            const iconContainer = focusedItem.querySelector('.icon-checkbox-container');
                            if (iconContainer && typeof iconContainer.click === 'function') {
                                iconContainer.click();
                            }
                        }
                        else {
                            // Otherwise, trigger the menu command
                            focusedItem.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
                        }
                    }
                },
                onEscape: () => {
                    this.disposeAllMenus();
                },
                onTab: (evt) => {
                    this.stopFullBubbling(evt);
                },
                eventServiceKey: 'grid-menu-keyboard',
            });
            this._isMenuOpen = true;
            // execute optional callback method defined by the user
            this.pubSubService.publish('onGridMenuAfterMenuShow', callbackArgs);
            if (typeof addonOptions?.onAfterMenuShow === 'function') {
                addonOptions.onAfterMenuShow(e, callbackArgs);
            }
            this.onAfterMenuShow.notify(callbackArgs, null, this);
        }
    }
    /** Translate the Grid Menu titles and column picker */
    translateGridMenu() {
        // update the properties by pointers, that is the only way to get Grid Menu Control to see the new values
        // we also need to call the control init so that it takes the new Grid object with latest values
        if (this.sharedService.gridOptions.gridMenu) {
            this.sharedService.gridOptions.gridMenu.commandItems = [];
            this.sharedService.gridOptions.gridMenu.commandTitle = this._originalGridMenu.commandTitle || '';
            this.sharedService.gridOptions.gridMenu.columnTitle = this._originalGridMenu.columnTitle || '';
            this.sharedService.gridOptions.gridMenu.forceFitTitle = this._originalGridMenu.forceFitTitle || '';
            this.sharedService.gridOptions.gridMenu.syncResizeTitle = this._originalGridMenu.syncResizeTitle || '';
            // merge original user grid menu items with internal items
            // then sort all Grid Menu command items (sorted by pointer, no need to use the return)
            // prettier-ignore
            const originalCommandItems = this._userOriginalGridMenu && Array.isArray(this._userOriginalGridMenu.commandItems) ? this._userOriginalGridMenu.commandItems : [];
            this.sharedService.gridOptions.gridMenu.commandItems = [
                ...originalCommandItems,
                ...this.addGridMenuCustomCommands(originalCommandItems),
            ];
            this.extensionUtility.translateMenuItemsFromTitleKey(this._addonOptions?.commandItems || [], 'commandItems');
            this.extensionUtility.sortItems(this.sharedService.gridOptions.gridMenu.commandItems, 'positionOrder');
            this.translateTitleLabels(this.sharedService.gridOptions.gridMenu);
            this.translateTitleLabels(this._addonOptions);
            // translate all columns (including non-visible)
            this.extensionUtility.translateItems(this._columns, 'nameKey', 'name');
        }
    }
    translateTitleLabels(gridMenuOptions) {
        if (gridMenuOptions) {
            gridMenuOptions.commandTitle = this.extensionUtility.getPickerTitleOutputString('commandTitle', 'gridMenu');
            gridMenuOptions.columnTitle = this.extensionUtility.getPickerTitleOutputString('columnTitle', 'gridMenu');
            gridMenuOptions.forceFitTitle = this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'gridMenu');
            gridMenuOptions.syncResizeTitle = this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'gridMenu');
        }
    }
    // --
    // protected functions
    // ------------------
    /** Create Grid Menu with Custom Commands if user has enabled Filters and/or uses a Backend Service (OData, GraphQL) */
    addGridMenuCustomCommands(originalCommandItems) {
        const backendApi = this.gridOptions.backendServiceApi || null;
        const gridMenuCommandItems = [];
        const gridOptions = this.gridOptions;
        const translationPrefix = getTranslationPrefix(gridOptions);
        const commandLabels = this._addonOptions?.commandLabels;
        if (this._addonOptions && this.gridOptions) {
            // show grid menu: Unfreeze Columns/Rows
            if (!this._addonOptions.hideClearFrozenColumnsCommand) {
                this.addMissingCommandOrAction({
                    iconCssClass: this._addonOptions.iconClearFrozenColumnsCommand || 'mdi mdi-pin-off-outline',
                    _orgTitle: commandLabels?.clearFrozenColumnsCommand || '',
                    titleKey: `${translationPrefix}${commandLabels?.clearFrozenColumnsCommandKey ?? 'CLEAR_PINNING'}`,
                    disabled: false,
                    command: 'clear-pinning',
                    positionOrder: 52,
                    action: this.clearPinning.bind(this),
                }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
            }
            if (this.gridOptions.enableFiltering && !this.sharedService.hideHeaderRowAfterPageLoad) {
                // show grid menu: Clear all Filters
                if (!this._addonOptions.hideClearAllFiltersCommand) {
                    this.addMissingCommandOrAction({
                        iconCssClass: this._addonOptions.iconClearAllFiltersCommand || 'mdi mdi-filter-remove-outline',
                        _orgTitle: commandLabels?.clearAllFiltersCommand || '',
                        titleKey: `${translationPrefix}${commandLabels?.clearAllFiltersCommandKey ?? 'CLEAR_ALL_FILTERS'}`,
                        disabled: false,
                        command: 'clear-filter',
                        positionOrder: 50,
                        action: () => {
                            this.filterService.clearFilters();
                            this.sharedService.dataView.refresh();
                            this.pubSubService.publish('onGridMenuClearAllFilters');
                        },
                    }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
                }
                // show grid menu: toggle filter row
                if (!this._addonOptions.hideToggleFilterCommand) {
                    this.addMissingCommandOrAction({
                        iconCssClass: this._addonOptions.iconToggleFilterCommand || 'mdi mdi-flip-vertical',
                        _orgTitle: commandLabels?.toggleFilterCommand || '',
                        titleKey: `${translationPrefix}${commandLabels?.toggleFilterCommandKey ?? 'TOGGLE_FILTER_ROW'}`,
                        disabled: false,
                        command: 'toggle-filter',
                        positionOrder: 53,
                        action: this.toggleFilterBar.bind(this),
                    }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
                }
                // show grid menu: refresh dataset
                if (backendApi && !this._addonOptions.hideRefreshDatasetCommand) {
                    this.addMissingCommandOrAction({
                        iconCssClass: this._addonOptions.iconRefreshDatasetCommand || 'mdi mdi-sync',
                        _orgTitle: commandLabels?.refreshDatasetCommand || '',
                        titleKey: `${translationPrefix}${commandLabels?.refreshDatasetCommandKey ?? 'REFRESH_DATASET'}`,
                        disabled: false,
                        command: 'refresh-dataset',
                        positionOrder: 58,
                        action: () => this.extensionUtility.refreshBackendDataset(),
                    }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
                }
            }
            // show grid menu: toggle dark mode
            if (!this._addonOptions.hideToggleDarkModeCommand) {
                this.addMissingCommandOrAction({
                    iconCssClass: this._addonOptions.iconToggleDarkModeCommand || 'mdi mdi-brightness-4',
                    _orgTitle: commandLabels?.toggleDarkModeCommand || '',
                    titleKey: `${translationPrefix}${commandLabels?.toggleDarkModeCommandKey ?? 'TOGGLE_DARK_MODE'}`,
                    disabled: false,
                    command: 'toggle-dark-mode',
                    positionOrder: 54,
                    action: () => {
                        const currentDarkMode = this.sharedService.gridOptions.darkMode;
                        this.grid.setOptions({ darkMode: !currentDarkMode });
                        this.sharedService.gridOptions.darkMode = !currentDarkMode;
                    },
                }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
            }
            if (this.gridOptions.showPreHeaderPanel) {
                // show grid menu: toggle pre-header row
                if (!this._addonOptions.hideTogglePreHeaderCommand) {
                    this.addMissingCommandOrAction({
                        iconCssClass: this._addonOptions.iconTogglePreHeaderCommand || 'mdi mdi-flip-vertical',
                        _orgTitle: commandLabels?.togglePreHeaderCommand || '',
                        titleKey: `${translationPrefix}${commandLabels?.togglePreHeaderCommandKey ?? 'TOGGLE_PRE_HEADER_ROW'}`,
                        disabled: false,
                        command: 'toggle-preheader',
                        positionOrder: 53,
                        action: () => {
                            const showPreHeaderPanel = this.gridOptions?.showPreHeaderPanel ?? false;
                            this.grid.setPreHeaderPanelVisibility(!showPreHeaderPanel);
                        },
                    }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
                }
            }
            if (this.gridOptions.enableSorting) {
                // show grid menu: Clear all Sorting
                if (!this._addonOptions.hideClearAllSortingCommand) {
                    this.addMissingCommandOrAction({
                        iconCssClass: this._addonOptions.iconClearAllSortingCommand || 'mdi mdi-sort-variant-off',
                        _orgTitle: commandLabels?.clearAllSortingCommand || '',
                        titleKey: `${translationPrefix}${commandLabels?.clearAllSortingCommandKey ?? 'CLEAR_ALL_SORTING'}`,
                        disabled: false,
                        command: 'clear-sorting',
                        positionOrder: 51,
                        action: () => {
                            this.sortService.clearSorting();
                            this.sharedService.dataView.refresh();
                            this.pubSubService.publish('onGridMenuClearAllSorting');
                        },
                    }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
                }
            }
            // show grid menu: Export to file
            if (this.gridOptions.enableTextExport && !this._addonOptions.hideExportCsvCommand) {
                this.addMissingCommandOrAction({
                    iconCssClass: this._addonOptions.iconExportCsvCommand || 'mdi mdi-download',
                    _orgTitle: commandLabels?.exportCsvCommand || '',
                    titleKey: `${translationPrefix}${commandLabels?.exportCsvCommandKey ?? 'EXPORT_TO_CSV'}`,
                    disabled: false,
                    command: 'export-csv',
                    positionOrder: 55,
                    action: this.exportCsv.bind(this),
                }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
            }
            // show grid menu: Export to Excel
            if (this.gridOptions.enableExcelExport && !this._addonOptions.hideExportExcelCommand) {
                this.addMissingCommandOrAction({
                    iconCssClass: this._addonOptions.iconExportExcelCommand || 'mdi mdi-file-excel-outline text-success',
                    _orgTitle: commandLabels?.exportExcelCommand || '',
                    titleKey: `${translationPrefix}${commandLabels?.exportExcelCommandKey ?? 'EXPORT_TO_EXCEL'}`,
                    disabled: false,
                    command: 'export-excel',
                    positionOrder: 56,
                    action: this.exportExcel.bind(this),
                }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
            }
            // show grid menu: Export to PDF
            if (this.gridOptions.enablePdfExport && !this._addonOptions.hideExportPdfCommand) {
                this.addMissingCommandOrAction({
                    iconCssClass: this._addonOptions.iconExportPdfCommand || 'mdi mdi-file-pdf-outline text-danger',
                    _orgTitle: commandLabels?.exportPdfCommand || '',
                    titleKey: `${translationPrefix}${commandLabels?.exportPdfCommandKey ?? 'EXPORT_TO_PDF'}`,
                    disabled: false,
                    command: 'export-pdf',
                    positionOrder: 57,
                    action: this.exportPdf.bind(this),
                }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
            }
            // show grid menu: export to text file as tab delimited
            if (this.gridOptions.enableTextExport && !this._addonOptions.hideExportTextDelimitedCommand) {
                this.addMissingCommandOrAction({
                    iconCssClass: this._addonOptions.iconExportTextDelimitedCommand || 'mdi mdi-download',
                    _orgTitle: commandLabels?.exportTextDelimitedCommand || '',
                    titleKey: `${translationPrefix}${commandLabels?.exportTextDelimitedCommandKey ?? 'EXPORT_TO_TAB_DELIMITED'}`,
                    disabled: false,
                    command: 'export-text-delimited',
                    positionOrder: 58,
                    action: this.exportTextDelimited.bind(this),
                }, this._addonOptions.hideCommands, gridMenuCommandItems, originalCommandItems);
            }
            // add the custom "Commands" title if there are any commands
            const commandItems = this._addonOptions?.commandItems || [];
            if ((Array.isArray(gridMenuCommandItems) && gridMenuCommandItems.length > 0) ||
                (Array.isArray(commandItems) && commandItems.length > 0)) {
                this._addonOptions.commandTitle ||= this.extensionUtility.getPickerTitleOutputString('commandTitle', 'gridMenu');
            }
        }
        return gridMenuCommandItems;
    }
    clearPinning() {
        // reset frozen props on both SlickGrid options and shared service options
        const newGridOptions = {
            frozenColumn: -1,
            frozenRow: -1,
            frozenBottom: false,
            enableMouseWheelScrollHandler: false,
        };
        this.grid.setOptions(newGridOptions);
        Object.keys(newGridOptions).forEach((c) => (this.sharedService.gridOptions[c] = newGridOptions[c]));
        // re-update columns to reflect any possible changes
        this.grid.updateColumns();
        // we also need to autosize columns if the option is enabled
        if (this.gridOptions.enableAutoSizeColumns) {
            this.grid.autosizeColumns();
        }
        this.pubSubService.publish('onGridMenuClearAllPinning');
    }
    exportCsv() {
        const registeredResources = this.sharedService?.externalRegisteredResources || [];
        // prettier-ignore
        const exportCsvService = registeredResources.find((service) => service.pluginName === 'TextExportService');
        if (exportCsvService?.exportToFile) {
            exportCsvService.exportToFile({ delimiter: ',', format: 'csv' });
        }
        else {
            console.error(`[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu. Example:: this.gridOptions = { enableTextExport: true, externalResources: [new TextExportService()] };`);
        }
    }
    exportExcel() {
        const registeredResources = this.sharedService?.externalRegisteredResources || [];
        // prettier-ignore
        const excelService = registeredResources.find((service) => service.pluginName === 'ExcelExportService');
        if (excelService?.exportToExcel) {
            excelService.exportToExcel();
        }
        else {
            console.error(`[Slickgrid-Universal] You must register the ExcelExportService to properly use Export to Excel in the Grid Menu. Example:: this.gridOptions = { enableExcelExport: true, externalResources: [new ExcelExportService()] };`);
        }
    }
    exportPdf() {
        const registeredResources = this.sharedService?.externalRegisteredResources || [];
        const pdfService = registeredResources.find((service) => service.pluginName === 'PdfExportService');
        if (pdfService?.exportToPdf) {
            pdfService.exportToPdf();
        }
        else {
            console.error(`[Slickgrid-Universal] You must register the PdfExportService to properly use Export to PDF in the Grid Menu. Example:: this.gridOptions = { enablePdfExport: true, externalResources: [new PdfExportService()] };`);
        }
    }
    exportTextDelimited() {
        const registeredResources = this.sharedService?.externalRegisteredResources || [];
        // prettier-ignore
        const exportTxtService = registeredResources.find((service) => service.pluginName === 'TextExportService');
        if (exportTxtService?.exportToFile) {
            exportTxtService.exportToFile({ delimiter: '\t', format: 'txt' });
        }
        else {
            console.error(`[Slickgrid-Universal] You must register the TextExportService to properly use Export to File in the Grid Menu. Example:: this.gridOptions = { enableTextExport: true, externalResources: [new TextExportService()] };`);
        }
    }
    toggleFilterBar() {
        let showHeaderRow = this.gridOptions?.showHeaderRow ?? false;
        showHeaderRow = !showHeaderRow; // inverse show header flag
        this.grid.setHeaderRowVisibility(showHeaderRow);
        // when displaying header row, we'll call "setColumns" which in terms will recreate the header row filters
        if (showHeaderRow === true) {
            this.grid.updateColumns();
            this.grid.scrollColumnIntoView(0); // quick fix to avoid filter being out of sync with horizontal scroll
        }
    }
    /** @return default Grid Menu options */
    getDefaultGridMenuOptions() {
        return {
            commandTitle: undefined,
            columnTitle: this.extensionUtility.getPickerTitleOutputString('columnTitle', 'gridMenu'),
            forceFitTitle: this.extensionUtility.getPickerTitleOutputString('forceFitTitle', 'gridMenu'),
            syncResizeTitle: this.extensionUtility.getPickerTitleOutputString('syncResizeTitle', 'gridMenu'),
            iconCssClass: 'mdi mdi-menu',
            menuWidth: 12,
            commandItems: [],
            hideClearAllFiltersCommand: false,
            hideRefreshDatasetCommand: false,
            hideToggleFilterCommand: false,
        };
    }
    /** Returns the CSS selectors for all focusable menu items (commands and column picker), skipping dividers/disabled/hidden. */
    getFocusableMenuSelectors() {
        return [
            '.slick-menu-command-list[role="menu"] > li[role="menuitem"]:not(.slick-menu-item-divider):not(.disabled):not(.slick-menu-item-hidden)',
            '.slick-column-picker-list[role="menu"] > li:not(.hidden)',
        ];
    }
    /** Returns all focusable menu items in DOM order, skipping hidden/disabled/dividers. */
    getAllFocusableMenuItems() {
        const selectors = this.getFocusableMenuSelectors();
        return Array.from(this._menuElm?.querySelectorAll(selectors.join(',')) ?? []).filter((item) => item.offsetParent !== null);
    }
    /** Mouse down handler when clicking anywhere in the DOM body */
    handleBodyMouseDown(e) {
        if (this.menuElement) {
            let isMenuClicked = false;
            const parentMenuElm = e.target.closest(`.${this.menuCssClass}`);
            // did we click inside the menu or any of its sub-menu(s)
            if (this.menuElement.contains(e.target) || parentMenuElm) {
                isMenuClicked = true;
            }
            if ((this._isMenuOpen && this.menuElement !== e.target && !isMenuClicked && !e.defaultPrevented) ||
                (e.target.className === 'close' && parentMenuElm)) {
                this.hideMenu(e);
            }
        }
    }
    stopFullBubbling(e) {
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
    }
    handleKeyDown(e) {
        if (e.key === 'Enter' || e.key === ' ') {
            this.stopFullBubbling(e);
            this.showGridMenu(e);
        }
        else if (e.key === 'Tab') {
            this.stopFullBubbling(e);
            if (e.shiftKey) {
                // Shift+Tab from Grid Menu button: focus last visible column header menu button
                this.grid.focusHeaderMenuOrColumn(this.getVisibleColumns().length - 1);
            }
            else {
                this.grid.focusHeaderRowFilter();
            }
        }
    }
    handleMenuItemCommandClick(event, _type, item, level = 0) {
        if (item !== 'divider' && !item.disabled && !item.divider) {
            const command = item.command || '';
            if (command && !item.commandItems) {
                const callbackArgs = {
                    grid: this.grid,
                    command: item.command,
                    item,
                    allColumns: this.columns,
                    visibleColumns: this.getVisibleColumns(),
                };
                // execute Grid Menu callback with command,
                // we'll also execute optional user defined onCommand callback when provided
                this.pubSubService.publish('onGridMenuCommand', callbackArgs);
                if (typeof this._addonOptions?.onCommand === 'function') {
                    this._addonOptions.onCommand(event, callbackArgs);
                }
                this.onCommand.notify(callbackArgs, null, this);
                // execute action callback when defined
                if (typeof item.action === 'function') {
                    item.action.call(this, event, callbackArgs);
                }
                // does the user want to leave open the Grid Menu after executing a command?
                if (!this._addonOptions?.leaveOpen && !event.defaultPrevented) {
                    this.hideMenu(event);
                }
                // Stop propagation so that it doesn't register as a header click event.
                this.stopFullBubbling(event);
            }
            else if (item.commandItems) {
                this.repositionSubMenu(event, item, level);
            }
        }
    }
    handleMenuItemMouseOver(e, _type, item, level = 0) {
        if (item !== 'divider' && !item.disabled && !item.divider) {
            if (item.commandItems) {
                this.repositionSubMenu(e, item, level);
            }
            else if (level === 0) {
                this.disposeSubMenus();
            }
        }
    }
    /** Re/Create Command List by adding title, close & list of commands */
    recreateCommandList(triggeredByElm, commandItems, menuElm, callbackArgs, item) {
        // -- Command List section
        const level = callbackArgs.level || 0;
        if (commandItems.length > 0) {
            const commandMenuElm = createDomElement('div', { className: `${this._menuCssPrefix}-command-list`, role: 'menu' }, menuElm);
            if (level === 0) {
                this.populateCommandOrOptionTitle('command', this.addonOptions, commandMenuElm, level);
                // prettier-ignore
                const commandMenuHeaderElm = menuElm.querySelector(`.slick-command-header`) ?? createDomElement('div', { className: 'slick-command-header' });
                commandMenuHeaderElm.classList.add('with-close');
                addCloseButtomElement.call(this, commandMenuHeaderElm);
                commandMenuElm.appendChild(commandMenuHeaderElm);
            }
            // when creating sub-menu also add its sub-menu title when exists
            if (item && level > 0) {
                this.addSubMenuTitleWhenExists(item, commandMenuElm); // add sub-menu title when exists
            }
            this.populateCommandOrOptionItems('command', this._addonOptions, commandMenuElm, commandItems, callbackArgs, triggeredByElm, this.handleMenuItemCommandClick, this.handleMenuItemMouseOver);
            return commandMenuElm;
        }
        return null;
    }
    repositionSubMenu(e, item, level) {
        // creating sub-menu, we'll also pass level & the item object since we might have "subMenuTitle" to show
        const commandItems = item?.commandItems || [];
        const subMenuElm = this.createCommandMenu(e.target, commandItems, level + 1, item);
        subMenuElm.style.display = 'block';
        document.body.appendChild(subMenuElm);
        this.repositionMenu(e, subMenuElm, this._gridMenuButtonElm, this._addonOptions);
    }
}
//# sourceMappingURL=slickGridMenu.js.map