sfc-utils 1.4.51 → 1.4.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import React, { useRef, useState } from 'react'
|
|
2
|
+
import * as geocoderStyles from '../styles/modules/geocoder.module.less'
|
|
3
|
+
|
|
4
|
+
// This is a singleton event listener that we can use to add/remove event listeners
|
|
5
|
+
// Don't call it during SSR
|
|
6
|
+
var setSingletonEventListener = (function (element) {
|
|
7
|
+
var handlers = {}
|
|
8
|
+
return function (evtName, func) {
|
|
9
|
+
handlers.hasOwnProperty(evtName) &&
|
|
10
|
+
element.removeEventListener(evtName, handlers[evtName])
|
|
11
|
+
if (func) {
|
|
12
|
+
handlers[evtName] = func
|
|
13
|
+
element.addEventListener(evtName, func)
|
|
14
|
+
} else {
|
|
15
|
+
delete handlers[evtName]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})(typeof document !== 'undefined' && document)
|
|
19
|
+
|
|
20
|
+
// Our new and improved geocoder! Hits PositionStack first and then falls back to classic geocoder
|
|
21
|
+
// Accepts a region to filter results by -- this can be custom but if it's not passed in, it will default to the state where the market resides
|
|
22
|
+
const Geocoder = ({
|
|
23
|
+
filterRegion, // You need to test results, but could also pass in a neighbourhood, district, city, county, state or administrative area
|
|
24
|
+
market, // Will filter by the market's state if no filterRegion provided
|
|
25
|
+
resultFunc,
|
|
26
|
+
}) => {
|
|
27
|
+
// Show a loader when we're requesting
|
|
28
|
+
const [loading, setLoading] = useState(false)
|
|
29
|
+
const [locData, setLocData] = useState(null)
|
|
30
|
+
const [inputValue, setInputValue] = useState('')
|
|
31
|
+
const [activeKeyboardIndex, setActiveKeyboardIndex] = useState(null)
|
|
32
|
+
const resultsRef = useRef(null)
|
|
33
|
+
const geocoderInputRef = useRef(null)
|
|
34
|
+
const latestFetchRef = useRef(null)
|
|
35
|
+
|
|
36
|
+
if (!filterRegion) {
|
|
37
|
+
switch (market) {
|
|
38
|
+
case 'SFC':
|
|
39
|
+
filterRegion = 'California'
|
|
40
|
+
break
|
|
41
|
+
case 'Houston':
|
|
42
|
+
case 'San Antonio':
|
|
43
|
+
filterRegion = 'Texas'
|
|
44
|
+
break
|
|
45
|
+
case 'Albany':
|
|
46
|
+
filterRegion = 'New York'
|
|
47
|
+
break
|
|
48
|
+
case 'CT':
|
|
49
|
+
filterRegion = 'Connecticut'
|
|
50
|
+
default:
|
|
51
|
+
filterRegion = 'United States'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// We don't want this CONSTANTLY firing, so we debounce it
|
|
56
|
+
const search = (query) => {
|
|
57
|
+
// Save value of query so we can check if it's the last one
|
|
58
|
+
latestFetchRef.current = query
|
|
59
|
+
// If we're already loading, don't fire another request, the request will be re-requested after the first one finishes
|
|
60
|
+
if (loading) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
// POST as form url encoded data
|
|
64
|
+
let formData = new FormData()
|
|
65
|
+
formData.append('query', query)
|
|
66
|
+
formData.append('region', filterRegion)
|
|
67
|
+
// Remove any existing event listeners
|
|
68
|
+
setSingletonEventListener('keydown')
|
|
69
|
+
// Make req
|
|
70
|
+
fetch('https://projects.sfchronicle.com/feeds/geocode/v2.php', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
body: formData,
|
|
73
|
+
})
|
|
74
|
+
.then((resp) => {
|
|
75
|
+
// Sometimes, there's a junk response, so let's handle that gracefully
|
|
76
|
+
if (!resp || !resp.ok) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
return resp.json()
|
|
80
|
+
})
|
|
81
|
+
.then((output) => {
|
|
82
|
+
// If this is not the latest fetch, bail and fetch latest
|
|
83
|
+
if (latestFetchRef.current !== query) {
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
// Delay a bit because it seems like we're still typing
|
|
86
|
+
search(latestFetchRef.current)
|
|
87
|
+
}, 1000)
|
|
88
|
+
return
|
|
89
|
+
} else {
|
|
90
|
+
// Uncomment this to see what request was actually honored
|
|
91
|
+
//console.log('OK VALID RESULTS FOR', latestFetchRef.current)
|
|
92
|
+
}
|
|
93
|
+
// Unset loading
|
|
94
|
+
setLoading(false)
|
|
95
|
+
// Remove any existing event listeners
|
|
96
|
+
setSingletonEventListener('keydown')
|
|
97
|
+
// Show results
|
|
98
|
+
setLocData(output.data)
|
|
99
|
+
// Bail early if there's no data
|
|
100
|
+
if (!output) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
// Handle result
|
|
104
|
+
if (output.data.length === 0) {
|
|
105
|
+
// We could show something saying "No results" ... or we could not
|
|
106
|
+
} else {
|
|
107
|
+
// Create a keydown event listener
|
|
108
|
+
setSingletonEventListener('keydown', function resultsKeyHandler(e) {
|
|
109
|
+
setActiveKeyboardIndex((prevIndex) => {
|
|
110
|
+
let newIndex = prevIndex
|
|
111
|
+
switch (e.key) {
|
|
112
|
+
case 'ArrowDown':
|
|
113
|
+
// Handle the index
|
|
114
|
+
if (prevIndex === null) {
|
|
115
|
+
newIndex = 0
|
|
116
|
+
} else if (prevIndex < output.data.length - 1) {
|
|
117
|
+
newIndex = prevIndex + 1
|
|
118
|
+
} else {
|
|
119
|
+
newIndex = 0
|
|
120
|
+
}
|
|
121
|
+
break
|
|
122
|
+
case 'ArrowUp':
|
|
123
|
+
// Handle the index
|
|
124
|
+
if (prevIndex === null) {
|
|
125
|
+
newIndex = output.data.length - 1
|
|
126
|
+
} else if (prevIndex > 0) {
|
|
127
|
+
newIndex = prevIndex - 1
|
|
128
|
+
} else {
|
|
129
|
+
newIndex = output.data.length - 1
|
|
130
|
+
}
|
|
131
|
+
break
|
|
132
|
+
case 'Enter':
|
|
133
|
+
// Do something with the data
|
|
134
|
+
const selectedLocation = output.data[prevIndex]
|
|
135
|
+
// Update the input value with the choice
|
|
136
|
+
if (selectedLocation) {
|
|
137
|
+
setInputValue(selectedLocation.name)
|
|
138
|
+
// Call function if it exists
|
|
139
|
+
if (resultFunc) {
|
|
140
|
+
resultFunc(selectedLocation)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Hide the list now
|
|
144
|
+
setLocData(null)
|
|
145
|
+
}
|
|
146
|
+
return newIndex
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Start listening for a click outside the div
|
|
151
|
+
document.addEventListener('click', function resultsClickHandler(e) {
|
|
152
|
+
// Whether we clicked inside or outside, we hide the list
|
|
153
|
+
setLocData(null)
|
|
154
|
+
// Also cancel the keydown listener
|
|
155
|
+
setSingletonEventListener('keydown')
|
|
156
|
+
// Received click, cancel this listener
|
|
157
|
+
this.removeEventListener('click', resultsClickHandler)
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Handle the change event
|
|
164
|
+
const handleChange = (event) => {
|
|
165
|
+
// Set the input value
|
|
166
|
+
const inputValue = event.target.value
|
|
167
|
+
setInputValue(inputValue)
|
|
168
|
+
setActiveKeyboardIndex(null)
|
|
169
|
+
// Only query if the value is not empty
|
|
170
|
+
if (inputValue) {
|
|
171
|
+
setLoading(true)
|
|
172
|
+
search(inputValue)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className={geocoderStyles.wrapper}>
|
|
178
|
+
<div className={geocoderStyles.icon} />
|
|
179
|
+
<input
|
|
180
|
+
ref={geocoderInputRef}
|
|
181
|
+
className={geocoderStyles.input}
|
|
182
|
+
placeholder="Enter an address"
|
|
183
|
+
name="address"
|
|
184
|
+
onChange={handleChange}
|
|
185
|
+
value={inputValue}
|
|
186
|
+
/>
|
|
187
|
+
{loading && (
|
|
188
|
+
<img src="https://projects.sfchronicle.com/shared/logos/loading.gif" />
|
|
189
|
+
)}
|
|
190
|
+
{locData && (
|
|
191
|
+
<ul className={geocoderStyles.resultsWrapper} ref={resultsRef}>
|
|
192
|
+
{locData.map((loc, i) => {
|
|
193
|
+
let thisClass = geocoderStyles.result
|
|
194
|
+
if (activeKeyboardIndex === i) {
|
|
195
|
+
thisClass += ' ' + geocoderStyles.active
|
|
196
|
+
}
|
|
197
|
+
return (
|
|
198
|
+
<li
|
|
199
|
+
className={thisClass}
|
|
200
|
+
key={i}
|
|
201
|
+
onClick={() => {
|
|
202
|
+
setInputValue(locData[i].name)
|
|
203
|
+
// Call function if it exists
|
|
204
|
+
if (resultFunc) {
|
|
205
|
+
resultFunc(locData[i])
|
|
206
|
+
}
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<button
|
|
210
|
+
className={geocoderStyles.button}
|
|
211
|
+
onFocus={(e) => {
|
|
212
|
+
// Set index
|
|
213
|
+
setActiveKeyboardIndex(i)
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<div className={geocoderStyles.name}>{loc.name}</div>
|
|
217
|
+
<div className={geocoderStyles.details}>
|
|
218
|
+
{loc.locality}
|
|
219
|
+
{loc.locality && loc.region && ', '}
|
|
220
|
+
{loc.region}
|
|
221
|
+
</div>
|
|
222
|
+
</button>
|
|
223
|
+
</li>
|
|
224
|
+
)
|
|
225
|
+
})}
|
|
226
|
+
</ul>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default Geocoder
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfc-utils",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.54",
|
|
4
4
|
"author": "ewagstaff <evanjwagstaff@gmail.com>",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"archieml": "^0.4.2",
|
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
"react-transition-group": "^4.4.5",
|
|
15
15
|
"write": "^2.0.0"
|
|
16
16
|
}
|
|
17
|
-
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
@import (less) "../variables.less";
|
|
2
|
+
|
|
3
|
+
.wrapper {
|
|
4
|
+
background-color: white;
|
|
5
|
+
border-radius: 4px;
|
|
6
|
+
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
|
|
7
|
+
position: relative;
|
|
8
|
+
font-family: @sans;
|
|
9
|
+
|
|
10
|
+
img {
|
|
11
|
+
position: absolute;
|
|
12
|
+
top: 10px;
|
|
13
|
+
right: 10px;
|
|
14
|
+
width: 20px;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.icon {
|
|
19
|
+
position: absolute;
|
|
20
|
+
left: 10px;
|
|
21
|
+
top: 10px;
|
|
22
|
+
width: 20px;
|
|
23
|
+
height: 20px;
|
|
24
|
+
background-color: #666;
|
|
25
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z'/%3E%3C/svg%3E");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.input {
|
|
29
|
+
border: none;
|
|
30
|
+
min-width: 300px;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 40px;
|
|
33
|
+
padding: 10px 10px 10px 40px;
|
|
34
|
+
font-family: @sans;
|
|
35
|
+
border: 1px solid transparent;
|
|
36
|
+
|
|
37
|
+
&:focus {
|
|
38
|
+
outline: none !important;
|
|
39
|
+
border: 1px dotted #666;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.results-wrapper {
|
|
44
|
+
position: absolute;
|
|
45
|
+
width: 100%;
|
|
46
|
+
background: white;
|
|
47
|
+
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.result {
|
|
51
|
+
padding: 10px;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
|
|
55
|
+
&:hover {
|
|
56
|
+
background-color: #eee;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&.active {
|
|
60
|
+
background-color: #ddd;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Need to make these pointer-events: none so that the result receives the click and we can get the index
|
|
65
|
+
.name {
|
|
66
|
+
font-weight: 700;
|
|
67
|
+
pointer-events: none;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.details {
|
|
71
|
+
pointer-events: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.button {
|
|
75
|
+
width: 100%;
|
|
76
|
+
text-align: left;
|
|
77
|
+
padding: 0;
|
|
78
|
+
background: transparent;
|
|
79
|
+
border: none;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
}
|