labo-components
Version:
312 lines (265 loc) • 11.1 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
//TODO fix autosuggest https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html
import Autosuggest from 'react-autosuggest'; //See: https://github.com/moroshko/react-autosuggest
import QuickEntityViewer from './QuickEntityViewer';
import Entity from './Types/Entity';
import FlexModal from '../FlexModal';
import IDUtil from '../../util/IDUtil';
import SearchAPI from '../../api/SearchAPI'
import debounce from 'debounce';
import classNames from 'classnames';
export default class SearchTermInput extends React.PureComponent {
constructor(props) {
super(props);
this.xhrs = [];
this.getSuggestions = debounce(this.getSuggestions.bind(this), 400); //no debouncing as gives annoying behaviour when e.g. type 'thom' quite fast, and only get results for 'tho'
this.state = {
vocabulary: this.__getAutocompleteParam('person', 'autocompleteVocabulary', ''),
value: this.props.term, //the label of the selected classification (autocomplete)
suggestions: [], //current list of suggestions shown
isLoading: false, //loading the suggestions from the server
entityID : null,
showMoreInfoModal : false,
entities: this.props.entities
//TODO add property that holds the entity type!! Now it's rigged for persons only
};
}
__getAutocompleteMapping = entityType => {
if(!this.props.collectionConfig || !this.props.collectionConfig.getEntityConfig() ||
!this.props.collectionConfig.getEntityConfig()[entityType]) {
return null;
}
return this.props.collectionConfig.getEntityConfig()[entityType]['autocompleteConfig'];
};
__getAutocompleteParam = (entityType, param, defaultNullValue=null) => {
const mapping = this.__getAutocompleteMapping(entityType);
return mapping ? mapping[param] : defaultNullValue;
};
/* --------------- ANNOTATION CLIENT EVENT HANDLING -------------- */
componentWillUnmount = () => {
//cancel all previous outgoing requests
for (let x = this.xhrs.length; x > 0; x--) {
this.xhrs[x - 1].abort();
this.xhrs.pop();
}
};
/* ------------------- CRUD / loading of classifications ------------------- */
getSuggestions = (value, callback) => {
//cancel all previous outgoing requests
for (let x = this.xhrs.length; x > 0; x--) {
this.xhrs[x - 1].abort();
this.xhrs.pop();
}
const autocompleteParams = this.__getAutocompleteParam('person', 'autocompleteParams')
//get autocomplete results for persons
const xhr = SearchAPI.autocomplete(
this.state.vocabulary,
value,
autocompleteParams && autocompleteParams['Method'] ?
autocompleteParams['Method'] :
'',
autocompleteParams && autocompleteParams['Fields'] ?
autocompleteParams['Fields'] :
'',
autocompleteParams && autocompleteParams['Lang'] ?
autocompleteParams['Lang'] :
'',
callback
);
this.xhrs.push(xhr);
};
/* ------------------- REACT-AUTOSUGGEST FUNCTIONS ------------------- */
//when the user clicks on the icon in the autocomplete information, show more information
onMoreInfo = e => {
const button = e.currentTarget;
e.stopPropagation(); //avoid the suggestion being selected
this.setState({
entityID: button.value,
showMoreInfoModal : true
});
};
loadSuggestions = value => {
this.setState({
isLoading: true,
suggestions: [],
});
if (value.value === this.state.chosenValue) {
this.setState({
isLoading: false,
});
} else {
this.getSuggestions(value.value, (data) => {
if (!data || data.error || !Array.isArray(data)) {
this.setState({
isLoading: false,
suggestions: [],
});
//console.error("Autocomplete failed");
} else {
this.setState({
isLoading: false,
suggestions: data,
});
}
});
}
};
onSuggestionsFetchRequested = inputValue => {
//currently not checking field categories as autocomplete is always on
//Leaving the code here so that if we later have autocomplete for
//more than just persons, then we can select the best autocomplete given
//the selected field cluster
//if(!this.props.fieldCategories) return;
//check if the a person-related field category has been selected
//const selection = this.props.fieldCategories.map(({ value }) => value);
//const personFieldCategories = this.__getAutocompleteParam('person', 'fieldClusters', []);
if (!this.state.vocabulary) return;
this.loadSuggestions(inputValue)
};
onSuggestionsClearRequested = () => this.setState({ suggestions: [] });
getSuggestionValue = suggestion => {
return ""; //as we do not fill in the selected suggestion in the input, but as a tag
};
onSuggestionSelected = (e, suggestion) => {
this.state.entities.push({
id: suggestion.suggestion.value,
label: suggestion.suggestion.label.split("|")[0].trim(),
type: suggestion.suggestion.label.split("|")[1].trim(),
otherLabels: suggestion.suggestion.label.split("|")[3].trim(),
});
if (this.props.onSuggestionOutput) {
this.props.onSuggestionOutput(this.constructor.name, this.state.entities);
}
}
//TODO the rendering should be adapted for different vocabularies
renderSuggestion = suggestion => {
const arr = suggestion.label.split("|");
const scopeNote = arr[2] ? "(" + arr[2] + ")" : "";
return (
<span>
{arr[0]} {scopeNote}
<button
className="btn btn-default"
onClick={this.onMoreInfo}
title="More information"
value={suggestion.value}
>
<span className="link-more-info" />
</button>
</span>
);
};
deleteEntity = e => {
const index = this.state.entities.indexOf(e);
if (index > -1) {
this.state.entities.splice(index, 1);
}
if (this.props.onSuggestionOutput) {
this.props.onSuggestionOutput(this.constructor.name, this.state.entities);
}
}
onChange(event, { newValue }) {
//update the state when the user enters text
this.setState({
chosenValue: newValue,
value: newValue,
});
}
onKeyDown = (event) => {
//search when the user clicks 'Enter'
if(event.key == "Enter") {
this.props.newSearch(this.state.value);
}
};
onSubmit = (e) => {
//search when the user presses the Search button
e.preventDefault();
this.props.newSearch(this.state.value);
};
/* ------------------- QUICK ENTITY VIEWER ------------------- */
openMoreInfoModal = entityID => {
this.setState({
entityID: entityID,
showMoreInfoModal : true
});
};
/* ------------------- RENDER FUNCTIONS ------------------- */
//this is the pop-up that appears when a user clicks the 'more info' icon in a search autocomplete suggestion
renderMoreInfoModal = (entityID, collectionConfig) => {
return (
<FlexModal
elementId="moreinfo__modal"
stateVariable="showMoreInfoModal"
owner={this}
size="medium"
title={"Person information"}>
<QuickEntityViewer
entityID={entityID}
entityType="person"
collectionConfig={collectionConfig}
fieldCategories={this.props.fieldCategories ? this.props.fieldCategories : []}
/>
</FlexModal>
)
};
render() {
const inputProps = {
placeholder: "Search",
value: this.state.value,
onChange: this.onChange.bind(this),
onKeyDown: this.onKeyDown.bind(this) //add this to detect pressing 'Enter'
};
// the selected entities
const selectedEntities = (
<div className={"type-annotations"}>
{this.props.entities.map((entity, index) =>{
return (
<Entity
key={"entity_" + index}
entity={entity}
delete={this.deleteEntity.bind(this)}
/>
);
}
)}
</div>
);
const moreInfoModal = this.state.showMoreInfoModal && this.state.entityID ? this.renderMoreInfoModal(
this.state.entityID,
this.props.collectionConfig
) : null;
return (
<div className={IDUtil.cssClassName('search-term-input')}>
<div className="entity-input">
<div className="entity-autosuggest">
<Autosuggest
suggestions={this.state.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
inputProps={inputProps}
onSuggestionSelected={this.onSuggestionSelected}
/>
</div>
<div className="entity-display">
{selectedEntities}
</div>
</div>
<span onClick={this.onSubmit} className="">
<i className="fas fa-search" />
</span>
{moreInfoModal}
</div>
);
}
}
SearchTermInput.propTypes = {
fieldCategories: PropTypes.array.isRequired, //which field clusters/categories are selected
entities: PropTypes.array.isRequired,
term: PropTypes.string.isRequired,
collectionConfig: PropTypes.object.isRequired,
newSearch: PropTypes.func.isRequired,
onSuggestionOutput: PropTypes.func.isRequired
};