mirror of
https://github.com/xvvvyz/tilde.git
synced 2026-03-11 14:44:24 +00:00
1514 lines
39 KiB
HTML
1514 lines
39 KiB
HTML
<!DOCTYPE html>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="robots" content="noindex" />
|
||
<title>~</title>
|
||
|
||
<script>
|
||
const CONFIG = {
|
||
/**
|
||
* Choose a predefined theme:
|
||
*
|
||
* - "ashes-dark"
|
||
* - "gruvbox-dark"
|
||
* - "nord-dark"
|
||
* - "ocean-light"
|
||
* - "tilde-dark"
|
||
* - "tokyo-dark"
|
||
*
|
||
* Alternatively, create your own in the <style> tag below!
|
||
*/
|
||
theme: 'ashes-dark',
|
||
|
||
/**
|
||
* Action to take when the clock is clicked. Options include:
|
||
*
|
||
* - "Help" to show the help menu
|
||
* - "Search" to show the search input (useful on mobile)
|
||
*/
|
||
clockOnClickAction: 'Help',
|
||
|
||
/**
|
||
* The delimiter between the hours, minutes and seconds on the clock.
|
||
*/
|
||
clockDelimiter: ' – ',
|
||
|
||
/**
|
||
* Show seconds on the clock. A monospaced font is recommended for this.
|
||
*/
|
||
clockShowSeconds: false,
|
||
|
||
/**
|
||
* Show AM/PM indication when CONFIG.clockTwentyFourHours is false.
|
||
*/
|
||
clockShowAmPm: true,
|
||
|
||
/**
|
||
* Show a twenty-four-hour clock instead of a twelve-hour clock.
|
||
*/
|
||
clockTwentyFourHour: true,
|
||
|
||
/**
|
||
* Force an IANA timezone. Useful when attempting to prevent browser
|
||
* fingerprinting. For example, "America/Los_Angeles" would force Pacific
|
||
* Time, "Asia/Kolkata" would force Indian Standard Time, etc. Read:
|
||
*
|
||
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||
*/
|
||
clockTimeZone: undefined,
|
||
|
||
/**
|
||
* Type this to toggle the help menu.
|
||
*/
|
||
helpKey: '?',
|
||
|
||
/**
|
||
* Instantly redirect when a key is matched. Put a space before any other
|
||
* queries to prevent unwanted redirects.
|
||
*/
|
||
queryInstantRedirect: false,
|
||
|
||
/**
|
||
* Open triggered queries in a new tab.
|
||
*/
|
||
queryNewTab: true,
|
||
|
||
/**
|
||
* 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".
|
||
*/
|
||
queryPathDelimiter: '/',
|
||
|
||
/**
|
||
* The delimiter between a command key and your search query. For example,
|
||
* to search GitHub for tilde, you'd type "g'tilde".
|
||
*/
|
||
querySearchDelimiter: "'",
|
||
|
||
/**
|
||
* Scripts allow you to open or search multiple sites at once. For example,
|
||
* to search Google, Bing, DuckDuckGo, Ecosia and Yahoo for cats at the same
|
||
* time, you'd type "q'cats".
|
||
*/
|
||
scripts: {
|
||
q: ['bin', 'yah', 'eco', 'goo', '*'],
|
||
},
|
||
|
||
/**
|
||
* The order, limit and minimum characters for each suggestion influencer.
|
||
*
|
||
* An "influencer" is just a suggestion source. "limit" is the max number of
|
||
* suggestions an influencer will produce. "minChars" determines how many
|
||
* characters need to be typed before the influencer kicks in.
|
||
*
|
||
* The following influencers are available:
|
||
*
|
||
* - "Default" suggestions come from CONFIG.suggestionDefaults (sync)
|
||
* - "History" suggestions come from your previously entered queries (sync)
|
||
* - "DuckDuckGo" suggestions come from the DuckDuckGo search API (async)
|
||
*
|
||
* To disable suggestions, remove all influencers.
|
||
*/
|
||
suggestionInfluencers: [
|
||
{ name: 'Default', limit: 4 },
|
||
{ name: 'History', limit: 4, minChars: 2 },
|
||
{ name: 'DuckDuckGo', limit: 4, minChars: 2 },
|
||
],
|
||
|
||
/**
|
||
* Max number of suggestions that will ever be shown.
|
||
*/
|
||
suggestionLimit: 4,
|
||
|
||
/**
|
||
* Default search suggestions for the specified queries.
|
||
*/
|
||
suggestionDefaults: {
|
||
0: ["0'8000", "0'8080"],
|
||
a: ['a/cognito/v2/idp/user-pools', 'a/dynamodbv2#item-explorer'],
|
||
c: ['c/calendar/u/1/r', 'c/calendar/u/2/r'],
|
||
d: ['d/drive/u/1/my-drive', 'd/drive/u/2/my-drive'],
|
||
e: ['e/brand/national-geographic', 'e/brand/marvel', 'e/brand/star-wars'],
|
||
g: ['g/trending', 'g/ossu', 'g/xvvvyz/tilde', 'gist.github.com'],
|
||
h: ['h/popular', 'h/popular/lastweek', 'h/tags'],
|
||
i: ['i/u/1/inbox', 'i/u/2/inbox'],
|
||
k: ['k/u/1', 'k/u/2'],
|
||
m: ['m/mail/u/1/#inbox', 'm/mail/u/2/#inbox'],
|
||
o: ['o/discover/sets/new-for-you', 'o/discover/sets/weekly'],
|
||
r: ['r/r/startpages', 'r/r/unixporn', 'r/r/onebag', 'r/r/fujix'],
|
||
s: ['s/collection/tracks', 's/playlist/37i9dQZEVXcXr3r4FYT3J7'],
|
||
u: ['u/explore', 'u/backgrounds'],
|
||
y: ['y/feed/trending'],
|
||
},
|
||
|
||
/**
|
||
* The name, key, url, search path and color for your commands. If none of
|
||
* the specified keys are matched, the * key is used. Commands without a
|
||
* name don't show up in the help menu.
|
||
*
|
||
* "hues" is an array of HSL hues that will be converted into a linear
|
||
* gradient. CSS variables defined below, prefixed with --command-color-,
|
||
* determine the saturation and lightness for each generated color.
|
||
*
|
||
* "color", if defined, will be applied to the command as-is.
|
||
*/
|
||
commands: [
|
||
{
|
||
key: '*',
|
||
search: '/?q={}',
|
||
url: 'https://duckduckgo.com',
|
||
},
|
||
{
|
||
key: 'bin',
|
||
search: '/search?q={}',
|
||
url: 'https://www.bing.com',
|
||
},
|
||
{
|
||
key: 'eco',
|
||
search: '/search?q={}',
|
||
url: 'https://www.ecosia.org',
|
||
},
|
||
{
|
||
key: 'goo',
|
||
search: '/search?q={}',
|
||
url: 'https://www.google.com',
|
||
},
|
||
{
|
||
key: 'yah',
|
||
search: '/search?p={}',
|
||
url: 'https://search.yahoo.com',
|
||
},
|
||
{
|
||
hues: ['0', '0'],
|
||
key: 'x',
|
||
name: 'Netflix',
|
||
search: '/search?q={}',
|
||
url: 'https://www.netflix.com/browse',
|
||
},
|
||
{
|
||
hues: ['0', '350'],
|
||
key: 'c',
|
||
name: 'Calendar',
|
||
search: '/calendar/u/0/r/search?q={}',
|
||
url: 'https://calendar.google.com/calendar/u/0/r',
|
||
},
|
||
{
|
||
hues: ['5', '345'],
|
||
key: 'y',
|
||
name: 'YouTube',
|
||
search: '/results?search_query={}',
|
||
url: 'https://youtube.com/feed/subscriptions',
|
||
},
|
||
{
|
||
hues: ['355', '335'],
|
||
key: 'm',
|
||
name: 'Mail',
|
||
search: '/mail/u/0/#search/{}',
|
||
url: 'https://mail.google.com/mail/u/0/#inbox',
|
||
},
|
||
{
|
||
hues: ['337', '317'],
|
||
key: 'b',
|
||
name: 'Dribbble',
|
||
search: '/search/{}',
|
||
url: 'https://dribbble.com/shots/popular',
|
||
},
|
||
{
|
||
hues: ['266', '286'],
|
||
key: 'f',
|
||
name: 'Figma',
|
||
url: 'https://www.figma.com/files/recent',
|
||
},
|
||
{
|
||
hues: ['264', '244'],
|
||
key: 'v',
|
||
name: 'Vercel',
|
||
url: 'https://vercel.com',
|
||
},
|
||
{
|
||
hues: ['254', '234'],
|
||
key: 'r',
|
||
name: 'Reddit',
|
||
search: '/search?q={}',
|
||
url: 'https://www.reddit.com',
|
||
},
|
||
{
|
||
hues: ['226', '236'],
|
||
key: 'e',
|
||
name: 'Disney',
|
||
url: 'https://www.disneyplus.com/home',
|
||
},
|
||
{
|
||
hues: ['214', '234'],
|
||
key: 'g',
|
||
name: 'GitHub',
|
||
search: '/search?q={}',
|
||
url: 'https://github.com',
|
||
},
|
||
{
|
||
hues: ['201', '221'],
|
||
key: 'l',
|
||
name: 'LinkedIn',
|
||
search: '/search/results/all/?keywords={}',
|
||
url: 'https://www.linkedin.com',
|
||
},
|
||
{
|
||
hues: ['198', '218'],
|
||
key: ';',
|
||
name: 'MDN',
|
||
search: '/en-US/search?q={}',
|
||
url: 'https://developer.mozilla.org/en-US',
|
||
},
|
||
{
|
||
hues: ['217', '197'],
|
||
key: 'i',
|
||
name: 'Inbox',
|
||
url: 'https://mail.proton.me/u/0/inbox',
|
||
},
|
||
{
|
||
hues: ['203', '183'],
|
||
key: 't',
|
||
name: 'Twitter',
|
||
search: '/search?q={}',
|
||
url: 'https://twitter.com/home',
|
||
},
|
||
{
|
||
hues: ['166', '146'],
|
||
key: '+',
|
||
name: 'Khan',
|
||
search: '/search?page_search_query={}',
|
||
url: 'https://www.khanacademy.org',
|
||
},
|
||
{
|
||
hues: ['136', '156'],
|
||
key: 'd',
|
||
name: 'Drive',
|
||
search: '/drive/u/0/search?q={}',
|
||
url: 'https://drive.google.com/drive/u/0/my-drive',
|
||
},
|
||
{
|
||
hues: ['124', '164'],
|
||
key: 's',
|
||
name: 'Spotify',
|
||
search: '/search/{}',
|
||
url: 'https://open.spotify.com',
|
||
},
|
||
{
|
||
hues: ['90'],
|
||
key: 'h',
|
||
name: 'Hypem',
|
||
search: '/search/{}',
|
||
url: 'https://hypem.com/latest',
|
||
},
|
||
{
|
||
hues: ['45', '40'],
|
||
key: 'k',
|
||
name: 'Keep',
|
||
search: '/u/0/#search/text={}',
|
||
url: 'https://keep.google.com/u/0',
|
||
},
|
||
{
|
||
hues: ['36', '26'],
|
||
key: 'a',
|
||
name: 'AWS',
|
||
url: 'https://us-west-2.console.aws.amazon.com/console/home',
|
||
},
|
||
{
|
||
hues: ['32', '22'],
|
||
key: 'o',
|
||
name: 'Sounds',
|
||
search: '/search?q={}',
|
||
url: 'https://soundcloud.com/discover',
|
||
},
|
||
{
|
||
hues: ['13', '33'],
|
||
key: 'p',
|
||
name: 'Hunt',
|
||
search: '/search?q={}',
|
||
url: 'https://www.producthunt.com',
|
||
},
|
||
{
|
||
hues: ['4', '24'],
|
||
key: 'n',
|
||
name: 'Notion',
|
||
url: 'https://www.notion.so',
|
||
},
|
||
{
|
||
key: 'u',
|
||
name: 'Unsplash',
|
||
search: '/search/{}',
|
||
url: 'https://unsplash.com/images',
|
||
},
|
||
{
|
||
key: '0',
|
||
name: 'Local',
|
||
search: ':{}',
|
||
url: 'http://localhost:3000',
|
||
},
|
||
],
|
||
};
|
||
</script>
|
||
|
||
<style>
|
||
/* themes */
|
||
|
||
.ashes-dark {
|
||
--base-background: #0e0e0e;
|
||
--base-foreground: #c7c7c7;
|
||
--command-color-complementary: var(--base-background);
|
||
--command-color-highlight-foreground: var(--base-foreground);
|
||
--command-color-lightness: 75%;
|
||
--command-color-saturation: 40%;
|
||
--help-command-background: #272e32;
|
||
}
|
||
|
||
.gruvbox-dark {
|
||
--base-background: #282828;
|
||
--base-foreground: #ebdbb2;
|
||
--command-color-complementary: #fbf1c7;
|
||
--command-color-highlight-foreground: var(--base-background);
|
||
--command-color-lightness: 45%;
|
||
--command-color-saturation: 34%;
|
||
--help-command-background: #3c3836;
|
||
}
|
||
|
||
.nord-dark {
|
||
--base-background: #2e3440;
|
||
--base-foreground: #d8dee9;
|
||
--command-color-complementary: #fff;
|
||
--command-color-highlight-foreground: var(--help-command-background);
|
||
--command-color-lightness: 56%;
|
||
--command-color-saturation: 42%;
|
||
--help-command-background: #3b4252;
|
||
}
|
||
|
||
.ocean-light {
|
||
--base-background: #eff1f5;
|
||
--base-foreground: #4f5b66;
|
||
--command-color-complementary: var(--base-background);
|
||
--command-color-highlight-foreground: var(--base-foreground);
|
||
--command-color-lightness: 56%;
|
||
--command-color-saturation: 32%;
|
||
--help-command-background: #e4e6ea;
|
||
}
|
||
|
||
.tilde-dark {
|
||
--base-background: #111;
|
||
--base-foreground: #ddd;
|
||
--command-color-complementary: #eee;
|
||
--command-color-highlight-foreground: var(--base-background);
|
||
--command-color-lightness: 50%;
|
||
--command-color-saturation: 40%;
|
||
--help-command-background: #222;
|
||
}
|
||
|
||
.tokyo-dark {
|
||
--base-background: #1a1b26;
|
||
--base-foreground: #a9b1d6;
|
||
--command-color-complementary: var(--base-background);
|
||
--command-color-highlight-foreground: var(--base-foreground);
|
||
--command-color-lightness: 72%;
|
||
--command-color-saturation: 89%;
|
||
--help-command-background: #24283b;
|
||
}
|
||
|
||
/* root variables */
|
||
|
||
:root {
|
||
--base-border-radius: 2px;
|
||
--base-font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue,
|
||
Helvetica, Ubuntu, Roboto, Noto, Segoe UI, Arial, sans-serif;
|
||
--base-font-size: 17px;
|
||
--base-font-weight-black: 900;
|
||
--base-font-weight-bold: 700;
|
||
--base-font-weight-normal: 400;
|
||
--clock-font-size: 3rem;
|
||
--command-height: 4.25rem;
|
||
--command-padding: calc(var(--command-height) / 2 - 1ch / 2);
|
||
--help-grid-columns: 1;
|
||
--help-grid-gap: 1rem;
|
||
--search-input-font-size: 2rem;
|
||
--search-input-text-align: left;
|
||
--suggestion-height: 3rem;
|
||
--suggestion-padding: calc(var(--suggestion-height) / 2 - 1ch / 2);
|
||
--suggestions-flex-direction: column;
|
||
}
|
||
|
||
@media (min-width: 600px) {
|
||
:root {
|
||
--clock-font-size: 5rem;
|
||
--help-grid-columns: 2;
|
||
}
|
||
}
|
||
|
||
@media (min-width: 900px) {
|
||
:root {
|
||
--clock-font-size: 7rem;
|
||
--help-grid-columns: 3;
|
||
--search-input-font-size: 3rem;
|
||
--suggestions-flex-direction: row;
|
||
}
|
||
}
|
||
|
||
@media (min-width: 1400px) {
|
||
:root {
|
||
--help-grid-columns: 5;
|
||
}
|
||
}
|
||
|
||
/* reset */
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html {
|
||
font-family: var(--base-font-family);
|
||
font-size: var(--base-font-size);
|
||
font-weight: var(--base-font-weight-normal);
|
||
}
|
||
|
||
body {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
margin: 0;
|
||
padding: 0;
|
||
background: var(--base-background);
|
||
color: var(--base-foreground);
|
||
}
|
||
|
||
input,
|
||
button {
|
||
display: block;
|
||
width: 100%;
|
||
margin: 0;
|
||
padding: 0;
|
||
background: transparent;
|
||
color: inherit;
|
||
font-family: var(--base-font-family);
|
||
font-weight: var(--base-font-weight-normal);
|
||
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;
|
||
}
|
||
|
||
/* reusable classes */
|
||
|
||
.center {
|
||
display: flex;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.center > * {
|
||
margin: auto;
|
||
}
|
||
|
||
.overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: auto;
|
||
}
|
||
|
||
/* main styles */
|
||
|
||
.clock {
|
||
display: block;
|
||
font-size: var(--clock-font-size);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.help {
|
||
background: var(--base-background);
|
||
visibility: hidden;
|
||
z-index: 1;
|
||
}
|
||
|
||
body.help .help {
|
||
visibility: visible;
|
||
}
|
||
|
||
body.form .help {
|
||
visibility: hidden;
|
||
}
|
||
|
||
.command-list {
|
||
display: grid;
|
||
grid-gap: var(--help-grid-gap);
|
||
grid-template-columns: repeat(var(--help-grid-columns), 1fr);
|
||
padding: 6rem 0;
|
||
}
|
||
|
||
.command {
|
||
display: flex;
|
||
background: var(--help-command-background);
|
||
border-radius: var(--base-border-radius);
|
||
opacity: 0.9;
|
||
line-height: var(--command-height);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.command:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
.command:hover .command-key {
|
||
background: transparent;
|
||
}
|
||
|
||
.command-key {
|
||
width: var(--command-height);
|
||
border-radius: var(--base-border-radius);
|
||
color: var(--command-color-complementary);
|
||
font-weight: var(--base-font-weight-bold);
|
||
text-align: center;
|
||
}
|
||
|
||
.command-name {
|
||
padding: 0 var(--command-padding);
|
||
}
|
||
|
||
.search-form {
|
||
background: var(--base-background);
|
||
visibility: hidden;
|
||
}
|
||
|
||
body.form .search-form {
|
||
visibility: visible;
|
||
}
|
||
|
||
body.color .search-form {
|
||
color: var(--command-color-complementary);
|
||
}
|
||
|
||
.search-form-content {
|
||
width: 100%;
|
||
}
|
||
|
||
.search-input {
|
||
width: 100%;
|
||
font-size: var(--search-input-font-size);
|
||
font-weight: var(--base-font-weight-black);
|
||
text-align: center;
|
||
}
|
||
|
||
.search-suggestions {
|
||
display: none;
|
||
justify-content: center;
|
||
align-items: center;
|
||
flex-direction: var(--suggestions-flex-direction);
|
||
flex-wrap: wrap;
|
||
margin-top: 2rem;
|
||
overflow: hidden;
|
||
}
|
||
|
||
body.suggestions .search-suggestions {
|
||
display: flex;
|
||
}
|
||
|
||
.search-suggestion {
|
||
border-radius: var(--base-border-radius);
|
||
cursor: pointer;
|
||
font-weight: var(--base-font-weight-bold);
|
||
height: var(--suggestion-height);
|
||
padding: 0 var(--suggestion-padding);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.search-suggestion.highlight {
|
||
background: var(--base-foreground);
|
||
color: var(--base-background);
|
||
}
|
||
|
||
body.color .search-suggestion.highlight {
|
||
background: var(--command-color-complementary);
|
||
color: var(--command-color-highlight-foreground);
|
||
}
|
||
|
||
.search-suggestion-match {
|
||
font-weight: var(--base-font-weight-normal);
|
||
}
|
||
</style>
|
||
|
||
<div class="center">
|
||
<time class="clock" id="clock" />
|
||
</div>
|
||
<form
|
||
autocomplete="off"
|
||
class="center overlay search-form"
|
||
id="search-form"
|
||
spellcheck="false"
|
||
>
|
||
<div class="search-form-content">
|
||
<input class="search-input" id="search-input" title="search" type="text" />
|
||
<ul class="search-suggestions" id="search-suggestions" />
|
||
</div>
|
||
</form>
|
||
<aside class="center help overlay" id="help" />
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
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))],
|
||
isDown: (e) => /^(ArrowDown|Tab|c-n)$/.test($.prefix(e) + e.key),
|
||
isEscape: (e) => /^Escape$/.test(e.key),
|
||
isModifier: (e) => /^(Alt|Control|Enter|Meta|Shift)$/.test(e.key),
|
||
isRemove: (e) => /^(Backspace|Delete)$/.test(e.key),
|
||
isUp: (e) => /^(ArrowUp|c-p|s-Tab)$/.test($.prefix(e) + e.key),
|
||
prefix: (e) => (e.ctrlKey ? 'c-' : '') + (e.shiftKey ? 's-' : ''),
|
||
};
|
||
|
||
class Clock {
|
||
static #EL = $.el('#clock');
|
||
|
||
#amPm = false;
|
||
#delimiter = ':';
|
||
#showSeconds = false;
|
||
#timeZone = null;
|
||
#twentyFourHour = true;
|
||
|
||
constructor(options) {
|
||
this.#amPm = options.amPm;
|
||
this.#delimiter = options.delimiter;
|
||
this.#showSeconds = options.showSeconds;
|
||
this.#timeZone = options.timeZone;
|
||
this.#twentyFourHour = options.twentyFourHour;
|
||
Clock.#EL.addEventListener('click', options.onClick);
|
||
this.#start();
|
||
}
|
||
|
||
#setTime = () => {
|
||
const date = new Date();
|
||
|
||
Clock.#EL.innerHTML = date
|
||
.toLocaleString('en-US', {
|
||
hour12: !this.#twentyFourHour,
|
||
hour: 'numeric',
|
||
minute: 'numeric',
|
||
second: this.#showSeconds ? 'numeric' : undefined,
|
||
timeZone: this.#timeZone,
|
||
})
|
||
.replace(this.#amPm ? '' : / (AM|PM)/, '')
|
||
.replace(/:/g, this.#delimiter)
|
||
.replace(/^0/, '');
|
||
|
||
Clock.#EL.setAttribute('datetime', date.toTimeString());
|
||
};
|
||
|
||
#start() {
|
||
this.#setTime();
|
||
setInterval(this.#setTime, 1000);
|
||
}
|
||
}
|
||
|
||
class Help {
|
||
static #CN_COMMAND = 'command';
|
||
static #CN_COMMAND_KEY = 'command-key';
|
||
static #CN_COMMAND_LIST = 'command-list';
|
||
static #CN_COMMAND_NAME = 'command-name';
|
||
static #CN_HELP = 'help';
|
||
static #EL = $.el('#help');
|
||
|
||
#commands = [];
|
||
#newTab = false;
|
||
#toggled = false;
|
||
|
||
constructor(options) {
|
||
this.#commands = options.commands;
|
||
this.#newTab = options.newTab;
|
||
this.#buildCommands();
|
||
this.#registerEvents();
|
||
}
|
||
|
||
toggle = (show) => {
|
||
this.#toggled = typeof show !== 'undefined' ? show : !this.#toggled;
|
||
if (this.#toggled) $.bodyClassAdd(Help.#CN_HELP);
|
||
else $.bodyClassRemove(Help.#CN_HELP);
|
||
};
|
||
|
||
#buildCommands() {
|
||
const list = document.createElement('ul');
|
||
list.classList.add(Help.#CN_COMMAND_LIST);
|
||
|
||
list.insertAdjacentHTML(
|
||
'beforeend',
|
||
this.#commands.reduce((acc, { color, name, key, url }, i) => {
|
||
if (!name) return acc;
|
||
const target = this.#newTab ? '_blank' : '_self';
|
||
|
||
return `
|
||
${acc}
|
||
<style>
|
||
.command-key-${i},
|
||
.command-${i}:hover {
|
||
background: ${color};
|
||
color: var(--command-color-complementary);
|
||
}
|
||
</style>
|
||
<li>
|
||
<a
|
||
class="${Help.#CN_COMMAND} command-${i}"
|
||
href="${url}"
|
||
rel="noopener noreferrer"
|
||
target="${target}"
|
||
>
|
||
<span
|
||
class="${Help.#CN_COMMAND_KEY} command-key-${i}"
|
||
>
|
||
${key}
|
||
</span>
|
||
<span class="${Help.#CN_COMMAND_NAME}">${name}</span>
|
||
</a>
|
||
</li>
|
||
`;
|
||
}, '')
|
||
);
|
||
|
||
Help.#EL.appendChild(list);
|
||
}
|
||
|
||
#handleKeydown = (e) => {
|
||
if ($.isEscape(e)) this.toggle(false);
|
||
};
|
||
|
||
#registerEvents() {
|
||
document.addEventListener('keydown', this.#handleKeydown);
|
||
}
|
||
}
|
||
|
||
class Influencer {
|
||
limit = 0;
|
||
minChars = 0;
|
||
|
||
constructor(options) {
|
||
this.limit = options.limit;
|
||
this.minChars = options.minChars;
|
||
}
|
||
|
||
addItem() {
|
||
// noop
|
||
}
|
||
|
||
getSuggestions() {
|
||
return Promise.resolve([]);
|
||
}
|
||
|
||
isTooShort(query) {
|
||
return query.length < this.minChars;
|
||
}
|
||
|
||
static addSearchPrefix(items, { isSearch, key, split }) {
|
||
const searchPrefix = isSearch ? `${key}${split}` : false;
|
||
return items.map((s) => (searchPrefix ? searchPrefix + s : s));
|
||
}
|
||
}
|
||
|
||
class DefaultInfluencer extends Influencer {
|
||
#suggestionDefaults = {};
|
||
|
||
constructor(options) {
|
||
super(...arguments);
|
||
this.#suggestionDefaults = options.suggestionDefaults;
|
||
}
|
||
|
||
getSuggestions({ raw }) {
|
||
return new Promise((resolve) =>
|
||
resolve((this.#suggestionDefaults[raw] || []).slice(0, this.limit))
|
||
);
|
||
}
|
||
}
|
||
|
||
class DuckDuckGoInfluencer extends Influencer {
|
||
constructor() {
|
||
super(...arguments);
|
||
}
|
||
|
||
getSuggestions(parsedQuery) {
|
||
const { lower, query } = parsedQuery;
|
||
if (this.isTooShort(query)) return Promise.resolve([]);
|
||
|
||
return new Promise((resolve) => {
|
||
window.autocompleteCallback = (res) =>
|
||
resolve(
|
||
Influencer.addSearchPrefix(
|
||
res
|
||
.map((s) => s.phrase)
|
||
.filter((s) => s.toLowerCase() !== lower)
|
||
.slice(0, this.limit),
|
||
parsedQuery
|
||
)
|
||
);
|
||
|
||
const script = document.createElement('script');
|
||
script.src = `https://duckduckgo.com/ac/?callback=autocompleteCallback&q=${query}`;
|
||
$.el('head').appendChild(script);
|
||
});
|
||
}
|
||
}
|
||
|
||
class HistoryInfluencer extends Influencer {
|
||
static #LOCALSTORAGE_KEY = 'history';
|
||
|
||
#history = null;
|
||
|
||
constructor() {
|
||
super(...arguments);
|
||
}
|
||
|
||
addItem({ isPath, lower }) {
|
||
if (isPath || this.isTooShort(lower)) return;
|
||
let exists;
|
||
|
||
const history = this.#getHistory().map(([item, count]) => {
|
||
const match = item === lower;
|
||
if (match) exists = true;
|
||
return [item, match ? count + 1 : count];
|
||
});
|
||
|
||
if (!exists) history.push([lower, 1]);
|
||
this.#setHistory(history.sort((a, b) => b[1] - a[1]));
|
||
}
|
||
|
||
getSuggestions(parsedQuery) {
|
||
const { lower } = parsedQuery;
|
||
if (this.isTooShort(lower)) return Promise.resolve([]);
|
||
|
||
return new Promise((resolve) =>
|
||
resolve(
|
||
Influencer.addSearchPrefix(
|
||
this.#getHistory()
|
||
.filter(([item]) => item !== lower && item.includes(lower))
|
||
.slice(0, this.limit)
|
||
.map(([item]) => item),
|
||
parsedQuery
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
#getHistory() {
|
||
this.#history =
|
||
this.#history ||
|
||
JSON.parse(localStorage.getItem(HistoryInfluencer.#LOCALSTORAGE_KEY)) ||
|
||
[];
|
||
|
||
return this.#history;
|
||
}
|
||
|
||
#setHistory(history) {
|
||
this.#history = history;
|
||
|
||
localStorage.setItem(
|
||
HistoryInfluencer.#LOCALSTORAGE_KEY,
|
||
JSON.stringify(history)
|
||
);
|
||
}
|
||
}
|
||
|
||
class Suggester {
|
||
static #ATTR_DATA_SUGGESTION = 'data-suggestion';
|
||
static #CN_HIGHLIGHT = 'highlight';
|
||
static #CN_SEARCH_SUGGESTION = 'search-suggestion';
|
||
static #CN_SEARCH_SUGGESTION_MATCH = 'search-suggestion-match';
|
||
static #CN_SUGGESTIONS = 'suggestions';
|
||
static #EL = $.el('#search-suggestions');
|
||
|
||
#highlightedSuggestion = null;
|
||
#influencers = [];
|
||
#limit = 0;
|
||
#onSuggestionClick = () => {};
|
||
#onSuggestionHighlight = () => {};
|
||
#onSuggestionUnhighlight = () => {};
|
||
#parsedQuery = '';
|
||
#suggestionEls = [];
|
||
|
||
constructor(options) {
|
||
this.#influencers = options.influencers;
|
||
this.#limit = options.limit;
|
||
this.#registerEvents();
|
||
}
|
||
|
||
onSuccess(parsedQuery) {
|
||
this.#influencers.forEach((i) => i.addItem(parsedQuery));
|
||
this.#clearSuggestions();
|
||
}
|
||
|
||
setOnClick(callback) {
|
||
this.#onSuggestionClick = callback;
|
||
}
|
||
|
||
setOnHighlight(callback) {
|
||
this.#onSuggestionHighlight = callback;
|
||
}
|
||
|
||
setOnUnhighlight(callback) {
|
||
this.#onSuggestionUnhighlight = callback;
|
||
}
|
||
|
||
suggest(parsedQuery) {
|
||
this.#parsedQuery = parsedQuery;
|
||
this.#highlightedSuggestion = null;
|
||
|
||
if (!parsedQuery.query) {
|
||
this.#clearSuggestions();
|
||
return;
|
||
}
|
||
|
||
Promise.all(this.#getInfluencerSuggestions()).then(this.#setSuggestions);
|
||
}
|
||
|
||
#buildSuggestionsHtml(suggestions) {
|
||
return suggestions.slice(0, this.#limit).reduce((acc, suggestion) => {
|
||
const match = new RegExp($.escapeRegex(this.#parsedQuery.query), 'i');
|
||
const matched = suggestion.match(match);
|
||
|
||
const suggestionHtml = matched
|
||
? suggestion.replace(
|
||
match,
|
||
`
|
||
<span class="${Suggester.#CN_SEARCH_SUGGESTION_MATCH}">
|
||
${matched[0]}
|
||
</span>
|
||
`
|
||
)
|
||
: suggestion;
|
||
|
||
return `
|
||
${acc}
|
||
<li>
|
||
<button
|
||
${Suggester.#ATTR_DATA_SUGGESTION}="${suggestion}"
|
||
class=${Suggester.#CN_SEARCH_SUGGESTION}
|
||
tabindex="-1"
|
||
type="button"
|
||
>
|
||
${suggestionHtml}
|
||
</button>
|
||
</li>
|
||
`;
|
||
}, '');
|
||
}
|
||
|
||
#clearSuggestionClickEvents() {
|
||
this.#suggestionEls.forEach((el) =>
|
||
el.removeEventListener('click', this.#onSuggestionClick)
|
||
);
|
||
}
|
||
|
||
#clearSuggestionHighlightEvents() {
|
||
this.#suggestionEls.forEach((el) => {
|
||
el.removeEventListener('mouseover', this.#highlightSuggestion);
|
||
el.removeEventListener('mouseout', this.#unhighlightSuggestion);
|
||
});
|
||
}
|
||
|
||
#clearSuggestions() {
|
||
$.bodyClassRemove(Suggester.#CN_SUGGESTIONS);
|
||
this.#clearSuggestionHighlightEvents();
|
||
this.#clearSuggestionClickEvents();
|
||
Suggester.#EL.innerHTML = '';
|
||
this.#highlightedSuggestion = null;
|
||
this.#suggestionEls = [];
|
||
}
|
||
|
||
#focusNext(e) {
|
||
const exists = this.#suggestionEls.some((el, i) => {
|
||
if (el.classList.contains(Suggester.#CN_HIGHLIGHT)) {
|
||
this.#highlightSuggestion(this.#suggestionEls[i + 1], e);
|
||
return true;
|
||
}
|
||
});
|
||
|
||
if (!exists) this.#highlightSuggestion(this.#suggestionEls[0], e);
|
||
}
|
||
|
||
#focusPrevious(e) {
|
||
const exists = this.#suggestionEls.some((el, i) => {
|
||
if (el.classList.contains(Suggester.#CN_HIGHLIGHT) && i) {
|
||
this.#highlightSuggestion(this.#suggestionEls[i - 1], e);
|
||
return true;
|
||
}
|
||
});
|
||
|
||
if (!exists) this.#unhighlightSuggestion(e);
|
||
}
|
||
|
||
#getInfluencerSuggestions() {
|
||
return this.#influencers.map((influencer) =>
|
||
influencer.getSuggestions(this.#parsedQuery)
|
||
);
|
||
}
|
||
|
||
#handleKeydown = (e) => {
|
||
if ($.isDown(e)) this.#focusNext(e);
|
||
if ($.isUp(e)) this.#focusPrevious(e);
|
||
};
|
||
|
||
#highlightSuggestion(el, e) {
|
||
this.#unhighlightSuggestion();
|
||
if (!el) return;
|
||
|
||
this.#highlightedSuggestion = el.getAttribute(
|
||
Suggester.#ATTR_DATA_SUGGESTION
|
||
);
|
||
|
||
this.#onSuggestionHighlight(this.#highlightedSuggestion);
|
||
el.classList.add(Suggester.#CN_HIGHLIGHT);
|
||
if (e) e.preventDefault();
|
||
}
|
||
|
||
#registerEvents() {
|
||
document.addEventListener('keydown', this.#handleKeydown);
|
||
}
|
||
|
||
#registerSuggestionClickEvents() {
|
||
this.#suggestionEls.forEach((el) =>
|
||
el.addEventListener('click', () =>
|
||
this.#onSuggestionClick(
|
||
el.getAttribute(Suggester.#ATTR_DATA_SUGGESTION)
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
#registerSuggestionHighlightEvents() {
|
||
const noHighlightUntilMouseMove = () => {
|
||
window.removeEventListener('mousemove', noHighlightUntilMouseMove);
|
||
|
||
this.#suggestionEls.forEach((el) => {
|
||
el.addEventListener('mouseover', () => this.#highlightSuggestion(el));
|
||
el.addEventListener('mouseout', this.#unhighlightSuggestion);
|
||
});
|
||
};
|
||
|
||
window.addEventListener('mousemove', noHighlightUntilMouseMove);
|
||
}
|
||
|
||
#rehighlightSuggestion() {
|
||
if (!this.#highlightedSuggestion) return;
|
||
const attr = Suggester.#ATTR_DATA_SUGGESTION;
|
||
const value = this.#highlightedSuggestion;
|
||
this.#highlightSuggestion($.el(`[${attr}="${value}"]`));
|
||
}
|
||
|
||
#setSuggestions = (newSuggestions) => {
|
||
const suggestions = $.flattenAndUnique(newSuggestions);
|
||
Suggester.#EL.innerHTML = this.#buildSuggestionsHtml(suggestions);
|
||
this.#suggestionEls = $.els(`.${Suggester.#CN_SEARCH_SUGGESTION}`);
|
||
this.#registerSuggestionHighlightEvents();
|
||
this.#registerSuggestionClickEvents();
|
||
if (this.#suggestionEls.length) $.bodyClassAdd(Suggester.#CN_SUGGESTIONS);
|
||
this.#rehighlightSuggestion();
|
||
};
|
||
|
||
#unhighlightSuggestion(e) {
|
||
const el = $.el(`.${Suggester.#CN_HIGHLIGHT}`);
|
||
if (!el) return;
|
||
this.#onSuggestionUnhighlight();
|
||
el.classList.remove(Suggester.#CN_HIGHLIGHT);
|
||
if (e) e.preventDefault();
|
||
}
|
||
}
|
||
|
||
class QueryParser {
|
||
static #DEFAULT_PROTOCOL = 'http://';
|
||
static #PROTOCOL_REGEX = /^[a-zA-Z]+:\/\//i;
|
||
|
||
static #URL_REGEX =
|
||
/^((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)$/i;
|
||
|
||
#commands = [];
|
||
#pathDelimiter = '/';
|
||
#scripts = {};
|
||
#searchDelimiter = "'";
|
||
|
||
constructor(options) {
|
||
this.#commands = options.commands;
|
||
this.#searchDelimiter = options.searchDelimiter;
|
||
this.#pathDelimiter = options.pathDelimiter;
|
||
this.#scripts = options.scripts;
|
||
}
|
||
|
||
parse = (query) => {
|
||
const res = [];
|
||
res.raw = query.trim();
|
||
res.query = res.raw;
|
||
res.lower = res.raw.toLowerCase();
|
||
res.split = null;
|
||
|
||
if (QueryParser.#URL_REGEX.test(query)) {
|
||
const hasProtocol = QueryParser.#PROTOCOL_REGEX.test(query);
|
||
|
||
res.redirect = hasProtocol
|
||
? query
|
||
: QueryParser.#DEFAULT_PROTOCOL + query;
|
||
|
||
res.color = QueryParser.#getColorFromUrl(this.#commands, res.redirect);
|
||
return res;
|
||
}
|
||
|
||
const splitSearch = res.query.split(this.#searchDelimiter);
|
||
const splitPath = res.query.split(this.#pathDelimiter);
|
||
|
||
const isScript = Object.entries(this.#scripts).some(([key, script]) => {
|
||
if (query === key) {
|
||
res.key = key;
|
||
res.isKey = true;
|
||
script.forEach((command) => res.push(this.parse(command)));
|
||
return true;
|
||
}
|
||
|
||
if (splitSearch[0] === key) {
|
||
res.key = key;
|
||
res.isSearch = true;
|
||
res.split = this.#searchDelimiter;
|
||
res.query = QueryParser.#shiftAndTrim(splitSearch, res.split);
|
||
res.lower = res.query.toLowerCase();
|
||
|
||
script.forEach((command) =>
|
||
res.push(this.parse(`${command}${res.split}${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);
|
||
|
||
script.forEach((command) =>
|
||
res.push(this.parse(`${command}${this.#pathDelimiter}${res.path}`))
|
||
);
|
||
|
||
return true;
|
||
}
|
||
});
|
||
|
||
if (isScript) return res;
|
||
|
||
this.#commands.some(({ key, search, url }) => {
|
||
if (query === key) {
|
||
res.key = key;
|
||
res.isKey = true;
|
||
res.redirect = url;
|
||
return true;
|
||
}
|
||
|
||
if (splitSearch[0] === key) {
|
||
res.key = key;
|
||
res.isSearch = true;
|
||
res.split = this.#searchDelimiter;
|
||
res.query = QueryParser.#shiftAndTrim(splitSearch, res.split);
|
||
res.lower = res.query.toLowerCase();
|
||
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;
|
||
const domainRegex = new RegExp(`${domain}$`);
|
||
|
||
return commands.find((c) => domainRegex.test(new URL(c.url).hostname))
|
||
?.color;
|
||
}
|
||
|
||
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(/{}/g, 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 {
|
||
static #CL_COLOR = 'color';
|
||
static #CL_FORM = 'form';
|
||
static #EL_FORM = $.el('#search-input');
|
||
static #EL_INPUT = $.el('#search-form');
|
||
static #URL_PARAM_Q = 'q';
|
||
|
||
#helpKey = '?';
|
||
#instantRedirect = false;
|
||
#toggleHelp = () => {};
|
||
#inputValue = '';
|
||
#suggester;
|
||
#parseQuery = () => {};
|
||
#newTab = false;
|
||
|
||
constructor(options) {
|
||
this.#helpKey = options.helpKey;
|
||
this.#instantRedirect = options.instantRedirect;
|
||
this.#newTab = options.newTab;
|
||
this.#parseQuery = options.parseQuery;
|
||
this.#suggester = options.suggester;
|
||
this.#toggleHelp = options.toggleHelp;
|
||
this.#registerEvents();
|
||
this.#loadQueryParam();
|
||
}
|
||
|
||
hide() {
|
||
$.bodyClassRemove(Form.#CL_FORM);
|
||
Form.#EL_FORM.value = '';
|
||
this.#inputValue = '';
|
||
this.#suggester.suggest('');
|
||
this.#setColorsFromQuery('');
|
||
}
|
||
|
||
show() {
|
||
$.bodyClassAdd(Form.#CL_FORM);
|
||
Form.#EL_FORM.focus();
|
||
}
|
||
|
||
#clearPreview = () => {
|
||
this.#previewValue(this.#inputValue);
|
||
Form.#EL_FORM.focus();
|
||
};
|
||
|
||
#handleInput = () => {
|
||
const newQuery = Form.#EL_FORM.value;
|
||
const isHelp = newQuery === this.#helpKey;
|
||
const parsedQuery = this.#parseQuery(newQuery);
|
||
this.#inputValue = newQuery;
|
||
this.#suggester.suggest(parsedQuery);
|
||
this.#setColorsFromQuery(newQuery);
|
||
if (!newQuery || isHelp) this.hide();
|
||
if (isHelp) this.#toggleHelp();
|
||
|
||
if (this.#instantRedirect && parsedQuery.isKey) {
|
||
this.#submitWithValue(newQuery);
|
||
}
|
||
};
|
||
|
||
#handleKeydown = (e) => {
|
||
if (
|
||
$.isDown(e) ||
|
||
$.isModifier(e) ||
|
||
$.isRemove(e) ||
|
||
$.isUp(e) ||
|
||
e.ctrlKey ||
|
||
e.metaKey
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if ($.isEscape(e)) {
|
||
this.hide();
|
||
return;
|
||
}
|
||
|
||
this.show();
|
||
};
|
||
|
||
#loadQueryParam() {
|
||
const q = new URLSearchParams(window.location.search).get(
|
||
Form.#URL_PARAM_Q
|
||
);
|
||
|
||
if (q) this.#submitWithValue(q);
|
||
}
|
||
|
||
#previewValue = (value) => {
|
||
Form.#EL_FORM.value = value;
|
||
this.#setColorsFromQuery(value);
|
||
};
|
||
|
||
#redirect(redirect, forceNewTab) {
|
||
if (this.#newTab || forceNewTab) {
|
||
window.open(redirect, '_blank', 'noopener noreferrer');
|
||
} else {
|
||
window.location.href = redirect;
|
||
}
|
||
}
|
||
|
||
#registerEvents() {
|
||
document.addEventListener('keydown', this.#handleKeydown);
|
||
Form.#EL_FORM.addEventListener('input', this.#handleInput);
|
||
Form.#EL_INPUT.addEventListener('submit', this.#submitForm, false);
|
||
|
||
if (this.#suggester) {
|
||
this.#suggester.setOnClick(this.#submitWithValue);
|
||
this.#suggester.setOnHighlight(this.#previewValue);
|
||
this.#suggester.setOnUnhighlight(this.#clearPreview);
|
||
}
|
||
}
|
||
|
||
#setColorsFromQuery(query) {
|
||
const { color } = this.#parseQuery(query);
|
||
|
||
if (color) {
|
||
Form.#EL_INPUT.style.background = color;
|
||
$.bodyClassAdd(Form.#CL_COLOR);
|
||
} else {
|
||
Form.#EL_INPUT.style.background = '';
|
||
$.bodyClassRemove(Form.#CL_COLOR);
|
||
}
|
||
}
|
||
|
||
#submitForm = (e) => {
|
||
if (e) e.preventDefault();
|
||
const parsedQuery = this.#parseQuery(Form.#EL_FORM.value);
|
||
|
||
if (parsedQuery.length) {
|
||
parsedQuery.forEach((r) => this.#redirect(r.redirect, true));
|
||
} else {
|
||
this.#redirect(parsedQuery.redirect);
|
||
}
|
||
|
||
this.#suggester.onSuccess(parsedQuery);
|
||
this.hide();
|
||
};
|
||
|
||
#submitWithValue = (value) => {
|
||
Form.#EL_FORM.value = value;
|
||
this.#submitForm();
|
||
};
|
||
}
|
||
|
||
class CommandFormatter {
|
||
static #CSS_VAR_COMMAND_COLOR_LIGHTNESS = 'var(--command-color-lightness)';
|
||
|
||
static #CSS_VAR_COMMAND_COLOR_SATURATION =
|
||
'var(--command-color-saturation)';
|
||
|
||
static format(commands) {
|
||
return commands.map((command) => {
|
||
if (command.color || !command.name) return command;
|
||
command.color = CommandFormatter.#huesToGradient(command.hues);
|
||
return command;
|
||
});
|
||
}
|
||
|
||
static #huesToGradient(hues) {
|
||
if (!Array.isArray(hues) || !hues.length) {
|
||
return CommandFormatter.#hsla(0, '0%');
|
||
}
|
||
|
||
if (hues.length === 1) {
|
||
return CommandFormatter.#hsla(hues[0]);
|
||
}
|
||
|
||
const c = hues.reduce((a, h) => `${a}, ${CommandFormatter.#hsla(h)}`, '');
|
||
return `linear-gradient(135deg ${c})`;
|
||
}
|
||
|
||
static #hsla(
|
||
hue,
|
||
saturation = CommandFormatter.#CSS_VAR_COMMAND_COLOR_SATURATION
|
||
) {
|
||
const lightness = CommandFormatter.#CSS_VAR_COMMAND_COLOR_LIGHTNESS;
|
||
return `hsla(${hue}, ${saturation}, ${lightness}, 1)`;
|
||
}
|
||
}
|
||
|
||
(() => {
|
||
$.bodyClassAdd(CONFIG.theme);
|
||
const commands = CommandFormatter.format(CONFIG.commands);
|
||
const help = new Help({ commands, newTab: CONFIG.queryNewTab });
|
||
|
||
const form = new Form({
|
||
helpKey: CONFIG.helpKey,
|
||
instantRedirect: CONFIG.queryInstantRedirect,
|
||
newTab: CONFIG.queryNewTab,
|
||
parseQuery: new QueryParser({
|
||
commands,
|
||
pathDelimiter: CONFIG.queryPathDelimiter,
|
||
scripts: CONFIG.scripts,
|
||
searchDelimiter: CONFIG.querySearchDelimiter,
|
||
}).parse,
|
||
suggester: new Suggester({
|
||
influencers: CONFIG.suggestionInfluencers.map(
|
||
(influencerConfig) =>
|
||
new {
|
||
Default: DefaultInfluencer,
|
||
DuckDuckGo: DuckDuckGoInfluencer,
|
||
History: HistoryInfluencer,
|
||
}[influencerConfig.name]({
|
||
limit: influencerConfig.limit,
|
||
minChars: influencerConfig.minChars,
|
||
suggestionDefaults: CONFIG.suggestionDefaults,
|
||
})
|
||
),
|
||
limit: CONFIG.suggestionLimit,
|
||
}),
|
||
toggleHelp: help.toggle,
|
||
});
|
||
|
||
new Clock({
|
||
amPm: CONFIG.clockShowAmPm,
|
||
delimiter: CONFIG.clockDelimiter,
|
||
onClick: CONFIG.clockOnClickAction === 'Search' ? form.show : help.toggle,
|
||
showSeconds: CONFIG.clockShowSeconds,
|
||
timeZone: CONFIG.clockTimeZone,
|
||
twentyFourHour: CONFIG.clockTwentyFourHour,
|
||
});
|
||
})();
|
||
</script>
|