Offer the searches from before while you type

Searches are kept in local storage and the five that best fit what is in
the box are offered under it: the ones it starts, then the ones it starts
a word of, then the ones it appears in anywhere, most recently searched
first. Walk them with the arrows, take one with enter or a tap, dismiss
with escape. The part that matched is picked out, and rows are built from
text nodes -- these strings are the reader's own and they are going back
on screen.

The box searches as you type, so there is no submit to record and storing
every pause would fill the list with the way to a query rather than the
query: a new entry swallows the ones it grew out of ("hot", "hote",
"hotel" leaves "hotel"), and going back to something shorter adds
nothing. Enter records straight away, since that is a reader saying they
meant it.

That turned up a bug the suggestions would have run into constantly: a
search started while the box's own last search was still out was dropped
-- loadVideos saw a load running and returned -- leaving the grid on the
previous query. Changing the result set now stops the load that is
running before starting the next one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
This commit is contained in:
Simon
2026-09-22 14:52:50 +00:00
parent 553d610e5b
commit f977852253
7 changed files with 569 additions and 2 deletions

View File

@@ -922,6 +922,22 @@ App.videos = App.videos || {};
// until the tab stops responding.
let loadRunning = false;
// Stops whatever load is running and releases the guard in the same breath,
// so the caller can start the next one straight away. Without this a search
// typed while the box's own last search was still out was simply dropped --
// loadVideos saw a load running and returned -- and the reader was left
// looking at results for what they had typed a moment before. The aborted
// call unwinds on its own: its fetch rejects, it commits nothing, and its
// `finally` clears these again.
const abortLoad = function() {
if (state.currentLoadController) {
state.currentLoadController.abort();
state.currentLoadController = null;
}
loadRunning = false;
state.isLoading = false;
};
// Renders the next page: the prefetched one when it's ready (the common
// case), otherwise fetched here and rendered as it lands. `opts.force` is for
// callers that know they need content and can't be judged by where the grid
@@ -1321,6 +1337,13 @@ App.videos = App.videos || {};
clearBtn.disabled = !hasValue;
}
}
// Every search worth running is a search worth remembering: this is
// the one place they all come through -- the box, a tag, the command
// palette. Storage decides what is worth keeping (see rememberSearch).
const searched = document.getElementById('search-input');
if (App.storage && typeof App.storage.rememberSearch === 'function') {
App.storage.rememberSearch(searched ? searched.value : value);
}
// A search is a new result set, so it leaves the favorites grid.
if (App.favoritesView && App.favoritesView.isActive()) {
App.favoritesView.close({ silent: true });
@@ -1334,6 +1357,7 @@ App.videos = App.videos || {};
App.videos.resetGrid = function() {
// The held/in-flight page belongs to the old result set.
App.videos.resetPrefetch();
abortLoad();
state.currentPage = 1;
state.hasNextPage = true;
state.renderedVideoIds.clear();