mirror of
https://github.com/mue/mue.git
synced 2026-06-08 14:10:42 +02:00
feat: autocomplete for search
Co-authored-by: Alex Sparkes <turbomarshmello@gmail.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
77
src/components/helpers/autocomplete/Autocomplete.jsx
Normal file
77
src/components/helpers/autocomplete/Autocomplete.jsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
38
src/components/helpers/autocomplete/autocomplete.scss
Normal file
38
src/components/helpers/autocomplete/autocomplete.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
17
src/components/widgets/search/autocomplete_providers.json
Normal file
17
src/components/widgets/search/autocomplete_providers.json
Normal 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="
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -202,5 +202,9 @@
|
||||
{
|
||||
"name": "datezero",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "autocompleteProvider",
|
||||
"value": "google"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -165,6 +165,8 @@
|
||||
"title": "Suche",
|
||||
"search_engine": "Suchmaschine",
|
||||
"custom": "Abfrage-URL",
|
||||
"autocomplete": "Autocomplete",
|
||||
"autocomplete_provider": "Autocomplete Provider",
|
||||
"voice_search": "Sprachsuche"
|
||||
},
|
||||
"weather": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -165,6 +165,8 @@
|
||||
"title": "Панель поиска",
|
||||
"search_engine": "Поисковый движок",
|
||||
"custom": "Пользовательский поисковый движок",
|
||||
"autocomplete": "Autocomplete",
|
||||
"autocomplete_provider": "Autocomplete Provider",
|
||||
"voice_search": "Поиск голосом"
|
||||
},
|
||||
"weather": {
|
||||
|
||||
@@ -165,6 +165,8 @@
|
||||
"title": "搜索栏",
|
||||
"search_engine": "搜索引擎",
|
||||
"custom": "自定义搜索链接",
|
||||
"autocomplete": "Autocomplete",
|
||||
"autocomplete_provider": "Autocomplete Provider",
|
||||
"voice_search": "语音搜索"
|
||||
},
|
||||
"weather": {
|
||||
|
||||
Reference in New Issue
Block a user