Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions assets/css/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,8 @@ span > select {
.light-theme .pure-button-primary:hover,
.light-theme .pure-button-primary:focus,
.light-theme .pure-button-secondary:hover,
.light-theme .pure-button-secondary:focus {
.light-theme .pure-button-secondary:focus,
.light-theme #suggestions .active {
color: #fff !important;
Comment thread
MiningTcup marked this conversation as resolved.
border-color: rgba(0, 182, 240, 0.75) !important;
background-color: rgba(0, 182, 240, 0.75) !important;
Expand Down Expand Up @@ -571,6 +572,10 @@ span > select {
border: 1px solid black;
}

.light-theme #suggestions {
background: rgb(255, 255, 255);
}

@media (prefers-color-scheme: light) {
.no-theme a:hover,
.no-theme a:active,
Expand All @@ -583,7 +588,8 @@ span > select {
.no-theme .pure-button-primary:hover,
.no-theme .pure-button-primary:focus,
.no-theme .pure-button-secondary:hover,
.no-theme .pure-button-secondary:focus {
.no-theme .pure-button-secondary:focus,
.no-theme #suggestions .active {
color: #fff !important;
Comment thread
MiningTcup marked this conversation as resolved.
border-color: rgba(0, 182, 240, 0.75) !important;
background-color: rgba(0, 182, 240, 0.75) !important;
Expand Down Expand Up @@ -621,6 +627,10 @@ span > select {
.no-theme .error-card {
border: 1px solid black;
}

.no-theme #suggestions {
background: rgb(255, 255, 255);
}
}


Expand All @@ -639,7 +649,8 @@ span > select {
.dark-theme .pure-button-primary:hover,
.dark-theme .pure-button-primary:focus,
.dark-theme .pure-button-secondary:hover,
.dark-theme .pure-button-secondary:focus {
.dark-theme .pure-button-secondary:focus,
.dark-theme #suggestions .active {
color: #fff !important;
Comment thread
MiningTcup marked this conversation as resolved.
border-color: rgb(0, 182, 240) !important;
background-color: rgba(0, 182, 240, 1) !important;
Expand Down Expand Up @@ -687,6 +698,10 @@ body.dark-theme {
border: 1px solid #5e5e5e;
}

.dark-theme #suggestions {
background: rgb(35, 35, 35);
}

@media (prefers-color-scheme: dark) {
.no-theme a:hover,
.no-theme a:active,
Expand All @@ -697,7 +712,8 @@ body.dark-theme {
.no-theme .pure-button-primary:hover,
.no-theme .pure-button-primary:focus,
.no-theme .pure-button-secondary:hover,
.no-theme .pure-button-secondary:focus {
.no-theme .pure-button-secondary:focus,
.no-theme #suggestions .active {
color: #fff !important;
Comment thread
MiningTcup marked this conversation as resolved.
border-color: rgb(0, 182, 240) !important;
background-color: rgba(0, 182, 240, 1) !important;
Expand Down Expand Up @@ -752,6 +768,10 @@ body.dark-theme {
.no-theme .error-card {
border: 1px solid #5e5e5e;
}

.no-theme #suggestions {
background: rgb(35, 35, 35);
}
}


Expand Down Expand Up @@ -909,4 +929,34 @@ h1, h2, h3, h4, h5, p,
padding-left: 10px;
display: inline-block;
vertical-align: top;
}
}

#search-container {
position: relative;
display: block !important;
}

#suggestions {
position: absolute;
width: 100%;
top: calc(100% + .25em);
border: 1px solid #a0a0a0;
z-index: 200;
padding: .25em 0;
margin: 0;
display: none;
}

#search-container:has(:active) #suggestions:has(*),
#search-container:has(:focus) #suggestions:has(*) {
display: block;
}

#suggestions>* {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: .25em .5em;
text-align: left;
}
123 changes: 123 additions & 0 deletions assets/js/suggestions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use strict";
const searchbox = document.getElementById("searchbox");
const searchContainer = document.getElementById("search-container");
const suggestions = document.getElementById("suggestions");
let selectedSuggestion = -1;
let suggestionController;
let queuedSuggestions = 0;

