labo-components
Version:
247 lines (213 loc) • 7.74 kB
JSX
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
};