From 9a5b313ea12bdb9dc3e3873ca3a2639bd7483e46 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Mon, 28 Jan 2019 11:35:25 +0100 Subject: Update packages --- packages/forms/src/select/index.tsx | 422 ++++++++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 packages/forms/src/select/index.tsx (limited to 'packages/forms/src/select/index.tsx') diff --git a/packages/forms/src/select/index.tsx b/packages/forms/src/select/index.tsx new file mode 100644 index 000000000..58bb7317a --- /dev/null +++ b/packages/forms/src/select/index.tsx @@ -0,0 +1,422 @@ +import { mdiArrowRightDropCircleOutline, mdiCloseCircle, mdiMagnify } from '@mdi/js'; +import Icon from '@mdi/react'; +import { Theme } from '@meetfranz/theme'; +import classnames from 'classnames'; +import debounce from 'lodash/debounce'; +import { observer } from 'mobx-react'; +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, + }, + 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: { + fontWeight: 'bold', + }, + 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, + }, +}); + +@observer +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() { + const { + open, + } = this.state; + + if (this.searchInputRef && this.searchInputRef.current) { + if (open) { + this.searchInputRef.current.focus(); + } + } + } + + componentDidMount() { + if (this.componentRef && this.componentRef.current) { + this.keyListener = this.componentRef.current.addEventListener('keydown', debounce((e) => { + const { + selected, + open, + options, + } = this.state; + + if (!open) return; + + 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; + } + }, 10, { + leading: true, + })); + } + + if (this.inputRef && this.inputRef.current) { + const { + data, + } = this.props; + + if (data) { + Object.keys(data).map(key => this.inputRef.current!.dataset[key] = data[key]); + } + } + } + + 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(); + } + + 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); + } + } + + render() { + const { + actionText, + classes, + className, + defaultValue, + disabled, + error, + id, + inputClassName, + name, + label, + showLabel, + showSearch, + onChange, + } = 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); -- cgit v1.2.3-70-g09d2