import { mdiArrowRightDropCircleOutline, mdiCloseCircle, mdiMagnify } from '@mdi/js'; import Icon from '@mdi/react'; import { Theme } from '@meetfranz/theme'; import classnames from 'classnames'; import React, { Component, createRef } from 'react'; import injectStyle from 'react-jss'; import { IFormField, IWithStyle } from '../typings/generic'; import { NONAME } from 'dns'; import { Error } from '../error'; import { Label } from '../label'; import { Wrapper } from '../wrapper'; interface IOptions { [index: string]: string; } interface IData { [index: string]: string; } interface IProps extends IFormField, IWithStyle { actionText: string; className?: string; inputClassName?: string; defaultValue?: string; disabled?: boolean; id?: string; name: string; options: IOptions; value: string; onChange: (event: React.ChangeEvent) => void; showSearch: boolean; data: IData; } interface IState { open: boolean; value: string; needle: string; selected: number; options: IOptions; } const styles = (theme: Theme) => ({ select: { background: theme.selectBackground, border: theme.selectBorder, borderRadius: theme.borderRadiusSmall, height: theme.selectHeight, fontSize: theme.uiFontSize, width: '100%', display: 'flex', alignItems: 'center', textAlign: 'left', color: theme.selectColor, }, label: { '& > div': { marginTop: 5, } }, popup: { opacity: 0, height: 0, overflowX: 'scroll', border: theme.selectBorder, borderTop: 0, transition: 'all 0.3s', }, open: { opacity: 1, height: 350, background: theme.selectPopupBackground, }, option: { padding: 10, borderBottom: theme.selectOptionBorder, color: theme.selectOptionColor, '&:hover': { background: theme.selectOptionItemHover, color: theme.selectOptionItemHoverColor, }, '&:active': { background: theme.selectOptionItemActive, color: theme.selectOptionItemActiveColor, }, }, selected: { background: theme.selectOptionItemActive, color: theme.selectOptionItemActiveColor, }, toggle: { marginLeft: 'auto', fill: theme.selectToggleColor, transition: 'transform 0.3s', }, toggleOpened: { transform: 'rotateZ(90deg)', }, searchContainer: { display: 'flex', background: theme.selectSearchBackground, alignItems: 'center', paddingLeft: 10, color: theme.selectColor, '& svg': { fill: theme.selectSearchColor, }, }, search: { border: 0, width: '100%', fontSize: theme.uiFontSize, background: 'none', marginLeft: 10, padding: [10, 0], color: theme.selectSearchColor, }, clearNeedle: { background: 'none', border: 0, }, focused: { fontWeight: 'bold', background: theme.selectOptionItemHover, color: theme.selectOptionItemHoverColor, }, hasError: { borderColor: theme.brandDanger, }, disabled: { opacity: theme.selectDisabledOpacity, }, }); class SelectComponent extends Component { public static defaultProps = { onChange: () => {}, showLabel: true, disabled: false, error: '', }; state = { open: false, value: '', needle: '', selected: 0, options: null, }; private componentRef = createRef(); private inputRef = createRef(); private searchInputRef = createRef(); private scrollContainerRef = createRef(); private activeOptionRef = createRef(); private keyListener: any; componentWillReceiveProps(nextProps: IProps) { if (nextProps.value && nextProps.value !== this.props.value) { this.setState({ value: nextProps.value, }); } } componentDidUpdate(prevProps: IProps, prevState: IState) { const { open, } = this.state; if (this.searchInputRef && this.searchInputRef.current) { if (open) { this.searchInputRef.current.focus(); } } } componentDidMount() { if (this.inputRef && this.inputRef.current) { const { data, } = this.props; if (data) { Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]); } } window.addEventListener('keydown', this.arrowKeysHandler.bind(this), false); } componentWillMount() { const { value, } = this.props; if (this.componentRef && this.componentRef.current) { this.componentRef.current.removeEventListener('keydown', this.keyListener); } if (value) { this.setState({ value, }); } this.setFilter(); } componentWillUnmount() { window.removeEventListener('keydown', this.arrowKeysHandler.bind(this)); } setFilter(needle: string = '') { const { options } = this.props; let filteredOptions = {}; if (needle) { Object.keys(options).map((key) => { if (key.toLocaleLowerCase().startsWith(needle.toLocaleLowerCase()) || options[key].toLocaleLowerCase().startsWith(needle.toLocaleLowerCase())) { Object.assign(filteredOptions, { [`${key}`]: options[key], }); } }); } else { filteredOptions = options; } this.setState({ needle, options: filteredOptions, selected: 0, }); } select(key: string) { this.setState((state: IState) => ({ value: key, open: false, })); this.setFilter(); if (this.props.onChange) { this.props.onChange(key as any); } } arrowKeysHandler(e: KeyboardEvent) { const { selected, open, options, } = this.state; if (!open) return; if (e.keyCode === 38 || e.keyCode === 40) { e.preventDefault(); } if (this.componentRef && this.componentRef.current) { if (e.keyCode === 38 && selected > 0) { this.setState((state: IState) => ({ selected: state.selected - 1, })); } else if (e.keyCode === 40 && selected < Object.keys(options!).length - 1) { this.setState((state: IState) => ({ selected: state.selected + 1, })); } else if (e.keyCode === 13) { this.select(Object.keys(options!)[selected]); } if (this.activeOptionRef && this.activeOptionRef.current && this.scrollContainerRef && this.scrollContainerRef.current) { const containerTopOffset = this.scrollContainerRef.current.offsetTop; const optionTopOffset = this.activeOptionRef.current.offsetTop; const topOffset = optionTopOffset - containerTopOffset; this.scrollContainerRef.current.scrollTop = topOffset - 35; } } switch (e.keyCode){ case 37: case 39: case 38: case 40: // Arrow keys case 32: break; // Space default: break; // do not block other keys } } render() { const { actionText, classes, className, defaultValue, disabled, error, id, inputClassName, name, label, showLabel, showSearch, onChange, required, } = this.props; const { open, needle, value, selected, options, } = this.state; let selection = ''; if (!value && defaultValue && options![defaultValue]) { selection = options![defaultValue]; } else if (value && options![value]) { selection = options![value]; } else { selection = actionText; } return ( {error && ( )} ); } } export const Select = injectStyle(styles)(SelectComponent);