Skip to content

Commit 900fc83

Browse files
authored
Merge pull request #1523 from KodeStar/add_autocomplete_suggestions
Add autocomplete suggestions support
2 parents 045bdf0 + 4f30332 commit 900fc83

10 files changed

Lines changed: 7036 additions & 10 deletions

File tree

app/Http/Controllers/SearchController.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
use App\Search;
66
use Illuminate\Contracts\Foundation\Application;
7+
use Illuminate\Http\JsonResponse;
78
use Illuminate\Http\RedirectResponse;
89
use Illuminate\Http\Request;
910
use Illuminate\Routing\Redirector;
11+
use Illuminate\Support\Facades\Http;
1012

1113
class SearchController extends Controller
1214
{
@@ -41,4 +43,97 @@ public function index(Request $request)
4143

4244
abort(404, 'Provider type not supported');
4345
}
46+
47+
/**
48+
* Get autocomplete suggestions for a search query
49+
*
50+
* @return JsonResponse
51+
*/
52+
public function autocomplete(Request $request)
53+
{
54+
$requestprovider = $request->input('provider');
55+
$query = $request->input('q');
56+
57+
if (!$query || trim($query) === '') {
58+
return response()->json([]);
59+
}
60+
61+
$provider = Search::providerDetails($requestprovider);
62+
63+
if (!$provider || !isset($provider->autocomplete)) {
64+
return response()->json([]);
65+
}
66+
67+
// Replace {query} placeholder with actual query
68+
$autocompleteUrl = str_replace('{query}', urlencode($query), $provider->autocomplete);
69+
70+
try {
71+
$response = Http::timeout(5)->get($autocompleteUrl);
72+
73+
if ($response->successful()) {
74+
$data = $response->body();
75+
76+
// Parse the response based on provider
77+
$suggestions = $this->parseAutocompleteResponse($data, $provider->id);
78+
79+
return response()->json($suggestions);
80+
}
81+
} catch (\Exception $e) {
82+
// Return empty array on error
83+
return response()->json([]);
84+
}
85+
86+
return response()->json([]);
87+
}
88+
89+
/**
90+
* Parse autocomplete response based on provider format
91+
*
92+
* @param string $data
93+
* @param string $providerId
94+
* @return array
95+
*/
96+
private function parseAutocompleteResponse($data, $providerId)
97+
{
98+
$suggestions = [];
99+
100+
switch ($providerId) {
101+
case 'google':
102+
// Google returns XML format
103+
if (strpos($data, '<?xml') === 0) {
104+
$xml = simplexml_load_string($data);
105+
if ($xml && isset($xml->CompleteSuggestion)) {
106+
foreach ($xml->CompleteSuggestion as $suggestion) {
107+
if (isset($suggestion->suggestion['data'])) {
108+
$suggestions[] = (string) $suggestion->suggestion['data'];
109+
}
110+
}
111+
}
112+
}
113+
break;
114+
115+
case 'bing':
116+
case 'ddg':
117+
// Bing and DuckDuckGo return JSON array format
118+
$json = json_decode($data, true);
119+
if (is_array($json) && isset($json[1]) && is_array($json[1])) {
120+
$suggestions = $json[1];
121+
}
122+
break;
123+
124+
default:
125+
// Try to parse as JSON array
126+
$json = json_decode($data, true);
127+
if (is_array($json)) {
128+
if (isset($json[1]) && is_array($json[1])) {
129+
$suggestions = $json[1];
130+
} else {
131+
$suggestions = $json;
132+
}
133+
}
134+
break;
135+
}
136+
137+
return $suggestions;
138+
}
44139
}

public/css/app.css

Lines changed: 2172 additions & 2 deletions
Large diffs are not rendered by default.

public/js/app.js

Lines changed: 4635 additions & 1 deletion
Large diffs are not rendered by default.

public/js/dummy.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

public/mix-manifest.json

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/assets/js/app.js

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,87 @@ $.when($.ready).then(() => {
108108
}
109109
});
110110

