summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/geocode/autocomplete.component.js208
-rw-r--r--client/geocode/geocode.component.js16
-rw-r--r--client/geocode/index.js10
3 files changed, 234 insertions, 0 deletions
diff --git a/client/geocode/autocomplete.component.js b/client/geocode/autocomplete.component.js
new file mode 100644
index 00000000..12419cf1
--- /dev/null
+++ b/client/geocode/autocomplete.component.js
@@ -0,0 +1,208 @@
+import React, { Component } from 'react'
+// import PropTypes from 'prop-types'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+function formatLabel(label, value) {
+ if (!value) {
+ return label
+ }
+ let len = 0
+ return (
+ <span>
+ {
+ label.split(new RegExp(value.replace(/[-\[\]\(\)\+\*\\\^\$\{\}\.\?\&\|\<\>]/g, ''), "i")) // eslint-disable-line
+ .reduce((prev, current, i) => {
+ if (!i) {
+ len += current.length
+ return [current]
+ }
+ const ret = prev.concat(<b key={i}>{label.substr(len, value.length)}</b>, current)
+ len += value.length + current.length
+ return ret
+ }, [])
+ }
+ </span>
+ )
+}
+
+function sanitizeForAutocomplete(s) {
+ return (s || "")
+ .toLowerCase()
+ .replace(/[^a-zA-Z0-9 ]/g, '')
+ .trim()
+ .replace(/\\/g, '')
+}
+
+class Autocomplete extends Component {
+ constructor(props) {
+ super()
+ this.state = {
+ q: props.q || "",
+ selected: 0,
+ matches: []
+ }
+ this.handleKeyDown = this.handleKeyDown.bind(this)
+ this.handleChange = this.handleChange.bind(this)
+ this.handleCancel = this.handleCancel.bind(this)
+ }
+
+ componentWillMount() {
+ // build index based on what's in the hierarchy
+ const { nodes } = this.props.hierarchy
+ let index = []
+ this.index = index
+ Object.keys(nodes).forEach(key => {
+ const node = nodes[key]
+ if (!key || !node || !node.name || !node.parent) return
+ let { name } = node
+ let prefixName = name
+ if (node.is_attribute) {
+ const parent = nodes[node.parent]
+ if (parent) {
+ prefixName = parent.name + " (" + name + ")"
+ }
+ }
+ index.push([sanitizeForAutocomplete(prefixName), name, node.id])
+ node.synonyms
+ .split("\n")
+ .map(word => word = word.trim())
+ .filter(word => !!word)
+ .forEach(word => index.push([prefixName, name, node.id]))
+ })
+ }
+
+ handleKeyDown(e) {
+ let id
+ switch (e.keyCode) {
+ case 27: // escape
+ e.preventDefault()
+ this.handleCancel()
+ break
+ case 37: // left
+ case 38: // up
+ e.preventDefault()
+ this.setState({
+ selected: (this.state.matches.length + this.state.selected - 1) % this.state.matches.length
+ })
+ return false
+ case 39: // right
+ case 40: // down
+ e.preventDefault()
+ this.setState({
+ selected: (this.state.selected + 1) % this.state.matches.length
+ })
+ return false
+ case 13: // enter
+ id = this.state.matches[this.state.selected]
+ e.preventDefault()
+ this.handleSelect(id)
+ return false
+ default:
+ break
+ }
+ }
+
+ handleChange(e) {
+ // search for the given string in our index
+ const q = e.target.value
+ let value = sanitizeForAutocomplete(q)
+ if (!value.length) {
+ this.setState({
+ q,
+ selected: 0,
+ matches: [],
+ })
+ return
+ }
+ const re = new RegExp(value)
+ let matches = []
+ let seen = {}
+ this.index.forEach(pair => {
+ if (seen[pair[2]]) return
+ if (pair[0].match(re)) {
+ seen[pair[2]] = true
+ if (pair[1].indexOf(value) === 0) {
+ matches.unshift(pair[2])
+ } else {
+ matches.push(pair[2])
+ }
+ }
+ })
+ this.setState({
+ q,
+ selected: 0,
+ matches: matches.slice(0, 10),
+ })
+ }
+
+ handleSelect(id) {
+ const { nodes } = this.props.hierarchy
+ const node = nodes[id]
+ if (this.props.onSelect) this.props.onSelect(node)
+ this.setState({ q: "", selected: 0, matches: [] })
+ }
+
+ handleCancel() {
+ if (this.props.onCancel) this.props.onCancel()
+ this.setState({ q: "", selected: 0, matches: [] })
+ }
+
+ render() {
+ // const suggestions = this.state.suggestions.map((suggestion))
+ const { nodes } = this.props.hierarchy
+ const { q, selected } = this.state
+ const matches = this.state.matches.map((match, i) => {
+ const node = nodes[match]
+ const parent = nodes[node.parent]
+ let label
+ if (node.is_attribute) {
+ label = (
+ <span>
+ {formatLabel(parent.name, q)}
+ {' '}<small>{'('}{formatLabel(node.name, q)}{')'}</small>
+ </span>
+ )
+ } else {
+ label = formatLabel(node.name, q)
+ }
+ return (
+ <div
+ key={i}
+ className={selected === i ? 'selected' : ''}
+ onClick={() => this.handleSelect(node.id)}
+ onMouseEnter={() => this.setState({ selected: i })}
+ >
+ {label}
+ </div>
+ )
+ })
+ return (
+ <div className="autocomplete">
+ <input
+ type="text"
+ name="q"
+ value={this.state.q}
+ onKeyDown={this.handleKeyDown}
+ onChange={this.handleChange}
+ autoFocus
+ autoCapitalize="off"
+ autoComplete="off"
+ placeholder="Start typing a name"
+ />
+ <div className="matches">
+ {matches}
+ </div>
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+ onSelect: ownProps.onSelect,
+})
+
+const mapDispatchToProps = (dispatch) => ({
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(Autocomplete)
diff --git a/client/geocode/geocode.component.js b/client/geocode/geocode.component.js
new file mode 100644
index 00000000..a6766bec
--- /dev/null
+++ b/client/geocode/geocode.component.js
@@ -0,0 +1,16 @@
+import React, { Component } from 'react'
+
+import Autocomplete from './autocomplete.component'
+
+class GeocodeContainer extends Component {
+ render() {
+ // const { } = this.props
+ return (
+ <div className=''>
+ </div>
+ )
+ }
+}
+
+
+export default GeocodeContainer
diff --git a/client/geocode/index.js b/client/geocode/index.js
new file mode 100644
index 00000000..4f0da3f6
--- /dev/null
+++ b/client/geocode/index.js
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+
+// import { toArray } from '../util'
+
+import GeocodeContainer from './autocomplete.component'
+
+ReactDOM.render(
+ <GeocodeContainer />, document.querySelector('#container')
+)