diff --git a/index.html b/index.html
index 6ac4d76..1ac8d72 100644
--- a/index.html
+++ b/index.html
@@ -41,16 +41,24 @@
// e.g. type "r/r/unixporn" to go to "reddit.com/r/unixporn".
pathDelimiter: '/',
- // set to true to instantly redirect when a key is matched.
- // put a space before any search queries to prevent unwanted redirects.
+ // instantly redirect when a key is matched.
+ // put a space before any other queries to prevent unwanted redirects.
instantRedirect: false,
- // suggest your most popular queries as you type.
- suggestions: false,
+ // give suggestions as you type.
+ suggestions: true,
- // max amount of suggestions to display.
+ // max amount of suggestions that will ever be displayed.
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.
newTab: true,
@@ -279,6 +287,12 @@
const $ = {
el: s => document.querySelector(s),
els: s => [].slice.call(document.querySelectorAll(s) || []),
+
+ jsonp: url => {
+ let script = document.createElement('script');
+ script.src = url;
+ $.el('head').appendChild(script);
+ },
};
class Clock {
@@ -355,7 +369,8 @@
}
class History {
- constructor() {
+ constructor(suggestionsLimit = 0) {
+ this._suggestionsLimit = suggestionsLimit;
this._history = this._getFromStorage('history');
}
@@ -370,7 +385,7 @@
return new Promise(resolve => {
const suggestions = this._history
.filter(item => this._itemContainsQuery(query, item[0]))
- .slice(0, CONFIG.suggestionsLimit)
+ .slice(0, this._suggestionsLimit)
.map(item => item[0]);
resolve(suggestions);
@@ -382,8 +397,7 @@
}
_itemContainsQuery(query, item) {
- const match = item.indexOf(query) !== -1;
- return query && match && query !== item;
+ return query && item.indexOf(query) !== -1;
}
_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 {
constructor(influencers) {
this._inputEl = $.el('#js-search-input');
this._suggestionsEl = $.el('#js-search-suggestions');
+ this._totalSuggestions = 0;
this._suggestionEls = [];
this._influencers = influencers;
this._handleKeydown = this._handleKeydown.bind(this);
document.addEventListener('keydown', this._handleKeydown);
}
- suggest(input, clickCallback = () => {}) {
- this._clearSuggestions();
- input = input.trim();
- if (!input) return;
- this._handleClick = clickCallback;
+ add(query) {
+ this._influencers.forEach(i => i.addItem(query));
+ }
- this._influencers
- .map(influencer => influencer.getSuggestionsPromise(input))
- .forEach(promise => {
- promise.then(suggestions => {
- suggestions.forEach(item => this._appendSuggestion(item));
- this._suggestionEls = $.els('.js-search-suggestion');
- this._registerClickEvents();
- });
- });
+ suggest(input, clickCallback = () => {}) {
+ input = input.trim();
+
+ if (!input) {
+ this._clearSuggestions();
+ return;
+ }
+
+ this._handleClick = clickCallback;
+ this._suggest(input);
}
_appendSuggestion(suggestion) {
@@ -452,6 +491,7 @@
);
this._inputEl.classList.add('bottom-no-radius');
+ return ++this._totalSuggestions === CONFIG.suggestionsLimit;
}
_clearClickEvents() {
@@ -462,11 +502,17 @@
}
_clearSuggestions() {
+ this._totalSuggestions = 0;
this._clearClickEvents();
this._suggestionsEl.innerHTML = '';
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() {
if (this._suggestionEls.length) {
const active = document.activeElement;
@@ -498,6 +544,11 @@
}
}
+ _gatherInfluencers(input) {
+ return this._influencers
+ .map(influencer => influencer.getSuggestionsPromise(input));
+ }
+
_handleKeydown(event) {
const isDown = event.which === 40;
const isUp = event.which === 38;
@@ -513,6 +564,18 @@
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 {
@@ -597,9 +660,8 @@
}
class Form {
- constructor(help, history, suggester, queryParser) {
+ constructor(help, suggester, queryParser) {
this._help = help;
- this._history = history;
this._suggester = suggester;
this._queryParser = queryParser;
this._formEl = $.el('#js-search-form');
@@ -636,7 +698,10 @@
}
_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._inputElVal = this._inputEl.value;
}
@@ -661,7 +726,7 @@
this._inputEl.value = '';
this._help.toggle();
} else {
- this._history.addItem(query);
+ this._suggester.add(query);
this._suggester.suggest('');
this._inputEl.value = '';
this._redirect(this._queryParser.generateRedirect(query));
@@ -678,8 +743,12 @@