UNPKG

labo-components

Version:
247 lines (213 loc) 7.74 kB
import React from 'react'; import PropTypes from 'prop-types'; import IDUtil from '../../../util/IDUtil'; import ExternalAPI from '../../../api/ExternalAPI'; import { AnnotationEvents } from '../AnnotationClient'; import debounce from 'debounce'; const CUSTOM_API = 'custom'; export default class LinkForm extends React.PureComponent { constructor(props) { super(props); this.config = this.props.annotationClient.config.motivationConfig[ 'link' ]; this.searchTermRef = React.createRef(); this.linkUrlRef = React.createRef(); this.linkLabelRef = React.createRef(); this.debounceSearch = debounce(this.search.bind(this), 400); this.state = { selectedApi: this.config.apis ? this.config.apis[0].name : CUSTOM_API, results: [] }; } /* ------------------- CRUD / loading of links ------------------- */ setApi = e => { this.setState({ selectedApi: e.target.value, results: [] }, () => { if (this.state.selectedApi !== CUSTOM_API) { this.debounceSearch(); } }); }; addLink = linkData => { if (!linkData) return null; // save the link this.props.annotationClient.saveBodyElement( Object.assign({ annotationType: 'link' }, linkData), false, true, this.props.annotation ); // remove from result set this.setState({ results: this.state.results.filter(result => result !== linkData) }); }; onInput = e => { if (e.charCode == 13) { // search right away this.search(); } else { // debounced search, automatically this.debounceSearch(); } }; onCustomInput = e => { if (e.charCode == 13) { this.save(); } }; isValidURL = url => { const urlPattern = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/i; return urlPattern.test(url); }; clear = () => { this.searchTermRef.current.value = ''; this.onSearched([]); }; search = () => { // empty term, empty results if (!this.searchTermRef.current.value) { this.onSearched([]); return; } console.debug('Searching external API'); // search term set, search ExternalAPI.search( this.state.selectedApi, this.searchTermRef.current.value, this.onSearched ); }; saveCustom = () => { if (this.isValidURL(this.linkUrlRef.current.value)) { this.addLink({ url: this.linkUrlRef.current.value, label: this.linkLabelRef.current.value }); } else { alert('Please enter a valid URL'); } }; onSearched = results => { if (results.error) { results = []; } this.setState({ results: results }); }; /* ------------------------------------- RENDER FUNCTIONS --------------------------- */ renderResultList = searchResults => { const results = searchResults.map((res, index) => { let poster = null; if (res.poster) { poster = <img src={res.poster} style={{ maxWidth: '100px' }} />; } const title = res.label ? res.label : res.title; const description = title === res.description ? null : res.description; return ( <div key={'result__' + index} className="link-result" title={res.description} onClick={this.addLink.bind(this, res)} > <strong>{res.label ? res.label : res.title}</strong> {poster} {description} </div> ); }); return results.length > 0 ? ( <div className="link-search-results"> <div className="link-result-list">{results}</div> <div onClick={this.clear} className="clear"> Clear results </div> </div> ) : null; }; renderApiSelector = (apis, selectedApi, setApiFunc) => { const options = apis.map(api => { return ( <option key={api.name} value={api.name}> {api.name} </option> ); }); return ( <div className="filter"> <strong>API:</strong> <select onChange={setApiFunc} value={selectedApi}> {options} </select> </div> ); }; renderFormFields = (onInput, showClear) => { const fields = ( <div key="l_api" className="link-row input-row"> <strong>Search: </strong> <input type="text" ref={this.searchTermRef} placeholder="Search through the selected API" onInput={onInput} /> {showClear && ( <div className="clear" title="Clear search results" onClick={this.clear} /> )} </div> ); return <div className="link-form">{fields}</div>; }; renderCustomFormFields = onInput => { const fields = [ <div key="l_url" className="link-row"> <strong>URL</strong> <input type="text" ref={this.linkUrlRef} onInput={onInput} /> </div>, <div key="l_label" className="link-row"> <strong>Label</strong> <input type="text" ref={this.linkLabelRef} onInput={onInput} /> </div> ]; return <div className="link-form">{fields}</div>; }; //TODO replace all the bootstrap stuff render() { //draw the list of search results const resultList = this.renderResultList(this.state.results); //draw radio buttons for selecting an API const apiSelect = this.renderApiSelector( this.config.apis || [], this.state.selectedApi, this.setApi ); //draw a URL and link label field (custom mode) OR draw a search field (if an API is selected) const formFields = this.state.selectedApi == CUSTOM_API ? this.renderCustomFormFields(this.onCustomInput) : this.renderFormFields( this.onInput, this.searchTermRef.current && this.searchTermRef.current.value ); return ( <div className={IDUtil.cssClassName('link-form')}> {apiSelect} {formFields} {resultList} </div> ); } } LinkForm.propTypes = { annotationClient: PropTypes.object.isRequired, annotation: PropTypes.object.isRequired };