Removed downshift dependency

This commit is contained in:
Rainer Simon 2021-11-19 10:13:50 +01:00
parent d21f29abc6
commit 94d9915140
4 changed files with 1271 additions and 1256 deletions

2300
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,6 @@
}, },
"dependencies": { "dependencies": {
"core-js": "^3.18.3", "core-js": "^3.18.3",
"downshift": "^6.1.3",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"node-polyglot": "^2.4.0", "node-polyglot": "^2.4.0",
"react-autosize-textarea": "^7.1.0", "react-autosize-textarea": "^7.1.0",

View File

@ -1,101 +1,162 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useCombobox } from 'downshift';
const getVocabSuggestions = (query, vocabulary) =>
vocabulary.filter(item => {
// Item could be string or { label, uri } tuple
const label = item.label ? item.label : item;
return label.toLowerCase().startsWith(query.toLowerCase());
});
const getFnSuggestions = (query, fn) =>
fn(query);
const Autocomplete = props => { const Autocomplete = props => {
const element = useRef(); const element = useRef();
const [ inputItems, setInputItems ] = useState(props.vocabulary || []); // Current value of the input field
const [ value, setValue ] = useState(props.initialValue || '');
// Current list of suggestions
const [ suggestions, setSuggestions ] = useState([]);
// Highlighted suggestion, if any
const [ highlightedIndex, setHighlightedIndex ] = useState(null);
useEffect(() => { useEffect(() => {
if (props.initialValue)
element.current.querySelector('input').value = props.initialValue;
if (props.focus) if (props.focus)
element.current.querySelector('input').focus({ preventScroll: true }); element.current.querySelector('input').focus({ preventScroll: true });
}, []) }, []);
const onInputValueChange = ({ inputValue }) => {
if (inputValue.length > 0) {
const prefixMatches = props.vocabulary.filter(item => {
// Item could be string or { label, uri } tuple
const label = item.label ? item.label : item;
return label.toLowerCase().startsWith(inputValue.toLowerCase());
});
setInputItems(prefixMatches); useEffect(() => {
props.onChange(value);
}, [ value ]);
const getSuggestions = value => {
if (typeof props.vocabulary === 'function') {
const result = getFnSuggestions(value, props.vocabulary);
// Result could be suggestions or Promise
if (result.then)
result.then(setSuggestions);
else
setSuggestions(result);
} else { } else {
// ...or none, if the input is empty const suggestions = getVocabSuggestions(value, props.vocabulary);
setInputItems([]); setSuggestions(suggestions);
}
}
const {
isOpen,
getMenuProps,
getInputProps,
getComboboxProps,
highlightedIndex,
getItemProps,
setInputValue
} = useCombobox({
items: inputItems,
onInputValueChange: onInputValueChange,
onSelectedItemChange: ({ selectedItem }) => {
onSubmit(selectedItem);
}
});
const onSubmit = inputValue => {
setInputValue('');
const label = inputValue.label ? inputValue.label : inputValue;
if (label.trim().length > 0)
props.onSubmit(inputValue);
}
const onKeyUp = evt => {
const { value } = evt.target;
if (evt.which == 13 && highlightedIndex == -1) {
onSubmit(value);
} else if (evt.which == 40 && value.length == 0) {
setInputItems(props.vocabulary); // Show all options on key down
} else if (evt.which == 27) {
props.onCancel && props.onCancel();
} else {
props.onChange && props.onChange(value);
} }
} }
// This is a horrible hack - need to get rid of downshift altogether! const onSubmit = () => {
const inputProps = getInputProps({ onKeyUp }); if (highlightedIndex !== null) {
if (inputProps.value === '[object Object]') // Submit highligted suggestion
inputProps.value = ''; props.onSubmit(suggestions[highlightedIndex]);
} else {
// Submit input value
const trimmed = value.trim();
if (trimmed) {
// If there is a vocabulary with the same label, use that
const matchingTerm = Array.isArray(props.vocabulary) ?
props.vocabulary.find(t => {
const label = t.label || t;
return label.toLowerCase() === trimmed.toLowerCase();
}) : null;
if (matchingTerm) {
props.onSubmit(matchingTerm);
} else {
// Otherwise, just use as a freetext tag
props.onSubmit(trimmed);
}
}
}
setValue('');
setSuggestions([]);
setHighlightedIndex(null);
}
const onKeyDown = evt => {
if (evt.which === 13) {
// Enter
onSubmit();
} else if (evt.which === 27) {
props.onCancel && props.onCancel();
} else {
// Neither enter nor cancel
if (suggestions.length > 0) {
if (evt.which === 38) {
// Key up
if (highlightedIndex === null) {
setHighlightedIndex(0);
} else {
const prev = Math.max(0, highlightedIndex - 1);
setHighlightedIndex(prev);
}
} else if (evt.which === 40) {
// Key down
if (highlightedIndex === null) {
setHighlightedIndex(0);
} else {
const next = Math.min(suggestions.length - 1, highlightedIndex + 1);
setHighlightedIndex(next);
}
}
} else {
// No suggestions: key down shows all vocab
// options (only for hard-wired vocabs!)
if (evt.which === 40) {
if (Array.isArray(props.vocabulary))
setSuggestions(props.vocabulary);
}
}
}
}
const onChange = evt => {
const { value } = evt.target;
// Set controlled input value
setValue(value);
// Typing on the input resets the highlight
setHighlightedIndex(null);
if (value)
getSuggestions(value);
else
setSuggestions([]);
}
return ( return (
<div className="r6o-autocomplete" ref={element}> <div
<div {...getComboboxProps()}> ref={element}
<input className="r6o-autocomplete">
{...inputProps} <div>
<input
onKeyDown={onKeyDown}
onChange={onChange}
value={value}
placeholder={props.placeholder} /> placeholder={props.placeholder} />
</div> </div>
<ul {...getMenuProps()}> <ul>
{isOpen && inputItems.map((item, index) => ( {suggestions.length > 0 && suggestions.map((item, index) => (
<li style={ <li
key={`${item.label ? item.label : item}${index}`}
onClick={onSubmit}
onMouseEnter={() => setHighlightedIndex(index)}
style={
highlightedIndex === index highlightedIndex === index
? { backgroundColor: '#bde4ff' } ? { backgroundColor: '#bde4ff' }
: {} : {}
} }>
key={`${item}${index}`}
{...getItemProps({ item, index })}>
{item.label ? item.label : item} {item.label ? item.label : item}
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
) )
} }
export default Autocomplete; export default Autocomplete;

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import { CloseIcon } from '../../../Icons'; import { CloseIcon } from '../../../Icons';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import Autocomplete from '../Autocomplete'; import Autocomplete from '../Autocomplete';
const getDraftTag = existingDraft => const getDraftTag = existingDraft =>
@ -9,7 +10,6 @@ const getDraftTag = existingDraft =>
type: 'TextualBody', value: '', purpose: 'tagging', draft: true type: 'TextualBody', value: '', purpose: 'tagging', draft: true
}; };
/** The basic freetext tag control from original Recogito **/
const TagWidget = props => { const TagWidget = props => {
// All tags (draft + non-draft) // All tags (draft + non-draft)
@ -31,11 +31,6 @@ const TagWidget = props => {
setShowDelete(tag); // Sets delete button on a different tag setShowDelete(tag); // Sets delete button on a different tag
} }
const onDelete = tag => evt => {
evt.stopPropagation();
props.onRemoveBody(tag);
}
const onDraftChange = value => { const onDraftChange = value => {
const prev = draftTag.value.trim(); const prev = draftTag.value.trim();
const updated = value.trim(); const updated = value.trim();
@ -49,6 +44,11 @@ const TagWidget = props => {
} }
} }
const onDelete = tag => evt => {
evt.stopPropagation();
props.onRemoveBody(tag);
}
const onSubmit = tag => { const onSubmit = tag => {
const { draft, ...toSubmit } = tag.label ? const { draft, ...toSubmit } = tag.label ?
{ ...draftTag, value: tag.label, source: tag.uri } : { ...draftTag, value: tag.label, source: tag.uri } :
@ -84,17 +84,16 @@ const TagWidget = props => {
} }
{!props.readOnly && {!props.readOnly &&
<Autocomplete <Autocomplete
focus={props.focus} focus={props.focus}
placeholder={i18n.t('Add tag...')} placeholder={i18n.t('Add tag...')}
initialValue={draftTag.value} vocabulary={props.vocabulary || []}
onChange={onDraftChange} onChange={onDraftChange}
onSubmit={onSubmit} onSubmit={onSubmit}/>
vocabulary={props.vocabulary || []} />
} }
</div> </div>
) )
}; }
export default TagWidget; export default TagWidget;