function resetSuggestions() {
suggestions.textContent = "";
selectedSuggestion = -1;
}

searchbox?.addEventListener("input", function () {
// If there is already a request in progress,
// abort. The user has made another input,
// invalidating the old query.
if (suggestionController != undefined) {
suggestionController.abort();
}

// Only make a request for suggestions after
// there is a delay of at least 150 milliseconds
// between inputs.
queuedSuggestions++;
setTimeout(async function () {
queuedSuggestions--;
if (queuedSuggestions != 0) {
return;
}

// Only continue for queries of more than one
// character.
if (searchbox.value.length < 2) {
resetSuggestions();
return;
}

// Create the controller which will be
// used to abort this request if another
// is made before it finishes.
suggestionController = new AbortController();
const signal = suggestionController.signal;

// Make the request for suggestions.
try {
const resp = await fetch(
`/api/v1/search/suggestions?q=${encodeURIComponent(searchbox.value)}`,
{ signal },
);
const body = await resp.json();

// Put the results into DOM.
resetSuggestions();
const results = body.suggestions;
for (let i = 0; i < results.length; i++) {
const s = document.createElement("a");
s.href = "/search";
s.innerText = results[i];
s.setAttribute("role", "option");
s.setAttribute("aria-selected", "false");
s.addEventListener("click", function (e) {
searchbox.value = results[i];
if (searchbox.form?.requestSubmit) {
searchbox.form.requestSubmit();
} else {
searchbox.form?.submit();
}
e.preventDefault();
});
suggestions.appendChild(s);
}
} catch (err) {
// Discard AbortError as it is expected.
if (err.name !== "AbortError") {
console.error(err);
}
}
}, 150);
});

searchbox?.addEventListener("keydown", function (e) {
const currentSuggestions = suggestions.children;

// Navigate suggestions by key.
switch (e.key) {
case "ArrowDown":
selectedSuggestion++;
e.preventDefault();
break;
case "ArrowUp":
selectedSuggestion--;
e.preventDefault();
break;
case "Enter":
if (selectedSuggestion != -1) {
currentSuggestions[selectedSuggestion].click();
e.preventDefault();
return;
}
break;
}

// Loop around if you go beyond the end
// or before -1 (the resting position).
if (selectedSuggestion >= currentSuggestions.length) {
selectedSuggestion = -1;
} else if (selectedSuggestion < -1) {
selectedSuggestion = currentSuggestions.length - 1;
}

// Remove .active class from all elements
// except the active one.
for (let i = 0; i < currentSuggestions.length; i++) {
if (i == selectedSuggestion) {
currentSuggestions[i].classList.add("active");
currentSuggestions[i].setAttribute("aria-selected", "true");
} else if (currentSuggestions[i].classList.contains("active")) {
currentSuggestions[i].classList.remove("active");
currentSuggestions[i].setAttribute("aria-selected", "false");
}
}
});
4 changes: 3 additions & 1 deletion src/invidious/views/components/search_box.ecr
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
<% else %>
<form class="pure-form" action="/search" method="get">
<% end %>
<fieldset>
<fieldset id="search-container">
<input type="search" id="searchbox" autocorrect="off"
aria-autocomplete="list" aria-controls="suggestions"
autocapitalize="none" spellcheck="false" <% if autofocus %>autofocus<% end %>
name="q" placeholder="<%= I18n.translate(locale, "search") %>"
title="<%= I18n.translate(locale, "search") %>"
value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>">
<ul id="suggestions"></ul>
</fieldset>
<button type="submit" id="searchbutton" aria-label="<%= I18n.translate(locale, "search") %>">
<i class="icon ion-ios-search"></i>
Expand Down
1 change: 1 addition & 0 deletions src/invidious/views/template.ecr
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
</div>
<script src="/js/handlers.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/suggestions.js?v=<%= ASSET_COMMIT %>"></script>
<% if env.get? "user" %>
<script src="/js/sse.js?v=<%= ASSET_COMMIT %>"></script>
<script id="notification_data" type="application/json">
Expand Down
Loading