"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UITreeDropdown = exports.EdgePosition = exports.UIDirectionalHint = void 0;
const react_1 = __importDefault(require("react"));
const uuid_1 = __importDefault(require("uuid"));
const react_2 = require("@fluentui/react");
Object.defineProperty(exports, "UIDirectionalHint", { enumerable: true, get: function () { return react_2.DirectionalHint; } });
const UIInput_1 = require("../UIInput");
const UIContextualMenu_1 = require("../UIContextualMenu");
const UIIconButton_1 = require("../UIButton/UIIconButton");
const Icons_1 = require("../Icons");
const ValidationMessage_1 = require("../../helper/ValidationMessage");
require("./UITreeDropdown.scss");
const SELECTOR_CLASSES = {
    callout: 'ui-tree-callout',
    scrollArea: 'ms-ContextualMenu-container',
    splitButton: 'ms-ContextualMenu-splitMenu'
};
var EdgePosition;
(function (EdgePosition) {
    EdgePosition["First"] = "First";
    EdgePosition["Last"] = "Last";
})(EdgePosition || (exports.EdgePosition = EdgePosition = {}));
const KEYBOARD_KEYS = {
    ArrowUp: 'ArrowUp',
    ArrowDown: 'ArrowDown',
    Enter: 'Enter',
    Escape: 'Escape'
};
/**
 * UITreeDropdown component.
 *
 * @exports
 * @class UIVerticalDivider
 * @extends {React.Component<UITreeDropdownProps, UITreeDropdownState>}
 */
