A fast, local-first "redirection engine" for !bang users with a few extra features ^-^
1import { bangs } from "./bang"; 2 3function getFocusableElements( 4 root: HTMLElement = document.body, 5): HTMLElement[] { 6 return Array.from( 7 root.querySelectorAll<HTMLElement>( 8 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])', 9 ), 10 ); 11} 12 13function setOutsideElementsTabindex(modal: HTMLElement, tabindex: number) { 14 const modalElements = getFocusableElements(modal); 15 const allElements = getFocusableElements(); 16 17 for (const element of allElements) { 18 if (!modalElements.includes(element)) { 19 element.setAttribute("tabindex", tabindex.toString()); 20 } 21 } 22} 23 24function noSearchDefaultPageRender() { 25 const app = document.querySelector<HTMLDivElement>("#app"); 26 if (!app) throw new Error("App element not found"); 27 app.innerHTML = ` 28 <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh;"> 29 <header style="position: absolute; top: 1rem; right: 1rem;"> 30 <button class="settings-button"> 31 <img src="/gear.svg" alt="Settings" class="settings" /> 32 </button> 33 </header> 34 <div class="content-container"> 35 <h1>┐( ˘_˘ )┌</h1> 36 <p>DuckDuckGo's bang redirects are too slow. Add the following URL as a custom search engine to your browser. Enables <a href="https://duckduckgo.com/bang.html" target="_blank">all of DuckDuckGo's bangs.</a></p> 37 <div class="url-container"> 38 <input 39 type="text" 40 class="url-input" 41 value="https://unduck.link?q=%s" 42 readonly 43 /> 44 <button class="copy-button"> 45 <img src="/clipboard.svg" alt="Copy" /> 46 </button> 47 </div> 48 </div> 49 <footer class="footer"> 50 made with ♥ by <a href="https://github.com/taciturnaxolotl" target="_blank">Kieran Klukas</a> as <a href="https://github.com/taciturnaxolotl/unduck" target="_blank">open source</a> software 51 </footer> 52 <div class="modal" id="settings-modal"> 53 <div class="modal-content"> 54 <button class="close-modal">&times;</button> 55 <h2>Settings</h2> 56 <div> 57 <label for="default-bang" id="bang-description">${bangs.find((b) => b.t === LS_DEFAULT_BANG)?.s || "Unknown bang"}</label> 58 <div class="bang-select-container"> 59 <input type="text" id="default-bang" class="bang-select" value="${LS_DEFAULT_BANG}"> 60 </div> 61 </div> 62 </div> 63 </div> 64 </div> 65 </div> 66 `; 67 68 const copyButton = app.querySelector<HTMLButtonElement>(".copy-button"); 69 if (!copyButton) throw new Error("Copy button not found"); 70 const copyIcon = copyButton.querySelector("img"); 71 if (!copyIcon) throw new Error("Copy icon not found"); 72 const urlInput = app.querySelector<HTMLInputElement>(".url-input"); 73 if (!urlInput) throw new Error("URL input not found"); 74 const settingsButton = 75 app.querySelector<HTMLButtonElement>(".settings-button"); 76 if (!settingsButton) throw new Error("Settings button not found"); 77 const modal = app.querySelector<HTMLDivElement>("#settings-modal"); 78 if (!modal) throw new Error("Modal not found"); 79 const closeModal = app.querySelector<HTMLSpanElement>(".close-modal"); 80 if (!closeModal) throw new Error("Close modal not found"); 81 const defaultBangSelect = 82 app.querySelector<HTMLSelectElement>("#default-bang"); 83 if (!defaultBangSelect) throw new Error("Default bang select not found"); 84 const description = 85 app.querySelector<HTMLParagraphElement>("#bang-description"); 86 if (!description) throw new Error("Bang description not found"); 87 88 urlInput.value = `${window.location.protocol}//${window.location.host}?q=%s`; 89 90 copyButton.addEventListener("click", async () => { 91 await navigator.clipboard.writeText(urlInput.value); 92 copyIcon.src = "/clipboard-check.svg"; 93 94 setTimeout(() => { 95 copyIcon.src = "/clipboard.svg"; 96 }, 2000); 97 }); 98 99 const prefersReducedMotion = window.matchMedia( 100 "(prefers-reduced-motion: reduce)", 101 ).matches; 102 if (!prefersReducedMotion) { 103 const audio = new Audio("/heavier-tick-sprite.mp3"); 104 105 settingsButton.addEventListener("mouseenter", () => { 106 audio.play(); 107 }); 108 109 settingsButton.addEventListener("mouseleave", () => { 110 audio.pause(); 111 audio.currentTime = 0; 112 }); 113 } 114 115 settingsButton.addEventListener("click", () => { 116 modal.style.display = "block"; 117 setOutsideElementsTabindex(modal, -1); 118 }); 119 120 closeModal.addEventListener("click", () => { 121 modal.style.display = "none"; 122 setOutsideElementsTabindex(modal, 0); 123 }); 124 125 window.addEventListener("click", (event) => { 126 if (event.target === modal) { 127 modal.style.display = "none"; 128 setOutsideElementsTabindex(modal, 0); 129 } 130 }); 131 132 // Save default bang 133 defaultBangSelect.addEventListener("change", (event) => { 134 const newDefaultBang = (event.target as HTMLSelectElement).value; 135 const bang = bangs.find((b) => b.t === newDefaultBang); 136 137 if (!bang) { 138 // Invalid bang entered 139 defaultBangSelect.value = LS_DEFAULT_BANG; // Reset to previous value 140 defaultBangSelect.classList.add("shake", "flash-red"); 141 142 // Remove animation classes after animation completes 143 setTimeout(() => { 144 defaultBangSelect.classList.remove("shake", "flash-red"); 145 }, 300); 146 147 return; 148 } 149 150 localStorage.setItem("default-bang", newDefaultBang); 151 description.innerText = bang.s; 152 }); 153} 154 155const LS_DEFAULT_BANG = localStorage.getItem("default-bang") ?? "ddg"; 156const defaultBang = bangs.find((b) => b.t === LS_DEFAULT_BANG); 157 158function getBangredirectUrl() { 159 const url = new URL(window.location.href); 160 const query = url.searchParams.get("q")?.trim() ?? ""; 161 if (!query) { 162 noSearchDefaultPageRender(); 163 return null; 164 } 165 166 const match = query.match(/!(\S+)/i); 167 const selectedBang = match 168 ? bangs.find((b) => b.t === match[1].toLowerCase()) 169 : defaultBang; 170 const cleanQuery = match ? query.replace(/!\S+\s*/i, "").trim() : query; 171 172 return selectedBang?.u.replace( 173 "{{{s}}}", 174 encodeURIComponent(cleanQuery).replace(/%2F/g, "/"), 175 ); 176} 177 178function doRedirect() { 179 const searchUrl = getBangredirectUrl(); 180 if (!searchUrl) return; 181 window.location.replace(searchUrl); 182} 183 184doRedirect();