/* eslint-disable prefer-const, max-len */ import BaseFoundation, { DefaultAdapter } from '../base/foundation'; import { isString, isNumber, isUndefined, isObject } from 'lodash'; import warning from '../utils/warning'; import KeyCode from '../utils/keyCode'; interface KeyboardAdapter

, S = Record> extends DefaultAdapter { registerKeyDown: (callback: (event: any) => void) => void; unregisterKeyDown: (callback: (event: any) => void) => void; updateFocusIndex: (focusIndex: number) => void; } export interface DataItem { [x: string]: any; value?: string | number; label?: any; // reactNode } export interface StateOptionItem extends DataItem { show?: boolean; key?: string | number; } export type AutoCompleteData = Array; export interface AutoCompleteAdapter

, S = Record> extends KeyboardAdapter { getTriggerWidth: () => number | undefined; setOptionWrapperWidth: (width: number) => void; updateInputValue: (inputValue: string | number) => void; toggleListVisible: (isShow: boolean) => void; updateOptionList: (optionList: Array) => void; updateSelection: (selection: Map) => void; notifySearch: (inputValue: string) => void; notifyChange: (value: string | number) => void; notifySelect: (option: StateOptionItem | string | number) => void; notifyDropdownVisibleChange: (isVisible: boolean) => void; notifyClear: () => void; notifyFocus: (event?: any) => void; notifyBlur: (event?: any) => void; rePositionDropdown: () => void; } class AutoCompleteFoundation

, S = Record> extends BaseFoundation, P, S> { private _keydownHandler: (args: any) => void | null; constructor(adapter: AutoCompleteAdapter) { super({ ...adapter }); } isPanelOpen = false; init(): void { this._setDropdownWidth(); const { defaultOpen, data, defaultValue, value } = this.getProps(); if (data && data.length) { const initOptions = this._generateList(data); this._adapter.updateOptionList(initOptions); } if (defaultOpen) { this.openDropdown(); } // When both defaultValue and value exist, finally the value of value will be taken as initValue let initValue: string; if (typeof defaultValue !== 'undefined') { initValue = defaultValue; } if (typeof value !== 'undefined') { initValue = value; } if (typeof initValue !== 'undefined') { this.handleValueChange(initValue); } } destroy(): void { // this._adapter.unregisterClickOutsideHandler(); // this.unBindKeyBoardEvent(); } _setDropdownWidth(): void { const { style, dropdownMatchSelectWidth } = this.getProps(); let width; if (dropdownMatchSelectWidth) { if (style && isNumber(style.width)) { width = style.width; } else if (style && isString(style.width) && !style.width.includes('%')) { width = style.width; } else { width = this._adapter.getTriggerWidth(); } this._adapter.setOptionWrapperWidth(width); } } handleInputClick(e?: MouseEvent): void { const { options } = this.getStates(); const { disabled } = this.getProps(); if (!disabled) { if (this.isPanelOpen) { this.closeDropdown(); } else { this.openDropdown(); } } } openDropdown(): void { this.isPanelOpen = true; this._adapter.toggleListVisible(true); this._setDropdownWidth(); // this._adapter.registerClickOutsideHandler(e => this.closeDropdown(e)); this._adapter.notifyDropdownVisibleChange(true); this._modifyFocusIndexOnPanelOpen(); } closeDropdown(e?: any): void { this.isPanelOpen = false; this._adapter.toggleListVisible(false); // this._adapter.unregisterClickOutsideHandler(); this._adapter.notifyDropdownVisibleChange(false); // After closing the panel, you can still open the panel by pressing the enter key // this.unBindKeyBoardEvent(); } // props.data => optionList _generateList(data: AutoCompleteData): Array { const { renderItem } = this.getProps(); const options: Array = []; if (data && data.length) { data.forEach((item, i) => { const key = String(new Date().getTime()) + i; let option: StateOptionItem = {}; if (isString(item) || isNumber(item)) { option = { value: item as string, key, label: item, show: true }; } else if (isObject(item) && !isUndefined(item.value)) { option = { show: true, ...item }; } if (renderItem && typeof renderItem === 'function') { option.label = renderItem(item); } options.push(option); }); } return options; } handleSearch(inputValue: string): void { this._adapter.updateInputValue(inputValue); this._adapter.notifySearch(inputValue); this._adapter.notifyChange(inputValue); this._modifyFocusIndex(inputValue); if (!this.isPanelOpen){ this.openDropdown(); } } handleSelect(option: StateOptionItem, optionIndex?: number): void { const { renderSelectedItem } = this.getProps(); let newInputValue: string | number = ''; if (renderSelectedItem && typeof renderSelectedItem === 'function') { newInputValue = renderSelectedItem(option); warning( typeof newInputValue !== 'string', 'Warning: [Semi AutoComplete] renderSelectedItem must return string, please check your function return' ); } else { newInputValue = option.value; } // 1. trigger onSelect // 2. close Dropdown if (this._isControlledComponent()) { this.closeDropdown(); this.notifySelect(option); } else { // 1. update Input // 2. update Selection // 3. trigger onSelect // 4. close Dropdown this._adapter.updateInputValue(newInputValue); this.updateSelection(option); this.notifySelect(option); this.closeDropdown(); } this._adapter.notifyChange(newInputValue); this._adapter.updateFocusIndex(optionIndex); } updateSelection(option: StateOptionItem) { const selection = new Map(); if (option) { selection.set(option.label, option); } this._adapter.updateSelection(selection); } notifySelect(option: StateOptionItem) { if (this._backwardLabelInValue()) { this._adapter.notifySelect(option); } else { this._adapter.notifySelect(option.value); } } _backwardLabelInValue() { const props = this.getProps(); let { onSelectWithObject } = props; return onSelectWithObject; } handleDataChange(newData: any[]) { const options = this._generateList(newData); this._adapter.updateOptionList(options); this._adapter.rePositionDropdown(); } handleValueChange(propValue: any) { let { data, defaultActiveFirstOption } = this.getProps(); let selectedValue = ''; if (this._backwardLabelInValue() && Object.prototype.toString.call(propValue) === '[object Object]') { selectedValue = propValue.value; } else { selectedValue = propValue; } let renderSelectedItem = this._getRenderSelectedItem(); const options = this._generateList(data); // Get the option whose value match from options let selectedOption: StateOptionItem | Array = options.filter(option => renderSelectedItem(option) === selectedValue); const canMatchInData = selectedOption.length; const selectedOptionIndex = options.findIndex(option => renderSelectedItem(option) === selectedValue); let inputValue = ''; if (canMatchInData) { selectedOption = selectedOption[0]; inputValue = renderSelectedItem(selectedOption); } else { const cbItem = this._backwardLabelInValue() ? propValue : { label: selectedValue, value: selectedValue }; inputValue = renderSelectedItem(cbItem); } this._adapter.updateInputValue(inputValue); this.updateSelection(canMatchInData ? selectedOption : null); if (selectedOptionIndex === -1 && defaultActiveFirstOption) { this._adapter.updateFocusIndex(0); } else { this._adapter.updateFocusIndex(selectedOptionIndex); } } _modifyFocusIndex(searchValue) { let { focusIndex } = this.getStates(); let { data, defaultActiveFirstOption } = this.getProps(); let renderSelectedItem = this._getRenderSelectedItem(); const options = this._generateList(data); const selectedOptionIndex = options.findIndex(option => renderSelectedItem(option) === searchValue); if (selectedOptionIndex === -1 && defaultActiveFirstOption) { if (focusIndex !== 0) { this._adapter.updateFocusIndex(0); } } else { if (selectedOptionIndex !== focusIndex) { this._adapter.updateFocusIndex(selectedOptionIndex); } } } _modifyFocusIndexOnPanelOpen() { let { inputValue } = this.getStates(); this._modifyFocusIndex(inputValue); } _getRenderSelectedItem() { let { renderSelectedItem } = this.getProps(); if (typeof renderSelectedItem === 'undefined') { renderSelectedItem = (option: any) => option.value; } else if (renderSelectedItem && typeof renderSelectedItem === 'function') { // do nothing } return renderSelectedItem; } handleClear() { this._adapter.notifyClear(); } bindKeyBoardEvent() { this._keydownHandler = (event: KeyboardEvent): void => { this._handleKeyDown(event); }; this._adapter.registerKeyDown(this._keydownHandler); } // unBindKeyBoardEvent() { // if (this._keydownHandler) { // this._adapter.unregisterKeyDown(this._keydownHandler); // } // } _handleKeyDown(event: KeyboardEvent) { const key = event.keyCode; const { visible } = this.getStates(); switch (key) { case KeyCode.UP: // Prevent Input's cursor from following the movement event.preventDefault(); this._handleArrowKeyDown(-1); break; case KeyCode.DOWN: // Prevent Input's cursor from following the movement event.preventDefault(); this._handleArrowKeyDown(1); break; case KeyCode.ENTER: // when custom trigger, prevent outer open panel again event.preventDefault(); this._handleEnterKeyDown(); break; case KeyCode.ESC: this.closeDropdown(); break; default: break; } } _getEnableFocusIndex(offset: number) { const { focusIndex, options } = this.getStates(); const visibleOptions = options.filter((item: StateOptionItem) => item.show); const optionsLength = visibleOptions.length; let index = focusIndex + offset; if (index < 0) { index = optionsLength - 1; } if (index >= optionsLength) { index = 0; } // avoid newIndex option is disabled if (offset > 0) { let nearestActiveOption = -1; for (let i = 0; i < visibleOptions.length; i++) { const optionIsActive = !visibleOptions[i].disabled; if (optionIsActive) { nearestActiveOption = i; } if (nearestActiveOption >= index) { break; } } index = nearestActiveOption; } else { let nearestActiveOption = visibleOptions.length; for (let i = optionsLength - 1; i >= 0; i--) { const optionIsActive = !visibleOptions[i].disabled; if (optionIsActive) { nearestActiveOption = i; } if (nearestActiveOption <= index) { break; } } index = nearestActiveOption; } this._adapter.updateFocusIndex(index); } _handleArrowKeyDown(offset: number): void { const { visible } = this.getStates(); if (!visible){ this.openDropdown(); } else { this._getEnableFocusIndex(offset); } } _handleEnterKeyDown() { const { visible, options, focusIndex } = this.getStates(); if (!visible){ this.openDropdown(); } else { if (focusIndex !== undefined && focusIndex !== -1 && options.length !== 0) { const visibleOptions = options.filter((item: StateOptionItem) => item.show); const selectedOption = visibleOptions[focusIndex]; this.handleSelect(selectedOption, focusIndex); } else { this.closeDropdown(); } } } handleOptionMouseEnter(optionIndex: number): void { this._adapter.updateFocusIndex(optionIndex); } handleFocus(e: FocusEvent) { // If you get the focus through the tab key, you need to manually bind keyboard events // Then you can open the panel by pressing the enter key this.bindKeyBoardEvent(); this._adapter.notifyFocus(e); } handleBlur(e: FocusEvent) { // In order to handle the problem of losing onClick binding when clicking on the padding area, the onBlur event is triggered first to cause the react view to be updated // internal-issues:1231 setTimeout(() => { this._adapter.notifyBlur(e); this.closeDropdown(); }, 100); } } export default AutoCompleteFoundation;