UNPKG

labo-components

Version:
312 lines (265 loc) 11.1 kB
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]}&nbsp;{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 };