absolutely massive refactor

This commit is contained in:
Cade Scroggins
2017-07-10 23:04:57 -07:00
parent 4db2b0030b
commit 96504778f4
+378 -292
View File
@@ -2,61 +2,63 @@
<script> <script>
const CONFIG = { const CONFIG = {
categories: [
{ name: "Work", commands: [ // the key, name, redirect url and search path for your commands
{ key: 'g', name: 'GitHub', url: 'https://github.com', search: '/search?q={}' }, commands: [
{ key: 'm', name: 'Inbox', url: 'https://inbox.google.com', search: '/search/{}' }, ['7', '7digital', 'https://us.7digital.com', '/search?q={}'],
{ key: 'k', name: 'Keep', url: 'https://keep.google.com', search: '/#search/text={}' }, ['g', 'GitHub', 'https://github.com', '/search?q={}'],
] }, ['h', 'Hypem', 'http://hypem.com/popular', '/search/{}'],
{ name: "Lurk", commands: [ ['m', 'Inbox', 'https://inbox.google.com', '/search/{}'],
{ key: 'i', name: 'Instagram', url: 'https://www.instagram.com', search: false }, ['i', 'Instagram', 'https://www.instagram.com', false],
{ key: 'r', name: 'Reddit', url: 'https://www.reddit.com', search: '/search?q={}' }, ['k', 'Keep', 'https://keep.google.com', '/#search/text={}'],
{ key: 't', name: 'Twitter', url: 'https://twitter.com', search: '/search?q={}' }, ['l', 'Line Radio', 'https://linerad.io', '/#{}'],
] }, ['n', 'Netflix', 'https://www.netflix.com/browse', '/search?q={}'],
{ name: "Listen", commands: [ ['r', 'Reddit', 'https://www.reddit.com', '/search?q={}'],
{ key: 'h', name: 'Hypem', url: 'http://hypem.com/popular', search: '/search/{}' }, ['s', 'SoundCloud', 'https://soundcloud.com/discover', '/search?q={}'],
{ key: 'l', name: 'Line Radio', url: 'https://linerad.io', search: '/#{}' }, ['T', 'TPB', 'https://thepiratebay.org', '/search/{}'],
{ key: 's', name: 'SoundCloud', url: 'https://soundcloud.com/discover', search: '/search?q={}' }, ['w', 'Twitch', 'https://www.twitch.tv/directory/following', false],
] }, ['t', 'Twitter', 'https://twitter.com', '/search?q={}'],
{ name: "Watch", commands: [ ['Y', 'YIFY', 'https://yts.ag/browse-movies/0/1080p/all/7/latest', '/browse-movies/{}/1080p/all/0/rating'],
{ key: 'n', name: 'Netflix', url: 'https://www.netflix.com/browse', search: '/search?q={}' }, ['y', 'YouTube', 'https://youtube.com/feed/subscriptions', '/results?search_query={}'],
{ key: 'w', name: 'Twitch', url: 'https://www.twitch.tv/directory/following', search: false },
{ key: 'y', name: 'YouTube', url: 'https://youtube.com/feed/subscriptions', search: '/results?search_query={}' },
] },
{ name: "Download", commands: [
{ key: 'T', name: 'TPB', url: 'https://thepiratebay.org', search: '/search/{}' },
{ key: 'Y', name: 'YIFY', url: 'https://yts.ag/browse-movies/0/1080p/all/7/latest', search: '/browse-movies/{}/1080p/all/0/rating' },
{ key: '7', name: '7digital', url: 'https://us.7digital.com', search: '/search?q={}' },
] },
], ],
// if none of the keys are matched, this is used for searching. // the categories & commands that show up on the help overlay
// use the exact name of each command
categories: {
'Work': ['GitHub', 'Inbox', 'Keep'],
'Lurk': ['Instagram', 'Reddit', 'Twitter'],
'Listen': ['Hypem', 'Line Radio', 'SoundCloud'],
'Watch': ['Netflix', 'Twitch', 'YouTube'],
'Download': ['7digital', 'TPB', 'YIFY'],
},
// if none of the specified keys are matched, this is used for searching
defaultSearch: 'https://encrypted.google.com/search?q={}', defaultSearch: 'https://encrypted.google.com/search?q={}',
// the delimiter between the key and your search query. // the delimiter between the key and your search query
// e.g. to search GitHub for potatoes you'd type "g:potatoes". // e.g. to search GitHub for potatoes you'd type "g:potatoes"
searchDelimiter: ':', searchDelimiter: ':',
// the delimiter between the key and a path. // the delimiter between the key and a path
// 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: '/',
// instantly redirect when a key is matched. // instantly redirect when a key is matched
// put a space before any other queries to prevent unwanted redirects. // put a space before any other queries to prevent unwanted redirects
instantRedirect: false, instantRedirect: false,
// give suggestions as you type. // give suggestions as you type
suggestions: true, suggestions: true,
// max amount of suggestions that will ever be displayed. // max amount of suggestions that will ever be displayed
suggestionsLimit: 6, suggestionsLimit: 4,
// the order and limit for each suggestion influencer. // the order and limit for each suggestion influencer
// the following would give you 1 suggestion from your search history // the following would give you 2 suggestion from your search history
// and 4 suggestions from Duck Duck Go. // and 4 suggestions from Duck Duck Go.
influencers: [ influencers: [
{ name: 'History', limit: 1 }, { name: 'History', limit: 2 },
{ name: 'DuckDuckGo', limit: 6 }, { name: 'DuckDuckGo', limit: 4 },
], ],
// open queries in a new tab. // open queries in a new tab.
@@ -64,13 +66,6 @@
// the delimiter between the hours and minutes in the clock. // the delimiter between the hours and minutes in the clock.
clockDelimiter: '&nbsp;', clockDelimiter: '&nbsp;',
// used for determining when to redirect directly to a url.
urlRegex: /^(?:(http|https)?:\/\/)?(?:[\w-]+\.)+([a-z]|[A-Z]|[0-9]){2,6}/i,
// if "urlRegex" matches but this doesn't, "http://" will be
// prepended to the beginning of the query before redirecting.
protocolRegex: /^[a-zA-Z]+:\/\//i
}; };
</script> </script>
@@ -140,19 +135,19 @@
list-style: none; list-style: none;
} }
.search-input, #search-input,
.search-input:focus { #search-input:focus {
padding: 12px; padding: 12px;
border-radius: 2px; border-radius: 2px;
background: #222; background: #222;
font-size: 1.1rem; font-size: 1.1rem;
} }
.search-input.bottom-no-radius { body.suggestions #search-input {
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
} }
.search-suggestions { #search-suggestions {
display: none; display: none;
padding: 4px 0; padding: 4px 0;
border-radius: 0 0 2px 2px; border-radius: 0 0 2px 2px;
@@ -160,7 +155,7 @@
overflow: hidden; overflow: hidden;
} }
.search-suggestions.active { body.suggestions #search-suggestions {
display: block; display: block;
} }
@@ -178,7 +173,7 @@
background: #333; background: #333;
} }
.overlay { #help {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -197,7 +192,7 @@
font-size: 1.2rem; font-size: 1.2rem;
} }
.overlay[data-toggled='true'] { body.help #help {
visibility: visible; visibility: visible;
transform: translateY(0); transform: translateY(0);
opacity: 1; opacity: 1;
@@ -261,7 +256,7 @@
} }
@media (min-width: 1000px) { @media (min-width: 1000px) {
.overlay { #help {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -269,7 +264,7 @@
font-size: 1.3vw; font-size: 1.3vw;
} }
.lists { #help>ul {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
width: 100%; width: 100%;
@@ -283,23 +278,25 @@
</style> </style>
<main> <main>
<time id="js-clock"></time> <time id="clock"></time>
<form id="js-search-form" autocomplete="off"> <form id="search-form" autocomplete="off">
<input id="js-search-input" class="search-input" type="text" autofocus> <input id="search-input" type="text" autofocus>
<ul id="js-search-suggestions" class="search-suggestions"></ul> <ul id="search-suggestions"></ul>
</form> </form>
</main> </main>
<aside id="js-overlay" class="overlay"> <aside id="help"></aside>
<ul id="js-lists" class="lists"></ul>
</aside>
<script> <script>
const $ = { const $ = {
bodyClassRemove: c => $.el('body').classList.remove(c),
bodyClassAdd: c => $.el('body').classList.add(c),
el: s => document.querySelector(s), el: s => document.querySelector(s),
els: s => [].slice.call(document.querySelectorAll(s) || []), els: s => [].slice.call(document.querySelectorAll(s) || []),
isDown: e => ['c-n', 'down'].includes($.key(e)), escapeRegex: s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),
isUp: e => ['c-p', 'up'].includes($.key(e)), isDown: e => ['c-n', 'down', 'tab'].includes($.key(e)),
isRemove: e => ['backspace', 'delete'].includes($.key(e)),
isUp: e => ['c-p', 'up', 's-tab'].includes($.key(e)),
jsonp: url => { jsonp: url => {
let script = document.createElement('script'); let script = document.createElement('script');
@@ -309,12 +306,16 @@
key: e => { key: e => {
const ctrl = e.ctrlKey; const ctrl = e.ctrlKey;
const shift = e.shiftKey;
switch (e.which) { switch (e.which) {
case 8: return 'backspace';
case 9: return shift ? 's-tab' : 'tab';
case 13: return 'enter'; case 13: return 'enter';
case 27: return 'escape'; case 27: return 'escape';
case 38: return 'up'; case 38: return 'up';
case 40: return 'down'; case 40: return 'down';
case 46: return 'delete';
case 78: return ctrl ? 'c-n' : 'n'; case 78: return ctrl ? 'c-n' : 'n';
case 80: return ctrl ? 'c-p' : 'n'; case 80: return ctrl ? 'c-p' : 'n';
} }
@@ -322,21 +323,22 @@
}; };
class Clock { class Clock {
constructor() { constructor({ delimiter }) {
this._clockEl = $.el('#js-clock'); this._el = $.el('#clock');
this._delimiter = delimiter;
this._setTime = this._setTime.bind(this); this._setTime = this._setTime.bind(this);
this._start(); this._start();
} }
_pad(num) { _pad(num) {
return (`0${num.toString()}`).slice(-2); return ('0' + num.toString()).slice(-2);
} }
_setTime() { _setTime() {
const date = new Date(); const date = new Date();
const hours = this._pad(date.getHours()); const hours = this._pad(date.getHours());
const minutes = this._pad(date.getMinutes()); const minutes = this._pad(date.getMinutes());
this._clockEl.innerHTML = `${hours}${CONFIG.clockDelimiter}${minutes}`; this._el.innerHTML = hours + this._delimiter + minutes;
} }
_start() { _start() {
@@ -346,42 +348,59 @@
} }
class Help { class Help {
constructor() { constructor({ commands, categories, newTab }) {
this._overlayEl = $.el('#js-overlay'); this._el = $.el('#help');
this._listsEl = $.el('#js-lists'); this._commands = commands;
this._handleKeydown = this._handleKeydown.bind(this); this._categories = categories;
this._newTab = newTab;
this._toggled = false;
this._buildAndAppendLists(); this._buildAndAppendLists();
this._bindMethods();
this._registerEvents(); this._registerEvents();
} }
toggle(show) { toggle(show) {
const toggle = (typeof show !== 'undefined') ? show : this._toggled = (typeof show !== 'undefined') ? show : !this._toggled;
this._overlayEl.getAttribute('data-toggled') !== 'true'; this._toggled ? $.bodyClassAdd('help') : $.bodyClassRemove('help');
}
this._overlayEl.setAttribute('data-toggled', toggle); _bindMethods() {
this._handleKeydown = this._handleKeydown.bind(this);
} }
_buildAndAppendLists() { _buildAndAppendLists() {
CONFIG.categories.forEach(category => { const lists = document.createElement('ul');
this._listsEl.insertAdjacentHTML(
for (let name in this._categories) {
lists.insertAdjacentHTML(
'beforeend', 'beforeend',
`<li class="category"> `<li class="category">
<h2 class="category-name">${category.name}</h2> <h2 class="category-name">${name}</h2>
<ul>${this._buildListCommands(category)}</ul> <ul>${this._buildListCommands(this._categories[name])}</ul>
</li>` </li>`
); );
});
} }
_buildListCommands(category) { this._el.appendChild(lists);
return category.commands.map(({ url, key, name }) => ( }
_buildListCommands(commandNames) {
return commandNames.map(commandName => {
const [key, name, url] = this._getCommandFromName(commandName);
return (
`<li class="command"> `<li class="command">
<a href="${url}" target="${CONFIG.newTab ? '_blank' : '_self'}"> <a href="${url}" target="${this._newTab ? '_blank' : '_self'}">
<span class="command-key">${key}</span> <span class="command-key">${key}</span>
<span class="command-name">${name}</span> <span class="command-name">${name}</span>
</a> </a>
</li>` </li>`
)).join(''); );
}).join('');
}
_getCommandFromName(commandName) {
return this._commands.filter(([key, name]) => name === commandName)[0];
} }
_handleKeydown(e) { _handleKeydown(e) {
@@ -393,147 +412,176 @@
} }
} }
class History { class Influencer {
constructor(suggestionsLimit = 0) { constructor({ limit }) {
this._suggestionsLimit = suggestionsLimit; this._limit = limit;
this._history = this._getFromStorage('history'); }
addItem() {}
getSuggestions() {}
}
class History extends Influencer {
constructor() {
super(...arguments);
this._storeName = 'history';
} }
addItem(query) { addItem(query) {
if (query.length < 2) return; if (query.length < 2) return;
this._updateHistory(query); let exists;
this._sortHistory();
this._saveToStorage('history', this._history); const history = this._getHistory().map(([item, count]) => {
const match = item === query;
if (match) exists = true;
return [item, match ? count + 1 : count];
});
if (!exists) history.push([query, 1]);
this._setHistory(this._sort(history));
} }
getSuggestionsPromise(query) { getSuggestions(query) {
return new Promise(resolve => { return new Promise(resolve => {
const suggestions = this._history const suggestions = this._getHistory()
.filter(item => this._itemContainsQuery(query, item[0])) .filter(([item]) => this._itemContainsQuery(query, item))
.slice(0, this._suggestionsLimit) .slice(0, this._limit)
.map(item => item[0]); .map(([item]) => item);
resolve(suggestions); resolve(suggestions);
}); });
} }
_getFromStorage(name) { _fetch() {
return JSON.parse(localStorage.getItem(name)) || []; return JSON.parse(localStorage.getItem(this._storeName)) || [];
}
_getHistory() {
this._history = this._history || this._fetch();
return this._history;
} }
_itemContainsQuery(query, item) { _itemContainsQuery(query, item) {
return query && item.indexOf(query) !== -1; return query && item.indexOf(query) !== -1;
} }
_saveToStorage(name, data) { _save(history) {
localStorage.setItem(name, JSON.stringify(data)); localStorage.setItem(this._storeName, JSON.stringify(history));
} }
_sortHistory() { _setHistory(history) {
this._history = this._history this._history = history;
.sort((current, next) => current[1] - next[1]) this._save(history);
.reverse();
} }
_updateHistory(query) { _sort(history) {
let exists = false; return history.sort((current, next) => current[1] - next[1]).reverse();
this._history = this._history.map(item => {
if (item[0] === query) {
item[1]++;
exists = true;
}
return item
});
if (!exists) this._history.push([query, 1]);
} }
} }
class DuckDuckGo { class DuckDuckGo extends Influencer {
constructor(suggestionsLimit = 0) { constructor() {
this._endpoint = 'https://duckduckgo.com/ac/'; super(...arguments);
this._callback = 'autocompleteCallback';
this._suggestionsLimit = suggestionsLimit;
} }
addItem() {} getSuggestions(query) {
getSuggestionsPromise(query) {
return new Promise(resolve => { return new Promise(resolve => {
this._resolve = resolve; const endpoint = 'https://duckduckgo.com/ac/';
window[this._callback] = this._handleResponse.bind(this); const callback = 'autocompleteCallback';
$.jsonp(`${this._endpoint}?callback=${this._callback}&q=${query}`);
window[callback] = res => {
resolve(res.slice(0, this._limit).map(i => i.phrase));
};
$.jsonp(`${endpoint}?callback=${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, limit }) {
this._inputEl = $.el('#js-search-input'); this._el = $.el('#search-suggestions');
this._suggestionsEl = $.el('#js-search-suggestions');
this._totalSuggestions = 0;
this._suggestionEls = [];
this._influencers = influencers; this._influencers = influencers;
this._handleKeydown = this._handleKeydown.bind(this); this._limit = limit;
document.addEventListener('keydown', this._handleKeydown); this._suggestionEls = [];
this._bindMethods();
this._registerEvents();
} }
add(query) { setOnClick(callback) {
this._onClick = callback;
}
setOnHighlight(callback) {
this._onHighlight = callback;
}
setOnUnhighlight(callback) {
this._onUnhighlight = callback;
}
success(query) {
this._clearSuggestions();
this._influencers.forEach(i => i.addItem(query)); this._influencers.forEach(i => i.addItem(query));
} }
suggest(input, clickCallback = () => {}) { suggest(input) {
if (!input) { input = input.trim();
if (input === '') this._clearSuggestions();
Promise.all(this._getInfluencerPromises(input)).then(res => {
const suggestions = this._flattenAndUnique(res);
if (suggestions.length) {
this._clearSuggestions(); this._clearSuggestions();
return; this._appendSuggestions(suggestions, input);
this._registerSuggestionEvents();
$.bodyClassAdd('suggestions');
}
});
} }
this._handleClick = clickCallback; _appendSuggestions(suggestions, input) {
this._suggest(input); suggestions.some((suggestion, i) => {
} const match = new RegExp($.escapeRegex(input), 'g');
const suggestionHtml = suggestion.replace(match, `<b>${input}</b>`);
_appendSuggestion(suggestion, input) { this._el.insertAdjacentHTML(
const suggestionHtml = suggestion
.replace(new RegExp(input, 'g'), `<b>${input}</b>`);
this._suggestionsEl.insertAdjacentHTML(
'beforeend', 'beforeend',
`<li> `<li>
<button <button
type="button" type="button"
class="js-search-suggestion search-suggestion" class="js-search-suggestion search-suggestion"
data-suggestion="${suggestion}" data-suggestion="${suggestion}"
tabindex="-1"
> >
${suggestionHtml} ${suggestionHtml}
</button> </button>
</li>` </li>`
); );
this._showSuggestions(); return i + 1 >= this._limit;
return ++this._totalSuggestions === CONFIG.suggestionsLimit; });
this._suggestionEls = $.els('.js-search-suggestion');
}
_bindMethods() {
this._handleKeydown = this._handleKeydown.bind(this);
} }
_clearClickEvents() { _clearClickEvents() {
this._suggestionEls.forEach(el => { this._suggestionEls.forEach(el => {
const callback = this._handleClick.bind(null, el.value); const callback = this._onClick.bind(null, el.value);
el.removeEventListener('click', callback); el.removeEventListener('click', callback);
}); });
} }
_clearSuggestions() { _clearSuggestions() {
this._hideSuggestions(); $.bodyClassRemove('suggestions');
this._clearClickEvents(); this._clearClickEvents();
this._suggestionsEl.innerHTML = ''; this._suggestionEls = [];
this._totalSuggestions = 0; this._el.innerHTML = '';
} }
// [[1, 2], [1, 2, 3, 4]] -> [1, 2, 3, 4] // [[1, 2], [1, 2, 3, 4]] -> [1, 2, 3, 4]
@@ -541,113 +589,104 @@
return [...new Set([].concat.apply([], array))]; return [...new Set([].concat.apply([], array))];
} }
_focusNext() { _focusNext(e) {
const active = $.el('.highlight'); const exists = this._suggestionEls.some((el, i) => {
if (el.classList.contains('highlight')) {
if (active) { this._highlight(this._suggestionEls[i + 1], e);
this._suggestionEls.forEach((el, index) => { return true;
const next = this._suggestionEls[index + 1];
if (el === active && next) this._highlight(next);
});
} else {
this._highlight(this._suggestionEls[0]);
}
}
_focusPrevious() {
const active = $.el('.highlight');
if (active) {
this._suggestionEls.forEach((el, index) => {
if (el === active) {
const previous = this._suggestionEls[index - 1];
this._noHighlight();
if (previous) this._highlight(previous);
} }
}); });
}
if (!exists) this._highlight(this._suggestionEls[0], e);
} }
_gatherInfluencers(input) { _focusPrevious(e) {
const exists = this._suggestionEls.some((el, i) => {
if (el.classList.contains('highlight') && i) {
this._highlight(this._suggestionEls[i - 1], e);
return true;
}
});
if (!exists) this._unHighlight(e);
}
_getInfluencerPromises(input) {
return this._influencers return this._influencers
.map(influencer => influencer.getSuggestionsPromise(input)); .map(influencer => influencer.getSuggestions(input));
} }
_handleKeydown(e) { _handleKeydown(e) {
if ($.isDown(e) || $.isUp(e)) e.preventDefault(); if ($.isDown(e)) this._focusNext(e);
if ($.isDown(e)) this._focusNext(); if ($.isUp(e)) this._focusPrevious(e);
if ($.isUp(e)) this._focusPrevious();
} }
_highlight(el) { _highlight(el, e) {
this._noHighlight(); this._unHighlight();
if (el) {
this._onHighlight(el.getAttribute('data-suggestion'));
el.classList.add('highlight'); el.classList.add('highlight');
this._inputEl.value = el.getAttribute('data-suggestion'); e.preventDefault();
this._inputEl.focus()
} }
_noHighlight() {
this._inputEl.value = this._originalValue;
this._suggestionEls.forEach(el => el.classList.remove('highlight'));
}
_showSuggestions() {
this._inputEl.classList.add('bottom-no-radius');
this._suggestionsEl.classList.add('active');
}
_hideSuggestions() {
this._inputEl.classList.remove('bottom-no-radius');
this._suggestionsEl.classList.remove('active');
} }
_registerEvents() { _registerEvents() {
document.addEventListener('keydown', this._handleKeydown);
}
_registerSuggestionEvents() {
this._suggestionEls.forEach(el => { this._suggestionEls.forEach(el => {
const value = el.getAttribute('data-suggestion'); const value = el.getAttribute('data-suggestion');
el.addEventListener('mouseover', this._highlight.bind(this, el)); el.addEventListener('mouseover', this._highlight.bind(this, el));
el.addEventListener('mouseout', this._noHighlight.bind(this)); el.addEventListener('mouseout', this._unHighlight.bind(this));
el.addEventListener('click', this._handleClick.bind(null, value)); el.addEventListener('click', this._onClick.bind(null, value));
}); });
} }
_suggest(input) { _unHighlight(e) {
this._originalValue = this._inputEl.value; const el = $.el('.highlight');
Promise.all(this._gatherInfluencers(input)).then(res => { if (el) {
this._clearSuggestions(); this._onUnhighlight();
el.classList.remove('highlight');
this._flattenAndUnique(res) if (e) e.preventDefault();
.some(item => this._appendSuggestion(item, input)); }
this._suggestionEls = $.els('.js-search-suggestion');
this._registerEvents();
});
} }
} }
class QueryParser { class QueryParser {
constructor({ commands, defaultSearch, pathDelimiter, searchDelimiter }) {
this._commands = commands;
this._defaultSearch = defaultSearch;
this._searchDelimiter = searchDelimiter;
this._pathDelimiter = pathDelimiter;
this._protocolRegex = /^[a-zA-Z]+:\/\//i;
this._urlRegex = /^(?:(http|https)?:\/\/)?(?:[\w-]+\.)+([a-z]|[A-Z]|[0-9]){2,6}/i;
}
generateRedirect(query) { generateRedirect(query) {
const encodedQuery = encodeURIComponent(query); const encodedQuery = encodeURIComponent(query);
let redirectUrl = CONFIG.defaultSearch.replace('{}', encodedQuery); let redirectUrl = this._defaultSearch.replace('{}', encodedQuery);
if (query.match(CONFIG.urlRegex)) { if (query.match(this._urlRegex)) {
const hasProtocol = query.match(CONFIG.protocolRegex); const hasProtocol = query.match(this._protocolRegex);
redirectUrl = hasProtocol ? query : 'http://' + query; redirectUrl = hasProtocol ? query : 'http://' + query;
} else { } else {
const splitSearch = query.split(CONFIG.searchDelimiter); const splitSearch = query.split(this._searchDelimiter);
const splitPath = query.split(CONFIG.pathDelimiter); const splitPath = query.split(this._pathDelimiter);
this._loopThroughCommands(command => { this._commands.some(([key, name, url, searchPath]) => {
const isSearch = splitSearch[0] === command.key; const isSearch = splitSearch[0] === key;
const isPath = splitPath[0] === command.key; const isPath = splitPath[0] === key;
if (isSearch || isPath) { if (isSearch || isPath) {
if (splitSearch[1] && command.search) { if (splitSearch[1] && searchPath) {
redirectUrl = this._prepSearch(command, splitSearch); redirectUrl = this._prepSearch(url, searchPath, splitSearch);
} else if (splitPath[1]) { } else if (splitPath[1]) {
redirectUrl = this._prepPath(command, splitPath); redirectUrl = this._prepPath(url, splitPath);
} else { } else {
redirectUrl = command.url; redirectUrl = url;
} }
return true; return true;
@@ -658,35 +697,31 @@
return redirectUrl; return redirectUrl;
} }
instantRedirect(keypressEvent, query, callback) { instantRedirect(query, callback) {
this._loopThroughCommands(command => { let exists;
if (command.key === query) {
keypressEvent.preventDefault(); this._commands.forEach(([key, name, url]) => {
callback(command.url); if (key === query) {
callback(url);
exists = true;
} }
}); });
return exists;
} }
_loopThroughCommands(callback) { _prepPath(url, splitPath) {
CONFIG.categories const path = this._shiftAndTrim(splitPath, this._pathDelimiter);
.map(category => category.commands) return this._stripUrlPath(url) + '/' + path;
.forEach(commands => commands.forEach(command => {
if (callback(command)) return;
}));
} }
_prepPath(command, query) { _prepSearch(url, searchPath, splitSearch) {
const baseUrl = this._stripUrlPath(command.url); if (!searchPath) return url;
const path = this._shiftAndTrim(query, CONFIG.pathDelimiter); const baseUrl = this._stripUrlPath(url);
return `${baseUrl}/${path}`; const query = this._shiftAndTrim(splitSearch, this._searchDelimiter);
} const urlQuery = encodeURIComponent(query);
searchPath = searchPath.replace('{}', urlQuery);
_prepSearch(command, query) { return baseUrl + searchPath;
if (!command.search) return command.url;
const baseUrl = this._stripUrlPath(command.url);
const search = this._shiftAndTrimAndEncode(query);
const searchPath = command.search.replace('{}', search);
return `${baseUrl}${searchPath}`;
} }
_shiftAndTrim(arr, delimiter) { _shiftAndTrim(arr, delimiter) {
@@ -694,11 +729,6 @@
return arr.join(delimiter).trim(); return arr.join(delimiter).trim();
} }
_shiftAndTrimAndEncode(arr) {
const clean = this._shiftAndTrim(arr, CONFIG.searchDelimiter);
return encodeURIComponent(clean);
}
_stripUrlPath(url) { _stripUrlPath(url) {
const parser = document.createElement('a'); const parser = document.createElement('a');
parser.href = url; parser.href = url;
@@ -707,73 +737,93 @@
} }
class Form { class Form {
constructor(help, suggester, queryParser) { constructor({ help, instantRedirect, newTab, suggester, queryParser }) {
this._formEl = $.el('#search-form');
this._inputEl = $.el('#search-input');
this._help = help; this._help = help;
this._suggester = suggester; this._suggester = suggester;
this._queryParser = queryParser; this._queryParser = queryParser;
this._formEl = $.el('#js-search-form'); this._instantRedirect = instantRedirect;
this._inputEl = $.el('#js-search-input'); this._newTab = newTab;
this._inputElVal = ''; this._inputElVal = '';
this._bindMethods(); this._bindMethods();
this._registerEvents(); this._registerEvents();
} }
_bindMethods() { _bindMethods() {
this._clearPreview = this._clearPreview.bind(this);
this._handleKeypress = this._handleKeypress.bind(this); this._handleKeypress = this._handleKeypress.bind(this);
this._submitForm = this._submitForm.bind(this);
this._handleKeyup = this._handleKeyup.bind(this); this._handleKeyup = this._handleKeyup.bind(this);
this._previewValue = this._previewValue.bind(this);
this._submitForm = this._submitForm.bind(this);
this._submitWithValue = this._submitWithValue.bind(this); this._submitWithValue = this._submitWithValue.bind(this);
} }
_clearInput() {
this._inputEl.value = '';
this._inputElVal = '';
}
_clearPreview() {
this._inputEl.value = this._inputElVal;
this._inputEl.focus();
}
_handleKeypress(e) { _handleKeypress(e) {
const newChar = String.fromCharCode(e.which); const newChar = String.fromCharCode(e.which);
const newQuery = this._inputEl.value + newChar; const newQuery = this._inputEl.value + newChar;
const isNotEmpty = newChar.length; const validChar = newChar.length && $.key(e) !== 'enter';
const queryDiffers = this._inputElVal !== newQuery;
if (isNotEmpty && $.key(e) !== 'enter') { if (validChar) this._help.toggle(false);
this._help.toggle(false);
this._inputEl.focus();
}
if (CONFIG.instantRedirect) { if (
this._queryParser this._instantRedirect &&
.instantRedirect(e, newQuery, this._submitWithValue); this._queryParser.instantRedirect(newQuery, this._submitWithValue)
) {
e.preventDefault();
} else if (this._suggester && validChar && queryDiffers) {
this._suggester.suggest(newQuery);
this._inputElVal = newQuery;
} }
} }
_handleKeyup(e) { _handleKeyup(e) {
const oldVal = this._inputElVal; const newQuery = this._inputEl.value;
const newVal = this._inputEl.value.trim();
const ignored = $.isDown(e) || $.isUp(e);
this._inputElVal = newVal;
if (CONFIG.suggestions && oldVal !== newVal && !ignored) { if (this._suggester && $.isRemove(e) && this._inputElVal !== newQuery) {
this._suggester.suggest(newVal, this._submitWithValue); this._suggester.suggest(newQuery);
this._inputElVal = newQuery;
} }
} }
_previewValue(value) {
this._inputEl.value = value;
}
_redirect(redirect) { _redirect(redirect) {
if (CONFIG.newTab) window.open(redirect, '_blank'); if (this._newTab) window.open(redirect, '_blank');
else window.location.href = redirect; else window.location.href = redirect;
} }
_registerEvents() { _registerEvents() {
document.addEventListener('keypress', this._handleKeypress); this._inputEl.addEventListener('keypress', this._handleKeypress);
this._inputEl.addEventListener('keyup', this._handleKeyup); this._inputEl.addEventListener('keyup', this._handleKeyup);
this._formEl.addEventListener('submit', this._submitForm, false); this._formEl.addEventListener('submit', this._submitForm, false);
this._suggester.setOnClick(this._submitWithValue);
this._suggester.setOnHighlight(this._previewValue);
this._suggester.setOnUnhighlight(this._clearPreview);
} }
_submitForm(e) { _submitForm(e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
const query = this._inputEl.value.trim(); const query = this._inputEl.value.trim();
this._clearInput();
if (!query || query === '?') { if (!query || query === '?') {
this._inputEl.value = '';
this._help.toggle(); this._help.toggle();
} else { } else {
this._suggester.add(query); this._suggester.success(query);
this._suggester.suggest('');
this._inputEl.value = '';
this._redirect(this._queryParser.generateRedirect(query)); this._redirect(this._queryParser.generateRedirect(query));
} }
} }
@@ -786,14 +836,50 @@
</script> </script>
<script> <script>
const clock = new Clock(); const getHelp = () => {
const help = new Help(); return new Help({
const influencers = { History: History, DuckDuckGo: DuckDuckGo }; categories: CONFIG.categories,
commands: CONFIG.commands,
newTab: CONFIG.newTab,
});
};
const suggester = new Suggester( const getInfluencers = () => {
CONFIG.influencers.map(i => new influencers[i.name](i.limit)) const availableInfluencers = {
); DuckDuckGo: DuckDuckGo,
History: History,
};
const parser = new QueryParser(); return CONFIG.influencers.map(i => {
const form = new Form(help, suggester, parser); return new availableInfluencers[i.name]({ limit: i.limit });
});
};
const getSuggester = () => {
return new Suggester({
influencers: getInfluencers(),
limit: CONFIG.suggestionsLimit,
});
};
const getQueryParser = () => {
return new QueryParser({
commands: CONFIG.commands,
defaultSearch: CONFIG.defaultSearch,
pathDelimiter: CONFIG.pathDelimiter,
searchDelimiter: CONFIG.searchDelimiter,
});
};
new Clock({
delimiter: CONFIG.clockDelimiter,
});
new Form({
help: getHelp(),
instantRedirect: CONFIG.instantRedirect,
newTab: CONFIG.newTab,
queryParser: getQueryParser(),
suggester: CONFIG.suggestions ? getSuggester() : false,
});
</script> </script>