feat: autocomplete for search

Co-authored-by: Alex Sparkes <turbomarshmello@gmail.com>
This commit is contained in:
David Ralph
2021-05-01 18:29:07 +01:00
parent 7942c367a7
commit a1b6832747
21 changed files with 221 additions and 14 deletions

View File

@@ -15,6 +15,7 @@
"@fontsource/montserrat": "^4.2.2",
"@material-ui/core": "4.11.4",
"@material-ui/icons": "4.11.2",
"fetch-jsonp": "^1.1.3",
"react": "17.0.2",
"react-clock": "3.0.0",
"react-color-gradient-picker": "0.1.2",

View File

@@ -0,0 +1,77 @@
import React from 'react';
import './autocomplete.scss';
export default class Autocomplete extends React.Component {
constructor(props) {
super(props);
this.state = {
filtered: [],
showList: false,
input: ''
};
this.enabled = (localStorage.getItem('autocomplete') === 'true')
}
onChange = (e) => {
if (this.enabled === false) {
return this.setState({
input: e.target.value
});
}
this.setState({
filtered: this.props.suggestions.filter((suggestion) => suggestion.toLowerCase().indexOf(e.target.value.toLowerCase()) > -1),
showList: true,
input: e.target.value
});
this.props.onChange(e.target.value);
};
onClick = (e) => {
this.setState({
filtered: [],
showList: false,
input: e.target.innerText
});
this.props.onClick(e.target.innerText);
};
render() {
let autocomplete = null;
if (this.state.showList && this.state.input) {
if (this.state.filtered.length && localStorage.getItem('autocomplete') === 'true') {
autocomplete = (
<ul className='suggestions'>
{this.state.filtered.map((suggestion) => {
let className;
return (
<li className={className} key={suggestion} onClick={this.onClick}>
{suggestion}
</li>
);
})}
</ul>
);
}
}
return (
<>
<input
type='text'
onChange={this.onChange}
value={this.state.input}
name={this.props.name || 'name'}
placeholder={this.props.placeholder || ''}
autoComplete='off'
id={this.props.id || ''} />
{autocomplete}
</>
);
}
}

View File

@@ -0,0 +1,38 @@
.suggestions {
text-align: left;
font-size: calc(5px + 1.2vmin);
background-color: rgba(0, 0, 0, 0.5);
border-top-width: 0;
list-style: none;
margin-top: 40px;
max-height: 143px;
overflow: hidden;
position: relative;
display: inline-block;
margin-left: 40px;
border-radius: 24px;
width: 430px;
opacity: 0;
li {
padding: 0.5rem;
padding-left: 20px;
&:hover {
background-color: rgba(0, 0, 0, 0.5);
cursor: pointer;
}
}
}
.searchBar {
input[type=text]:focus+.suggestions {
opacity: 1;
}
}
@media screen and (min-width: 1400px) {
.suggestions {
margin-top: 50px;
}
}

View File

