import React, { Component } from 'react';
import Input from '../Input';
import omit from 'omit';
import DropdownLayout from '../DropdownLayout/DropdownLayout';
import { st, classes } from './InputWithOptions.st.css.js';
import uniqueId from 'lodash/uniqueId';
import HighlightContext from './HighlightContext';
import PopoverNext from '../PopoverNext/PopoverNext';
import { WixDesignSystemContext } from '../WixDesignSystemProvider/context';
import Drawer from '../Drawer';
export const DEFAULT_VALUE_PARSER = option => typeof option.value === 'string' ? option.value : option.label;
const INPUT_WITH_OPTIONS_DOUBLE_CLICK_THRESHOLD = 2000;
export const DEFAULT_POPOVER_PROPS = {
    appendTo: 'parent',
    flip: false,
    fixed: true,
    placement: 'bottom',
    width: '100%',
    minWidth: 192,
    onMouseEnter: undefined,
    onMouseLeave: undefined,
};
class InputWithOptions extends Component {
    // Abstraction
    inputClasses() { }
    dropdownClasses() { }
    dropdownAdditionalProps() { }
    inputAdditionalProps() { }
    rootAdditionalProps() {
        return {};
    }
    /**
     * An array of key codes that act as manual submit. Will be used within
     * onKeyDown(event).
     *
     * @returns {KeyboardEvent.key[]}
     */
    getManualSubmitKeys() {
        return ['Enter', 'Tab'];
    }
    constructor(props) {
        super(props);
        this.input = React.createRef();
        this._onOptionMarked = (option, optionElementId) => {
            const { onOptionMarked } = this.props;
            this.setState({ activeDescendentId: optionElementId });
            if (onOptionMarked) {
                onOptionMarked(option, optionElementId);
            }
        };
        /** Checks if focus event is related to selecting an option */
        this._didSelectOption = event => {
            const focusedElement = event && event.relatedTarget;
            const dropdownContainer = this.dropdownLayout && this.dropdownLayout.containerRef.current;
            // Check if user has focused other input component
            const isInput = focusedElement instanceof HTMLInputElement;
            if (!focusedElement || !dropdownContainer || isInput) {
                return false;
            }
            const isInDropdown = dropdownContainer.contains(focusedElement);
            // Returns true if element is the dropdown container or is inside of it
            return isInDropdown;
        };
        /**
         * Clears the input.
         *
         * @param event delegated to the onClear call
         */
        this.clear = event => {
            this.input.current && this.input.current.clear(event);
        };
        this.state = {
            inputValue: props.value || '',
            showOptions: false,
            lastOptionsShow: 0,
            isEditing: false,
        };
        this.uniqueId = uniqueId('InputWithOptions');
        this._onSelect = this._onSelect.bind(this);
        this._onFocus = this._onFocus.bind(this);
        this._onBlur = this._onBlur.bind(this);
        this._onChange = this._onChange.bind(this);
        this._onKeyDown = this._onKeyDown.bind(this);
        this.focus = this.focus.bind(this);
        this.blur = this.blur.bind(this);
        this.select = this.select.bind(this);
        this.hideOptions = this.hideOptions.bind(this);
        this.showOptions = this.showOptions.bind(this);
        this._onManuallyInput = this._onManuallyInput.bind(this);
        this._renderDropdownLayout = this._renderDropdownLayout.bind(this);
        this.isDropdownLayoutVisible = this.isDropdownLayoutVisible.bind(this);
        this._onInputClicked = this._onInputClicked.bind(this);
        this._onOpenChange = this._onOpenChange.bind(this);
        this.closeOnSelect = this.closeOnSelect.bind(this);
        this.onCompositionChange = this.onCompositionChange.bind(this);
    }
    componentDidUpdate(prevProps, prevState) {
        if (!this.props.showOptionsIfEmptyInput &&
            ((!prevProps.value && this.props.value) ||
                (!prevState.inputValue && this.state.inputValue))) {
            this.showOptions();
        }
        // Clear value in controlled mode
        if (prevProps.value !== this.props.value && this.props.value === '') {
            this.setState({ inputValue: '' });
        }
    }
    onCompositionChange(isComposing) {
        this.setState({ isComposing });
    }
    renderInput(mobile) {
        const inputAdditionalProps = this.inputAdditionalProps();
        const inputProps = Object.assign(omit([
            'onChange',
            'dataHook',
            'dropDirectionUp',
            'focusOnSelectedOption',
            'onClose',
            'onSelect',
            'onOptionMarked',
            'overflow',
            'visible',
            'options',
            'selectedId',
            'tabIndex',
            'fixedHeader',
            'fixedFooter',
            'maxHeightPixels',
            'minWidthPixels',
            'withArrow',
            'closeOnSelect',
            'onMouseEnter',
            'onMouseLeave',
            'itemHeight',
            'selectedHighlight',
            'inContainer',
            'infiniteScroll',
            'loadMore',
            'hasMore',
            'markedOption',
            'className',
        ], this.props), inputAdditionalProps);
        const { inputElement } = inputProps;
        return React.cloneElement(inputElement, {
            menuArrow: true,
            ref: this.input,
            ariaExpanded: this.state.showOptions,
            ariaControls: `${this.uniqueId}-listbox`,
            ariaActivedescendant: this.state.activeDescendentId,
            ...inputProps,
            onChange: this._onChange,
            onInputClicked: event => this._onInputClicked(event, mobile),
            onFocus: this._onFocus,
            onBlur: this._onBlur,
            onCompositionChange: this.onCompositionChange,
            width: inputElement.props.width,
            textOverflow: this.props.textOverflow || inputElement.props.textOverflow,
            tabIndex: this.props.native ? -1 : 0,
        });
    }
    isDropdownLayoutVisible() {
        return (this.state.showOptions &&
            (this.props.showOptionsIfEmptyInput || this.state.inputValue.length > 0));
    }
    _renderDropdownLayout() {
        const { highlight, value } = this.props;
        const inputPropNames = [
            'children',
            'dataHook',
            'className',
            'id',
            'role',
            'ariaControls',
            'ariaDescribedby',
            'ariaLabel',
            'autoFocus',
            'autoSelect',
            'autocomplete',
            'defaultValue',
            'disabled',
            'status',
            'statusMessage',
            'statusMessageTooltipProps',
            'hideStatusSuffix',
            'forceFocus',
            'forceHover',
            'maxLength',
            'menuArrow',
            'clearButton',
            'focusOnClearClick',
            'name',
            'border',
            'noLeftBorderRadius',
            'noRightBorderRadius',
            'onBlur',
            'onChange',
            'onClear',
            'onCompositionChange',
            'onEnterPressed',
            'onEscapePressed',
            'onFocus',
            'onInputClicked',
            'onKeyDown',
            'onKeyUp',
            'onPaste',
            'onCopy',
            'placeholder',
            'prefix',
            'readOnly',
            'disableEditing',
            'rtl',
            'size',
            'suffix',
            'textOverflow',
            'tooltipPlacement',
            'type',
            'value',
            'withSelection',
            'required',
            'min',
            'max',
            'step',
            'customInput',
            'pattern',
            'inputRef',
            'inputmode',
            'ariaRoledescription',
            'clearButtonTooltipContent',
            'clearButtonTooltipProps',
            'clearButtonAriaLabel',
            'inputElementRef',
        ];
        const inputOnlyProps = inputPropNames.filter(k => k !== 'tabIndex');
        const dropdownProps = Object.assign(omit(inputOnlyProps.concat(['dataHook', 'onClickOutside']), this.props), this.dropdownAdditionalProps());
        const customStyle = {
            marginLeft: this.props.dropdownOffsetLeft,
            width: 'inherit',
        };
        return (React.createElement("div", { className: `${this.uniqueId} ${this.dropdownClasses()}`, style: customStyle, "data-hook": "dropdown-layout-wrapper" },
            React.createElement(HighlightContext.Provider, { value: { highlight, match: value } },
                React.createElement(DropdownLayout, { ref: dropdownLayout => {
                        this.dropdownLayout = dropdownLayout;
                    }, ...dropdownProps, dataHook: "inputwithoptions-dropdownlayout", visible: true, className: classes.dropdownLayout, onClose: this.hideOptions, onSelect: this._onSelect, onOptionMarked: this._onOptionMarked, isComposing: this.state.isComposing, listboxId: `${this.uniqueId}-listbox`, inContainer: true, tabIndex: -1, onDrillOut: () => this.focus() }))));
    }
    _renderNativeSelect() {
        const { options, onSelect, disabled } = this.props;
        return (React.createElement("div", { className: classes.nativeSelectWrapper },
            this.renderInput(),
            React.createElement("select", { disabled: disabled, "data-hook": "native-select", className: classes.nativeSelect, onChange: event => {
                    this._onChange(event);
                    // In this case we don't use DropdownLayout so we need to invoke `onSelect` manually
                    onSelect(options[event.target.selectedIndex]);
                } }, options.map((option, index) => (React.createElement("option", { "data-hook": `native-option-${option.id}`, "data-index": index, key: option.id, value: option.value }, option.value))))));
    }
    render() {
        if (!!this.props.native) {
            return this._renderNativeSelect();
        }
        const { dataHook, popoverProps: { appendTo: popoverAppendTo, fixed: popoverFixed, flip: popoverFlip, placement: popoverPlacement, dynamicWidth: popoverDynamicWidth, ...restPopoverFields }, dropDirectionUp, dropdownWidth, className, } = this.props;
        const placement = dropDirectionUp ? 'top' : popoverPlacement;
        const dynamicWidth = popoverDynamicWidth ?? popoverAppendTo === 'window';
        const rootProps = this.rootAdditionalProps();
        return (React.createElement(WixDesignSystemContext.Consumer, null, ({ mobile }) => mobile && this.props.showDrawerOnMobile ? (React.createElement("div", null,
            React.createElement("div", { "data-input-parent": true, className: this.inputClasses() }, this.renderInput(mobile)),
            React.createElement(Drawer, { dataHook: dataHook, open: this.isDropdownLayoutVisible(), onClose: this._onOpenChange, zIndex: this.props?.popoverProps?.zIndex },
                React.createElement("div", { className: classes.drawerContent }, this.props.customDropdownContent ||
                    this._renderDropdownLayout())))) : (React.createElement(PopoverNext, { className: st(classes.root, { size: 'medium' }, rootProps.className ?? className), open: this.isDropdownLayoutVisible(), onOpenChange: this._onOpenChange, appendTo: popoverAppendTo ?? DEFAULT_POPOVER_PROPS.appendTo, flip: popoverFlip ?? DEFAULT_POPOVER_PROPS.flip, fixed: popoverFixed ?? DEFAULT_POPOVER_PROPS.fixed, placement: placement ?? DEFAULT_POPOVER_PROPS.placement, dynamicWidth: dynamicWidth, excludeClass: this.uniqueId, focusManagerEnabled: false, onClickOutside: this.props.onClickOutside, ...restPopoverFields, minWidth: dynamicWidth ? 'fit-content' : DEFAULT_POPOVER_PROPS.minWidth, width: dropdownWidth ??
                (dynamicWidth ? null : DEFAULT_POPOVER_PROPS.width), dataHook: dataHook, onKeyDown: this._onKeyDown },
            React.createElement(PopoverNext.Trigger, null,
                React.createElement("div", { "data-input-parent": true, className: this.inputClasses() }, this.renderInput())),
            React.createElement(PopoverNext.Content, null, this.props.customDropdownContent ||
                this._renderDropdownLayout())))));
    }
    /**
     * Shows dropdown options
     */
    showOptions() {
        if (!this.state.showOptions) {
            this.setState({ showOptions: true, lastOptionsShow: Date.now() });
            this.props.onOptionsShow && this.props.onOptionsShow();
        }
    }
    /**
     * Hides dropdown options
     */
    hideOptions() {
        if (this.state.showOptions) {
            this.setState({ showOptions: false, activeDescendentId: undefined });
            this.props.onOptionsHide && this.props.onOptionsHide();
            this.props.onClose && this.props.onClose();
        }
    }
    closeOnSelect() {
        return this.props.closeOnSelect;
    }
    get isReadOnly() {
        const { readOnly } = this.inputAdditionalProps() || {
            readOnly: this.props.readOnly,
        };
        return readOnly;
    }
    /**
     * Determine if the provided key should cause the dropdown to be opened.
     *
     * @param {KeyboardEvent.key}
     * @returns {boolean}
     */
    shouldOpenDropdown(key) {
        const openKeys = this.isReadOnly
            ? ['Enter', 'Spacebar', ' ', 'ArrowDown']
            : ['ArrowDown'];
        return openKeys.includes(key);
    }
    /**
     * Determine if the provided key should delegate the keydown event to the
     * DropdownLayout.
     *
     * @param {KeyboardEvent.key}
     * @returns {boolean}
     */
    shouldDelegateKeyDown(key) {
        return (this.isReadOnly ||
            !['Spacebar', ' '].includes(key) ||
            this.shouldPerformManualSubmit(' '));
    }
    /**
     * Determine if the provided key should cause manual submit.
     *
     * @param {KeyboardEvent.key}
     * @returns {boolean}
     */
    shouldPerformManualSubmit(key) {
        return this.getManualSubmitKeys().includes(key);
    }
    _onManuallyInput(inputValue = '') {
        if (this.state.isComposing) {
            return;
        }
        inputValue = inputValue.trim();
        const suggestedOption = this.props.options.find(element => element.value === inputValue);
        if (this.props.onManuallyInput) {
            this.props.onManuallyInput(inputValue, suggestedOption);
        }
    }
    _onSelect(option, isSelectedOption) {
        const { onSelect } = this.props;
        if (this.closeOnSelect() || isSelectedOption) {
            this._onOpenChange(false, 'select-option');
        }
        if (onSelect) {
            onSelect(this.props.highlight
                ? this.props.options.find(opt => opt.id === option.id)
                : option);
        }
    }
    _onChange(event) {
        this.setState({ inputValue: event.target.value });
        if (this.props.onChange) {
            this.props.onChange(event);
        }
        // If the input value is not empty, should show the options
        if (event.target.value.trim() && !this.props.native) {
            this.showOptions();
        }
    }
    _onInputClicked(event, mobile) {
        if (mobile) {
            this._onOpenChange(true);
        }
        this.props.onInputClicked?.(event);
    }
    _onOpenChange(open, reason, doubleClickThreshold = INPUT_WITH_OPTIONS_DOUBLE_CLICK_THRESHOLD) {
        if (this.props.disabled || this.isReadOnly) {
            return;
        }
        if (open) {
            this.showOptions();
        }
        else if (reason === 'outside-press' || reason === 'select-option') {
            this.hideOptions();
        }
        else if (Date.now() - this.state.lastOptionsShow > doubleClickThreshold) {
            this.hideOptions();
        }
    }
    _onFocus(e) {
        /** Don't call onFocus if input is already focused or is disabled
         * can occur when input is re-focused after selecting an option
         */
        if (this._focused || this.props.disabled) {
            return;
        }
        this._focused = true;
        this.setState({ isEditing: false });
        if (this.props.onFocus) {
            this.props.onFocus(e);
        }
    }
    _onBlur(event) {
        const isFocusInsideDropdown = this._didSelectOption(event);
        if (isFocusInsideDropdown) {
            const focusedEl = event.relatedTarget;
            if (focusedEl?.tabIndex >= 0) {
                return;
            }
            this.focus();
            return;
        }
        this._focused = false;
        if (this.props.onBlur) {
            this.props.onBlur(event);
        }
    }
    _onKeyDown(event) {
        if (this.props.disabled || this.isReadOnly) {
            return;
        }
        const { key } = event;
        /* Enter - prevent a wrapping form from submitting when hitting Enter */
        /* ArrowUp - prevent input's native behaviour from moving the text cursor to the beginning */
        if (key === 'Enter' || key === 'ArrowUp') {
            event.preventDefault();
        }
        if (key !== 'ArrowDown' && key !== 'ArrowUp') {
            this.setState({ isEditing: true });
        }
        if (this.shouldOpenDropdown(key)) {
            this.showOptions();
            event.preventDefault();
        }
        if (this.shouldDelegateKeyDown(key)) {
            // Delegate event and get result
            if (this.dropdownLayout) {
                const eventWasHandled = this.dropdownLayout._onSelectListKeyDown(event);
                if (eventWasHandled || this.isReadOnly) {
                    // Stop propagation when Escape is handled to prevent it from bubbling to parent elements
                    if (key === 'Escape' && eventWasHandled) {
                        event.stopPropagation();
                    }
                    return;
                }
            }
            // For editing mode, we want to *submit* only for specific keys.
            if (this.shouldPerformManualSubmit(key)) {
                this._onManuallyInput(this.state.inputValue, event);
                const inputIsEmpty = !event.target.value;
                if (this.closeOnSelect() || (key === 'Tab' && inputIsEmpty)) {
                    this.hideOptions();
                }
            }
        }
    }
    /**
     * Sets focus on the input element
     * @param {FocusOptions} options
     */
    focus(options = {}) {
        this.input.current && this.input.current.focus(options);
    }
    /**
     * Removes focus on the input element
     */
    blur() {
        this.input.current && this.input.current.blur();
    }
    /**
     * Selects all text in the input element
     */
    select() {
        this.input.current && this.input.current.select();
    }
}
InputWithOptions.defaultProps = {
    ...Input.defaultProps,
    ...DropdownLayout.defaultProps,
    onSelect: () => { },
    options: [],
    closeOnSelect: true,
    inputElement: React.createElement(Input, null),
    valueParser: DEFAULT_VALUE_PARSER,
    dropdownWidth: null,
    popoverProps: DEFAULT_POPOVER_PROPS,
    dropdownOffsetLeft: '0',
    showOptionsIfEmptyInput: true,
    autocomplete: 'off',
    native: false,
    showDrawerOnMobile: true,
};
InputWithOptions.displayName = 'InputWithOptions';
export default InputWithOptions;
//# sourceMappingURL=InputWithOptions.js.map