Removed downshift dependency
This commit is contained in:
parent
d21f29abc6
commit
94d9915140
File diff suppressed because it is too large
Load Diff
|
@ -34,7 +34,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.18.3",
|
||||
"downshift": "^6.1.3",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"node-polyglot": "^2.4.0",
|
||||
"react-autosize-textarea": "^7.1.0",
|
||||
|
|
|
@ -1,101 +1,162 @@
|
|||
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 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(() => {
|
||||
if (props.initialValue)
|
||||
element.current.querySelector('input').value = props.initialValue;
|
||||
|
||||
if (props.focus)
|
||||
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 {
|
||||
// ...or none, if the input is empty
|
||||
setInputItems([]);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const suggestions = getVocabSuggestions(value, props.vocabulary);
|
||||
setSuggestions(suggestions);
|
||||
}
|
||||
}
|
||||
|
||||
// This is a horrible hack - need to get rid of downshift altogether!
|
||||
const inputProps = getInputProps({ onKeyUp });
|
||||
if (inputProps.value === '[object Object]')
|
||||
inputProps.value = '';
|
||||
const onSubmit = () => {
|
||||
if (highlightedIndex !== null) {
|
||||
// Submit highligted suggestion
|
||||
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 (
|
||||
<div className="r6o-autocomplete" ref={element}>
|
||||
<div {...getComboboxProps()}>
|
||||
<input
|
||||
{...inputProps}
|
||||
<div
|
||||
ref={element}
|
||||
className="r6o-autocomplete">
|
||||
<div>
|
||||
<input
|
||||
onKeyDown={onKeyDown}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
placeholder={props.placeholder} />
|
||||
</div>
|
||||
<ul {...getMenuProps()}>
|
||||
{isOpen && inputItems.map((item, index) => (
|
||||
<li style={
|
||||
<ul>
|
||||
{suggestions.length > 0 && suggestions.map((item, index) => (
|
||||
<li
|
||||
key={`${item.label ? item.label : item}${index}`}
|
||||
onClick={onSubmit}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
style={
|
||||
highlightedIndex === index
|
||||
? { backgroundColor: '#bde4ff' }
|
||||
: {}
|
||||
}
|
||||
key={`${item}${index}`}
|
||||
{...getItemProps({ item, index })}>
|
||||
}>
|
||||
{item.label ? item.label : item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default Autocomplete;
|
||||
export default Autocomplete;
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||
import { CSSTransition } from 'react-transition-group';
|
||||
import { CloseIcon } from '../../../Icons';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
import Autocomplete from '../Autocomplete';
|
||||
|
||||
const getDraftTag = existingDraft =>
|
||||
|
@ -9,7 +10,6 @@ const getDraftTag = existingDraft =>
|
|||
type: 'TextualBody', value: '', purpose: 'tagging', draft: true
|
||||
};
|
||||
|
||||
/** The basic freetext tag control from original Recogito **/
|
||||
const TagWidget = props => {
|
||||
|
||||
// All tags (draft + non-draft)
|
||||
|
@ -31,11 +31,6 @@ const TagWidget = props => {
|
|||
setShowDelete(tag); // Sets delete button on a different tag
|
||||
}
|
||||
|
||||
const onDelete = tag => evt => {
|
||||
evt.stopPropagation();
|
||||
props.onRemoveBody(tag);
|
||||
}
|
||||
|
||||
const onDraftChange = value => {
|
||||
const prev = draftTag.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 { draft, ...toSubmit } = tag.label ?
|
||||
{ ...draftTag, value: tag.label, source: tag.uri } :
|
||||
|
@ -84,17 +84,16 @@ const TagWidget = props => {
|
|||
}
|
||||
|
||||
{!props.readOnly &&
|
||||
<Autocomplete
|
||||
<Autocomplete
|
||||
focus={props.focus}
|
||||
placeholder={i18n.t('Add tag...')}
|
||||
initialValue={draftTag.value}
|
||||
vocabulary={props.vocabulary || []}
|
||||
onChange={onDraftChange}
|
||||
onSubmit={onSubmit}
|
||||
vocabulary={props.vocabulary || []} />
|
||||
onSubmit={onSubmit}/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
export default TagWidget;
|
||||
export default TagWidget;
|
Loading…
Reference in New Issue