make suggestions a bit more robust
This commit is contained in:
+99
-30
@@ -41,16 +41,24 @@
|
|||||||
// e.g. type "r/r/unixporn" to go to "reddit.com/r/unixporn".
|
// e.g. type "r/r/unixporn" to go to "reddit.com/r/unixporn".
|
||||||
pathDelimiter: '/',
|
pathDelimiter: '/',
|
||||||
|
|
||||||
// set to true to instantly redirect when a key is matched.
|
// instantly redirect when a key is matched.
|
||||||
// put a space before any search queries to prevent unwanted redirects.
|
// put a space before any other queries to prevent unwanted redirects.
|
||||||
instantRedirect: false,
|
instantRedirect: false,
|
||||||
|
|
||||||
// suggest your most popular queries as you type.
|
// give suggestions as you type.
|
||||||
suggestions: false,
|
suggestions: true,
|
||||||
|
|
||||||
// max amount of suggestions to display.
|
// max amount of suggestions that will ever be displayed.
|
||||||
suggestionsLimit: 4,
|
suggestionsLimit: 4,
|
||||||
|
|
||||||
|
// the order and limit for each suggestion influencer.
|
||||||
|
// the following would give you 1 suggestion from your search history
|
||||||
|
// and 4 suggestions from Duck Duck Go.
|
||||||
|
influencers: [
|
||||||
|
{ name: 'History', limit: 1 },
|
||||||
|
{ name: 'DuckDuckGo', limit: 4 },
|
||||||
|
],
|
||||||
|
|
||||||
// open queries in a new tab.
|
// open queries in a new tab.
|
||||||
newTab: true,
|
newTab: true,
|
||||||
|
|
||||||
@@ -279,6 +287,12 @@
|
|||||||
const $ = {
|
const $ = {
|
||||||
el: s => document.querySelector(s),
|
el: s => document.querySelector(s),
|
||||||
els: s => [].slice.call(document.querySelectorAll(s) || []),
|
els: s => [].slice.call(document.querySelectorAll(s) || []),
|
||||||
|
|
||||||
|
jsonp: url => {
|
||||||
|
let script = document.createElement('script');
|
||||||
|
script.src = url;
|
||||||
|
$.el('head').appendChild(script);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class Clock {
|
class Clock {
|
||||||
@@ -355,7 +369,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
class History {
|
class History {
|
||||||
constructor() {
|
constructor(suggestionsLimit = 0) {
|
||||||
|
this._suggestionsLimit = suggestionsLimit;
|
||||||
this._history = this._getFromStorage('history');
|
this._history = this._getFromStorage('history');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +385,7 @@
|
|||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const suggestions = this._history
|
const suggestions = this._history
|
||||||
.filter(item => this._itemContainsQuery(query, item[0]))
|
.filter(item => this._itemContainsQuery(query, item[0]))
|
||||||
.slice(0, CONFIG.suggestionsLimit)
|
.slice(0, this._suggestionsLimit)
|
||||||
.map(item => item[0]);
|
.map(item => item[0]);
|
||||||
|
|
||||||
resolve(suggestions);
|
resolve(suggestions);
|
||||||
@@ -382,8 +397,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_itemContainsQuery(query, item) {
|
_itemContainsQuery(query, item) {
|
||||||
const match = item.indexOf(query) !== -1;
|
return query && item.indexOf(query) !== -1;
|
||||||
return query && match && query !== item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_saveToStorage(name, data) {
|
_saveToStorage(name, data) {
|
||||||
@@ -412,31 +426,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DuckDuckGo {
|
||||||
|
constructor(suggestionsLimit = 0) {
|
||||||
|
this._endpoint = 'https://duckduckgo.com/ac';
|
||||||
|
this._callback = 'autocompleteCallback';
|
||||||
|
this._suggestionsLimit = suggestionsLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem() {}
|
||||||
|
|
||||||
|
getSuggestionsPromise(query) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this._resolve = resolve;
|
||||||
|
window[this._callback] = this._handleResponse.bind(this);
|
||||||
|
$.jsonp(`${this._endpoint}?callback=${this._callback}&q=${query}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleResponse(res) {
|
||||||
|
const suggestions = res.slice(0, this._suggestionsLimit)
|
||||||
|
.map(i => i.phrase);
|
||||||
|
|
||||||
|
this._resolve(suggestions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Suggester {
|
class Suggester {
|
||||||
constructor(influencers) {
|
constructor(influencers) {
|
||||||
this._inputEl = $.el('#js-search-input');
|
this._inputEl = $.el('#js-search-input');
|
||||||
this._suggestionsEl = $.el('#js-search-suggestions');
|
this._suggestionsEl = $.el('#js-search-suggestions');
|
||||||
|
this._totalSuggestions = 0;
|
||||||
this._suggestionEls = [];
|
this._suggestionEls = [];
|
||||||
this._influencers = influencers;
|
this._influencers = influencers;
|
||||||
this._handleKeydown = this._handleKeydown.bind(this);
|
this._handleKeydown = this._handleKeydown.bind(this);
|
||||||
document.addEventListener('keydown', this._handleKeydown);
|
document.addEventListener('keydown', this._handleKeydown);
|
||||||
}
|
}
|
||||||
|
|
||||||
suggest(input, clickCallback = () => {}) {
|
add(query) {
|
||||||
this._clearSuggestions();
|
this._influencers.forEach(i => i.addItem(query));
|
||||||
input = input.trim();
|
}
|
||||||
if (!input) return;
|
|
||||||
this._handleClick = clickCallback;
|
|
||||||
|
|
||||||
this._influencers
|
suggest(input, clickCallback = () => {}) {
|
||||||
.map(influencer => influencer.getSuggestionsPromise(input))
|
input = input.trim();
|
||||||
.forEach(promise => {
|
|
||||||
promise.then(suggestions => {
|
if (!input) {
|
||||||
suggestions.forEach(item => this._appendSuggestion(item));
|
this._clearSuggestions();
|
||||||
this._suggestionEls = $.els('.js-search-suggestion');
|
return;
|
||||||
this._registerClickEvents();
|
}
|
||||||
});
|
|
||||||
});
|
this._handleClick = clickCallback;
|
||||||
|
this._suggest(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
_appendSuggestion(suggestion) {
|
_appendSuggestion(suggestion) {
|
||||||
@@ -452,6 +491,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
this._inputEl.classList.add('bottom-no-radius');
|
this._inputEl.classList.add('bottom-no-radius');
|
||||||
|
return ++this._totalSuggestions === CONFIG.suggestionsLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearClickEvents() {
|
_clearClickEvents() {
|
||||||
@@ -462,11 +502,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_clearSuggestions() {
|
_clearSuggestions() {
|
||||||
|
this._totalSuggestions = 0;
|
||||||
this._clearClickEvents();
|
this._clearClickEvents();
|
||||||
this._suggestionsEl.innerHTML = '';
|
this._suggestionsEl.innerHTML = '';
|
||||||
this._inputEl.classList.remove('bottom-no-radius');
|
this._inputEl.classList.remove('bottom-no-radius');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [[1, 2], [1, 2, 3, 4]] -> [1, 2, 3, 4]
|
||||||
|
_flattenAndUnique(array) {
|
||||||
|
return [...new Set([].concat.apply([], array))];
|
||||||
|
}
|
||||||
|
|
||||||
_focusNext() {
|
_focusNext() {
|
||||||
if (this._suggestionEls.length) {
|
if (this._suggestionEls.length) {
|
||||||
const active = document.activeElement;
|
const active = document.activeElement;
|
||||||
@@ -498,6 +544,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gatherInfluencers(input) {
|
||||||
|
return this._influencers
|
||||||
|
.map(influencer => influencer.getSuggestionsPromise(input));
|
||||||
|
}
|
||||||
|
|
||||||
_handleKeydown(event) {
|
_handleKeydown(event) {
|
||||||
const isDown = event.which === 40;
|
const isDown = event.which === 40;
|
||||||
const isUp = event.which === 38;
|
const isUp = event.which === 38;
|
||||||
@@ -513,6 +564,18 @@
|
|||||||
el.addEventListener('click', this._handleClick.bind(null, el.value));
|
el.addEventListener('click', this._handleClick.bind(null, el.value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_suggest(input) {
|
||||||
|
Promise.all(this._gatherInfluencers(input)).then(res => {
|
||||||
|
this._clearSuggestions();
|
||||||
|
|
||||||
|
this._flattenAndUnique(res)
|
||||||
|
.some(item => this._appendSuggestion(item));
|
||||||
|
|
||||||
|
this._suggestionEls = $.els('.js-search-suggestion');
|
||||||
|
this._registerClickEvents();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class QueryParser {
|
class QueryParser {
|
||||||
@@ -597,9 +660,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Form {
|
class Form {
|
||||||
constructor(help, history, suggester, queryParser) {
|
constructor(help, suggester, queryParser) {
|
||||||
this._help = help;
|
this._help = help;
|
||||||
this._history = history;
|
|
||||||
this._suggester = suggester;
|
this._suggester = suggester;
|
||||||
this._queryParser = queryParser;
|
this._queryParser = queryParser;
|
||||||
this._formEl = $.el('#js-search-form');
|
this._formEl = $.el('#js-search-form');
|
||||||
@@ -636,7 +698,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleKeyup(event) {
|
_handleKeyup(event) {
|
||||||
if (CONFIG.suggestions && this._inputElVal !== this._inputEl.value) {
|
if (
|
||||||
|
CONFIG.suggestions &&
|
||||||
|
this._inputElVal.trim() !== this._inputEl.value.trim()
|
||||||
|
) {
|
||||||
this._suggester.suggest(this._inputEl.value, this._submitWithValue);
|
this._suggester.suggest(this._inputEl.value, this._submitWithValue);
|
||||||
this._inputElVal = this._inputEl.value;
|
this._inputElVal = this._inputEl.value;
|
||||||
}
|
}
|
||||||
@@ -661,7 +726,7 @@
|
|||||||
this._inputEl.value = '';
|
this._inputEl.value = '';
|
||||||
this._help.toggle();
|
this._help.toggle();
|
||||||
} else {
|
} else {
|
||||||
this._history.addItem(query);
|
this._suggester.add(query);
|
||||||
this._suggester.suggest('');
|
this._suggester.suggest('');
|
||||||
this._inputEl.value = '';
|
this._inputEl.value = '';
|
||||||
this._redirect(this._queryParser.generateRedirect(query));
|
this._redirect(this._queryParser.generateRedirect(query));
|
||||||
@@ -678,8 +743,12 @@
|
|||||||
<script>
|
<script>
|
||||||
const clock = new Clock();
|
const clock = new Clock();
|
||||||
const help = new Help();
|
const help = new Help();
|
||||||
const history = new History();
|
const influencers = { History: History, DuckDuckGo: DuckDuckGo };
|
||||||
const suggester = new Suggester([history]);
|
|
||||||
|
const suggester = new Suggester(
|
||||||
|
CONFIG.influencers.map(i => new influencers[i.name](i.limit))
|
||||||
|
);
|
||||||
|
|
||||||
const parser = new QueryParser();
|
const parser = new QueryParser();
|
||||||
const form = new Form(help, history, suggester, parser);
|
const form = new Form(help, suggester, parser);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user