mirror of
https://github.com/Findus23/RPGnotes.git
synced 2024-09-19 15:43:45 +02:00
autocomplete
This commit is contained in:
parent
24d6e6dd24
commit
fb3878ad94
11 changed files with 156 additions and 9 deletions
|
@ -30,6 +30,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
{{ super() }}
|
||||||
{% if "Safari/" not in request.META.get('HTTP_USER_AGENT', '') %}
|
{% if "Safari/" not in request.META.get('HTTP_USER_AGENT', '') %}
|
||||||
<script src="{{ static("libs/easymde.min.js") }}"></script>
|
<script src="{{ static("libs/easymde.min.js") }}"></script>
|
||||||
<script src="{{ static("libs/fontawesome-solid.min.js") }}"></script>
|
<script src="{{ static("libs/fontawesome-solid.min.js") }}"></script>
|
||||||
|
|
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -7,6 +7,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||||
"@sentry/browser": "^6.12.0",
|
"@sentry/browser": "^6.12.0",
|
||||||
|
"@trevoreyre/autocomplete-js": "^2.2.0",
|
||||||
"bootstrap": "^5.1.0",
|
"bootstrap": "^5.1.0",
|
||||||
"easymde": "^2.15.0",
|
"easymde": "^2.15.0",
|
||||||
"luminous-lightbox": "^2.3.5"
|
"luminous-lightbox": "^2.3.5"
|
||||||
|
@ -106,6 +107,11 @@
|
||||||
"node": ">=6"
|
"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": {
|
"node_modules/@types/codemirror": {
|
||||||
"version": "5.60.5",
|
"version": "5.60.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
||||||
|
@ -265,6 +271,11 @@
|
||||||
"tslib": "^1.9.3"
|
"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": {
|
"@types/codemirror": {
|
||||||
"version": "5.60.5",
|
"version": "5.60.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||||
"@sentry/browser": "^6.12.0",
|
"@sentry/browser": "^6.12.0",
|
||||||
|
"@trevoreyre/autocomplete-js": "^2.2.0",
|
||||||
"bootstrap": "^5.1.0",
|
"bootstrap": "^5.1.0",
|
||||||
"easymde": "^2.15.0",
|
"easymde": "^2.15.0",
|
||||||
"luminous-lightbox": "^2.3.5"
|
"luminous-lightbox": "^2.3.5"
|
||||||
|
|
|
@ -4,5 +4,6 @@ from search import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.SearchResultsView.as_view(), name="search"),
|
path("", views.SearchResultsView.as_view(), name="search"),
|
||||||
|
path("autocomplete/", views.autocomplete, name="autocomplete"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
from itertools import chain
|
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 django.views.generic import TemplateView
|
||||||
|
|
||||||
from campaigns.models import Campaign
|
from campaigns.models import Campaign
|
||||||
|
@ -12,6 +13,8 @@ from locations.models import Location
|
||||||
from loot.models import Loot
|
from loot.models import Loot
|
||||||
from notes.models import Note
|
from notes.models import Note
|
||||||
|
|
||||||
|
search_models = [Location, Character, Faction, IngameDay, Note, Loot]
|
||||||
|
|
||||||
|
|
||||||
class SearchResultsView(TemplateView):
|
class SearchResultsView(TemplateView):
|
||||||
template_name = "search/search_results.jinja"
|
template_name = "search/search_results.jinja"
|
||||||
|
@ -27,16 +30,15 @@ class SearchResultsView(TemplateView):
|
||||||
name_vector = SearchVector('name', weight="A", config=config)
|
name_vector = SearchVector('name', weight="A", config=config)
|
||||||
description_vector = SearchVector("description_html", weight="B", config=config)
|
description_vector = SearchVector("description_html", weight="B", config=config)
|
||||||
query = SearchQuery(query_string, search_type='websearch', config=config)
|
query = SearchQuery(query_string, search_type='websearch', config=config)
|
||||||
models = [Location, Character, Faction, IngameDay, Note, Loot]
|
|
||||||
all_results = []
|
all_results = []
|
||||||
all_similar = []
|
all_similar = []
|
||||||
for m in models:
|
for m in search_models:
|
||||||
if m == IngameDay:
|
if m == IngameDay:
|
||||||
vector = description_vector
|
vector = description_vector
|
||||||
else:
|
else:
|
||||||
vector = description_vector + name_vector
|
vector = description_vector + name_vector
|
||||||
similar = m.objects.annotate(
|
similar = m.objects.annotate(
|
||||||
distance=TrigramWordSimilarity(query_string, "name")
|
distance=TrigramWordDistance(query_string, "name")
|
||||||
# distance=TrigramDistance("name", query_string)
|
# distance=TrigramDistance("name", query_string)
|
||||||
).filter(name__trigram_word_similar=query_string).order_by('distance')
|
).filter(name__trigram_word_similar=query_string).order_by('distance')
|
||||||
all_similar.extend(list(similar))
|
all_similar.extend(list(similar))
|
||||||
|
@ -58,3 +60,29 @@ class SearchResultsView(TemplateView):
|
||||||
context["similars"] = all_similar
|
context["similars"] = all_similar
|
||||||
context["query"] = query_string
|
context["query"] = query_string
|
||||||
return context
|
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)
|
||||||
|
|
25
static/js/autocomplete.js
Normal file
25
static/js/autocomplete.js
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
1
static/libs/autocomplete.min.js
vendored
Symbolic link
1
static/libs/autocomplete.min.js
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../node_modules/@trevoreyre/autocomplete-js/dist/autocomplete.min.js
|
71
static/scss/_autocomple.scss
Normal file
71
static/scss/_autocomple.scss
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
@import "node_modules/bootstrap/scss/bootstrap";
|
@import "node_modules/bootstrap/scss/bootstrap";
|
||||||
@import "node_modules/easymde/dist/easymde.min";
|
@import "node_modules/easymde/dist/easymde.min";
|
||||||
@import "node_modules/luminous-lightbox/dist/luminous-basic";
|
@import "node_modules/luminous-lightbox/dist/luminous-basic";
|
||||||
|
@import "autocomple";
|
||||||
|
|
||||||
@import "misc";
|
@import "misc";
|
||||||
@import "avatar";
|
@import "avatar";
|
||||||
|
|
|
@ -48,7 +48,6 @@
|
||||||
<script nonce="{{ request.csp_nonce }}">
|
<script nonce="{{ request.csp_nonce }}">
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const galleries = document.querySelectorAll("a.image-viewer");
|
const galleries = document.querySelectorAll("a.image-viewer");
|
||||||
console.info(galleries)
|
|
||||||
new LuminousGallery(galleries)
|
new LuminousGallery(galleries)
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -45,9 +45,12 @@
|
||||||
</ul>
|
</ul>
|
||||||
<form class="d-flex" action="{{ url("search") }}">
|
<form class="d-flex" action="{{ url("search") }}">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" name="q" type="search"
|
<div id="autocomplete" class="autocomplete">
|
||||||
|
<input class="form-control autocomplete-input" name="q" type="search"
|
||||||
placeholder="{% trans %}Search{% endtrans %}"
|
placeholder="{% trans %}Search{% endtrans %}"
|
||||||
aria-label="{% trans %}Search{% endtrans %}">
|
aria-label="{% trans %}Search{% endtrans %}">
|
||||||
|
<ul class="autocomplete-result-list"></ul>
|
||||||
|
</div>
|
||||||
<button class="btn btn-outline-secondary" type="submit">
|
<button class="btn btn-outline-secondary" type="submit">
|
||||||
{% trans %}Go!{% endtrans %}
|
{% trans %}Go!{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -83,3 +86,8 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="/static/libs/autocomplete.min.js"></script>
|
||||||
|
<script src="/static/js/autocomplete.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in a new issue