From 9a5b313ea12bdb9dc3e3873ca3a2639bd7483e46 Mon Sep 17 00:00:00 2001
From: Stefan Malzner <stefan@adlk.io>
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<HTMLInputElement>) => 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<IProps> {
+  public static defaultProps = {
+    onChange: () => {},
+    showLabel: true,
+    disabled: false,
+    error: '',
+  };
+
+  state = {
+    open: false,
+    value: '',
+    needle: '',
+    selected: 0,
+    options: null,
+  };
+
+  private componentRef = createRef<HTMLDivElement>();
+  private inputRef = createRef<HTMLInputElement>();
+  private searchInputRef = createRef<HTMLInputElement>();
+  private scrollContainerRef = createRef<HTMLDivElement>();
+  private activeOptionRef = createRef<HTMLDivElement>();
+
+  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 (
+      <Wrapper
+        className={className}
+      >
+        <Label
+          title={label}
+          showLabel={showLabel}
+          htmlFor={id}
+        >
+          <div
+            className={classnames({
+              [`${classes.hasError}`]: error,
+              [`${classes.disabled}`]: disabled,
+            })}
+            ref={this.componentRef}
+          >
+            <button
+              type="button"
+               className={classnames({
+                 [`${inputClassName}`]: inputClassName,
+                 [`${classes.select}`]: true,
+                 [`${classes.hasError}`]: error,
+               })}
+              onClick= {!disabled ? () => this.setState((state: IState) => ({
+                open: !state.open,
+              })) : () => {}}
+            >
+              {selection}
+              <Icon
+                path={mdiArrowRightDropCircleOutline}
+                size={0.8}
+                className={classnames({
+                  [`${classes.toggle}`]: true,
+                  [`${classes.toggleOpened}`]: open,
+                })}
+              />
+            </button>
+            {showSearch && open && (
+              <div className={classes.searchContainer}>
+                <Icon
+                  path={mdiMagnify}
+                  size={0.8}
+                />
+                <input
+                  type="text"
+                  value={needle}
+                  onChange={e => this.setFilter(e.currentTarget.value)}
+                  placeholder="Search"
+                  className={classes.search}
+                  ref={this.searchInputRef}
+                />
+                {needle && (
+                  <button
+                    type="button"
+                    className={classes.clearNeedle}
+                    onClick={() => this.setState({ needle: '', selected: -1 })}
+                  >
+                    <Icon
+                      path={mdiCloseCircle}
+                      size={0.7}
+                    />
+                  </button>
+                )}
+              </div>
+            )}
+            <div
+              className={classnames({
+                [`${classes.popup}`]: true,
+                [`${classes.open}`]: open,
+              })}
+              ref={this.scrollContainerRef}
+            >
+              {Object.keys(options!).map(((key, i) => (
+                <div
+                  key={key}
+                  onClick={() => this.select(key)}
+                  className={classnames({
+                    [`${classes.option}`]: true,
+                    [`${classes.selected}`]: options![key] === selection,
+                    [`${classes.focused}`]: selected === i,
+                  })}
+                  onMouseOver={() => this.setState({ selected: i })}
+                  ref={selected === i ? this.activeOptionRef : null}
+                >
+                  {options![key]}
+                </div>
+              )))}
+            </div>
+          </div>
+          <input
+            className={classes.input}
+            id={id}
+            name={name}
+            type="hidden"
+            defaultValue={value}
+            onChange={onChange}
+            disabled={disabled}
+            ref={this.inputRef}
+          />
+        </Label>
+        {error && (
+          <Error message={error} />
+        )}
+      </Wrapper>
+    );
+  }
+}
+
+export const Select = injectStyle(styles)(SelectComponent);
-- 
cgit v1.2.3-70-g09d2

