From fb3878ad941f352210fe5db7be9fb4768c3463b8 Mon Sep 17 00:00:00 2001 From: Lukas Winkler Date: Mon, 11 Apr 2022 23:52:19 +0200 Subject: [PATCH] autocomplete --- loot/templates/loot/edit.jinja | 1 + package-lock.json | 11 +++++ package.json | 1 + search/urls.py | 3 +- search/views.py | 36 +++++++++++++++-- static/js/autocomplete.js | 25 ++++++++++++ static/libs/autocomplete.min.js | 1 + static/scss/_autocomple.scss | 71 +++++++++++++++++++++++++++++++++ static/scss/main.scss | 1 + templates/base.jinja | 1 - templates/tenantbase.jinja | 14 +++++-- 11 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 static/js/autocomplete.js create mode 120000 static/libs/autocomplete.min.js create mode 100644 static/scss/_autocomple.scss diff --git a/loot/templates/loot/edit.jinja b/loot/templates/loot/edit.jinja index 5d4dd26..6d08e69 100644 --- a/loot/templates/loot/edit.jinja +++ b/loot/templates/loot/edit.jinja @@ -30,6 +30,7 @@ {% endblock %} {% block extra_js %} + {{ super() }} {% if "Safari/" not in request.META.get('HTTP_USER_AGENT', '') %} diff --git a/package-lock.json b/package-lock.json index 5027842..1b36487 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@sentry/browser": "^6.12.0", + "@trevoreyre/autocomplete-js": "^2.2.0", "bootstrap": "^5.1.0", "easymde": "^2.15.0", "luminous-lightbox": "^2.3.5" @@ -106,6 +107,11 @@ "node": ">=6" } }, + "node_modules/@trevoreyre/autocomplete-js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@trevoreyre/autocomplete-js/-/autocomplete-js-2.2.0.tgz", + "integrity": "sha512-emHJWZBPWdB5iDW9MrLSfq3lopyDlIhYXa8ttnCX9kQp1g+G0Lmfu/v6fW2aggjAfsZX8ksuZSG65o+EdwoN0g==" + }, "node_modules/@types/codemirror": { "version": "5.60.5", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz", @@ -265,6 +271,11 @@ "tslib": "^1.9.3" } }, + "@trevoreyre/autocomplete-js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@trevoreyre/autocomplete-js/-/autocomplete-js-2.2.0.tgz", + "integrity": "sha512-emHJWZBPWdB5iDW9MrLSfq3lopyDlIhYXa8ttnCX9kQp1g+G0Lmfu/v6fW2aggjAfsZX8ksuZSG65o+EdwoN0g==" + }, "@types/codemirror": { "version": "5.60.5", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz", diff --git a/package.json b/package.json index 3d30bb1..0e07a90 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@sentry/browser": "^6.12.0", + "@trevoreyre/autocomplete-js": "^2.2.0", "bootstrap": "^5.1.0", "easymde": "^2.15.0", "luminous-lightbox": "^2.3.5" diff --git a/search/urls.py b/search/urls.py index a5faa9a..9b56a4c 100644 --- a/search/urls.py +++ b/search/urls.py @@ -2,7 +2,8 @@ from django.urls import path from search import views -urlpatterns=[ +urlpatterns = [ path("", views.SearchResultsView.as_view(), name="search"), + path("autocomplete/", views.autocomplete, name="autocomplete"), ] diff --git a/search/views.py b/search/views.py index 8657bbc..8235f98 100644 --- a/search/views.py +++ b/search/views.py @@ -1,7 +1,8 @@ # Create your views here. from itertools import chain -from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank, SearchHeadline, TrigramWordSimilarity +from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank, SearchHeadline, TrigramWordDistance +from django.http import JsonResponse from django.views.generic import TemplateView from campaigns.models import Campaign @@ -12,6 +13,8 @@ from locations.models import Location from loot.models import Loot from notes.models import Note +search_models = [Location, Character, Faction, IngameDay, Note, Loot] + class SearchResultsView(TemplateView): template_name = "search/search_results.jinja" @@ -27,16 +30,15 @@ class SearchResultsView(TemplateView): name_vector = SearchVector('name', weight="A", config=config) description_vector = SearchVector("description_html", weight="B", config=config) query = SearchQuery(query_string, search_type='websearch', config=config) - models = [Location, Character, Faction, IngameDay, Note, Loot] all_results = [] all_similar = [] - for m in models: + for m in search_models: if m == IngameDay: vector = description_vector else: vector = description_vector + name_vector similar = m.objects.annotate( - distance=TrigramWordSimilarity(query_string, "name") + distance=TrigramWordDistance(query_string, "name") # distance=TrigramDistance("name", query_string) ).filter(name__trigram_word_similar=query_string).order_by('distance') all_similar.extend(list(similar)) @@ -58,3 +60,29 @@ class SearchResultsView(TemplateView): context["similars"] = all_similar context["query"] = query_string return context + + +def autocomplete(request): + if "q" not in request.GET: + return "" + query_string = request.GET['q'] + all_similar = [] + + for m in search_models: + if m == IngameDay: + continue + similar = m.objects.annotate( + distance=TrigramWordDistance(query_string, "name") + # distance=TrigramDistance("name", query_string) + ).order_by('distance') + similar = [s for s in similar if s.distance < 0.5] + all_similar.extend(list(similar)) + all_similar.sort(key=lambda s: s.distance) + data = [] + for s in all_similar: + data.append({ + "url": s.get_absolute_url(), + "name": s.name, + "distance": s.distance + }) + return JsonResponse(data, safe=False) diff --git a/static/js/autocomplete.js b/static/js/autocomplete.js new file mode 100644 index 0000000..1b96e4d --- /dev/null +++ b/static/js/autocomplete.js @@ -0,0 +1,25 @@ +new Autocomplete('#autocomplete', { + search: input => { + const url = `/search/autocomplete/?q=${encodeURI(input)}` + + return new Promise(resolve => { + if (input.length === 0) { + return resolve([]) + } + + fetch(url) + .then(response => response.json()) + .then(data => { + resolve(data) + }) + }) + }, + getResultValue: result => result.name, + onSubmit: result => { + if (!result) { + return + } + location.href = result.url + } + +}) diff --git a/static/libs/autocomplete.min.js b/static/libs/autocomplete.min.js new file mode 120000 index 0000000..c0ca5c7 --- /dev/null +++ b/static/libs/autocomplete.min.js @@ -0,0 +1 @@ +../../node_modules/@trevoreyre/autocomplete-js/dist/autocomplete.min.js \ No newline at end of file diff --git a/static/scss/_autocomple.scss b/static/scss/_autocomple.scss new file mode 100644 index 0000000..90b346f --- /dev/null +++ b/static/scss/_autocomple.scss @@ -0,0 +1,71 @@ +// based on https://github.com/trevoreyre/autocomplete/blob/4caf5f8107365c268a0543c652f6ad44a91fe488/packages/style.css + +[data-position="below"] .autocomplete-input[aria-expanded="true"] { + border-bottom-color: transparent; +} + +[data-position="above"] .autocomplete-input[aria-expanded="true"] { + border-top-color: transparent; + z-index: 2; +} + +/* Loading spinner */ +.autocomplete[data-loading="true"]::after { + content: ""; + border: 3px solid rgba(0, 0, 0, 0.12); + border-right: 3px solid rgba(0, 0, 0, 0.48); + border-radius: 100%; + width: 20px; + height: 20px; + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + animation: rotate 1s infinite linear; +} + +.autocomplete-result-list { + margin: 0; + border: 1px solid rgba(0, 0, 0, 0.12); + padding: 0; + box-sizing: border-box; + max-height: 296px; + overflow-y: auto; + background: #fff; + list-style: none; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.16); +} + +[data-position="below"] .autocomplete-result-list { + margin-top: -1px; + border-top-color: transparent; + border-radius: 0 0 8px 8px; + padding-bottom: 8px; +} + +[data-position="above"] .autocomplete-result-list { + margin-bottom: -1px; + border-bottom-color: transparent; + border-radius: 8px 8px 0 0; + padding-top: 8px; +} + +/* Single result item */ +.autocomplete-result { + cursor: default; + padding: 12px; +} + +.autocomplete-result:hover, +.autocomplete-result[aria-selected="true"] { + background-color: rgba(0, 0, 0, 0.06); +} + +@keyframes rotate { + from { + transform: translateY(-50%) rotate(0deg); + } + to { + transform: translateY(-50%) rotate(359deg); + } +} diff --git a/static/scss/main.scss b/static/scss/main.scss index 5b054a5..7d424f8 100644 --- a/static/scss/main.scss +++ b/static/scss/main.scss @@ -3,6 +3,7 @@ @import "node_modules/bootstrap/scss/bootstrap"; @import "node_modules/easymde/dist/easymde.min"; @import "node_modules/luminous-lightbox/dist/luminous-basic"; +@import "autocomple"; @import "misc"; @import "avatar"; diff --git a/templates/base.jinja b/templates/base.jinja index 03cb152..9ed38c5 100644 --- a/templates/base.jinja +++ b/templates/base.jinja @@ -48,7 +48,6 @@ diff --git a/templates/tenantbase.jinja b/templates/tenantbase.jinja index 852699c..415c1e7 100644 --- a/templates/tenantbase.jinja +++ b/templates/tenantbase.jinja @@ -45,9 +45,12 @@
- +
+ +
    +
    @@ -83,3 +86,8 @@ {% block content %} {% endblock %} {% endblock %} + +{% block extra_js %} + + +{% endblock %}