forked from
me.webbeef.org/browser.html
Rewild Your Web
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";