@@ -48,10 +48,10 @@ export default class Modals extends React.PureComponent {
<Main modalClose={() => this.setState({ mainModal: false })}/>
</Modal>
<React.Suspense fallback={renderLoader()}>
<Modal closeTimeoutMS={300} onRequestClose={() => this.closeWelcome()} isOpen={this.state.welcomeModal} className='Modal welcomemodal' overlayClassName='Overlay' ariaHideApp={false}>
<Modal closeTimeoutMS={300} onRequestClose={() => this.closeWelcome()} isOpen={this.state.welcomeModal} className='Modal welcomemodal mainModal' overlayClassName='Overlay' ariaHideApp={false}>
<Welcome modalClose={() => this.closeWelcome()}/>
</Modal>
<Modal closeTimeoutMS={300} onRequestClose={() => this.setState({ feedbackModal: false })} isOpen={this.state.feedbackModal} className='Modal' overlayClassName='Overlay' ariaHideApp={false}>
<Modal closeTimeoutMS={300} onRequestClose={() => this.setState({ feedbackModal: false })} isOpen={this.state.feedbackModal} className='Modal mainModal' overlayClassName='Overlay' ariaHideApp={false}>
<Feedback modalClose={() => this.setState({ feedbackModal: false })}/>
</Modal>
</React.Suspense>

View File

@@ -32,13 +32,21 @@
}
.modalLink {
display: block !important;
margin-top: 5px;
color: var(--modal-link);
cursor: pointer;
padding-left: 10px;
&:hover {
opacity: 0.8;
}
span {
font-size: 1.2rem;
color: var(--modal-link);
vertical-align: text-bottom;
margin-left: 5px;
}
}
.closeModal {
@@ -102,12 +110,6 @@
}
}
@media only screen and (min-height: 700px) {
.ReactModal__Content {
//min-height: 600px;
}
}
ul.sidebar {
position: absolute;
top: 0;

View File

@@ -207,3 +207,16 @@ input::-webkit-inner-spin-button {
input[type=number] {
-moz-appearance: textfield;
}
.resetArea {
h2 {
font-size: 2rem !important;
}
h2, span, svg {
display: inline;
}
svg {
vertical-align: sub;
font-size: 1.4rem;
}
}

View File

@@ -53,7 +53,7 @@ export default class AdvancedSettings extends React.PureComponent {
<p style={{ 'maxWidth': '75%'}}>{advanced.experimental_warning}</p>
<Switch name='experimental' text={this.language.enabled} element='.other'/>
<Modal closeTimeoutMS={100} onRequestClose={() => this.setState({ resetModal: false })} isOpen={this.state.resetModal} className='Modal resetmodal' overlayClassName='Overlay resetoverlay' ariaHideApp={false}>
<Modal closeTimeoutMS={100} onRequestClose={() => this.setState({ resetModal: false })} isOpen={this.state.resetModal} className='Modal resetmodal mainModal' overlayClassName='Overlay resetoverlay' ariaHideApp={false}>
<ResetModal modalClose={() => this.setState({ resetModal: false })} />
</Modal>
</>

View File

@@ -3,6 +3,7 @@ import React from 'react';
import Dropdown from '../Dropdown';
import Checkbox from '../Checkbox';
import Switch from '../Switch';
import Radio from '../Radio';
import EventBus from '../../../../../modules/helpers/eventbus';
@@ -10,6 +11,7 @@ import { isChrome } from 'react-device-detect';
import { toast } from 'react-toastify';
const searchEngines = require('../../../../widgets/search/search_engines.json');
const autocompleteProviders = require('../../../../widgets/search/autocomplete_providers.json')
export default class SearchSettings extends React.PureComponent {
constructor() {
@@ -76,6 +78,9 @@ export default class SearchSettings extends React.PureComponent {
<h2>{search.title}</h2>
<Switch name='searchBar' text={language.enabled} category='widgets' />
{isChrome ? <Checkbox name='voiceSearch' text={search.voice_search} /> : null}
<Checkbox name='autocomplete' text={search.autocomplete} category='search'/>
<Radio title={search.autocomplete_provider} options={autocompleteProviders} name='autocompleteProvider' category='search'/>
<br/>
<Dropdown label={search.search_engine} name='searchEngine' onChange={(value) => this.setSearchEngine(value)}>
{searchEngines.map((engine) => (

View File

@@ -1,6 +1,9 @@
import React from 'react';
import EventBus from '../../../modules/helpers/eventbus';
import fetchJSONP from 'fetch-jsonp';
import AutocompleteInput from '../../helpers/autocomplete/Autocomplete';
import SearchIcon from '@material-ui/icons/Search';
import MicIcon from '@material-ui/icons/Mic';
@@ -8,6 +11,7 @@ import MicIcon from '@material-ui/icons/Mic';
import './search.scss';
const searchEngines = require('./search_engines.json');
const autocompleteProviders = require('./autocomplete_providers.json');
export default class Search extends React.PureComponent {
constructor() {
@@ -15,7 +19,11 @@ export default class Search extends React.PureComponent {
this.state = {
url: '',
query: '',
microphone: null
autocompleteURL: '',
autocompleteQuery: '',
autocompleteCallback: '',
microphone: null,
suggestions: []
};
this.language = window.language.widgets.search;
}
@@ -42,6 +50,15 @@ export default class Search extends React.PureComponent {
window.location.href = this.state.url + `?${this.state.query}=` + value;
}
async getSuggestions(input) {
const data = await (await fetchJSONP(this.state.autocompleteURL + this.state.autocompleteQuery + input, {
jsonpCallback: this.state.autocompleteCallback
})).json();
this.setState({
suggestions: data[1].splice(0, 3)
});
}
init() {
let url;
let query = 'q';
@@ -65,9 +82,23 @@ export default class Search extends React.PureComponent {
microphone = <MicIcon className='micIcon' onClick={this.startSpeechRecognition}/>;
}
let autocompleteURL;
let autocompleteQuery;
let autocompleteCallback;
if (localStorage.getItem('autocomplete') === 'true') {
const info = autocompleteProviders.find((i) => i.value === localStorage.getItem('autocompleteProvider'));
autocompleteURL = info.url;
autocompleteQuery = info.query;
autocompleteCallback = info.callback;
}
this.setState({
url: url,
query: query,
autocompleteURL: autocompleteURL,
autocompleteQuery: autocompleteQuery,
autocompleteCallback: autocompleteCallback,
microphone: microphone
});
}
@@ -91,7 +122,7 @@ export default class Search extends React.PureComponent {
<form action={this.state.url} className='searchBar'>
{this.state.microphone}
<SearchIcon onClick={this.searchButton}/>
<input type='text' placeholder={this.language} name={this.state.query} id='searchtext'/>
<AutocompleteInput placeholder={this.language} name={this.state.query} id='searchtext' suggestions={this.state.suggestions} onChange={(e) => this.getSuggestions(e)} onClick={this.searchButton}/>
</form>
);
}

View File

@@ -0,0 +1,17 @@
[
{
"name": "Google",
"value": "google",
"url": "https://www.google.com/complete/search?client=chrome",
"callback": "callback",
"query": "&q="
},
{
"name": "Bing",
"value": "bing",
"url": "https://api.bing.com/osjson.aspx?JsonType=callback",
"callback": "JsonCallback",
"query": "&query="
}
]

View File

@@ -8,7 +8,7 @@
input[type=text] {
width: 140px;
margin-left: 12px;
margin-left: 40px;
border-radius: 24px;
font-size: calc(5px + 1.2vmin);
border: none;
@@ -28,7 +28,8 @@
}
.MuiSvgIcon-root {
margin-top: 4px;
position: absolute;
margin-top: 6px;
font-size: 30px;
filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.3));
cursor: pointer;

View File

@@ -202,5 +202,9 @@
{
"name": "datezero",
"value": true
},
{
"name": "autocompleteProvider",
"value": "google"
}
]

View File

@@ -165,6 +165,8 @@
"title": "Suche",
"search_engine": "Suchmaschine",
"custom": "Abfrage-URL",
"autocomplete": "Autocomplete",
"autocomplete_provider": "Autocomplete Provider",
"voice_search": "Sprachsuche"
},
"weather": {

View File

@@ -165,6 +165,8 @@
"title": "Search",
"search_engine": "Search engine",
"custom": "Custom search URL",
"autocomplete": "Autocomplete",
"autocomplete_provider": "Autocomplete Provider",
"voice_search": "Voice search"
},
"weather": {

View File

@@ -165,6 +165,8 @@
"title": "Search",
"search_engine": "Search engine",
"custom": "Custom search URL",
"autocomplete": "Autocomplete",
"autocomplete_provider": "Autocomplete Provider",
"voice_search": "Voice search"
},
"weather": {

View File

@@ -165,6 +165,8 @@
"title": "Búsqueda",
"search_engine": "Motor de búsqueda",
"custom": "URL de búsqueda personalizada",
"autocomplete": "Autocomplete",
"autocomplete_provider": "Autocomplete Provider",
"voice_search": "Búsqueda por voz"
},
"weather": {

View File

@@ -165,6 +165,8 @@
"title": "Barre de Recherche",
"search_engine": "Moteur de recherche",
"custom": "URL de recherche personnalisée",
"autocomplete": "Autocomplete",
"autocomplete_provider": "Autocomplete Provider",
"voice_search": "Recherche vocale"
},
"weather": {

View File

@@ -165,6 +165,8 @@
"title": "Zoekbalk",
"search_engine": "Zoekmachine",
"custom": "Aangepaste zoekmachine-url",
"autocomplete": "Autocomplete",
"autocomplete_provider": "Autocomplete Provider",
"voice_search": "Spraakgestuurd zoeken gebruiken"
},
"weather": {

View File

@@ -165,6 +165,8 @@
"title": "Søkebar",
"search_engine": "Søkemotor",
"custom": "Custom search URL",
"autocomplete": "Autocomplete",
"autocomplete_provider": "Autocomplete Provider",
"voice_search": "Voice search"
},
"weather": {

View File

@@ -165,6 +165,8 @@
"title": "Панель поиска",
"search_engine": "Поисковый движок",
"custom": "Пользовательский поисковый движок",
"autocomplete": "Autocomplete",
"autocomplete_provider": "Autocomplete Provider",
"voice_search": "Поиск голосом"
},
"weather": {

View File

@@ -165,6 +165,8 @@
"title": "搜索栏",
"search_engine": "搜索引擎",
"custom": "自定义搜索链接",
"autocomplete": "Autocomplete",
"autocomplete_provider": "Autocomplete Provider",
"voice_search": "语音搜索"
},
"weather": {