Rewild Your Web
at main 186 lines 5.1 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3import { ResultsAggregator } from "//system.localhost:8888/results_aggregator.js"; 4import { TopSitesProvider } from "//shared.localhost:8888/search/providers/topsites.js"; 5import { FendProvider } from "//shared.localhost:8888/search/providers/fend.js"; 6import { OpenViewsProvider } from "//shared.localhost:8888/search/providers/openviews.js"; 7import { RemoteViewsProvider } from "//shared.localhost:8888/search/providers/remoteviews.js"; 8import SearchEngines from "//shared.localhost:8888/search/engines.js"; 9import { isUrl, normalizeUrl, debounce, groupResults } from "./utils.js"; 10 11/** 12 * SearchController - Handles search logic with customizable callbacks 13 * 14 * Uses composition pattern: consumers provide callbacks for navigation 15 * and result display, while the controller handles the search logic. 16 */ 17export class SearchController { 18 /** 19 * Create a new SearchController 20 * @param {Object} options - Configuration options 21 * @param {Function} options.onNavigate - Callback when navigating to a URL: (url) => void 22 * @param {Function} options.onSelectWebView - Callback when selecting a webview: (windowId, webviewId) => void 23 * @param {Function} options.onResultsChanged - Callback when results change: (results, groups) => void 24 * @param {number} options.debounceDelay - Debounce delay in ms (default: 150) 25 */ 26 constructor({ 27 onNavigate = null, 28 onSelectWebView = null, 29 onResultsChanged = null, 30 debounceDelay = 150, 31 } = {}) { 32 this.onNavigate = onNavigate; 33 this.onSelectWebView = onSelectWebView; 34 this.onResultsChanged = onResultsChanged; 35 36 // Initialize providers 37 this.aggregator = new ResultsAggregator([ 38 new OpenViewsProvider(), 39 new RemoteViewsProvider(), 40 new FendProvider(), 41 new TopSitesProvider(), 42 ]); 43 44 // Initialize search engines 45 this.searchEngines = new SearchEngines(); 46 this.searchEngines.ensureReady(); 47 48 // Current results 49 this.results = []; 50 this.groups = []; 51 this.queryGeneration = 0; 52 53 // Create debounced query function 54 this.debouncedQuery = debounce( 55 (query) => this.executeQuery(query), 56 debounceDelay, 57 ); 58 } 59 60 /** 61 * Query with debouncing - use this for input events 62 * @param {string} query - The search query 63 */ 64 query(query) { 65 this.debouncedQuery(query.trim()); 66 } 67 68 /** 69 * Query immediately without debouncing 70 * @param {string} query - The search query 71 */ 72 queryImmediate(query) { 73 this.executeQuery(query.trim()); 74 } 75 76 /** 77 * Execute the query and update results 78 * @private 79 */ 80 executeQuery(query) { 81 const generation = ++this.queryGeneration; 82 83 this.aggregator.query(query, (results) => { 84 if (generation !== this.queryGeneration) { 85 return; 86 } 87 this.results = results; 88 this.groups = groupResults(results); 89 if (this.onResultsChanged) { 90 this.onResultsChanged(this.results, this.groups); 91 } 92 }); 93 } 94 95 /** 96 * Handle form submission - navigate to URL or first result 97 * @param {string} query - The current query text 98 * @param {Function} getFirstLinkUrl - Optional function to get first link URL from DOM 99 */ 100 handleSubmit(query, getFirstLinkUrl = null) { 101 const trimmed = query.trim(); 102 if (!trimmed) { 103 return; 104 } 105 106 let url; 107 108 // Check if it looks like a URL 109 if (isUrl(trimmed)) { 110 url = normalizeUrl(trimmed); 111 } else { 112 // Try to get first link result 113 let firstLinkUrl = null; 114 115 // Check internal results first 116 const firstLink = this.results.find((r) => r.kind === "link"); 117 if (firstLink) { 118 firstLinkUrl = firstLink.value.url; 119 } 120 121 // Allow consumer to override (e.g., from DOM) 122 if (!firstLinkUrl && getFirstLinkUrl) { 123 firstLinkUrl = getFirstLinkUrl(); 124 } 125 126 if (firstLinkUrl) { 127 url = firstLinkUrl; 128 } else { 129 // Default to search 130 url = this.searchEngines.queryUrl(trimmed); 131 } 132 } 133 134 this.navigate(url); 135 } 136 137 /** 138 * Handle clicking on a result 139 * @param {Object} result - The result object 140 */ 141 handleResultClick(result) { 142 if (result.kind === "link") { 143 this.navigate(result.value.url); 144 } else if (result.kind === "webview") { 145 if (result.value.webviewId != null) { 146 this.selectWebView(result.value.windowId, result.value.webviewId); 147 } else { 148 this.navigate(result.value.url); 149 } 150 } 151 } 152 153 /** 154 * Navigate to a URL 155 * @private 156 */ 157 navigate(url) { 158 if (this.onNavigate) { 159 this.onNavigate(url); 160 } 161 } 162 163 /** 164 * Select a webview 165 * @private 166 */ 167 selectWebView(windowId, webviewId) { 168 if (this.onSelectWebView) { 169 this.onSelectWebView(windowId, webviewId); 170 } 171 } 172 173 /** 174 * Clear current results 175 */ 176 clear() { 177 this.results = []; 178 this.groups = []; 179 if (this.onResultsChanged) { 180 this.onResultsChanged(this.results, this.groups); 181 } 182 } 183} 184 185// Re-export utilities for convenience 186export { isUrl, normalizeUrl, groupResults } from "./utils.js";