From e57f09052b4d8a84df6c94186d454d2a3426e58b Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Fri, 22 Mar 2024 00:17:00 -0400 Subject: [PATCH 1/6] Implemented `keybind` module to handle keyboard shortcuts --- frontend/src/lib/keybind.ts | 246 ++++++++++++++++++ frontend/src/lib/stores.ts | 3 + frontend/src/routes/search/+page.svelte | 30 ++- frontend/src/routes/search/Result.svelte | 4 +- frontend/src/routes/settings/+page.svelte | 8 + .../settings/UseKeyboardShortcuts.svelte | 6 + 6 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 frontend/src/lib/keybind.ts create mode 100644 frontend/src/routes/settings/UseKeyboardShortcuts.svelte diff --git a/frontend/src/lib/keybind.ts b/frontend/src/lib/keybind.ts new file mode 100644 index 00000000..9c5fb0f9 --- /dev/null +++ b/frontend/src/lib/keybind.ts @@ -0,0 +1,246 @@ +/** + * Enum to represent the key strings provided by {@link KeyboardEvent} + */ +export enum Keys { + J = 'j', + D = 'd', + K = 'k', + H = 'h', + V = 'v', + T = 't', + M = 'm', + L = 'l', + O = 'o', + S = 's', + SINGLE_QUOTE = "'", + FORWARD_SLASH = '/', + CTRL = 'Control', + ENTER = 'Enter', + ARROW_UP = 'ArrowUp', + ARROW_DOWN = 'ArrowDown', +} + +// The type used to declare the key downs that will trigger the callback +export interface KeybindCallback { + key: Keys; + callback: (e: KeyboardEvent) => void; + shift?: boolean; + ctrl?: boolean; + alt?: boolean; +} + +/** + * Used to create and interface with key bindings + */ +export class Keybind { + // All of the keybindings to their callbacks + private bindings: KeybindCallback[]; + private bindingEntries: Keys[]; + + /** + * Constructor + * + * @param bindings An array of {@link KeybindCallback}s to be evaluated and utilized with {@link Keybind:onKeyDown}. + */ + constructor(bindings: KeybindCallback[]) { + this.bindings = bindings; + this.bindingEntries = this.bindings.map((x) => x.key); + } + + /** + * Attempt to convert a string to {@link Keys} enum + * + * @param str - The string to attempt to convert + * @returns undefined if unable to convert, otherwise {@link Keys} + */ + private keyEnumFromString(str: string): Keys | undefined { + for (const [_, key] of Object.entries(Keys)) { + if (key == str) return key; + } + + return undefined; + } + + /** + * Handler for `keydown` events + * + * @remarks + * Requires a wrapper function to pass in `useKeyboardShortcuts` boolean store. + * + * Will run the given callback if all requirements are met from an item in the + * previously given {@link KeybindCallback} array. + * + * @param e - The `keydown` event of `KeyboardEvent` type + * @param useKeyboardShortcuts - A boolean of the user's `useKeyboardShortcuts` preference + */ + onKeyDown(e: KeyboardEvent, useKeyboardShortcuts: boolean) { + if (!useKeyboardShortcuts || e.repeat) return; + + const enum_key = this.keyEnumFromString(e.key); + + if (!(enum_key && this.bindingEntries.includes(enum_key))) return; + + // Conditionals to be able to later compare to a potentially undefined binding.shift/.al/.ctrl + const shift = e.shiftKey ? true : undefined; + const ctrl = e.ctrlKey ? true : undefined; + const alt = e.altKey ? true : undefined; + + const binding = this.bindings.find((binding) => binding.key === enum_key); + if (binding && binding.alt == alt && binding.shift == shift && binding.ctrl == ctrl) { + e.preventDefault(); + binding.callback(e); + } + } +} + +/* +Search callbacks +*/ + +/** + * Used to indicate what direction {@link navigateResults} should go (up/down) + */ +type Direction = 1 | -1; + +/** + * Utilized to bring the next/previous result into focus + * + * @remarks + * Utilizes the element that is currently in focus and the given + * direction to determine where and how to traverse the array of + * results. Moves a single result per call. + * + * @param direction - How to maneuver through the result list (up/down), indicated by {@link Direction} + */ +const navigateResults = (direction: Direction) => { + const currentActive = document.activeElement; + + const results = [...document.getElementsByClassName('result')] as HTMLElement[]; + let currentResultIndex: number | null = null; + let newIndex = 0; + + results.forEach((el, index) => { + if (el.contains(currentActive)) currentResultIndex = index; + }); + + if (currentResultIndex != null) { + newIndex = currentResultIndex + direction; + if (0 > newIndex || newIndex >= results.length) { + newIndex = currentResultIndex; + } + } + + const nextResult = results[newIndex]; + + // Get the anchor element and focus it + (nextResult.getElementsByClassName('result-main-link')[0] as HTMLElement)?.focus(); +}; + +/** + * Wrapper for {@link navigateResults} that will focus on the next result + */ +const focusNextResult = () => navigateResults(1); + +/** + * Wrapper for {@link navigateResults} that will focus on the previous result + */ +const focusPrevResult = () => navigateResults(-1); + +/** + * Focus the first result (if any) + */ +const focusMainResult = () => { + const results = document.getElementsByClassName('result-main-link'); + if (results.length > 0) { + (results[0] as HTMLElement).focus(); + } +}; + +/** + * Focus the search bar + */ +const focusSearchBar = () => { + (document.getElementById('searchbar') as HTMLInputElement)?.select(); +}; + +/** + * Scroll to the top of the window and reset focus + */ +const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); // reset focus + } +}; + +/** + * Redirect to the currently focused result + * + * @param e - The triggering {@link KeyboardEvent} + */ +const openResult = (e: KeyboardEvent) => { + if ((e.target as HTMLElement).className.includes('result-main-link')) { + const resultLink = e.target as HTMLAnchorElement; + window.location.href = resultLink.href; + } else { + const results = document.getElementsByClassName('result-main-link'); + if (results.length > 0) { + window.location.href = (results[0] as HTMLAnchorElement).href; + } + } +}; + +/** + * Open the currently focused result in a new tab + * + * @remarks + * Requires pop-ups to be allowed for the window + * + * @param e - The triggering {@link KeyboardEvent} + */ +const openResultInNewTab = (e: KeyboardEvent) => { + // Ensure target event is a result url + if ((e.target as HTMLElement).className.includes('result-main-link')) { + const resultLink = e.target as HTMLAnchorElement; + window.open(resultLink.href, '_blank', 'noopener'); + } +}; + +/** + * Do a domain search using the domain of the currently focused result + * + * @param e - The triggering {@link KeyboardEvent} + */ +const domainSearch = (e: KeyboardEvent) => { + // Ensure target event is a result url + if ((e.target as HTMLElement).className.includes('result-main-link')) { + const searchBar = document.getElementById('searchbar') as HTMLInputElement; + if (searchBar) { + // Parse the result url, pull out the hostname then resubmit search + const resultLink = new URL((e.target as HTMLAnchorElement)?.href); + searchBar.value += ` site:${resultLink.hostname}`; + searchBar.form?.submit(); + } + } +}; + +/** + * Redirect to the `Did you mean:` link (if exists) + */ +const goToMisspellLink = () => { + const msLink = document.getElementById('misspell-link'); + if (msLink) window.location.href = (msLink as HTMLAnchorElement).href; +}; + +// Packaged callbacks to keep imports clean +export const searchCb = { + focusNextResult, + focusPrevResult, + focusMainResult, + focusSearchBar, + scrollToTop, + openResult, + openResultInNewTab, + domainSearch, + goToMisspellLink, +}; diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index 07b4a3fa..7246703c 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -76,6 +76,9 @@ export const markPagesWithAdsStore = writableLocalStorage(MARK_PAGES_WI const RESULTS_IN_NEW_TAB_KEY = 'resultsInNewTab'; export const resultsInNewTab = writableLocalStorage(RESULTS_IN_NEW_TAB_KEY, false); +const USE_KEYBOARD_SHORTCUTS = 'useKeyboardShortcuts'; +export const useKeyboardShortcuts = writableLocalStorage(USE_KEYBOARD_SHORTCUTS, true); + const MARK_PAGES_WITH_PAYWALL_KEY = 'markPagesWithPaywall'; export const markPagesWithPaywallStore = writableLocalStorage( MARK_PAGES_WITH_PAYWALL_KEY, diff --git a/frontend/src/routes/search/+page.svelte b/frontend/src/routes/search/+page.svelte index bd7e9a8f..64f05971 100644 --- a/frontend/src/routes/search/+page.svelte +++ b/frontend/src/routes/search/+page.svelte @@ -7,7 +7,8 @@ import RegionSelect from '$lib/components/RegionSelect.svelte'; import type { DisplayedWebpage } from '$lib/api'; import { onMount } from 'svelte'; - import { searchQueryStore } from '$lib/stores'; + import { searchQueryStore, useKeyboardShortcuts } from '$lib/stores'; + import { Keys, Keybind, searchCb } from '$lib/keybind'; import { flip } from 'svelte/animate'; import Result from './Result.svelte'; import Sidebar from './Sidebar.svelte'; @@ -24,6 +25,30 @@ let modal: { top: number; left: number; site: DisplayedWebpage } | undefined; + const keybind = new Keybind([ + { key: Keys.J, callback: searchCb.focusNextResult }, + { key: Keys.ARROW_DOWN, callback: searchCb.focusNextResult }, + { key: Keys.K, callback: searchCb.focusPrevResult }, + { key: Keys.ARROW_UP, callback: searchCb.focusPrevResult }, + { key: Keys.H, callback: searchCb.focusSearchBar }, + { key: Keys.FORWARD_SLASH, callback: searchCb.focusSearchBar }, + { key: Keys.V, callback: searchCb.openResultInNewTab }, + { key: Keys.SINGLE_QUOTE, callback: searchCb.openResultInNewTab }, + { key: Keys.T, callback: searchCb.scrollToTop }, + { key: Keys.D, callback: searchCb.domainSearch }, + { key: Keys.L, callback: searchCb.openResult }, + { key: Keys.O, callback: searchCb.openResult }, + { key: Keys.M, callback: searchCb.focusMainResult }, + { key: Keys.S, callback: searchCb.goToMisspellLink }, + ]); + + const onKeyDown = (event: KeyboardEvent) => { + // Only call onKeyDown if the target is not an input element (ie. search bar) + if (!((event.target as HTMLElement).nodeName === 'INPUT')) { + keybind.onKeyDown(event, $useKeyboardShortcuts); + } + }; + onMount(() => { const listener = () => { modal = void 0; @@ -67,6 +92,8 @@ } + + {#if modal} {/if} @@ -110,6 +137,7 @@
Did you mean:{' '} {#each results.spellCorrection.highlighted as frag} diff --git a/frontend/src/routes/search/Result.svelte b/frontend/src/routes/search/Result.svelte index baf9d219..04eebc5f 100644 --- a/frontend/src/routes/search/Result.svelte +++ b/frontend/src/routes/search/Result.svelte @@ -24,7 +24,7 @@ const dispatch = createEventDispatcher<{ modal: HTMLButtonElement }>(); -
+
@@ -37,7 +37,7 @@
@@ -65,6 +71,8 @@ {:else if setting.type == 'results-in-new-tabs'} + {:else if setting.type == 'use-keeb-shortcuts'} + {/if}
diff --git a/frontend/src/routes/settings/UseKeyboardShortcuts.svelte b/frontend/src/routes/settings/UseKeyboardShortcuts.svelte new file mode 100644 index 00000000..3030a5fa --- /dev/null +++ b/frontend/src/routes/settings/UseKeyboardShortcuts.svelte @@ -0,0 +1,6 @@ + + + From dae5d01a313e87331bb7dbabcf71e3326ecd54b5 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Thu, 28 Mar 2024 23:24:29 -0400 Subject: [PATCH 2/6] Removal of direct DOM querying and the addition of searchbar keybindings --- frontend/src/lib/components/Searchbar.svelte | 50 +++-- frontend/src/lib/keybind.ts | 193 +++++++++--------- frontend/src/routes/search/+page.svelte | 74 ++++--- frontend/src/routes/search/Result.svelte | 11 +- frontend/src/routes/search/ResultLink.svelte | 20 ++ frontend/src/routes/search/Serp.svelte | 25 +-- .../src/routes/search/SpellCorrection.svelte | 28 +++ 7 files changed, 242 insertions(+), 159 deletions(-) create mode 100644 frontend/src/routes/search/SpellCorrection.svelte diff --git a/frontend/src/lib/components/Searchbar.svelte b/frontend/src/lib/components/Searchbar.svelte index 422d8a44..ce7d880d 100644 --- a/frontend/src/lib/components/Searchbar.svelte +++ b/frontend/src/lib/components/Searchbar.svelte @@ -2,12 +2,18 @@ import MagnifyingGlass from '~icons/heroicons/magnifying-glass'; import Button from '$lib/components/Button.svelte'; import { api, type HighlightedFragment } from '$lib/api'; - import { safeSearchStore, hostRankingsStore, postSearchStore } from '$lib/stores'; + import { + safeSearchStore, + hostRankingsStore, + postSearchStore, + useKeyboardShortcuts, + } from '$lib/stores'; import { browser } from '$app/environment'; import { derived } from 'svelte/store'; import { compressRanked, rankingsToRanked } from '$lib/rankings'; import { twJoin } from 'tailwind-merge'; import { P, match } from 'ts-pattern'; + import { Keybind } from '$lib/keybind'; export let autofocus = false; @@ -57,26 +63,34 @@ didChangeInput = false; }; - const onKeydown = (ev: KeyboardEvent) => { - match(ev.key) - .with('ArrowUp', () => { - ev.preventDefault(); - moveSelection(-1); - }) - .with('ArrowDown', () => { - ev.preventDefault(); - moveSelection(1); - }) - .with('Enter', () => { - hasFocus = false; - }) - .otherwise(() => { - didChangeInput = true; - }); + let keybind = new Keybind([ + { key: 'ArrowDown', callback: () => moveSelection(1) }, + { key: 'ArrowUp', callback: () => moveSelection(-1) }, + { key: 'Enter', callback: () => (hasFocus = true) }, + ]); + + const onKeydown = (e: KeyboardEvent) => { + if (keybind.bindings.find((binding) => binding.key === e.key)) { + keybind.onKeyDown(e, $useKeyboardShortcuts); + } else { + didChangeInput = true; + } }; let suggestionsDiv: HTMLDivElement | undefined; let hasFocus = autofocus; + + let formElem: HTMLFormElement; + let inputElem: HTMLInputElement; + export const getForm = () => formElem; + export const select = () => inputElem.select(); + export const userQuery = () => lastRealQuery; + export const search = (q: string) => { + if (formElem && inputElem) { + inputElem.value = q; + formElem.submit(); + } + };
@@ -124,6 +139,7 @@ }} bind:value={query} on:keydown={onKeydown} + bind:this={inputElem} />
diff --git a/frontend/src/lib/keybind.ts b/frontend/src/lib/keybind.ts index 9c5fb0f9..16a47099 100644 --- a/frontend/src/lib/keybind.ts +++ b/frontend/src/lib/keybind.ts @@ -1,29 +1,33 @@ -/** - * Enum to represent the key strings provided by {@link KeyboardEvent} - */ -export enum Keys { - J = 'j', - D = 'd', - K = 'k', - H = 'h', - V = 'v', - T = 't', - M = 'm', - L = 'l', - O = 'o', - S = 's', - SINGLE_QUOTE = "'", - FORWARD_SLASH = '/', - CTRL = 'Control', - ENTER = 'Enter', - ARROW_UP = 'ArrowUp', - ARROW_DOWN = 'ArrowDown', -} +import { match } from 'ts-pattern'; +import Result from '../routes/search/Result.svelte'; +import type Searchbar from './components/Searchbar.svelte'; +import type SpellCorrection from '../routes/search/SpellCorrection.svelte'; + +export const Keys = [ + 'j', + 'd', + 'k', + 'h', + 'v', + 't', + 'm', + 'l', + 'o', + 's', + "'", + '/', + 'Control', + 'Enter', + 'ArrowUp', + 'ArrowDown', +]; +export type Key = (typeof Keys)[number]; // The type used to declare the key downs that will trigger the callback export interface KeybindCallback { - key: Keys; - callback: (e: KeyboardEvent) => void; + key: Key; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (e: KeyboardEvent, context: any) => void; shift?: boolean; ctrl?: boolean; alt?: boolean; @@ -32,10 +36,10 @@ export interface KeybindCallback { /** * Used to create and interface with key bindings */ -export class Keybind { +export class Keybind { // All of the keybindings to their callbacks - private bindings: KeybindCallback[]; - private bindingEntries: Keys[]; + public bindings: KeybindCallback[]; + private bindingEntries: Key[]; /** * Constructor @@ -53,8 +57,8 @@ export class Keybind { * @param str - The string to attempt to convert * @returns undefined if unable to convert, otherwise {@link Keys} */ - private keyEnumFromString(str: string): Keys | undefined { - for (const [_, key] of Object.entries(Keys)) { + private keyEnumFromString(str: string): Key | undefined { + for (const key of Keys) { if (key == str) return key; } @@ -73,7 +77,7 @@ export class Keybind { * @param e - The `keydown` event of `KeyboardEvent` type * @param useKeyboardShortcuts - A boolean of the user's `useKeyboardShortcuts` preference */ - onKeyDown(e: KeyboardEvent, useKeyboardShortcuts: boolean) { + onKeyDown(e: KeyboardEvent, useKeyboardShortcuts: boolean, context?: T) { if (!useKeyboardShortcuts || e.repeat) return; const enum_key = this.keyEnumFromString(e.key); @@ -88,7 +92,7 @@ export class Keybind { const binding = this.bindings.find((binding) => binding.key === enum_key); if (binding && binding.alt == alt && binding.shift == shift && binding.ctrl == ctrl) { e.preventDefault(); - binding.callback(e); + binding.callback(e, context); } } } @@ -97,10 +101,13 @@ export class Keybind { Search callbacks */ -/** - * Used to indicate what direction {@link navigateResults} should go (up/down) - */ -type Direction = 1 | -1; +export type Refs = { + results: Result[]; + searchbar: Searchbar; + spellCorrection?: SpellCorrection; +}; + +type Direction = 'up' | 'down'; /** * Utilized to bring the next/previous result into focus @@ -112,55 +119,39 @@ type Direction = 1 | -1; * * @param direction - How to maneuver through the result list (up/down), indicated by {@link Direction} */ -const navigateResults = (direction: Direction) => { +const navigateResults = (direction: Direction, context: Refs) => { const currentActive = document.activeElement; + const results = context.results; - const results = [...document.getElementsByClassName('result')] as HTMLElement[]; - let currentResultIndex: number | null = null; let newIndex = 0; + const currentResultIndex = results.findIndex((el) => el.getMainDiv().contains(currentActive)); - results.forEach((el, index) => { - if (el.contains(currentActive)) currentResultIndex = index; - }); + if (currentResultIndex > -1) { + newIndex = + currentResultIndex + + match(direction) + .with('up', () => -1) + .with('down', () => 1) + .exhaustive(); - if (currentResultIndex != null) { - newIndex = currentResultIndex + direction; - if (0 > newIndex || newIndex >= results.length) { + if (newIndex < 0 || newIndex >= results.length) { newIndex = currentResultIndex; } } - const nextResult = results[newIndex]; - - // Get the anchor element and focus it - (nextResult.getElementsByClassName('result-main-link')[0] as HTMLElement)?.focus(); + results[newIndex].getMainResultLink().focus(); }; -/** - * Wrapper for {@link navigateResults} that will focus on the next result - */ -const focusNextResult = () => navigateResults(1); +const focusNextResult = (_e: KeyboardEvent, context: Refs) => navigateResults('down', context); -/** - * Wrapper for {@link navigateResults} that will focus on the previous result - */ -const focusPrevResult = () => navigateResults(-1); +const focusPrevResult = (_e: KeyboardEvent, context: Refs) => navigateResults('up', context); -/** - * Focus the first result (if any) - */ -const focusMainResult = () => { - const results = document.getElementsByClassName('result-main-link'); - if (results.length > 0) { - (results[0] as HTMLElement).focus(); - } +const focusMainResult = (_e: KeyboardEvent, context: Refs) => { + if (context.results[0]) context.results[0].getMainResultLink().focus(); }; -/** - * Focus the search bar - */ -const focusSearchBar = () => { - (document.getElementById('searchbar') as HTMLInputElement)?.select(); +const selectSearchBar = (_e: KeyboardEvent, context: Refs) => { + context.searchbar.select(); }; /** @@ -176,17 +167,15 @@ const scrollToTop = () => { /** * Redirect to the currently focused result * - * @param e - The triggering {@link KeyboardEvent} + * @param _e - The triggering {@link KeyboardEvent} */ -const openResult = (e: KeyboardEvent) => { - if ((e.target as HTMLElement).className.includes('result-main-link')) { - const resultLink = e.target as HTMLAnchorElement; - window.location.href = resultLink.href; - } else { - const results = document.getElementsByClassName('result-main-link'); - if (results.length > 0) { - window.location.href = (results[0] as HTMLAnchorElement).href; - } +const openResult = (e: KeyboardEvent, context: Refs) => { + if (e.target && e.target instanceof HTMLElement) { + const activeElement = e.target as HTMLElement; + const currentResultIndex = context.results.findIndex((el) => + el.getMainDiv().contains(activeElement), + ); + if (currentResultIndex > -1) context.results[currentResultIndex].getMainResultLink().open(); } }; @@ -198,11 +187,14 @@ const openResult = (e: KeyboardEvent) => { * * @param e - The triggering {@link KeyboardEvent} */ -const openResultInNewTab = (e: KeyboardEvent) => { - // Ensure target event is a result url - if ((e.target as HTMLElement).className.includes('result-main-link')) { - const resultLink = e.target as HTMLAnchorElement; - window.open(resultLink.href, '_blank', 'noopener'); +const openResultInNewTab = (e: KeyboardEvent, context: Refs) => { + if (e.target && e.target instanceof HTMLElement) { + const activeElement = e.target as HTMLElement; + const currentResultIndex = context.results.findIndex((el) => + el.getMainDiv().contains(activeElement), + ); + if (currentResultIndex > -1) + context.results[currentResultIndex].getMainResultLink().openInNewTab(); } }; @@ -211,25 +203,26 @@ const openResultInNewTab = (e: KeyboardEvent) => { * * @param e - The triggering {@link KeyboardEvent} */ -const domainSearch = (e: KeyboardEvent) => { - // Ensure target event is a result url - if ((e.target as HTMLElement).className.includes('result-main-link')) { - const searchBar = document.getElementById('searchbar') as HTMLInputElement; - if (searchBar) { - // Parse the result url, pull out the hostname then resubmit search - const resultLink = new URL((e.target as HTMLAnchorElement)?.href); - searchBar.value += ` site:${resultLink.hostname}`; - searchBar.form?.submit(); +const domainSearch = (e: KeyboardEvent, context: Refs) => { + if (e.target && e.target instanceof HTMLElement) { + const activeElement = e.target as HTMLElement; + const currentResultIndex = context.results.findIndex((el) => + el.getMainDiv().contains(activeElement), + ); + if (currentResultIndex > -1) { + const focusedResult = context.results[currentResultIndex]; + const query = context.searchbar.userQuery(); + const domain = focusedResult.getMainResultLink().getUrl().hostname; + const domainQuery = `site:${domain}`; + + // Only run the domain query if it isn't already in the query + if (!query.includes(domainQuery)) context.searchbar.search(`${query} ${domainQuery}`); } } }; -/** - * Redirect to the `Did you mean:` link (if exists) - */ -const goToMisspellLink = () => { - const msLink = document.getElementById('misspell-link'); - if (msLink) window.location.href = (msLink as HTMLAnchorElement).href; +const openSpellCorrection = (_e: KeyboardEvent, context: Refs) => { + if (context.spellCorrection) context.spellCorrection.open(); }; // Packaged callbacks to keep imports clean @@ -237,10 +230,10 @@ export const searchCb = { focusNextResult, focusPrevResult, focusMainResult, - focusSearchBar, + selectSearchBar, scrollToTop, openResult, openResultInNewTab, domainSearch, - goToMisspellLink, + openSpellCorrection, }; diff --git a/frontend/src/routes/search/+page.svelte b/frontend/src/routes/search/+page.svelte index def0ad63..fbda564d 100644 --- a/frontend/src/routes/search/+page.svelte +++ b/frontend/src/routes/search/+page.svelte @@ -8,36 +8,15 @@ import { updateQueryId } from '$lib/improvements'; import { browser } from '$app/environment'; import Serp from './Serp.svelte'; + import Result from './Result.svelte'; import { search } from '$lib/search'; + import { Keybind, searchCb, type Refs } from '$lib/keybind'; + import SpellCorrection from './SpellCorrection.svelte'; export let data: PageData; $: results = data.results; $: query = data.params.query; - const keybind = new Keybind([ - { key: Keys.J, callback: searchCb.focusNextResult }, - { key: Keys.ARROW_DOWN, callback: searchCb.focusNextResult }, - { key: Keys.K, callback: searchCb.focusPrevResult }, - { key: Keys.ARROW_UP, callback: searchCb.focusPrevResult }, - { key: Keys.H, callback: searchCb.focusSearchBar }, - { key: Keys.FORWARD_SLASH, callback: searchCb.focusSearchBar }, - { key: Keys.V, callback: searchCb.openResultInNewTab }, - { key: Keys.SINGLE_QUOTE, callback: searchCb.openResultInNewTab }, - { key: Keys.T, callback: searchCb.scrollToTop }, - { key: Keys.D, callback: searchCb.domainSearch }, - { key: Keys.L, callback: searchCb.openResult }, - { key: Keys.O, callback: searchCb.openResult }, - { key: Keys.M, callback: searchCb.focusMainResult }, - { key: Keys.S, callback: searchCb.goToMisspellLink }, - ]); - - const onKeyDown = (event: KeyboardEvent) => { - // Only call onKeyDown if the target is not an input element (ie. search bar) - if (!((event.target as HTMLElement).nodeName === 'INPUT')) { - keybind.onKeyDown(event, $useKeyboardShortcuts); - } - }; - let prevPageSearchParams: URLSearchParams | null = null; let nextPageSearchParams: URLSearchParams | null = null; @@ -85,8 +64,49 @@ if (browser && results && results.type == 'websites') updateQueryId({ query, webpages: results.webpages }); } + + let resultElems: Result[] = []; + let spellCorrectElem: SpellCorrection; + let searchbarElem: Searchbar; + + let context: Refs; + $: context = { + results: resultElems, + searchbar: searchbarElem, + spellCorrection: spellCorrectElem, + }; + + let keybind = new Keybind([ + { key: 'j', callback: searchCb.focusNextResult }, + { key: 'ArrowDown', callback: searchCb.focusNextResult }, + { key: 'k', callback: searchCb.focusPrevResult }, + { key: 'ArrowUp', callback: searchCb.focusPrevResult }, + { key: 'h', callback: searchCb.selectSearchBar }, + { key: '/', callback: searchCb.selectSearchBar }, + { key: 'v', callback: searchCb.openResultInNewTab }, + { key: "'", callback: searchCb.openResultInNewTab }, + { key: 't', callback: searchCb.scrollToTop }, + { key: 'd', callback: searchCb.domainSearch }, + { key: 'l', callback: searchCb.openResult }, + { key: 'o', callback: searchCb.openResult }, + { key: 'm', callback: searchCb.focusMainResult }, + { key: 's', callback: searchCb.openSpellCorrection }, + ]); + + const onKeyDown = (event: KeyboardEvent) => { + // Only call onKeyDown if the target is not an input element (ie. search bar) + if ( + context && + searchbarElem && + !(event.target instanceof HTMLElement && searchbarElem.getForm().contains(event.target)) + ) { + keybind.onKeyDown(event, $useKeyboardShortcuts, context); + } + }; + + {#if !serverSearch}