Rewild Your Web
web
browser
dweb
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";