From 5dbe792579764d7f06f440847581b00e4fddc847 Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Sun, 5 Nov 2023 21:20:48 +0000 Subject: [PATCH] Update title in response --- front-end/src/components/organisms/results.js | 6 - .../src/components/organisms/search-bar.js | 180 ++++++++++++++++++ mwmbl/templates/results.html | 1 + mwmbl/views.py | 7 +- 4 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 front-end/src/components/organisms/search-bar.js diff --git a/front-end/src/components/organisms/results.js b/front-end/src/components/organisms/results.js index 7a0e6b4..f699f3f 100644 --- a/front-end/src/components/organisms/results.js +++ b/front-end/src/components/organisms/results.js @@ -1,11 +1,5 @@ import {globalBus} from '../../utils/events.js'; - -document.body.addEventListener('htmx:load', function(evt) { - -}); - - class ResultsHandler { constructor() { this.results = null; diff --git a/front-end/src/components/organisms/search-bar.js b/front-end/src/components/organisms/search-bar.js new file mode 100644 index 0000000..f700fda --- /dev/null +++ b/front-end/src/components/organisms/search-bar.js @@ -0,0 +1,180 @@ +import define from '../../utils/define.js'; +import config from '../../../config.js'; +import { globalBus } from '../../utils/events.js'; +import debounce from '../../utils/debounce.js' + +const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion)').matches; + +const template = () => /*html*/` + +`; + +export default define('search-bar', class extends HTMLElement { + constructor() { + super(); + this.searchInput = null; + this.searchForm = null; + this.abortController = new AbortController(); + this.__setup(); + } + + __setup() { + this.innerHTML = template(); + this.searchInput = this.querySelector('input'); + this.searchForm = this.querySelector('form'); + this.__events(); + } + + __dispatchSearch({ results = null, error = null }) { + const searchEvent = new CustomEvent('search', { + detail: { + results, + error, + }, + }); + globalBus.dispatch(searchEvent) + } + + /** + * Updates the overall layout of the page. + * + * `home` centers the search bar on the page. + * `compact` raises it to the top and makes room for displaying results. + * + * @param {'compact' | 'home'} mode + * @return {void} + */ + __setDisplayMode(mode) { + switch (mode) { + case 'compact': { + document.body.style.paddingTop = '25px'; + document.querySelector('.search-menu').classList.add('compact'); + break; + } + case 'home': { + document.body.style.paddingTop = '30vh'; + document.querySelector('.search-menu').classList.remove('compact'); + break; + } + } + } + + async __executeSearch() { + this.abortController.abort(); + this.abortController = new AbortController(); + // Get response from API + const response = await fetch(`${config.publicApiURL}search?s=${encodeURIComponent(this.searchInput.value)}`, { + signal: this.abortController.signal + }); + // Getting results from API + const search = await (response).json(); + return search; + } + + __handleSearch = async () => { + // Update page title + document.title = `MWMBL - ${this.searchInput.value || "Search"}`; + + // Update query params + const queryParams = new URLSearchParams(document.location.search); + // Sets query param if search value is not empty + if (this.searchInput.value) queryParams.set(config.searchQueryParam, this.searchInput.value); + else queryParams.delete(config.searchQueryParam); + // New URL with query params + const newURL = + document.location.protocol + + "//" + + document.location.host + + document.location.pathname + + (this.searchInput.value ? '?' : '') + + queryParams.toString(); + // Replace history state + window.history.replaceState({ path: newURL }, '', newURL); + + if (this.searchInput.value) { + this.__setDisplayMode('compact') + + try { + const search = await this.__executeSearch() + // This is a guess at an explanation + // Check the searcInput.value before setting the results to prevent + // race condition where the user has cleared the search input after + // submitting an original search but before the search results have + // come back from the API + this.__dispatchSearch({ results: this.searchInput.value ? search : null }); + } + catch(error) { + this.__dispatchSearch({ error }) + } + } + else { + this.__setDisplayMode('home') + this.__dispatchSearch({ results: null }); + } + } + + __events() { + /** + * Always add the submit event, it makes things feel faster if + * someone does not prefer reduced motion and reflexively hits + * return once they've finished typing. + */ + this.searchForm.addEventListener('submit', (e) => { + e.preventDefault(); + this.__handleSearch(e); + }); + + /** + * Only add the "real time" search behavior when the client does + * not prefer reduced motion; this prevents the page from changing + * while the user is still typing their query. + */ + if (!prefersReducedMotion) { + this.searchInput.addEventListener('input', debounce(this.__handleSearch, 500)) + } + + // Focus search bar when pressing `ctrl + k` or `/` + document.addEventListener('keydown', (e) => { + if ((e.key === 'k' && e.ctrlKey) || e.key === '/' || e.key === 'Escape') { + e.preventDefault(); + this.searchInput.focus(); + } + }); + + // Focus first result when pressing down arrow + this.addEventListener('keydown', (e) => { + if (e.key === 'ArrowDown' && this.searchInput.value) { + e.preventDefault(); + const focusResultEvent = new CustomEvent('focus-result'); + globalBus.dispatch(focusResultEvent); + } + }); + + globalBus.on('focus-search', (e) => { + this.searchInput.focus(); + }); + } + + connectedCallback() { + // Focus search input when component is connected + this.searchInput.focus(); + + const searchQuery = new URLSearchParams(document.location.search).get(config.searchQueryParam); + this.searchInput.value = searchQuery; + /** + * Trigger search handling to coordinate the value pulled from the query string + * across the rest of the UI and to actually retrieve the results if the search + * value is now non-empty. + */ + this.__handleSearch(); + } +}); diff --git a/mwmbl/templates/results.html b/mwmbl/templates/results.html index 11fa224..869db07 100644 --- a/mwmbl/templates/results.html +++ b/mwmbl/templates/results.html @@ -1,4 +1,5 @@ {% load result_filters %} +Mwmbl - {{ query }} {% for result in results %}
  • diff --git a/mwmbl/views.py b/mwmbl/views.py index a3fb215..0958053 100644 --- a/mwmbl/views.py +++ b/mwmbl/views.py @@ -48,7 +48,7 @@ def profile(request): def search_results(request): query = request.GET["query"] results = ranker.search(query) - return render(request, "results.html", {"results": results}) + return render(request, "results.html", {"results": results, "query": query}) def fetch_url(request): @@ -63,4 +63,7 @@ def fetch_url(request): extract = extract[:NUM_EXTRACT_CHARS - 1] + '…' result = Document(title=title, url=url, extract=extract, score=0.0) - return render(request, "results.html", {"results": [format_result(result, query)]}) + return render(request, "results.html", { + "results": [format_result(result, query)], + "query": query, + })