sfc-utils 1.4.50 → 1.4.53

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
@@ -348,6 +348,10 @@ const Topper2 = ({ settings, wcmData, mods }) => {
348
348
 
349
349
  const wcmIdList = getWcmIdList(Image);
350
350
  const TopperHtml = () => {
351
+ // Remove <a> tags from Title and Deck, links in h1/h2 are bad for SEO
352
+ Title = removeLinksFromText(Title)
353
+ Deck = removeLinksFromText(Deck)
354
+
351
355
  switch (Topper_Style) {
352
356
  case "full-screen":
353
357
  let containerCss = isSlideshow(wcmIdList) ? topperStyles.topperContainerSlideshowFullScreen : topperStyles.topperContainerFullScreen;
@@ -588,6 +592,28 @@ const Topper2 = ({ settings, wcmData, mods }) => {
588
592
  }
589
593
  }
590
594
 
595
+ /** Removes html links from string */
596
+ const removeLinksFromText = (str) => {
597
+ // Matches to text with <a> tags
598
+ let reStr = "(<a | <a)(.*)(?=>)(.*)(<\/a>)";
599
+ let reEndStr = "(<\/a>)"
600
+
601
+ let re = RegExp(reStr, 'g');
602
+ let reEnd = RegExp(reEndStr, 'g')
603
+
604
+ let linkText = re.exec(str)
605
+ // If text does not contain links, return early
606
+ if (linkText === null) return str
607
+
608
+ let linkedText = linkText[0].split(reEnd)[0].split(">")[1].trim()
609
+
610
+ let textArr = str.split(re)
611
+ let preLink = (textArr.length > 2 || re.lastIndex === str.length) ? textArr[0].trim() : "";
612
+ let postLink = (textArr.length > 2 || re.lastIndex !== str.length) ? String(textArr.slice(-1)).trim() : "";
613
+
614
+ return `${preLink} ${linkedText} ${postLink}`
615
+ }
616
+
591
617
  return (
592
618
  <TopperHtml />
593
619
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sfc-utils",
3
- "version": "1.4.50",
3
+ "version": "1.4.53",
4
4
  "author": "ewagstaff <evanjwagstaff@gmail.com>",
5
5
  "dependencies": {
6
6
  "archieml": "^0.4.2",
@@ -0,0 +1,80 @@
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
+ height: 40px;
32
+ padding: 10px 10px 10px 40px;
33
+ font-family: @sans;
34
+ border: 1px solid transparent;
35
+
36
+ &:focus {
37
+ outline: none !important;
38
+ border: 1px dotted #666;
39
+ }
40
+ }
41
+
42
+ .results-wrapper {
43
+ position: absolute;
44
+ width: 100%;
45
+ background: white;
46
+ box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
47
+ }
48
+
49
+ .result {
50
+ padding: 10px;
51
+ font-size: 14px;
52
+ cursor: pointer;
53
+
54
+ &:hover {
55
+ background-color: #eee;
56
+ }
57
+
58
+ &.active {
59
+ background-color: #ddd;
60
+ }
61
+ }
62
+
63
+ // Need to make these pointer-events: none so that the result receives the click and we can get the index
64
+ .name {
65
+ font-weight: 700;
66
+ pointer-events: none;
67
+ }
68
+
69
+ .details {
70
+ pointer-events: none;
71
+ }
72
+
73
+ .button {
74
+ width: 100%;
75
+ text-align: left;
76
+ padding: 0;
77
+ background: transparent;
78
+ border: none;
79
+ pointer-events: none;
80
+ }