legacy repo
This commit is contained in:
6
favicon/about.txt
Executable file
6
favicon/about.txt
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
This favicon was generated using the following graphics from Twitter Twemoji:
|
||||||
|
|
||||||
|
- Graphics Title: 1f310.svg
|
||||||
|
- Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji)
|
||||||
|
- Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f310.svg
|
||||||
|
- Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
||||||
BIN
favicon/android-chrome-192x192.png
Executable file
BIN
favicon/android-chrome-192x192.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
favicon/android-chrome-512x512.png
Executable file
BIN
favicon/android-chrome-512x512.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
favicon/apple-touch-icon.png
Executable file
BIN
favicon/apple-touch-icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
favicon/favicon-16x16.png
Executable file
BIN
favicon/favicon-16x16.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 490 B |
BIN
favicon/favicon-32x32.png
Executable file
BIN
favicon/favicon-32x32.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
favicon/favicon.ico
Executable file
BIN
favicon/favicon.ico
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
favicon/site.webmanifest
Executable file
1
favicon/site.webmanifest
Executable file
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
626
index.html
Executable file
626
index.html
Executable file
@@ -0,0 +1,626 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="color-scheme" content="dark" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="./favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="./favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="./favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="./favicon/site.webmanifest">
|
||||||
|
|
||||||
|
<title>Cab's Startpage</title>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const CONFIG = {
|
||||||
|
commands: {
|
||||||
|
a: {
|
||||||
|
name: 'Anilist',
|
||||||
|
searchTemplate: '/search/anime?search={}',
|
||||||
|
url: 'https://anilist.co',
|
||||||
|
},
|
||||||
|
c: {
|
||||||
|
name: 'UCSC Canvas',
|
||||||
|
url: 'https://canvas.ucsc.edu/',
|
||||||
|
},
|
||||||
|
d: {
|
||||||
|
name: 'Drive',
|
||||||
|
searchTemplate: '/drive/u/0/search?q={}',
|
||||||
|
url: 'https://drive.google.com/drive/u/0/my-drive',
|
||||||
|
},
|
||||||
|
e: {
|
||||||
|
name: 'Etsy',
|
||||||
|
searchTemplate: '/search?q={}',
|
||||||
|
url: 'https://etsy.com',
|
||||||
|
},
|
||||||
|
g: {
|
||||||
|
name: 'Gmail',
|
||||||
|
searchTemplate: '/mail/u/0/#search/{}',
|
||||||
|
url: 'https://mail.google.com/mail/u/0/#inbox',
|
||||||
|
},
|
||||||
|
h: {
|
||||||
|
name: 'Kaiser',
|
||||||
|
url: 'https://healthy.kaiserpermanente.org/northern-california/front-door',
|
||||||
|
},
|
||||||
|
k: {
|
||||||
|
name: 'Kemono Party',
|
||||||
|
url: 'https://kemono.party',
|
||||||
|
},
|
||||||
|
l: {
|
||||||
|
name: 'Symbolab',
|
||||||
|
url: 'https://www.symbolab.com',
|
||||||
|
},
|
||||||
|
m: {
|
||||||
|
name: 'Monkeytype',
|
||||||
|
url: 'https://monkeytype.com',
|
||||||
|
},
|
||||||
|
n: {
|
||||||
|
name: 'New Doc',
|
||||||
|
url: 'https://doc.new',
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
name: 'UCSC Portal',
|
||||||
|
url: 'https://my.ucsc.edu/',
|
||||||
|
},
|
||||||
|
r: {
|
||||||
|
name: 'Reddit',
|
||||||
|
searchTemplate: '/search?q={}',
|
||||||
|
url: 'https://www.reddit.com',
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
name: 'Xfinity Max',
|
||||||
|
searchTemplate: '/search?q={}',
|
||||||
|
url: 'https://play.max.com',
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
name: 'YouTube',
|
||||||
|
searchTemplate: '/results?search_query={}',
|
||||||
|
url: 'https://youtube.com/feed/subscriptions',
|
||||||
|
},
|
||||||
|
z: {
|
||||||
|
name: 'ChatGPT',
|
||||||
|
url: 'https://chat.openai.com/chat'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultCommand: {
|
||||||
|
searchTemplate: '/search?q={}',
|
||||||
|
url: 'https://google.com',
|
||||||
|
},
|
||||||
|
openLinksInNewTab: false,
|
||||||
|
pathDelimiter: '/',
|
||||||
|
searchDelimiter: "'",
|
||||||
|
suggestionLimit: 4,
|
||||||
|
suggestions: {
|
||||||
|
a: ['a/user/3cab/', 'a/user/3cab/animelist', 'a/user/3cab/mangalist'],
|
||||||
|
c: ['c/calendar'],
|
||||||
|
d: ['d/drive/u/0/my-drive', 'd/drive/u/1/my-drive', 'd/drive/u/2/my-drive'],
|
||||||
|
g: ['g/mail/u/0/#inbox', 'g/mail/u/1/#inbox', 'g/mail/u/2/#inbox'],
|
||||||
|
k: ['k/patreon/user/47400827', 'k/patreon/user/34232701', 'k/patreon/user/11661205', 'k/patreon/user/2543591'],
|
||||||
|
r: ['r/r/all', 'r/r/progressionfantasy', 'r/r/196'],
|
||||||
|
y: ['y/feed/trending'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--color-background: #111;
|
||||||
|
--color-text-subtle: #999;
|
||||||
|
--color-text: #eee;
|
||||||
|
--font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, Helvetica,
|
||||||
|
Ubuntu, Roboto, Noto, Segoe UI, Arial, sans-serif;
|
||||||
|
--font-size-1: 1rem;
|
||||||
|
--font-size-2: 3rem;
|
||||||
|
--font-size-base: 110%;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--line-height-base: 1.4;
|
||||||
|
--space-1: 1rem;
|
||||||
|
--space-2: 2rem;
|
||||||
|
--space-3: 4rem;
|
||||||
|
--transition-speed: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
line-height: var(--line-height-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template id="commands-template">
|
||||||
|
<style>
|
||||||
|
.commands {
|
||||||
|
display: grid;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command {
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
outline: 0;
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
width: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: var(--color-text-subtle);
|
||||||
|
margin-left: var(--space-2);
|
||||||
|
transition: color var(--transition-speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command:hover .name {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 500px) {
|
||||||
|
.commands {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
.commands {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.commands {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1100px) {
|
||||||
|
.commands {
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<nav>
|
||||||
|
<menu class="commands"></menu>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="command-template">
|
||||||
|
<li>
|
||||||
|
<a class="command" rel="noopener noreferrer">
|
||||||
|
<span class="key"></span>
|
||||||
|
<span class="name"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
class Commands extends HTMLElement {
|
||||||
|
#refs = {
|
||||||
|
commands: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
const template = document.getElementById('commands-template');
|
||||||
|
const clone = template.content.cloneNode(true);
|
||||||
|
this.#refs.commands = clone.querySelector('.commands');
|
||||||
|
this.#renderCommands();
|
||||||
|
this.shadowRoot.append(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderCommands() {
|
||||||
|
const template = document.getElementById('command-template');
|
||||||
|
|
||||||
|
for (const [key, { name, url }] of Object.entries(CONFIG.commands)) {
|
||||||
|
const clone = template.content.cloneNode(true);
|
||||||
|
const command = clone.querySelector('.command');
|
||||||
|
command.href = url;
|
||||||
|
if (CONFIG.openLinksInNewTab) command.target = '_blank';
|
||||||
|
clone.querySelector('.key').innerText = key;
|
||||||
|
clone.querySelector('.name').innerText = name;
|
||||||
|
this.#refs.commands.append(clone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('commands-component', Commands);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template id="search-template">
|
||||||
|
<style>
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
display: block;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--color-background);
|
||||||
|
border: none;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
left: 0;
|
||||||
|
padding: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog[open] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
font-size: var(--font-size-2);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
list-style: none;
|
||||||
|
margin: var(--space-2) 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-1);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
position: relative;
|
||||||
|
transition: color var(--transition-speed);
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion:focus,
|
||||||
|
.suggestion:hover {
|
||||||
|
color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion::before,
|
||||||
|
.suggestion::before {
|
||||||
|
background-color: var(--color-text);
|
||||||
|
bottom: var(--space-1);
|
||||||
|
content: ' ';
|
||||||
|
left: var(--space-2);
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: var(--space-2);
|
||||||
|
top: var(--space-1);
|
||||||
|
transform: translateY(0.5em);
|
||||||
|
transition: all var(--transition-speed);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion:focus::before,
|
||||||
|
.suggestion:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match {
|
||||||
|
color: var(--color-text-subtle);
|
||||||
|
transition: color var(--transition-speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion:focus .match,
|
||||||
|
.suggestion:hover .match {
|
||||||
|
color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
.suggestions {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<dialog class="dialog">
|
||||||
|
<form autocomplete="off" class="form" method="dialog" spellcheck="false">
|
||||||
|
<input class="input" title="search" type="text" />
|
||||||
|
<menu class="suggestions"></menu>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="suggestion-template">
|
||||||
|
<li>
|
||||||
|
<button class="suggestion" type="button"></button>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="match-template">
|
||||||
|
<span class="match"></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
class Search extends HTMLElement {
|
||||||
|
#refs = {
|
||||||
|
dialog: null,
|
||||||
|
form: null,
|
||||||
|
input: null,
|
||||||
|
suggestions: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
const template = document.getElementById('search-template');
|
||||||
|
const clone = template.content.cloneNode(true);
|
||||||
|
this.#refs.dialog = clone.querySelector('.dialog');
|
||||||
|
this.#refs.form = clone.querySelector('.form');
|
||||||
|
this.#refs.input = clone.querySelector('.input');
|
||||||
|
this.#refs.suggestions = clone.querySelector('.suggestions');
|
||||||
|
this.#refs.form.addEventListener('submit', this.#onSubmit, false);
|
||||||
|
this.#refs.input.addEventListener('input', this.#onInput);
|
||||||
|
this.#refs.suggestions.addEventListener('click', this.#onSuggestionClick);
|
||||||
|
document.addEventListener('keydown', this.#onKeydown);
|
||||||
|
this.shadowRoot.append(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #attachSearchPrefix(array, { key, splitBy }) {
|
||||||
|
if (!splitBy) return array;
|
||||||
|
return array.map((search) => `${key}${splitBy}${search}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #escapeRegexCharacters(s) {
|
||||||
|
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
static #fetchDuckDuckGoSuggestions(search) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
window.autocompleteCallback = (res) => {
|
||||||
|
const suggestions = [];
|
||||||
|
|
||||||
|
for (const item of res) {
|
||||||
|
if (item.phrase === search.toLowerCase()) continue;
|
||||||
|
suggestions.push(item.phrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(suggestions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
document.querySelector('head').appendChild(script);
|
||||||
|
script.src = `https://duckduckgo.com/ac/?callback=autocompleteCallback&q=${search}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static #formatSearchUrl(url, searchPath, search) {
|
||||||
|
if (!searchPath) return url;
|
||||||
|
const baseUrl = Search.#stripUrlPath(url);
|
||||||
|
const urlQuery = encodeURIComponent(search);
|
||||||
|
searchPath = searchPath.replace(/{}/g, urlQuery);
|
||||||
|
return baseUrl + searchPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
static #hasProtocol(s) {
|
||||||
|
return /^[a-zA-Z]+:\/\//i.test(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #isUrl(s) {
|
||||||
|
return /^((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)$/i.test(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #parseQuery = (raw) => {
|
||||||
|
const query = raw.trim();
|
||||||
|
|
||||||
|
if (this.#isUrl(query)) {
|
||||||
|
const url = this.#hasProtocol(query) ? query : `https://${query}`;
|
||||||
|
return { query, url };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CONFIG.commands[query]) {
|
||||||
|
const { key, url } = CONFIG.commands[query];
|
||||||
|
return { key, query, url };
|
||||||
|
}
|
||||||
|
|
||||||
|
let splitBy = CONFIG.searchDelimiter;
|
||||||
|
const [searchKey, rawSearch] = query.split(new RegExp(`${splitBy}(.*)`));
|
||||||
|
|
||||||
|
if (CONFIG.commands[searchKey]) {
|
||||||
|
const { searchTemplate, url: base } = CONFIG.commands[searchKey];
|
||||||
|
const search = rawSearch.trim();
|
||||||
|
const url = Search.#formatSearchUrl(base, searchTemplate, search);
|
||||||
|
return { key: searchKey, query, search, splitBy, url };
|
||||||
|
}
|
||||||
|
|
||||||
|
splitBy = CONFIG.pathDelimiter;
|
||||||
|
const [pathKey, path] = query.split(new RegExp(`${splitBy}(.*)`));
|
||||||
|
|
||||||
|
if (CONFIG.commands[pathKey]) {
|
||||||
|
const { url: base } = CONFIG.commands[pathKey];
|
||||||
|
const url = `${Search.#stripUrlPath(base)}/${path}`;
|
||||||
|
return { key: pathKey, path, query, splitBy, url };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchTemplate, url: base } = CONFIG.defaultCommand;
|
||||||
|
const url = Search.#formatSearchUrl(base, searchTemplate, query);
|
||||||
|
return { query, search: query, url };
|
||||||
|
};
|
||||||
|
|
||||||
|
static #stripUrlPath(url) {
|
||||||
|
const parser = document.createElement('a');
|
||||||
|
parser.href = url;
|
||||||
|
return `${parser.protocol}//${parser.hostname}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#close() {
|
||||||
|
this.#refs.input.value = '';
|
||||||
|
this.#refs.input.blur();
|
||||||
|
this.#refs.dialog.close();
|
||||||
|
this.#refs.suggestions.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
#execute(query) {
|
||||||
|
const { url } = Search.#parseQuery(query);
|
||||||
|
const target = CONFIG.openLinksInNewTab ? '_blank' : '_self';
|
||||||
|
window.open(url, target, 'noopener noreferrer');
|
||||||
|
this.#close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#focusNextSuggestion(previous = false) {
|
||||||
|
const active = this.shadowRoot.activeElement;
|
||||||
|
let nextIndex;
|
||||||
|
|
||||||
|
if (active.dataset.index) {
|
||||||
|
const activeIndex = Number(active.dataset.index);
|
||||||
|
nextIndex = previous ? activeIndex - 1 : activeIndex + 1;
|
||||||
|
} else {
|
||||||
|
nextIndex = previous ? this.#refs.suggestions.childElementCount - 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = this.#refs.suggestions.children[nextIndex];
|
||||||
|
if (next) next.querySelector('.suggestion').focus();
|
||||||
|
else this.#refs.input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
#onInput = async () => {
|
||||||
|
const q = Search.#parseQuery(this.#refs.input.value);
|
||||||
|
|
||||||
|
if (!q.query) {
|
||||||
|
this.#close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let suggestions = CONFIG.suggestions[q.query] ?? [];
|
||||||
|
|
||||||
|
if (q.search && suggestions.length < CONFIG.suggestionLimit) {
|
||||||
|
const res = await Search.#fetchDuckDuckGoSuggestions(q.search);
|
||||||
|
const formatted = Search.#attachSearchPrefix(res, q);
|
||||||
|
suggestions = suggestions.concat(formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#renderSuggestions(suggestions, q.query);
|
||||||
|
};
|
||||||
|
|
||||||
|
#onKeydown = (e) => {
|
||||||
|
if (!this.#refs.dialog.open) {
|
||||||
|
this.#refs.dialog.show();
|
||||||
|
this.#refs.input.focus();
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// close the search dialog before the next repaint if a character is
|
||||||
|
// not produced (e.g. if you type shift, control, alt etc.)
|
||||||
|
if (!this.#refs.input.value) this.#close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.#close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alt = e.altKey ? 'alt-' : '';
|
||||||
|
const ctrl = e.ctrlKey ? 'ctrl-' : '';
|
||||||
|
const meta = e.metaKey ? 'meta-' : '';
|
||||||
|
const shift = e.shiftKey ? 'shift-' : '';
|
||||||
|
const modifierPrefixedKey = `${alt}${ctrl}${meta}${shift}${e.key}`;
|
||||||
|
|
||||||
|
if (/^(ArrowDown|Tab|ctrl-n)$/.test(modifierPrefixedKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.#focusNextSuggestion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^(ArrowUp|ctrl-p|shift-Tab)$/.test(modifierPrefixedKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.#focusNextSuggestion(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#onSubmit = () => {
|
||||||
|
this.#execute(this.#refs.input.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
#onSuggestionClick = (e) => {
|
||||||
|
const ref = e.target.closest('.suggestion');
|
||||||
|
if (!ref) return;
|
||||||
|
this.#execute(ref.dataset.suggestion);
|
||||||
|
};
|
||||||
|
|
||||||
|
#renderSuggestions(suggestions, query) {
|
||||||
|
this.#refs.suggestions.innerHTML = '';
|
||||||
|
const sliced = suggestions.slice(0, CONFIG.suggestionLimit);
|
||||||
|
const template = document.getElementById('suggestion-template');
|
||||||
|
|
||||||
|
for (const [index, suggestion] of sliced.entries()) {
|
||||||
|
const clone = template.content.cloneNode(true);
|
||||||
|
const ref = clone.querySelector('.suggestion');
|
||||||
|
ref.dataset.index = index;
|
||||||
|
ref.dataset.suggestion = suggestion;
|
||||||
|
const escapedQuery = Search.#escapeRegexCharacters(query);
|
||||||
|
const matched = suggestion.match(new RegExp(escapedQuery, 'i'));
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
const template = document.getElementById('match-template');
|
||||||
|
const clone = template.content.cloneNode(true);
|
||||||
|
const matchRef = clone.querySelector('.match');
|
||||||
|
const pre = suggestion.slice(0, matched.index);
|
||||||
|
const post = suggestion.slice(matched.index + matched[0].length);
|
||||||
|
matchRef.innerText = matched[0];
|
||||||
|
matchRef.insertAdjacentHTML('beforebegin', pre);
|
||||||
|
matchRef.insertAdjacentHTML('afterend', post);
|
||||||
|
ref.append(clone);
|
||||||
|
} else {
|
||||||
|
ref.innerText = suggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#refs.suggestions.append(clone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('search-component', Search);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
commands-component {
|
||||||
|
margin: auto;
|
||||||
|
padding: var(--space-3) 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<commands-component></commands-component>
|
||||||
|
<search-component></search-component>
|
||||||
|
</main>
|
||||||
Reference in New Issue
Block a user