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