@financial-times/x-topic-search 8.0.1 → 8.0.4
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.
- package/dist/TopicSearch.es5.js +2 -3
- package/package.json +3 -3
- package/rollup.js +4 -0
- package/src/NoSuggestions.jsx +18 -0
- package/src/SuggestionList.jsx +40 -0
- package/src/TopicSearch.jsx +134 -0
- package/src/TopicSearch.scss +125 -0
- package/src/lib/get-suggestions.js +20 -0
package/dist/TopicSearch.es5.js
CHANGED
|
@@ -52,18 +52,17 @@ function _inherits(subClass, superClass) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function _getPrototypeOf(o) {
|
|
55
|
-
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
|
|
55
|
+
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) {
|
|
56
56
|
return o.__proto__ || Object.getPrototypeOf(o);
|
|
57
57
|
};
|
|
58
58
|
return _getPrototypeOf(o);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function _setPrototypeOf(o, p) {
|
|
62
|
-
_setPrototypeOf = Object.setPrototypeOf
|
|
62
|
+
_setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
|
|
63
63
|
o.__proto__ = p;
|
|
64
64
|
return o;
|
|
65
65
|
};
|
|
66
|
-
|
|
67
66
|
return _setPrototypeOf(o, p);
|
|
68
67
|
}
|
|
69
68
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@financial-times/x-topic-search",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/TopicSearch.cjs.js",
|
|
6
6
|
"module": "dist/TopicSearch.esm.js",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"author": "",
|
|
17
17
|
"license": "ISC",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@financial-times/x-engine": "^8.0.
|
|
20
|
-
"@financial-times/x-follow-button": "^8.0.
|
|
19
|
+
"@financial-times/x-engine": "^8.0.4",
|
|
20
|
+
"@financial-times/x-follow-button": "^8.0.4",
|
|
21
21
|
"debounce-promise": "^3.1.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
package/rollup.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { h } from '@financial-times/x-engine'
|
|
2
|
+
|
|
3
|
+
export default ({ searchTerm }) => (
|
|
4
|
+
<div className="x-topic-search-no-suggestions" aria-live="polite">
|
|
5
|
+
<h2 className="x-topic-search-no-suggestions__title">
|
|
6
|
+
No topics matching <b>{searchTerm}</b>
|
|
7
|
+
</h2>
|
|
8
|
+
|
|
9
|
+
<p>Suggestions:</p>
|
|
10
|
+
|
|
11
|
+
<ul className="x-topic-search-no-suggestions__message">
|
|
12
|
+
<li>Make sure that all words are spelled correctly.</li>
|
|
13
|
+
<li>Try different keywords.</li>
|
|
14
|
+
<li>Try more general keywords.</li>
|
|
15
|
+
<li>Try fewer keywords.</li>
|
|
16
|
+
</ul>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { h } from '@financial-times/x-engine'
|
|
2
|
+
import { FollowButton } from '@financial-times/x-follow-button'
|
|
3
|
+
|
|
4
|
+
const defaultFollowButtonRender = (concept, csrfToken, followedTopicIds) => (
|
|
5
|
+
<FollowButton
|
|
6
|
+
conceptId={concept.id}
|
|
7
|
+
conceptName={concept.prefLabel}
|
|
8
|
+
csrfToken={csrfToken}
|
|
9
|
+
isFollowed={followedTopicIds.includes(concept.id)}
|
|
10
|
+
/>
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export default ({ suggestions, renderFollowButton, searchTerm, csrfToken, followedTopicIds = [] }) => {
|
|
14
|
+
renderFollowButton =
|
|
15
|
+
typeof renderFollowButton === 'function' ? renderFollowButton : defaultFollowButtonRender
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<ul className="x-topic-search-suggestions" aria-live="polite">
|
|
19
|
+
{suggestions.map((suggestion) => (
|
|
20
|
+
<li
|
|
21
|
+
className="x-topic-search-suggestions__suggestion"
|
|
22
|
+
key={suggestion.id}
|
|
23
|
+
data-trackable="myft-topic"
|
|
24
|
+
data-concept-id={suggestion.id}
|
|
25
|
+
data-trackable-meta={'{"search-term":"' + searchTerm + '"}'}
|
|
26
|
+
>
|
|
27
|
+
<a
|
|
28
|
+
data-trackable="topic-link"
|
|
29
|
+
className="x-topic-search-suggestions__suggestion-name"
|
|
30
|
+
href={suggestion.url || `/stream/${suggestion.id}`}
|
|
31
|
+
>
|
|
32
|
+
{suggestion.prefLabel}
|
|
33
|
+
</a>
|
|
34
|
+
|
|
35
|
+
{renderFollowButton(suggestion, csrfToken, followedTopicIds)}
|
|
36
|
+
</li>
|
|
37
|
+
))}
|
|
38
|
+
</ul>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { h, Component } from '@financial-times/x-engine'
|
|
2
|
+
import getSuggestions from './lib/get-suggestions.js'
|
|
3
|
+
import debounce from 'debounce-promise'
|
|
4
|
+
import SuggestionList from './SuggestionList'
|
|
5
|
+
import NoSuggestions from './NoSuggestions'
|
|
6
|
+
|
|
7
|
+
class TopicSearch extends Component {
|
|
8
|
+
constructor(props) {
|
|
9
|
+
super(props)
|
|
10
|
+
|
|
11
|
+
this.minSearchLength = props.minSearchLength || 2
|
|
12
|
+
this.maxSuggestions = props.maxSuggestions || 5
|
|
13
|
+
this.apiUrl = props.apiUrl
|
|
14
|
+
this.getSuggestions = debounce(getSuggestions, 150)
|
|
15
|
+
this.outsideEvents = ['focusout', 'focusin', 'click']
|
|
16
|
+
this.handleInputChange = this.handleInputChange.bind(this)
|
|
17
|
+
this.handleInputClick = this.handleInputClick.bind(this)
|
|
18
|
+
this.handleInputFocus = this.handleInputFocus.bind(this)
|
|
19
|
+
this.handleInteractionOutside = this.handleInteractionOutside.bind(this)
|
|
20
|
+
|
|
21
|
+
this.state = {
|
|
22
|
+
followedTopicIds: props.followedTopicIds || [],
|
|
23
|
+
searchTerm: '',
|
|
24
|
+
showResult: false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
componentDidMount() {
|
|
29
|
+
this.outsideEvents.forEach((action) => {
|
|
30
|
+
document.body.addEventListener(action, this.handleInteractionOutside)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
componentWillUnmount() {
|
|
35
|
+
this.outsideEvents.forEach((action) => {
|
|
36
|
+
document.body.removeEventListener(action, this.handleInteractionOutside)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
handleInputChange(event) {
|
|
41
|
+
const searchTerm = event.target.value.trim()
|
|
42
|
+
|
|
43
|
+
this.setState({ searchTerm })
|
|
44
|
+
|
|
45
|
+
if (searchTerm.length >= this.minSearchLength) {
|
|
46
|
+
this.getSuggestions(searchTerm, this.maxSuggestions, this.apiUrl)
|
|
47
|
+
.then(({ suggestions }) => {
|
|
48
|
+
this.setState({
|
|
49
|
+
suggestions,
|
|
50
|
+
showResult: true
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
.catch(() => {
|
|
54
|
+
this.setState({
|
|
55
|
+
showResult: false
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
} else {
|
|
59
|
+
this.setState({
|
|
60
|
+
showResult: false
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
handleInteractionOutside(event) {
|
|
66
|
+
if (!this.rootEl.contains(event.target)) {
|
|
67
|
+
this.setState({
|
|
68
|
+
showResult: false
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
handleInputClick() {
|
|
74
|
+
if (this.state.searchTerm.length >= this.minSearchLength) {
|
|
75
|
+
this.setState({
|
|
76
|
+
showResult: true
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
handleInputFocus(event) {
|
|
82
|
+
event.target.select()
|
|
83
|
+
this.handleInputClick()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render() {
|
|
87
|
+
const { csrfToken, followedTopicIds, renderFollowButton } = this.props
|
|
88
|
+
const { searchTerm, showResult, suggestions } = this.state
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="x-topic-search" ref={(el) => (this.rootEl = el)}>
|
|
92
|
+
<h2 className="o-normalise-visually-hidden">
|
|
93
|
+
Search for topics, authors, companies, or other areas of interest
|
|
94
|
+
</h2>
|
|
95
|
+
|
|
96
|
+
<label className="o-normalise-visually-hidden" htmlFor="topic-search-input">
|
|
97
|
+
Search and add topics
|
|
98
|
+
</label>
|
|
99
|
+
<div className="x-topic-search__input-wrapper">
|
|
100
|
+
<i className="x-topic-search__search-icon" />
|
|
101
|
+
<input
|
|
102
|
+
type="search"
|
|
103
|
+
id="topic-search-input"
|
|
104
|
+
placeholder="Search and add topics"
|
|
105
|
+
className="x-topic-search__input"
|
|
106
|
+
data-trackable="topic-search"
|
|
107
|
+
autoComplete="off"
|
|
108
|
+
onInput={this.handleInputChange}
|
|
109
|
+
onClick={this.handleInputClick}
|
|
110
|
+
onFocus={this.handleInputFocus}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{showResult && searchTerm.length >= this.minSearchLength && (
|
|
115
|
+
<div className="x-topic-search__result-container">
|
|
116
|
+
{suggestions.length > 0 ? (
|
|
117
|
+
<SuggestionList
|
|
118
|
+
csrfToken={csrfToken}
|
|
119
|
+
followedTopicIds={followedTopicIds}
|
|
120
|
+
searchTerm={searchTerm}
|
|
121
|
+
suggestions={suggestions}
|
|
122
|
+
renderFollowButton={renderFollowButton}
|
|
123
|
+
/>
|
|
124
|
+
) : (
|
|
125
|
+
<NoSuggestions searchTerm={searchTerm} />
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { TopicSearch }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
$system-code: 'github:Financial-Times/x-dash' !default;
|
|
2
|
+
|
|
3
|
+
@import '@financial-times/o-icons/main';
|
|
4
|
+
@import '@financial-times/o-colors/main';
|
|
5
|
+
@import '@financial-times/o-typography/main';
|
|
6
|
+
@import '@financial-times/o-editorial-typography/main';
|
|
7
|
+
|
|
8
|
+
@import '@financial-times/x-follow-button/src/styles/main';
|
|
9
|
+
|
|
10
|
+
.x-topic-search {
|
|
11
|
+
position: relative;
|
|
12
|
+
text-align: center;
|
|
13
|
+
background-color: oColorsByName('claret-70');
|
|
14
|
+
color: oColorsByName('white');
|
|
15
|
+
width: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.x-topic-search__input-wrapper {
|
|
19
|
+
position: relative;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.x-topic-search__search-icon {
|
|
23
|
+
@include oIconsContent($icon-name: 'search', $color: oColorsByName('white'), $size: 32);
|
|
24
|
+
position: absolute;
|
|
25
|
+
top: 4px;
|
|
26
|
+
left: -7px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.x-topic-search__input {
|
|
30
|
+
@include oTypographySans($scale: 0);
|
|
31
|
+
-webkit-appearance: none;
|
|
32
|
+
width: 100%;
|
|
33
|
+
min-height: 40px;
|
|
34
|
+
margin: 0;
|
|
35
|
+
border: none;
|
|
36
|
+
border-bottom: 2px solid oColorsByName('white');
|
|
37
|
+
padding: 0 9px 0 24px;
|
|
38
|
+
max-width: none;
|
|
39
|
+
color: oColorsByName('white');
|
|
40
|
+
background: transparent;
|
|
41
|
+
|
|
42
|
+
&::placeholder {
|
|
43
|
+
color: oColorsByName('white');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&::-webkit-search-cancel-button {
|
|
47
|
+
@include oIconsContent($icon-name: 'cross', $color: oColorsByName('white'), $size: 26);
|
|
48
|
+
-webkit-appearance: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&::-webkit-search-decoration,
|
|
52
|
+
&::-webkit-search-results-button,
|
|
53
|
+
&::-webkit-search-results-decoration {
|
|
54
|
+
display: none;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.x-topic-search__result-container {
|
|
59
|
+
position: absolute;
|
|
60
|
+
background: oColorsByName('white');
|
|
61
|
+
top: 48px;
|
|
62
|
+
padding: 10px;
|
|
63
|
+
z-index: 1;
|
|
64
|
+
width: calc(100% - 20px);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.x-topic-search-suggestions {
|
|
68
|
+
list-style: none;
|
|
69
|
+
padding: 0;
|
|
70
|
+
text-align: left;
|
|
71
|
+
margin: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.x-topic-search-suggestions__suggestion {
|
|
75
|
+
display: flex;
|
|
76
|
+
justify-content: space-between;
|
|
77
|
+
align-items: center;
|
|
78
|
+
clear: right;
|
|
79
|
+
padding: 5px 0;
|
|
80
|
+
border-bottom: 1px solid oColorsByName('black-5');
|
|
81
|
+
|
|
82
|
+
&:last-child {
|
|
83
|
+
border-bottom: 0;
|
|
84
|
+
padding-bottom: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
&:first-child {
|
|
88
|
+
padding-top: 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.x-topic-search-suggestions__suggestion-name {
|
|
93
|
+
@include oTypographySans($scale: 0, $weight: 'semibold');
|
|
94
|
+
@include oEditorialTypographyTag($type: 'topic');
|
|
95
|
+
padding: 5px 0;
|
|
96
|
+
max-width: 50%;
|
|
97
|
+
word-wrap: break-word;
|
|
98
|
+
overflow-wrap: break-word;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.x-topic-search-no-suggestions {
|
|
102
|
+
@include oTypographySans($scale: 1);
|
|
103
|
+
color: oColorsByName('black-70');
|
|
104
|
+
text-align: left;
|
|
105
|
+
padding: 0 0 5px 0;
|
|
106
|
+
margin: 0;
|
|
107
|
+
p {
|
|
108
|
+
margin: 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.x-topic-search-no-suggestions__title {
|
|
113
|
+
@include oTypographySans($scale: 3);
|
|
114
|
+
font-weight: normal;
|
|
115
|
+
overflow-wrap: break-word;
|
|
116
|
+
margin: 0;
|
|
117
|
+
padding: 0 0 20px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.x-topic-search-no-suggestions__message {
|
|
121
|
+
margin-top: 12px;
|
|
122
|
+
margin-bottom: 0;
|
|
123
|
+
padding-left: 20px;
|
|
124
|
+
list-style-type: disc;
|
|
125
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const addQueryParamToUrl = (name, value, url, append = true) => {
|
|
2
|
+
const queryParam = `${name}=${value}`;
|
|
3
|
+
|
|
4
|
+
return append === true ? `${url}&${queryParam}` : `${url}?${queryParam}`;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default (searchTerm, maxSuggestions, apiUrl) => {
|
|
8
|
+
const dataSrc = addQueryParamToUrl('count', maxSuggestions, apiUrl, false);
|
|
9
|
+
const url = addQueryParamToUrl('partial', searchTerm.replace(' ', '+'), dataSrc);
|
|
10
|
+
|
|
11
|
+
return fetch(url)
|
|
12
|
+
.then(response => {
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(response.statusText);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return response.json();
|
|
18
|
+
})
|
|
19
|
+
.then(suggestions => ({ suggestions }));
|
|
20
|
+
};
|