Recommitting latest version due to an error

This commit is contained in:
Özenç Bilgili 2020-01-15 02:59:19 +03:00
parent c6f1c4b545
commit b1c68842b3
37 changed files with 1315 additions and 1267 deletions

View File

@ -1,21 +1,28 @@
# Tilde Enhanced
Based on [Cade Scroggins](https://github.com/cadejscroggins)'s [Tilde](https://github.com/cadejscroggins/tilde).
## Screenshots ## Screenshots
![Screenshot](assets/screenshot.png) ![Screenshot](assets/screenshot.png)
# Tilde Enhanced
A slightly modified version of [Cade Scroggins](https://github.com/cadejscroggins)'s homepage [Tilde](https://github.com/cadejscroggins/tilde).
Most of the features are carried over from the original source.
## Added Features ## Added Features
Most of the features are carried over from the original source.
Few of the added features are:
- Clicking on the clock will bring up pre-defined sites. - Added a "Quick Launch" functionality, which launches all the sites with `quickLaunch`
property set to true upon hitting `!` key.
- Clicking on the clock brings up pre-defined sites.
- Available sites show their icons instead of their corresponding keys. - Available sites show their icons instead of their corresponding keys.
- Other small changes on grids and boxes. - Added the option to invert colour theme. You can either edit config or use `invert!` command.
- Other small changes on grids.
## Usage ## Usage
To use Tilde as your homepage, simply set your browser's home URL to
Tilde's index file.
To go to a site, enter the first letter of the site then hit enter. To go to a site, enter the first letter of the site then hit enter.
To view the available sites, press `?` or click on the clock. To view the available sites, press `?` or click on the clock.
If your input doesn't match any of the commands, If your input doesn't match any of the commands,
@ -26,7 +33,9 @@ a generic DuckDuckGo search will be triggered. For example:
- Entering `cats` would search [Cats](https://duckduckgo.com/?q=cats). - Entering `cats` would search [Cats](https://duckduckgo.com/?q=cats).
Note that by default, queries are searched on DuckDuckGo but this can be Note that by default, queries are searched on DuckDuckGo but this can be
configured easily by updating two lines. configured easily by updating two lines on. Check [Configuration](#configuration) for details.
You might have to allow pop-ups for your homepage to use quick launch feature.
This version is not suitable for use on mobile as clicking on the clock will This version is not suitable for use on mobile as clicking on the clock will
only bring up pre-defined sites. only bring up pre-defined sites.
@ -73,9 +82,8 @@ This allows you to invoke Tilde with your native browser search bar.
## Configuration ## Configuration
The above is just the beginning. Open up the [index.html](index.html) file and Open up the [script.js](assets/script.js) file and read through the `CONFIG`!
read through the `CONFIG`!
## License ## License
Feel free to [use this and modify it however you like](https://github.com/cadejscroggins/tilde/blob/master/LICENSE). Feel free to [use this and modify it however you like](https://github.com/Ozencb/tilde-enhanced/blob/master/LICENSE).

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
assets/icons/drive.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/icons/github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/icons/imdb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/icons/linkedin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/icons/mail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
assets/icons/netflix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
assets/icons/reddit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
assets/icons/twitch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/icons/twitter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/icons/youtube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

1281
index.html

File diff suppressed because it is too large Load Diff

968
script/script.js Normal file
View File

@ -0,0 +1,968 @@
let invertColorCookie;
if (localStorage.getItem('invertColorCookie') === null){
invertColorCookie = false;
}
else {
invertColorCookie = JSON.parse(localStorage.getItem('invertColorCookie'));
}
const CONFIG = {
/**
* The category, name, key, url, search path, color, icon, and quicklaunch properties for your commands.
* Icons must be added to "icons" folder and their values/names must be updated.
* If none of the specified keys are matched, the '*' key is used.
* Commands without a category don't show up in the help menu.
* Update line 11 and 13 if you prefer using Google.
*/
commands: [
{
name: 'Duckduckgo',
key: '*',
url: 'https://duckduckgo.com',
search: '/?q={}',
color: '#DE5833',
},
{
category: 'General',
name: 'Mail',
key: 'm',
url: 'https://gmail.com',
search: '/#search/text={}',
color: 'linear-gradient(135deg, #dd5145, #dd5145)',
icon: 'mail',
quickLaunch: true,
},
{
category: 'General',
name: 'Drive',
key: 'd',
url: 'https://drive.google.com',
search: '/drive/search?q={}',
color: 'linear-gradient(135deg, #FFD04B, #1EA362, #4688F3)',
icon: 'drive',
quickLaunch: false,
},
{
category: 'General',
name: 'LinkedIn',
key: 'l',
url: 'https://linkedin.com',
search: '/search/results/all/?keywords={}',
color: 'linear-gradient(135deg, #006CA4, #0077B5)',
icon: 'linkedin',
quickLaunch: true,
},
{
category: 'Tech',
name: 'GitHub',
key: 'g',
url: 'https://github.com',
search: '/search?q={}',
color: 'linear-gradient(135deg, #2b2b2b, #3b3b3b)',
icon: 'github',
quickLaunch: true,
},
{
category: 'Tech',
name: 'StackOverflow',
key: 's',
url: 'https://stackoverflow.com',
search: '/search?q={}',
color: 'linear-gradient(135deg, #53341C, #F48024)',
icon: 'stackoverflow',
quickLaunch: true,
},
{
category: 'Tech',
name: 'Ars Technica',
key: 'a',
url: 'https://arstechnica.com',
search: '/search/?ie=UTF-8&q={}',
color: 'linear-gradient(135deg, #FF4E00, #B83800)',
icon: 'arstechnica',
quickLaunch: false,
},
{
category: 'Fun',
name: 'YouTube',
key: 'y',
url: 'https://youtube.com',
search: '/results?search_query={}',
color: 'linear-gradient(135deg, #cd201f, #cd4c1f)',
icon: 'youtube',
quickLaunch: false,
},
{
category: 'Fun',
name: 'Netflix',
key: 'n',
url: 'https://www.netflix.com',
color: 'linear-gradient(135deg, #E50914, #CB020C)',
icon: 'netflix',
quickLaunch: false,
},
{
category: 'Fun',
name: 'Twitch',
key: 't',
url: 'https://www.twitch.tv',
search: '/directory/game/{}',
color: 'linear-gradient(135deg, #6441a5, #4b367c)',
icon: 'twitch',
quickLaunch: false,
},
{
category: 'Other',
name: 'Reddit',
key: 'r',
url: 'https://reddit.com',
search: '/search?q={}',
color: 'linear-gradient(135deg, #FF8456, #FF4500)',
icon: 'reddit',
quickLaunch: false,
},
{
category: 'Other',
name: 'Twitter',
key: 'o',
url: 'https://twitter.com',
color: 'linear-gradient(135deg, #C0A886, #E2DBC8)',
icon: 'twitter',
quickLaunch: true,
},
{
category: 'Other',
name: 'IMDb',
key: 'i',
url: 'https://imdb.com',
search: '/find?ref_=nv_sr_fn&q={}',
color: 'linear-gradient(135deg, #7A5F00, #E8B708)',
icon: 'imdb',
quickLaunch: false,
},
],
/**
* Get suggestions as you type.
*/
suggestions: true,
suggestionsLimit: 4,
/**
* The order and limit for each suggestion influencer. An "influencer" is
* just a suggestion source. The following influencers are available:
*
* - "Default" suggestions come from CONFIG.defaultSuggestions
* - "DuckDuckGo" suggestions come from the duck duck go search api
* - "History" suggestions come from your previously entered queries
*/
influencers: [
{ name: 'Default', limit: 4 },
{ name: 'History', limit: 1 },
{ name: 'DuckDuckGo', limit: 4 },
],
/**
* Default search suggestions for the specified queries.
*/
defaultSuggestions: {
g: ['g/issues', 'g/pulls', 'gist.github.com'],
r: ['r/r/unixporn', 'r/r/startpages', 'r/r/webdev', 'r/r/technology'],
},
/**
* Instantly redirect when a key is matched. Put a space before any other
* queries to prevent unwanted redirects.
*/
instantRedirect: false,
/**
* Open triggered queries in a new tab.
*/
newTab: false,
/**
* Dynamic overlay background colors when command domains are matched.
*/
colors: true,
/**
* Invert color theme
*/
invertedColors: invertColorCookie,
/**
* The delimiter between a command key and your search query. For example,
* to search GitHub for potatoes, you'd type "g:potatoes".
*/
searchDelimiter: ':',
/**
* The delimiter between a command key and a path. For example, you'd type
* "r/r/unixporn" to go to "https://reddit.com/r/unixporn".
*/
pathDelimiter: '/',
/**
* The delimiter between the hours and minutes on the clock.
*/
clockDelimiter: ' ',
/**
* Show a twenty-four-hour clock instead of a twelve-hour clock with AM/PM.
*/
twentyFourHourClock: true,
};
const $ = {
bodyClassAdd: c => $.el('body').classList.add(c),
bodyClassRemove: c => $.el('body').classList.remove(c),
el: s => document.querySelector(s),
els: s => [].slice.call(document.querySelectorAll(s) || []),
escapeRegex: s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),
flattenAndUnique: arr => [...new Set([].concat.apply([], arr))],
ieq: (a, b) => a.toLowerCase() === b.toLowerCase(),
iin: (a, b) => a.toLowerCase().indexOf(b.toLowerCase()) !== -1,
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 => {
let script = document.createElement('script');
script.src = url;
$.el('head').appendChild(script);
},
key: e => {
const ctrl = e.ctrlKey;
const shift = e.shiftKey;
switch (e.which) {
case 8:
return 'backspace';
case 9:
return shift ? 's-tab' : 'tab';
case 13:
return 'enter';
case 16:
return 'shift';
case 17:
return 'ctrl';
case 18:
return 'alt';
case 27:
return 'escape';
case 38:
return 'up';
case 40:
return 'down';
case 46:
return 'delete';
case 78:
return ctrl ? 'c-n' : 'n';
case 80:
return ctrl ? 'c-p' : 'p';
case 189:
return 'dash';
case 91:
case 93:
case 224:
return 'super';
}
},
pad: v => ('0' + v.toString()).slice(-2),
};
class Clock {
constructor(options) {
this._el = $.el('#clock');
this._delimiter = options.delimiter;
this._twentyFourHourClock = options.twentyFourHourClock;
this._setTime = this._setTime.bind(this);
this._el.addEventListener('click', options.toggleHelp);
this._start();
}
_setTime() {
const date = new Date();
let hours = $.pad(date.getHours());
let amPm = '';
if (!this._twentyFourHourClock) {
hours = date.getHours();
if (hours > 12) hours -= 12;
else if (hours === 0) hours = 12;
amPm =
`&nbsp;<span class="am-pm">` +
`${date.getHours() >= 12 ? 'PM' : 'AM'}</span>`;
}
const minutes = $.pad(date.getMinutes());
this._el.innerHTML = `${hours}${this._delimiter}${minutes}${amPm}`;
this._el.setAttribute('datetime', date.toTimeString());
}
_start() {
this._setTime();
setInterval(this._setTime, 1000);
}
}
class Help {
constructor(options) {
this._el = $.el('#help');
this._commands = options.commands;
this._newTab = options.newTab;
this._toggled = false;
this._handleKeydown = this._handleKeydown.bind(this);
this.toggle = this.toggle.bind(this);
this.launch = this.launch.bind(this);
this._inputEl = $.el('#search-input');
this._inputElVal = '';
this._suggester = options.suggester;
this._invertColors = options.invertedColors;
this._buildAndAppendLists();
this._registerEvents();
this._invertValue;
}
toggle(show) {
this._toggled = typeof show !== 'undefined' ? show : !this._toggled;
this._toggled ? $.bodyClassAdd('help') : $.bodyClassRemove('help');
}
hide() {
$.bodyClassRemove('form');
this._inputEl.value = '';
this._inputElVal = '';
this._suggester.suggest('');
}
launch() {
this.hide();
this.toggle(true);
$.bodyClassAdd('help');
for (let i = 0; i < CONFIG.commands.length; i++) {
if (CONFIG.commands[i].quickLaunch === true) {
window.open(CONFIG.commands[i].url);
}
}
}
_buildAndAppendLists() {
const lists = document.createElement('ul');
lists.classList.add('categories');
this._getCategories().forEach(category => {
lists.insertAdjacentHTML(
'beforeend',
`<li class="category">
<h2 class="category-name">${category}</h2>
<ul>${this._buildListCommands(category)}</ul>
</li>`
);
});
this._el.appendChild(lists);
}
_buildListCommands(currentCategory) {
this._invertValue = this._invertColors ? 1: 0;
return this._commands
.map(({ category, name, key, url, icon }) => {
if (category === currentCategory) {
return `
<li class="command">
<a href="${url}" target="${this._newTab ? '_blank' : '_self'}">
<span class="command-key"><img src='assets/icons/${icon}.png' height = 36px center style="filter: invert(${this._invertValue});"></span>
<span class="command-name">${name}</span>
</a>
</li>
`;
}
})
.join('');
}
_getCategories() {
const categories = this._commands
.map(v => v.category)
.filter(category => category);
return [...new Set(categories)];
}
_handleKeydown(e) {
if ($.key(e) === 'escape') this.toggle(false);
}
_registerEvents() {
document.addEventListener('keydown', this._handleKeydown);
}
}
class Influencer {
constructor(options) {
this._limit = options.limit;
this._parseQuery = options.parseQuery;
}
addItem() {}
getSuggestions() {}
_addSearchPrefix(items, query) {
const searchPrefix = this._getSearchPrefix(query);
return items.map(s => (searchPrefix ? searchPrefix + s : s));
}
_getSearchPrefix(query) {
const { isSearch, key, split } = this._parseQuery(query);
return isSearch ? `${key}${split} ` : false;
}
}
class DefaultInfluencer extends Influencer {
constructor({ defaultSuggestions }) {
super(...arguments);
this._defaultSuggestions = defaultSuggestions;
}
getSuggestions(query) {
return new Promise(resolve => {
const suggestions = this._defaultSuggestions[query];
resolve(suggestions ? suggestions.slice(0, this._limit) : []);
});
}
}
class DuckDuckGoInfluencer extends Influencer {
constructor({ queryParser }) {
super(...arguments);
}
getSuggestions(rawQuery) {
const { query } = this._parseQuery(rawQuery);
if (!query) return Promise.resolve([]);
return new Promise(resolve => {
const endpoint = 'https://duckduckgo.com/ac/';
const callback = 'autocompleteCallback';
window[callback] = res => {
const suggestions = res
.map(i => i.phrase)
.filter(s => !$.ieq(s, query))
.slice(0, this._limit);
resolve(this._addSearchPrefix(suggestions, rawQuery));
};
$.jsonp(`${endpoint}?callback=${callback}&q=${query}`);
});
}
}
class HistoryInfluencer extends Influencer {
constructor() {
super(...arguments);
this._storeName = 'history';
}
addItem(query) {
if (query.length < 2) return;
let exists;
const history = this._getHistory().map(([item, count]) => {
const match = $.ieq(item, query);
if (match) exists = true;
return [item, match ? count + 1 : count];
});
if (!exists) history.push([query, 1]);
const sorted = history
.sort((current, next) => current[1] - next[1])
.reverse();
this._setHistory(sorted);
}
getSuggestions(query) {
return new Promise(resolve => {
const suggestions = this._getHistory()
.map(([item]) => item)
.filter(item => query && !$.ieq(item, query) && $.iin(item, query))
.slice(0, this._limit);
resolve(suggestions);
});
}
_fetch() {
return JSON.parse(localStorage.getItem(this._storeName)) || [];
}
_getHistory() {
this._history = this._history || this._fetch();
return this._history;
}
_save(history) {
localStorage.setItem(this._storeName, JSON.stringify(history));
}
_setHistory(history) {
this._history = history;
this._save(history);
}
}
class Suggester {
constructor(options) {
this._el = $.el('#search-suggestions');
this._enabled = options.enabled;
this._influencers = options.influencers;
this._limit = options.limit;
this._suggestionEls = [];
this._handleKeydown = this._handleKeydown.bind(this);
this._registerEvents();
}
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));
}
suggest(input) {
if (!this._enabled) return;
input = input.trim();
if (input === '') this._clearSuggestions();
Promise.all(this._getInfluencerPromises(input)).then(res => {
const suggestions = $.flattenAndUnique(res);
this._clearSuggestions();
if (suggestions.length) {
this._appendSuggestions(suggestions, input);
this._registerSuggestionHighlightEvents();
this._registerSuggestionClickEvents();
$.bodyClassAdd('suggestions');
}
});
}
_appendSuggestions(suggestions, input) {
suggestions.some((suggestion, i) => {
const match = new RegExp($.escapeRegex(input), 'ig');
const suggestionHtml = suggestion.replace(match, `<b>${input}</b>`);
this._el.insertAdjacentHTML(
'beforeend',
`<li>
<button
type="button"
class="js-search-suggestion search-suggestion"
data-suggestion="${suggestion}"
tabindex="-1"
>
${suggestionHtml}
</button>
</li>`
);
if (i + 1 >= this._limit) return true;
});
this._suggestionEls = $.els('.js-search-suggestion');
}
_clearSuggestionClickEvents() {
this._suggestionEls.forEach(el => {
el.removeEventListener('click', this._onClick);
});
}
_clearSuggestionHighlightEvents() {
this._suggestionEls.forEach(el => {
el.removeEventListener('mouseover', this._highlight);
el.removeEventListener('mouseout', this._unHighlight);
});
}
_clearSuggestions() {
$.bodyClassRemove('suggestions');
this._clearSuggestionHighlightEvents();
this._clearSuggestionClickEvents();
this._suggestionEls = [];
this._el.innerHTML = '';
}
_focusNext(e) {
const exists = this._suggestionEls.some((el, i) => {
if (el.classList.contains('highlight')) {
this._highlight(this._suggestionEls[i + 1], e);
return true;
}
});
if (!exists) this._highlight(this._suggestionEls[0], e);
}
_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.map(influencer =>
influencer.getSuggestions(input)
);
}
_handleKeydown(e) {
if ($.isDown(e)) this._focusNext(e);
if ($.isUp(e)) this._focusPrevious(e);
}
_highlight(el, e) {
this._unHighlight();
if (!el) return;
this._onHighlight(el.getAttribute('data-suggestion'));
el.classList.add('highlight');
e.preventDefault();
}
_registerEvents() {
document.addEventListener('keydown', this._handleKeydown);
}
_registerSuggestionClickEvents() {
this._suggestionEls.forEach(el => {
const value = el.getAttribute('data-suggestion');
el.addEventListener('click', this._onClick.bind(null, value));
});
}
_registerSuggestionHighlightEvents() {
const noHighlightUntilMouseMove = () => {
window.removeEventListener('mousemove', noHighlightUntilMouseMove);
this._suggestionEls.forEach(el => {
el.addEventListener('mouseover', this._highlight.bind(this, el));
el.addEventListener('mouseout', this._unHighlight.bind(this));
});
};
window.addEventListener('mousemove', noHighlightUntilMouseMove);
}
_unHighlight(e) {
const el = $.el('.highlight');
if (!el) return;
this._onUnhighlight();
el.classList.remove('highlight');
if (e) e.preventDefault();
}
}
class QueryParser {
constructor(options) {
this._commands = options.commands;
this._searchDelimiter = options.searchDelimiter;
this._pathDelimiter = options.pathDelimiter;
this._protocolRegex = /^[a-zA-Z]+:\/\//i;
this._urlRegex = /^((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)$/i;
this.parse = this.parse.bind(this);
}
parse(query) {
const res = { query: query, split: null };
if (this._urlRegex.test(query)) {
const hasProtocol = this._protocolRegex.test(query);
res.redirect = hasProtocol ? query : 'http://' + query;
} else {
const trimmed = query.trim();
const splitSearch = trimmed.split(this._searchDelimiter);
const splitPath = trimmed.split(this._pathDelimiter);
this._commands.some(({ category, key, name, search, url }) => {
if (query === key) {
res.key = key;
res.isKey = true;
res.redirect = url;
return true;
}
if (splitSearch[0] === key && search) {
res.key = key;
res.isSearch = true;
res.split = this._searchDelimiter;
res.query = QueryParser._shiftAndTrim(splitSearch, res.split);
res.redirect = QueryParser._prepSearch(url, search, res.query);
return true;
}
if (splitPath[0] === key) {
res.key = key;
res.isPath = true;
res.split = this._pathDelimiter;
res.path = QueryParser._shiftAndTrim(splitPath, res.split);
res.redirect = QueryParser._prepPath(url, res.path);
return true;
}
if (key === '*') {
res.redirect = QueryParser._prepSearch(url, search, query);
}
});
}
res.color = QueryParser._getColorFromUrl(this._commands, res.redirect);
return res;
}
static _getColorFromUrl(commands, url) {
const domain = new URL(url).hostname;
return (
commands
.filter(c => new URL(c.url).hostname.includes(domain))
.map(c => c.color)[0] || null
);
}
static _prepPath(url, path) {
return QueryParser._stripUrlPath(url) + '/' + path;
}
static _prepSearch(url, searchPath, query) {
if (!searchPath) return url;
const baseUrl = QueryParser._stripUrlPath(url);
const urlQuery = encodeURIComponent(query);
searchPath = searchPath.replace('{}', urlQuery);
return baseUrl + searchPath;
}
static _shiftAndTrim(arr, delimiter) {
arr.shift();
return arr.join(delimiter).trim();
}
static _stripUrlPath(url) {
const parser = document.createElement('a');
parser.href = url;
return `${parser.protocol}//${parser.hostname}`;
}
}
class Form {
constructor(options) {
this._colors = options.colors;
this._formEl = $.el('#search-form');
this._inputEl = $.el('#search-input');
this._inputElVal = '';
this._instantRedirect = options.instantRedirect;
this._newTab = options.newTab;
this._parseQuery = options.parseQuery;
this._suggester = options.suggester;
this._toggleHelp = options.toggleHelp;
this._quickLaunch = options.quickLaunch;
this._clearPreview = this._clearPreview.bind(this);
this._handleInput = this._handleInput.bind(this);
this._handleKeydown = this._handleKeydown.bind(this);
this._previewValue = this._previewValue.bind(this);
this._submitForm = this._submitForm.bind(this);
this._submitWithValue = this._submitWithValue.bind(this);
this._invertColors = options.invertedColors;
this.hide = this.hide.bind(this);
this.show = this.show.bind(this);
this._registerEvents();
this._loadQueryParam();
this.invert();
}
hide() {
$.bodyClassRemove('form');
this._inputEl.value = '';
this._inputElVal = '';
this._suggester.suggest('');
this._setBackgroundFromQuery('');
}
show() {
$.bodyClassAdd('form');
this._inputEl.focus();
}
invert() {
if (this._invertColors) {
const bgcolor = getComputedStyle(document.documentElement).getPropertyValue('--background');
const fgcolor = getComputedStyle(document.documentElement).getPropertyValue('--foreground');
document.documentElement.style.setProperty('--background', fgcolor);
document.documentElement.style.setProperty('--foreground', bgcolor); }
}
_invertConfig() {
invertColorCookie = !invertColorCookie;
localStorage.clear();
localStorage.setItem('invertColorCookie', JSON.stringify(invertColorCookie));
location.reload();
}
_clearPreview() {
this._previewValue(this._inputElVal);
this._inputEl.focus();
}
_handleInput() {
const newQuery = this._inputEl.value;
const isHelp = newQuery === '?';
const isLaunch = newQuery === '!';
const isInvert = newQuery === 'invert!';
const { isKey } = this._parseQuery(newQuery);
this._inputElVal = newQuery;
this._suggester.suggest(newQuery);
this._setBackgroundFromQuery(newQuery);
if (!newQuery || isHelp) this.hide();
if (isHelp) this._toggleHelp();
if (isLaunch) this._quickLaunch();
if (isInvert) this._invertConfig();
if (this._instantRedirect && isKey) this._submitWithValue(newQuery);
}
_handleKeydown(e) {
if ($.isUp(e) || $.isDown(e) || $.isRemove(e)) return;
switch ($.key(e)) {
case 'alt':
case 'ctrl':
case 'enter':
case 'shift':
case 'super':
return;
case 'escape':
this.hide();
return;
}
this.show();
}
_loadQueryParam() {
const q = new URLSearchParams(window.location.search).get('q');
if (q) this._submitWithValue(q);
}
_previewValue(value) {
this._inputEl.value = value;
this._setBackgroundFromQuery(value);
}
_redirect(redirect) {
if (this._newTab) window.open(redirect, '_blank');
else window.location.href = redirect;
}
_registerEvents() {
document.addEventListener('keydown', this._handleKeydown);
this._inputEl.addEventListener('input', this._handleInput);
this._formEl.addEventListener('submit', this._submitForm, false);
if (this._suggester) {
this._suggester.setOnClick(this._submitWithValue);
this._suggester.setOnHighlight(this._previewValue);
this._suggester.setOnUnhighlight(this._clearPreview);
}
}
_setBackgroundFromQuery(query) {
if (!this._colors) return;
this._formEl.style.background = this._parseQuery(query).color;
}
_submitForm(e) {
if (e) e.preventDefault();
const query = this._inputEl.value;
if (this._suggester) this._suggester.success(query);
this.hide();
this._redirect(this._parseQuery(query).redirect);
}
_submitWithValue(value) {
this._inputEl.value = value;
this._submitForm();
}
}
const queryParser = new QueryParser({
commands: CONFIG.commands,
pathDelimiter: CONFIG.pathDelimiter,
searchDelimiter: CONFIG.searchDelimiter,
});
const influencers = CONFIG.influencers.map(influencerConfig => {
return new {
Default: DefaultInfluencer,
DuckDuckGo: DuckDuckGoInfluencer,
History: HistoryInfluencer,
}[influencerConfig.name]({
defaultSuggestions: CONFIG.defaultSuggestions,
limit: influencerConfig.limit,
parseQuery: queryParser.parse,
});
});
const suggester = new Suggester({
enabled: CONFIG.suggestions,
influencers,
limit: CONFIG.suggestionsLimit,
});
const help = new Help({
commands: CONFIG.commands,
newTab: CONFIG.newTab,
suggester,
invertedColors: CONFIG.invertedColors,
});
const form = new Form({
colors: CONFIG.colors,
instantRedirect: CONFIG.instantRedirect,
newTab: CONFIG.newTab,
parseQuery: queryParser.parse,
suggester,
toggleHelp: help.toggle,
quickLaunch: help.launch,
invertedColors: CONFIG.invertedColors,
});
new Clock({
delimiter: CONFIG.clockDelimiter,
toggleHelp: help.toggle,
twentyFourHourClock: CONFIG.twentyFourHourClock,
});

305
style/style.css Normal file
View File

@ -0,0 +1,305 @@
:root {
/* colors */
--background: #000000;
--foreground: #FFFFFF;
/* fonts */
--font-family: 'Open Sans', sans-serif;
--base-font-size: 18px;
--font-weight-normal: 500;
--font-weight-bold: 900;
}
@font-face {
font-family: 'Open Sans', sans-serif;
font-style: normal;
font-weight: var(--font-weight-normal);
src: local('Open Sans'), local('Open Sans'),
url('../assets/fonts/OpenSans-Regular.ttf') format('ttf');
}
* {
box-sizing: border-box;
}
html {
font-family: var(--font-family);
font-size: var(--base-font-size);
font-weight: var(--font-weight-normal);
}
body {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
transition: background 0.2s;
background: var(--background);
color: var(--foreground);
}
input,
button {
display: block;
width: 100%;
margin: 0;
padding: 0;
background: transparent;
color: inherit;
font-family: var(--font-family);
font-size: 1rem;
}
input,
button,
input:focus,
button:focus {
border: 0;
outline: 0;
-webkit-appearance: none;
-moz-appearance: none;
}
ul,
li {
margin: 0;
padding: 0;
list-style: none;
}
a,
a:focus {
color: inherit;
outline: 0;
}
.center {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.overlay {
position: fixed;
top: 0;
left: 0;
overflow: auto;
width: 100%;
height: 100%;
visibility: hidden;
}
.clock {
display: block;
margin-top: -0.06rem;
font-size: 4rem;
font-weight: var(--font-weight-normal);
text-align: center;
letter-spacing: 0.05rem;
cursor: pointer;
}
.am-pm {
font-size: 1rem;
letter-spacing: 0.1rem;
}
.search-form {
background: var(--foreground);
color: var(--background);
z-index: 2;
}
.search-form>div {
width: 100%;
}
.search-input {
width: 100%;
padding: 0 1rem;
margin-bottom: 20px;
font-size: 1.5rem;
font-weight: var(--font-weight-bold);
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.search-suggestions {
display: none;
justify-content: center;
flex-direction: column;
flex-wrap: wrap;
overflow: hidden;
}
body.suggestions .search-suggestions {
display: flex;
}
.search-suggestion {
padding: 0.75rem 1rem;
text-align: left;
white-space: nowrap;
cursor: pointer;
}
.search-suggestion.highlight {
background: var(--background);
color: var(--foreground);
}
.search-suggestion b {
position: relative;
font-weight: var(--font-weight-normal);
}
.search-suggestion b::after {
content: ' ';
position: absolute;
right: 0;
bottom: -0.3rem;
left: 0;
height: 2px;
background: var(--background);
opacity: 0.4;
}
.search-suggestion.highlight b::after {
opacity: 0;
}
.help {
display: block;
padding: 8vw;
background: var(--background);
z-index: 1;
}
.category {
margin-bottom: 3rem;
padding: 25px;
width: 250px;
min-width: 225px;
}
.category-name {
margin-bottom: 1.5rem;
font-size: 0.75rem;
letter-spacing: 0.15rem;
text-transform: uppercase;
}
.command a {
display: flex;
position: relative;
margin: 1rem 0;
line-height: 2rem;
text-decoration: none;
}
.command:last-of-type a {
margin-bottom: 0;
}
.command-key {
display: block;
float: left;
width: 2rem;
height: 2rem;
margin-right: 1rem;
border-radius: 50%;
background: transparent;
color: var(--background);
font-size: 0.75rem;
line-height: 2rem;
text-align: center;
}
.command-name {
position: relative;
}
.command-name::after {
content: ' ';
display: none;
position: absolute;
right: 0;
bottom: 2px;
left: 0;
height: 2px;
transition: 0.2s;
transform: translateX(-2rem);
background: var(--foreground);
opacity: 0;
}
body.help .command-name::after {
display: block;
}
.command a:hover .command-name::after,
.command a:focus .command-name::after {
transform: translateX(0);
opacity: 1;
}
body.help .help,
body.form .search-form {
visibility: visible;
}
@media (min-width: 500px) {
.clock {
font-size: 6rem;
}
.search-input {
text-align: center;
}
.search-suggestions {
align-items: center;
}
.categories {
display: flex;
grid-template-columns: 250px 175px;
justify-content: space-around;
}
}
@media (min-width: 1000px) {
.help {
display: flex;
padding: 0;
}
.search-input {
font-size: 2.5rem;
}
.search-suggestions {
flex-direction: row;
}
.category {
margin: 2rem 0;
}
.categories {
grid-template-columns: repeat(2, 300px) 175px;
}
}
@media (min-width: 1500px) {
.categories {
grid-template-columns: repeat(5, 220px) 175px;
}
}