···
SEARCH_COUNT: "search-count",
HISTORY_ENABLED: "history-enabled",
DEFAULT_BANG: "default-bang",
25
+
CUSTOM_BANGS: "custom-bangs",
···
DOWN: ["(↓°□°)↓", "(´◕‿◕)↓", "↓(´・ω・)↓"],
47
+
const customBangs = JSON.parse(localStorage.getItem("custom-bangs") || "{}");
function getFocusableElements(
root: HTMLElement = document.body,
···
<button class="close-modal">×</button>
<div class="settings-section">
136
-
<label for="default-bang" id="bang-description">${bangs[data.LS_DEFAULT_BANG].s || "Unknown bang"}</label>
139
+
<label for="default-bang" id="bang-description">Default Bang: ${bangs[data.LS_DEFAULT_BANG].s || "Unknown bang"}</label>
<div class="bang-select-container">
<input type="text" id="default-bang" class="bang-select" value="${data.LS_DEFAULT_BANG}">
143
+
<p class="help-text">The best way to add new bangs is by submitting them on <a href="https://duckduckgo.com/newbang" target="_blank">DuckDuckGo</a> but you can also add them below</p>
144
+
<div style="margin-top: 16px;">
145
+
<h4>Add Custom Bang</h4>
146
+
<div class="custom-bang-inputs">
147
+
<input type="text" placeholder="Bang name" id="bang-name" class="bang-name">
148
+
<input type="text" placeholder="Shortcut (e.g. !ddg)" id="bang-shortcut" class="bang-shortcut">
149
+
<input type="text" placeholder="Search URL with {{{s}}}" id="bang-search-url" class="bang-search-url">
150
+
<input type="text" placeholder="Base domain" id="bang-base-url" class="bang-base-url">
151
+
<div style="text-align: right;">
152
+
<button class="add-bang">Add Bang</button>
156
+
Object.keys(customBangs).length > 0
158
+
<h4>Your Custom Bangs</h4>
159
+
<div class="custom-bangs-list">
160
+
${Object.entries(customBangs)
162
+
([shortcut, bang]) => `
163
+
<div class="custom-bang-item">
164
+
<table class="custom-bang-info">
166
+
<td class="custom-bang-name">${bang.t}</td>
167
+
<td class="custom-bang-shortcut"><code>!${shortcut}</code></td>
168
+
<td class="custom-bang-base">${bang.d}</td>
171
+
<div class="custom-bang-url">${bang.u}</div>
172
+
<button class="remove-bang" data-shortcut="${shortcut}">Remove</button>
<div class="settings-section">
<h3>Search History (${data.searchHistory.length}/500)</h3>
···
description: app.querySelector<HTMLParagraphElement>("#bang-description"),
historyToggle: app.querySelector<HTMLInputElement>("#history-toggle"),
clearHistory: app.querySelector<HTMLButtonElement>(".clear-history"),
230
+
bangName: app.querySelector<HTMLInputElement>(".bang-name"),
231
+
bangShortcut: app.querySelector<HTMLInputElement>(".bang-shortcut"),
232
+
bangSearchUrl: app.querySelector<HTMLInputElement>(".bang-search-url"),
233
+
bangBaseUrl: app.querySelector<HTMLInputElement>(".bang-base-url"),
234
+
addBang: app.querySelector<HTMLButtonElement>(".add-bang"),
235
+
removeBangs: app.querySelectorAll<HTMLButtonElement>(".remove-bang"),
// Validate all elements exist
···
audio.spin.playbackRate = 1;
349
+
validatedElements.addBang.addEventListener("click", () => {
350
+
audio.click.currentTime = 0.1;
351
+
audio.click.playbackRate = 2;
352
+
audio.click.play();
355
+
validatedElements.removeBangs.forEach((button) => {
356
+
button.addEventListener("click", () => {
357
+
audio.warning.currentTime = 0;
358
+
audio.warning.play();
validatedElements.copyButton.addEventListener("click", async () => {
···
validatedElements.defaultBangSelect.addEventListener("change", (event) => {
const newDefaultBang = (event.target as HTMLSelectElement).value;
345
-
const bang = bangs[newDefaultBang];
406
+
const bang = customBangs[newDefaultBang] || bangs[newDefaultBang];
validatedElements.defaultBangSelect.value = LS_DEFAULT_BANG;
···
new CustomEvent("bangSuccess"),
storage.set(CONSTANTS.LOCAL_STORAGE_KEYS.DEFAULT_BANG, newDefaultBang);
366
-
validatedElements.description.innerText = bang.s;
427
+
validatedElements.description.innerText = "Default Bang: " + bang.s;
validatedElements.historyToggle.addEventListener("change", (event) => {
···
else window.location.reload();
446
+
validatedElements.addBang.addEventListener("click", () => {
447
+
const name = validatedElements.bangName.value.trim();
448
+
const shortcut = validatedElements.bangShortcut.value.trim();
449
+
const searchUrl = validatedElements.bangSearchUrl.value.trim();
450
+
const baseUrl = validatedElements.bangBaseUrl.value.trim();
452
+
if (!name || !searchUrl || !baseUrl) return;
454
+
customBangs[shortcut] = {
461
+
CONSTANTS.LOCAL_STORAGE_KEYS.CUSTOM_BANGS,
462
+
JSON.stringify(customBangs),
465
+
if (!prefersReducedMotion)
467
+
window.location.reload();
469
+
else window.location.reload();
472
+
validatedElements.removeBangs.forEach((button) => {
473
+
button.addEventListener("click", (event) => {
474
+
const shortcut = (event.target as HTMLButtonElement).dataset.shortcut;
475
+
delete customBangs[shortcut];
477
+
CONSTANTS.LOCAL_STORAGE_KEYS.CUSTOM_BANGS,
478
+
JSON.stringify(customBangs),
481
+
if (!prefersReducedMotion)
483
+
window.location.reload();
485
+
else window.location.reload();
···
storage.set(CONSTANTS.LOCAL_STORAGE_KEYS.SEARCH_COUNT, count);
const match = query.toLowerCase().match(/^!(\S+)|!(\S+)$/i);
418
-
const selectedBang = match ? bangs[match[1] || match[2]] : defaultBang;
522
+
const selectedBang = match
523
+
? customBangs[match[1] || match[2]] || bangs[match[1] || match[2]]
? query.replace(/!\S+\s*|^(\S+!|!\S+)$/i, "").trim()