111+
// Autocomplete functionality
112+
let autocompleteTimeout = null;
113+
let currentAutocompleteRequest = null;
114+
115+
function hideAutocomplete() {
116+
$("#search-autocomplete").remove();
117+
}
118+
119+
function showAutocomplete(suggestions, inputElement) {
120+
hideAutocomplete();
121+
122+
if (!suggestions || suggestions.length === 0) {
123+
return;
124+
}
125+
126+
const $input = $(inputElement);
127+
const position = $input.position();
128+
const width = $input.outerWidth();
129+
130+
const $autocomplete = $('<div id="search-autocomplete"></div>');
131+
132+
suggestions.forEach((suggestion) => {
133+
const $item = $('<div class="autocomplete-item"></div>')
134+
.text(suggestion)
135+
.on("click", () => {
136+
$input.val(suggestion);
137+
hideAutocomplete();
138+
$input.closest("form").submit();
139+
});
140+
$autocomplete.append($item);
141+
});
142+
143+
$autocomplete.css({
144+
position: "absolute",
145+
top: `${position.top + $input.outerHeight()}px`,
146+
left: `${position.left}px`,
147+
width: `${width}px`,
148+
});
149+
150+
$input.closest("#search-container").append($autocomplete);
151+
}
152+
153+
function fetchAutocomplete(query, provider) {
154+
// Cancel previous request if any
155+
if (currentAutocompleteRequest) {
156+
currentAutocompleteRequest.abort();
157+
}
158+
159+
if (!query || query.trim().length < 2) {
160+
hideAutocomplete();
161+
return;
162+
}
163+
164+
currentAutocompleteRequest = $.ajax({
165+
url: `${base}search/autocomplete`,
166+
method: "GET",
167+
data: {
168+
q: query,
169+
provider,
170+
},
171+
success(data) {
172+
const inputElement = $("#search-container input[name=q]")[0];
173+
showAutocomplete(data, inputElement);
174+
},
175+
error() {
176+
hideAutocomplete();
177+
},
178+
complete() {
179+
currentAutocompleteRequest = null;
180+
},
181+
});
182+
}
183+
111184
$("#search-container")
112185
.on("input", "input[name=q]", function () {
113186
const search = this.value;
114187
const items = $("#sortable").find(".item-container");
115-
if ($("#search-container select[name=provider]").val() === "tiles") {
188+
const provider = $("#search-container select[name=provider]").val();
189+
190+
if (provider === "tiles") {
191+
hideAutocomplete();
116192
if (search.length > 0) {
117193
items.hide();
118194
items
@@ -126,6 +202,12 @@ $.when($.ready).then(() => {
126202
}
127203
} else {
128204
items.show();
205+
206+
// Debounce autocomplete requests
207+
clearTimeout(autocompleteTimeout);
208+
autocompleteTimeout = setTimeout(() => {
209+
fetchAutocomplete(search, provider);
210+
}, 300);
129211
}
130212
})
131213
.on("change", "select[name=provider]", function () {
@@ -147,9 +229,24 @@ $.when($.ready).then(() => {
147229
} else {
148230
$("#search-container button").show();
149231
items.show();
232+
hideAutocomplete();
150233
}
151234
});
152235

236+
// Hide autocomplete when clicking outside
237+
$(document).on("click", (e) => {
238+
if (!$(e.target).closest("#search-container").length) {
239+
hideAutocomplete();
240+
}
241+
});
242+
243+
// Hide autocomplete on Escape key
244+
$(document).on("keydown", (e) => {
245+
if (e.key === "Escape") {
246+
hideAutocomplete();
247+
}
248+
});
249+
153250
$("#search-container select[name=provider]").trigger("change");
154251

155252
$("#app")

resources/assets/sass/_app.scss

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,6 @@ div.create {
933933
background: white;
934934
border-radius: 5px;
935935
box-shadow: 0px 0px 5px 0 rgba(0,0,0,0.4);
936-
overflow: hidden;
937936
position: relative;
938937
display: flex;
939938

@@ -965,9 +964,39 @@ div.create {
965964
background: #f5f5f5;
966965
border: none;
967966
border-right: 1px solid #ddd;
967+
border-top-left-radius: 5px;
968+
border-bottom-left-radius: 5px;
968969
}
969970
}
970971

972+
#search-autocomplete {
973+
position: absolute;
974+
z-index: 1000;
975+
background: white;
976+
border: 1px solid #ddd;
977+
border-top: none;
978+
border-radius: 0 0 5px 5px;
979+
box-shadow: 0px 4px 8px 0 rgba(0,0,0,0.2);
980+
max-height: 300px;
981+
overflow-y: auto;
982+
983+
.autocomplete-item {
984+
padding: 12px 15px;
985+
cursor: pointer;
986+
font-size: 15px;
987+
border-bottom: 1px solid #f0f0f0;
988+
transition: background-color 0.2s ease;
989+
990+
&:last-child {
991+
border-bottom: none;
992+
}
993+
994+
&:hover {
995+
background-color: #f5f5f5;
996+
}
997+
}
998+
}
999+
9711000
.ui-autocomplete {
9721001
position: absolute;
9731002
top: 100%;

routes/web.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
Route::get('get_stats/{id}', [ItemController::class,'getStats'])->name('get_stats');
7676

7777
Route::get('/search', [SearchController::class,'index'])->name('search');
78+
Route::get('/search/autocomplete', [SearchController::class,'autocomplete'])->name('search.autocomplete');
7879

7980
Route::get('view/{name_view}', function ($name_view) {
8081
return view('SupportedApps::'.$name_view)->render();

storage/app/searchproviders.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ bing:
1818
method: get
1919
target: _blank
2020
query: q
21+
autocomplete: https://api.bing.com/osjson.aspx?query={query}
2122

2223
ddg:
2324
id: ddg
@@ -26,6 +27,7 @@ ddg:
2627
method: get
2728
target: _blank
2829
query: q
30+
autocomplete: https://duckduckgo.com/ac/?q={query}&type=list
2931

3032
google:
3133
id: google
@@ -34,6 +36,7 @@ google:
3436
method: get
3537
target: _blank
3638
query: q
39+
autocomplete: https://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q={query}
3740

3841
startpage:
3942
id: startpage

webpack.mix.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const mix = require("laravel-mix");
1212
*/
1313

1414
mix
15-
.js("resources/assets/js/app.js", "public/js/dummy.js")
1615
.babel(
1716
[
1817
"node_modules/sortablejs/Sortable.min.js",

0 commit comments

Comments
 (0)