/* Copyright (C) 2019 Monomax Software Pty Ltd * * This file is part of Dnote. * * Dnote is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Dnote is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Dnote. If not, see . */ import React, { useState, useRef, useEffect } from 'react'; import classnames from 'classnames'; import { booksToOptions, filterOptions, Option } from 'jslib/helpers/select'; import { KEYCODE_BACKSPACE } from 'jslib/helpers/keyboard'; import { useSearchMenuKeydown, useScrollToFocused } from 'web/libs/hooks/dom'; import { useSelector } from '../../store'; import PopoverContent from '../Common/Popover/PopoverContent'; import CloseIcon from '../Icons/Close'; import { usePrevious } from 'web/libs/hooks'; import styles from './MultiSelect.scss'; function getTextInputWidth(term: string, active: boolean) { if (!active && term === '') { return '100%'; } const val = 14 + term.length * 8; return `${val}px`; } interface Props { options: Option[]; currentOptions: Option[]; setCurrentOptions: (Option) => void; placeholder?: string; disabled?: boolean; textInputId?: string; wrapperClassName?: string; inputInnerRef?: React.MutableRefObject; } // TODO: Make a generic Select component that works for both single and multiple selection // by passing of a flag const MultiSelect: React.FunctionComponent = ({ options, currentOptions, setCurrentOptions, textInputId, placeholder, disabled, wrapperClassName, inputInnerRef }) => { const [isOpen, setIsOpen] = useState(false); const [focusedIdx, setFocusedIdx] = useState(0); const [focusedOptEl, setFocusedOptEl] = useState(null); const [term, setTerm] = useState(''); const wrapperRef = useRef(null); const inputRef = useRef(null); const listRef = useRef(null); const currentValues = currentOptions.map(o => { return o.value; }); const possibleOptions = options.filter(o => { return currentValues.indexOf(o.value) === -1; }); const filteredOptions = filterOptions(possibleOptions, term, false); function appendOption(o: Option | undefined) { if (!o) { return; } setTerm(''); const newVal = [...currentOptions, o]; setCurrentOptions(newVal); } function removeOption(o: Option) { setTerm(''); const newVal = currentOptions.filter(opt => { return opt.value !== o.value; }); setCurrentOptions(newVal); } function popOption() { if (currentOptions.length === 0) { return; } const newVal = currentOptions.slice(0, -1); setCurrentOptions(newVal); } useSearchMenuKeydown({ options: filteredOptions, containerEl: wrapperRef.current, focusedIdx, setFocusedIdx, onKeydownSelect: appendOption, disabled: !isOpen || disabled }); useScrollToFocused({ shouldScroll: true, focusedOptEl, containerEl: listRef.current }); useEffect(() => { if (!isOpen) { inputRef.current.blur(); setTerm(''); } }, [isOpen]); // useEffect(() => { // if (term !== '' && !isOpen) { // setIsOpen(true); // } // }, [term, isOpen]); const active = currentOptions.length > 0; const textInputWidth = getTextInputWidth(term, active); return (
{ if (inputRef.current) { inputRef.current.focus(); } // setIsOpen(!isOpen); }} >
    {placeholder} {currentOptions.map(o => { return (
  • {o.label}
  • ); })}
  • { inputRef.current = el; if (inputInnerRef) { inputInnerRef.current = el; } }} className={classnames(styles.input, { [styles['active-input']]: active })} value={term} disabled={disabled} onKeyDown={e => { if (e.keyCode === KEYCODE_BACKSPACE) { if (term === '') { popOption(); } } }} onChange={e => { const val = e.target.value; setTerm(val); }} onClick={e => { e.preventDefault(); }} onFocus={() => { setIsOpen(true); }} style={{ width: textInputWidth }} />
{ setIsOpen(false); }} alignment="left" direction="bottom" triggerEl={inputRef.current} wrapperEl={wrapperRef.current} contentClassName={classnames(styles['suggestion-wrapper'], { [styles['suggestions-wrapper-shown']]: isOpen })} closeOnOutsideClick closeOnEscapeKeydown >
    {filteredOptions.map((o, idx) => { const isFocused = idx === focusedIdx; return (
  • { if (isFocused) { setFocusedOptEl(el); } }} >
  • ); })}
); }; export default MultiSelect;