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": {
|
"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",
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
Loading…
Reference in New Issue