class UITreeDropdown extends react_1.default.Component {
    /**
     * Initializes component properties.
     *
     * @param {UITreeDropdownProps} props
     */
    constructor(props) {
        super(props);
        this.UITreeDropdownRef = react_1.default.createRef();
        this.UITreeDropdownFocusZoneRef = react_1.default.createRef();
        this.inputRef = react_1.default.createRef();
        this.submenuRefs = {};
        // Calculated offset for submenu positions
        // It is added because root menu can have scrollbar - in result root menu's items width is smaller than root menu.
        // In such case(when scrollbar) submenu is positioned near to expand/hovered item and position is not on the edge of root menu.
        // Using offset/margin we can do corrections to position of submenu and place it one the edge of root menu.
        this.submenuOffset = 0;
        this.lastKeyDown = '';
        this.componentDidMount = () => {
            if (this.props.items.length) {
                this.buildItems(this.props.items);
                this.setState({ isDisabled: false });
            }
        };
        this.componentDidUpdate = (prevProps) => {
            if (this.props.items.length !== prevProps.items.length) {
                this.setState({ isHidden: true });
                this.setState({ isDisabled: this.props.items.length ? false : true });
                this.buildItems(this.props.items);
            }
            if (this.props.value !== prevProps.value) {
                this.setState({ value: this.props.value });
            }
            // Calculate size for submenu offset
            this.calculateSubmenuOffset();
        };
        /**
         * Map the payload.
         *
         * @param {ItemsProps[]} items
         */
        this.buildItems = (items) => {
            if (this.state.items.length !== items.length) {
                items = items.map(this.buildSubItems);
                const mapedItems = this.mapValuesToContextMenu(items);
                this.setState({
                    originalItems: mapedItems,
                    items: mapedItems
                });
            }
        };
        /**
         * Sub items values and style.
         *
         * @param {ItemsProps} item
         * @returns {ItemsProps}
         */
        this.buildSubItems = (item) => {
            if (item.children?.length) {
                item.children = item.children.map((el) => {
                    const regex = new RegExp(item.value, 'ig');
                    const value = el.value.search(regex) === -1 ? `${item.value}${this.state.valueSeparator}${el.value}` : el.value;
                    return {
                        ...el,
                        split: false,
                        value
                    };
                });
            }
            return item;
        };
        /**
         * Map GD payload to ContextMenu payload.
         *
         * @param {ItemsProps[]} items
         * @param {number} level
         * @returns {IContextualMenuItem[]}
         */
        this.mapValuesToContextMenu = (items, level = 0) => {
            return items.map((item) => {
                if (item.children?.length) {
                    item.split = true;
                    const refId = this.getRefId(item.value, level);
                    if (!this.submenuRefs[refId]) {
                        this.submenuRefs[refId] = react_1.default.createRef();
                    }
                    item.subMenuProps = {
                        componentRef: this.submenuRefs[refId],
                        items: this.mapValuesToContextMenu(item.children, level + 1),
                        focusZoneProps: {
                            handleTabKey: react_2.FocusZoneTabbableElements.none,
                            onFocus: () => {
                                const openerItem = this.defaultSubmenuFocus?.parent;
                                if (openerItem && openerItem.item.value === item.value && openerItem.level === level) {
                                    this.focusItemWithValue(this.state.value, this.defaultSubmenuFocus?.parent?.item.subMenuProps?.items);
                                }
                            }
                        }
                    };
                }
                return {
                    ...item,
                    key: item.value,
                    text: item.label,
                    className: 'ui-tree-dropdown-list-item',
                    onClick: () => this.handleSelection(item.value),
                    onRenderContent: this.handleRenderContent
                };
            });
        };
        /**
         * Handle the selected value.
         *
         * @param {string} value
         */
        this.handleSelection = (value) => {
            this.hasSelected = true;
            this.setState({ value: value, valueChanged: false }, () => this.props.onParameterValueChange(value));
        };
        /**
         * Handle the keypress value.
         *
         * @param {React.KeyboardEvent<HTMLInputElement>} event
         */
        this.handleKeyPress = (event) => {
            switch (event.key) {
                case 'Enter':
                    if (!this.state.isMenuOpen) {
                        this.toggleMenu(false, event);
                    }
                    else {
                        this.setState({ valueChanged: true });
                        this.handleSelection(this.state.value ? this.state.value : '');
                    }
                    break;
                case 'ArrowDown':
                    if (!this.state.isMenuOpen) {
                        // Open dropdown contextMenu if closed
                        this.toggleMenu(false, event);
                    }
                    else {
                        this.focusDropdown(event, event.key);
                    }
                    break;
                case 'Tab':
                    if (this.state.isMenuOpen) {
                        // Close Dropdown if open
                        this.toggleMenu(true);
                    }
                    this.handleSelection(this.state.value ? this.state.value : '');
                    break;
                default: {
                    // do nothing
                }
            }
            this.lastKeyDown = event.key;
        };
        /**
         * Handle ContextMenu focus.
         *
         * @param {React.KeyboardEvent<HTMLInputElement>} event
         * @param {string} key
         */
        this.focusDropdown = (event, key) => {
            if (this.UITreeDropdownFocusZoneRef) {
                if (key === KEYBOARD_KEYS.Enter) {
                    this.focusItemWithValue(this.state.value, this.state.items);
                }
                else {
                    this.UITreeDropdownFocusZoneRef.current?.focus(true);
                }
                // disable scroll which sometimes triggers
                event.preventDefault();
            }
        };
        /**
         * Custom handle the render from subMenu to control the highlight and the .is-selected.
         *
         * @param {IContextualMenuListProps} props
         * @param {IContextualMenuItemRenderFunctions} defaultRenders
         * @returns { React.ReactNode | null}
         */
        this.handleRenderContent = (props, defaultRenders) => {
            props.item.className = props.item.value === this.props.value ? 'is-selected' : '';
            props.item.text = this.highlightQuery(props.item.label, this.state.query);
            this.applySubmenuPosition(props.item);
            return defaultRenders ? defaultRenders.renderItemName(props) : null;
        };
        /**
         * Custom handle the render to control the highlight and the .is-selected.
         *
         * @param {IContextualMenuListProps} props
         * @param {IRenderFunction<IContextualMenuListProps>} defaultRender
         * @returns {JSX.Element | null}
         */
        this.handleRenderMenuList = (props, defaultRender) => {
            let mappedItems = [];
            if (props?.items) {
                mappedItems = props.items.map((item) => {
                    item.className = item.value === this.props.value ? 'is-selected' : '';
                    item.text = this.highlightQuery(item.label, this.state.query);
                    this.applySubmenuPosition(item);
                    item.subMenuProps?.items.map((subItem) => {
                        subItem.className = subItem.value === this.props.value ? 'is-selected' : '';
                        return subItem;
                    });
                    return { ...item };
                });
            }
            return defaultRender ? defaultRender({ ...props, items: mappedItems }) : null;
        };
        /**
         * Handle on/off ContextualMenu.
         *
         * @param {boolean} status
         * @param {React.KeyboardEvent<HTMLInputElement>} event
         */
        this.toggleMenu = (status, event) => {
            if (this.props.readOnly) {
                return;
            }
            this.setState({
                isHidden: status
            });
            const key = event?.key;
            //select first item after contextMenu is opened
            if (event) {
                setTimeout(() => {
                    event.persist();
                    this.focusDropdown(event, key);
                }, 0);
            }
        };
        /**
         * Highlight the search string.
         *
         * @param {string} text
         * @param {string} query
         * @returns {JSX.Element}
         */
        this.highlightQuery = (text, query) => {
            return react_1.default.createElement(UIContextualMenu_1.UIHighlightMenuOption, { text: text, query: query });
        };
        /**
         * Filter all options that match the query string.
         *
         * @param {string} input
         * @param {IContextualMenuItem} item
         * @returns {boolean}
         */
        this.filterElement = (input, item) => {
            const regex = new RegExp(input, 'ig');
            if (item?.children?.length) {
                return item.children.filter((item) => this.filterElement(input, item)).length > 0;
            }
            if (item?.value) {
                return item.value.search(regex) !== -1;
            }
            return false;
        };
        /**
         * Update the query string and the prop value.
         *
         * @param {React.FormEvent<HTMLInputElement | HTMLTextAreaElement>} event
         */
        this.handleOnChangeValue = (event) => {
            const query = event.target;
            this.hasSelected = false;
            this.setState((prevState) => ({
                value: query.value,
                items: prevState.originalItems.filter((item) => this.filterElement(query.value, item)),
                query: query.value,
                valueChanged: true
            }));
            if (!this.state.isMenuOpen) {
                this.toggleMenu(false);
            }
        };
        /**
         * Method reset states.
         *
         * @param {Event | React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent} event
         */
        this.handleDismiss = (event) => {
            if (event && 'key' in event && event.key === KEYBOARD_KEYS.Escape) {
                this.resetValue();
            }
            else if (!this.hasSelected) {
                this.props.onParameterValueChange('');
            }
            this.setState((prevState) => ({
                items: prevState.originalItems,
                query: ''
            }));
            this.toggleMenu(true);
            this.originalValue = undefined;
        };
        /**
         * Method applies additional styling for submnu callout.
         * It is used to apply scroll width offset - in result submenu should be displayed on the edge of root menu.
         *
         * @param {IContextualMenuItem} item Context menu item.
         */
        this.applySubmenuPosition = (item) => {
            if (item.subMenuProps?.items) {
                if (!item.subMenuProps.calloutProps) {
                    item.subMenuProps.calloutProps = {};
                }
                item.subMenuProps.calloutProps.styles = {
                    root: {
                        marginLeft: this.submenuOffset
                    }
                };
            }
        };
        this.getCalloutDomRef = (submenu = false) => {
            const menuLayerClass = `${SELECTOR_CLASSES.callout}${this.state.uiidInput}`;
            const callout = document.querySelector(`.${menuLayerClass}`);
            return submenu && callout ? callout.nextSibling : callout;
        };
        /**
         * Method calculates offset size for submenus.
         * Calculated offset should be used to position submenu right to edge of root menu.
         *  - Detects if scrollbar exists.
         *  - Calculates size of scrollbar and stores it as value for offset.
         */
        this.calculateSubmenuOffset = () => {
            const callout = this.getCalloutDomRef();
            if (callout) {
                const scrollContainer = callout.querySelector(`.${SELECTOR_CLASSES.scrollArea}`);
                this.submenuOffset = 0;
                if (scrollContainer && scrollContainer.scrollHeight > scrollContainer.clientHeight) {
                    this.submenuOffset = scrollContainer.offsetWidth - scrollContainer.clientWidth;
                }
            }
        };
        /**
         * Method updates state, if focus visible, using arrow keys.
         *
         * @param {HTMLElement|React.FocusEvent<HTMLElement>} ev
         */
        this.onFocusElementChanged = (ev) => {
            const menuOption = ev.getElementsByClassName('ts-Menu-option');
            const isFocusVisible = document.getElementsByClassName('ms-Fabric--isFocusVisible');
            if (isFocusVisible.length > 0 && menuOption.length > 0) {
                this.setState({
                    value: ev.value ? ev.value : menuOption[0].innerText,
                    valueChanged: true
                });
            }
        };
        /**
         * Method handles window keydown event.
         * 1. Stores last keyboard pressed event.
         * 2. Disables CircularNavigation for menus.
         *
         * @param {KeyboardEvent | React.KeyboardEvent<HTMLInputElement>} event
         */
        this.onWindowKeyDown = (event) => {
            this.lastKeyDown = event.key;
            // Avoid circular navigation
            const activeElement = document.activeElement;
            if ([KEYBOARD_KEYS.ArrowDown, KEYBOARD_KEYS.ArrowUp].includes(event.key) && activeElement) {
                // Disable CircularNavigation
                // There is property in focusZoneProps, but it is overwritten by fluent ui and we can not change it from outside
                const positions = this.getEdgePosition(activeElement);
                const fromFirst = positions.includes(EdgePosition.First) && event.key === KEYBOARD_KEYS.ArrowUp;
                const fromLast = positions.includes(EdgePosition.Last) && event.key === KEYBOARD_KEYS.ArrowDown;
                if (fromFirst || fromLast) {
                    // Circular navigation case.
                    // Check if first item focused in root menu.
                    if (fromFirst && activeElement.closest(`.${SELECTOR_CLASSES.callout}${this.state.uiidInput}`)) {
                        // Focus input field if navigation triggered from first item using ArrowUp
                        this.inputRef.current?.focus();
                    }
                    event.stopPropagation();
                    event.preventDefault();
                }
            }
        };
        /**
         * Method handles focus logic if arrow key was pressed.
         *
         * @param {FocusEvent} event
         */
        this.handleCustomDownKey = (event) => {
            if (this.lastKeyDown.includes('Arrow')) {
                this.lastKeyDown = '';
                this.onFocusElementChanged(event.target);
            }
        };
        /**
         * Method appends custom keydown and focus event listeners when context menu is opened.
         */
        this.applyCustomKeyDownHandlingEvents = () => {
            window.addEventListener('keydown', this.onWindowKeyDown, true);
            window.addEventListener('focus', this.handleCustomDownKey, true);
        };
        /**
         * Method removes custom keydown and focus event listeners when context menu is dismissed.
         */
        this.removeCustomKeyDownHandlingEvents = () => {
            window.removeEventListener('keydown', this.onWindowKeyDown, true);
            window.removeEventListener('focus', this.handleCustomDownKey, true);
        };
        /**
         * Method receives any menu child element and returns edge positions if item is first or last in rendered menu.
         *
         * @param {Element} itemElement Item's DOM to check position.
         * @returns {EdgePosition[]} Returns positions if element is first or last in menu - also can be both.
         */
        this.getEdgePosition = (itemElement) => {
            const container = itemElement.closest('ul');
            const item = itemElement.closest('li');
            const position = [];
            if (container && item) {
                if (container.children[0] === item) {
                    position.push(EdgePosition.First);
                }
                if (container.children[container.children.length - 1] === item) {
                    position.push(EdgePosition.Last);
                }
            }
            return position;
        };
        /**
         * Recursive method finds menu item info object in tree menu items by passed value/key of item.
         *
         * @param {string} [value] Value/key of item.
         * @param {IContextualMenuItem[]} [items] Menu items.
         * @param {TreeItemInfo} [parent] Item's parent object.
         * @param {number} [level] Level of item in tree structure.
         * @returns {TreeItemInfo | undefined} Found menu item.
         */
        this.findItemByValue = (value, items = [], parent, level = 0) => {
            let selectedItem;
            for (let i = 0; i < items.length; i++) {
                const item = items[i];
                const selectedItemTemp = {
                    item,
                    index: i,
                    parent,
                    level
                };
                if (item.value === value) {
                    selectedItem = selectedItemTemp;
                }
                else if (item.subMenuProps?.items?.length) {
                    selectedItem = this.findItemByValue(value, item.subMenuProps.items, selectedItemTemp, level + 1);
                }
                if (selectedItem) {
                    break;
                }
            }
            return selectedItem;
        };
        /**
         * Method finds DOM node of menu item based on received item object and container DOM.
         *
         * @param {HTMLElement} container Menu container DOM.
         * @param {TreeItemInfo} item Menu item info object.
         * @returns {HTMLElement | undefined} Found DOM element of item.
         */
        this.getItemTarget = (container, item) => {
            let itemDom;
            const listDom = container.querySelector('.ms-ContextualMenu-list');
            if (listDom?.childNodes[item.index]) {
                const listItemDom = listDom.childNodes[item.index];
                const itemElement = listItemDom.firstChild;
                itemDom = itemElement;
            }
            return itemDom;
        };
        /**
         * Method focuses context menu item based on recieved value/key and menude data(items and hoisted object).
         * Method works with any level menu.
         *
         * @param {string} [value] Value/key of item.
         * @param {IContextualMenuItem[]} [items] Target menu items.
         */
        this.focusItemWithValue = (value, items = []) => {
            const selectedItem = this.findItemByValue(value, items);
            const callout = this.getCalloutDomRef(!!this.defaultSubmenuFocus);
            this.defaultSubmenuFocus = undefined;
            if (selectedItem && callout) {
                let itemDom;
                if (selectedItem.parent) {
                    // Item is in next level of menu - we need open submenu
                    const parentItemDom = this.getItemTarget(callout, selectedItem.parent);
                    if (parentItemDom) {
                        this.defaultSubmenuFocus = selectedItem;
                        parentItemDom.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 39, which: 39, bubbles: true }));
                    }
                }
                else {
                    // Item is in first level
                    itemDom = this.getItemTarget(callout, selectedItem);
                }
                // Focus target item or focus container while submenu is not opened
                if (itemDom) {
                    itemDom.focus();
                }
                else {
                    const menuContainer = callout.querySelector(`.${SELECTOR_CLASSES.scrollArea}`);
                    menuContainer?.focus();
                }
            }
        };
        /**
         * Generate unique id for menu component references.
         *
         * @param {string} value Value of item.
         * @param {number} level Level of item in tree structure.
         * @returns {string} Id containing value andf level in format "${value}__${level}".
         */
        this.getRefId = (value, level) => {
            return `${value}__${level}`;
        };
        this.state = {
            query: '',
            // value has to be set, otherwise react treats this as "uncontrolled" component
            // and displays warnings when value is set later on
            value: this.props.value ?? '',
            isHidden: true,
            originalItems: [],
            items: [],
            valueSeparator: this.props.valueSeparator ?? '.',
            uiidInput: uuid_1.default.v4(),
            isDisabled: !this.props.items.length,
            isMenuOpen: false,
            valueChanged: false
        };
        this.hasSelected = !!this.props.value;
        this.toggleMenu = this.toggleMenu.bind(this);
        this.onWindowKeyDown = this.onWindowKeyDown.bind(this);
        this.handleCustomDownKey = this.handleCustomDownKey.bind(this);
        // Suppress icon warnings, as they are irrelevant
        (0, react_2.setIconOptions)({ disableWarnings: true });
    }
    /**
     * Method resets value of dropdown input to original value, which was stored after open.
     */
    resetValue() {
        this.setState({
            value: this.originalValue,
            valueChanged: false
        });
    }
    /**
     * Method returns class names for wrapper element depending on props and component state.
     *
     * @returns {string} Class names of wrapper element.
     */
    getClassNames() {
        let classNames = `ui-treeDropdown-wrapper ui-treeDropdown-wrapper-menu${this.state.isMenuOpen ? '-open' : '-close'} ui-treeDropdown-wrapper-${this.state.uiidInput}`;
        if (this.state.isDisabled) {
            classNames += ' disabled';
        }
        if (this.props.readOnly) {
            classNames += ' readonly';
        }
        return classNames;
    }
    /**
     * @returns {JSX.Element}
     */
    render() {
        const messageInfo = (0, ValidationMessage_1.getMessageInfo)(this.props);
        let useTargetWidth = true;
        if (this.props.useTargetWidth) {
            useTargetWidth = false;
        }
        return (react_1.default.createElement("div", { className: `ui-treeDropdown ui-treeDropDown-${this.state.uiidInput} ${this.props.label ? 'ui-treeDropdown-with-label' : ''}` },
            this.props.label && (react_1.default.createElement("label", { className: `${this.props.required ? 'required' : ''} ${this.state.isDisabled ? 'disabled' : ''}` }, this.props.label)),
            react_1.default.createElement("div", { className: this.getClassNames() },
                react_1.default.createElement(UIInput_1.UITextInput, { ...this.props, componentRef: this.inputRef, disabled: this.state.isDisabled, readOnly: this.props.readOnly, autoComplete: "off", value: this.state.value, placeholder: this.props.placeholderText, onKeyDown: this.handleKeyPress, onChange: this.handleOnChangeValue, onClick: () => {
                        this.toggleMenu(false);
                    }, onFocus: (event) => {
                        // Select the text of the input
                        event.target.select();
                    }, errorMessage: messageInfo.message }),
                react_1.default.createElement(UIIconButton_1.UIIconButton, { tabIndex: -1, allowDisabledFocus: true, className: "ui-treeDropdown-caret", iconProps: { iconName: Icons_1.UiIcons.ArrowDown }, onClick: () => {
                        if (this.state.isHidden) {
                            // Menu would become visible - focus input
                            this.inputRef.current?.focus();
                        }
                        this.toggleMenu(!this.state.isHidden);
                    } })),
            !this.state.isHidden && (react_1.default.createElement(UIContextualMenu_1.UIContextualMenu, { componentRef: this.UITreeDropdownRef, onRenderMenuList: this.handleRenderMenuList, className: "ui-treeDropDown-context-menu", target: `.ui-treeDropDown-${this.state.uiidInput}`, onMenuOpened: () => {
                    this.originalValue = this.state.value;
                    this.applyCustomKeyDownHandlingEvents();
                    this.setState({
                        isMenuOpen: true
                    });
                }, onMenuDismissed: () => {
                    this.removeCustomKeyDownHandlingEvents();
                    this.setState({ isMenuOpen: false });
                    if (this.state.valueChanged) {
                        this.handleSelection(this.state.value ? this.state.value : '');
                    }
                }, useTargetWidth: useTargetWidth, useTargetAsMinWidth: true, onRestoreFocus: (params) => {
                    params.originalElement?.focus();
                }, shouldUpdateWhenHidden: true, items: this.state.items, onDismiss: this.handleDismiss, shouldFocusOnContainer: false, focusZoneProps: {
                    componentRef: this.UITreeDropdownFocusZoneRef,
                    handleTabKey: react_2.FocusZoneTabbableElements.none,
                    isCircularNavigation: false
                }, shouldFocusOnMount: false, directionalHint: this.props.directionalHint, calloutProps: {
                    layerProps: {
                        className: `${SELECTOR_CLASSES.callout}${this.state.uiidInput}`
                    },
                    onLayerMounted: () => {
                        this.calculateSubmenuOffset();
                    }
                }, styles: {
                    container: {
                        maxHeight: 192,
                        overflowY: 'auto'
                    }
                }, maxWidth: this.props.maxWidth }))));
    }
}
exports.UITreeDropdown = UITreeDropdown;
//# sourceMappingURL=UITreeDropdown.js.map