WIP: implement search using htmx

This commit is contained in:
Daoud Clarke 2023-10-30 08:53:25 +00:00
parent ff212d6e15
commit fb27053295
12 changed files with 153 additions and 105 deletions

View file

@ -5,22 +5,22 @@ import deleteButton from "./delete-button.js";
import validateButton from "./validate-button.js"; import validateButton from "./validate-button.js";
import addButton from "./add-button.js"; import addButton from "./add-button.js";
const template = ({ data }) => /*html*/` // const template = ({ data }) => /*html*/`
<div class="result-container"> // <div class="result-container">
<div class="curation-buttons"> // <div class="curation-buttons">
<button class="curation-button curate-delete" is="${deleteButton}"></button> // <button class="curation-button curate-delete" is="${deleteButton}">✕</button>
<button class="curation-button curate-approve" is="${validateButton}"></button> // <button class="curation-button curate-approve" is="${validateButton}">✓</button>
<button class="curation-button curate-add" is="${addButton}"></button> // <button class="curation-button curate-add" is="${addButton}"></button>
</div> // </div>
<div class="result-link"> // <div class="result-link">
<a href='${data.url}'> // <a href='${data.url}'>
<p class='link'>${data.url}</p> // <p class='link'>${data.url}</p>
<p class='title'>${data.title}</p> // <p class='title'>${data.title}</p>
<p class='extract'>${data.extract}</p> // <p class='extract'>${data.extract}</p>
</a> // </a>
</div> // </div>
</div> // </div>
`; // `;
export default define('result', class extends HTMLLIElement { export default define('result', class extends HTMLLIElement {
constructor() { constructor() {
@ -30,11 +30,11 @@ export default define('result', class extends HTMLLIElement {
} }
__setup() { __setup() {
this.innerHTML = template({ data: { // this.innerHTML = template({ data: {
url: this.dataset.url, // url: this.dataset.url,
title: this.__handleBold(JSON.parse(this.dataset.title)), // title: this.__handleBold(JSON.parse(this.dataset.title)),
extract: this.__handleBold(JSON.parse(this.dataset.extract)) // extract: this.__handleBold(JSON.parse(this.dataset.extract))
}}); // }});
this.__events(); this.__events();
} }

View file

@ -7,15 +7,22 @@ import emptyResult from '../molecules/empty-result.js';
import home from './home.js'; import home from './home.js';
import escapeString from '../../utils/escapeString.js'; import escapeString from '../../utils/escapeString.js';
const template = () => /*html*/` // const template = () => /*html*/`
<ul class='results'> // <ul class='results'>
<li is='${home}'></li> // <li is='${home}'></li>
</ul> // </ul>
`; // `;
export default define('results', class extends HTMLElement {
document.body.addEventListener('htmx:load', function(evt) {
});
// export default define('results', class extends HTMLElement {
class ResultsHandler {
constructor() { constructor() {
super();
this.results = null; this.results = null;
this.oldIndex = null; this.oldIndex = null;
this.curating = false; this.curating = false;
@ -23,50 +30,16 @@ export default define('results', class extends HTMLElement {
} }
__setup() { __setup() {
this.innerHTML = template(); // this.innerHTML = template();
this.results = this.querySelector('.results');
this.__events(); this.__events();
} }
__events() { __events() {
globalBus.on('search', (e) => { document.body.addEventListener('htmx:load', e => {
this.results.innerHTML = ''; // });
let resultsHTML = ''; //
if (!e.detail.error) { // globalBus.on('search', (e) => {
// If there is no details the input is empty this.results = document.querySelector('.results');
if (!e.detail.results) {
resultsHTML = /*html*/`
<li is='${home}'></li>
`;
}
// If the details array has results display them
else if (e.detail.results.length > 0) {
for(const resultData of e.detail.results) {
resultsHTML += /*html*/`
<li
is='${result}'
data-url='${escapeString(resultData.url)}'
data-title='${escapeString(JSON.stringify(resultData.title))}'
data-extract='${escapeString(JSON.stringify(resultData.extract))}'
></li>
`;
}
}
// If the details array is empty there is no result
else {
resultsHTML = /*html*/`
<li is='${emptyResult}'></li>
`;
}
}
else {
// If there is an error display an empty result
resultsHTML = /*html*/`
<li is='${emptyResult}'></li>
`;
}
// Bind HTML to the DOM
this.results.innerHTML = resultsHTML;
// Allow the user to re-order search results // Allow the user to re-order search results
$(".results").sortable({ $(".results").sortable({
@ -236,4 +209,7 @@ export default define('results', class extends HTMLElement {
}); });
globalBus.dispatch(curationMoveEvent); globalBus.dispatch(curationMoveEvent);
} }
}); }
const resultsHandler = new ResultsHandler();

View file

@ -48,6 +48,8 @@
<!-- <mwmbl-register></mwmbl-register>--> <!-- <mwmbl-register></mwmbl-register>-->
<mwmbl-app></mwmbl-app> <mwmbl-app></mwmbl-app>
<noscript> <noscript>
<!-- https://stackoverflow.com/a/431554 -->
<style> .jsonly { display: none } </style>
<main class="noscript"> <main class="noscript">
<img class="brand-icon" src="/static/images/logo.svg" width="40" height="40" alt="mwmbl logo"> <img class="brand-icon" src="/static/images/logo.svg" width="40" height="40" alt="mwmbl logo">
<h1> <h1>
@ -63,8 +65,46 @@
</p> </p>
</main> </main>
</noscript> </noscript>
<!-- Javasript entrypoint --> <!-- Javasript entrypoint -->
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
<script src="./index.js" type="module"></script> <script src="./index.js" type="module"></script>
<main class="jsonly">
<header class="search-menu">
<ul>
<li is="${save}"></li>
</ul>
<div><a href="/accounts/login/">Login</a> <a href="/accounts/signup/">Sign up</a> </div>
<div class="branding">
<img class="brand-icon" src="/static/images/logo.svg" width="40" height="40" alt="mwmbl logo">
<span class="brand-title">MWMBL</span>
</div>
<form class="search-bar">
<i class="ph-magnifying-glass-bold"></i>
<input
type='search'
name='query'
class='search-bar-input'
placeholder='Search on mwmbl...'
title='Use "CTRL+K" or "/" to focus.'
autocomplete='off'
hx-get="/app/search/"
hx-trigger="keyup changed delay:100ms"
hx-target=".results"
>
</form>
</header>
<main>
<mwmbl-results>
<ul class='results'>
<li is='${home}'></li>
</ul>
</mwmbl-results>
</main>
<div is="${addResult}"></div>
<footer is="mwmbl-footer"></footer>
</main>
</body> </body>
</html> </html>

View file

@ -14,7 +14,7 @@
if (!redirected) { if (!redirected) {
// Load components only after redirects are checked. // Load components only after redirects are checked.
import('./components/app.js'); // import('./components/app.js');
import('./components/login.js'); import('./components/login.js');
import('./components/register.js'); import('./components/register.js');
import("./components/organisms/search-bar.js"); import("./components/organisms/search-bar.js");

View file

@ -1,28 +1,10 @@
from multiprocessing import Queue
from pathlib import Path
from django.conf import settings
from ninja import NinjaAPI from ninja import NinjaAPI
from ninja.security import django_auth from ninja.security import django_auth
import mwmbl.crawler.app as crawler import mwmbl.crawler.app as crawler
from mwmbl.indexer.batch_cache import BatchCache
from mwmbl.indexer.paths import INDEX_NAME, BATCH_DIR_NAME
from mwmbl.platform import curate from mwmbl.platform import curate
from mwmbl.search_setup import queued_batches, index_path, ranker, batch_cache
from mwmbl.tinysearchengine import search from mwmbl.tinysearchengine import search
from mwmbl.tinysearchengine.completer import Completer
from mwmbl.tinysearchengine.indexer import TinyIndex, Document
from mwmbl.tinysearchengine.rank import HeuristicRanker
queued_batches = Queue()
completer = Completer()
index_path = Path(settings.DATA_PATH) / INDEX_NAME
tiny_index = TinyIndex(item_factory=Document, index_path=index_path)
tiny_index.__enter__()
ranker = HeuristicRanker(tiny_index, completer)
batch_cache = BatchCache(Path(settings.DATA_PATH) / BATCH_DIR_NAME)
def create_api(version): def create_api(version):

View file

@ -13,7 +13,7 @@ class MwmblConfig(AppConfig):
def ready(self): def ready(self):
# Imports here to avoid AppRegistryNotReady exception # Imports here to avoid AppRegistryNotReady exception
from mwmbl.api import queued_batches from mwmbl.search_setup import queued_batches
from mwmbl import background from mwmbl import background
from mwmbl.indexer.paths import INDEX_NAME from mwmbl.indexer.paths import INDEX_NAME
from mwmbl.indexer.update_urls import update_urls_continuously from mwmbl.indexer.update_urls import update_urls_continuously

19
mwmbl/search_setup.py Normal file
View file

@ -0,0 +1,19 @@
from multiprocessing import Queue
from pathlib import Path
from django.conf import settings
from mwmbl.indexer.batch_cache import BatchCache
from mwmbl.indexer.paths import INDEX_NAME, BATCH_DIR_NAME
from mwmbl.tinysearchengine.completer import Completer
from mwmbl.tinysearchengine.indexer import TinyIndex, Document
from mwmbl.tinysearchengine.rank import HeuristicRanker
queued_batches = Queue()
completer = Completer()
index_path = Path(settings.DATA_PATH) / INDEX_NAME
tiny_index = TinyIndex(item_factory=Document, index_path=index_path)
tiny_index.__enter__()
ranker = HeuristicRanker(tiny_index, completer)
batch_cache = BatchCache(Path(settings.DATA_PATH) / BATCH_DIR_NAME)

View file

@ -1,16 +1,19 @@
{% load result_filters %}
{% for result in results %} {% for result in results %}
<div class="result-container"> <li class="result" is="mwmbl-result">
<div class="curation-buttons"> <div class="result-container">
<button class="curation-button curate-delete" is="delete-button"></button> <div class="curation-buttons">
<button class="curation-button curate-approve" is="validate-button"></button> <button class="curation-button curate-delete" is="mwmbl-delete-button"></button>
<button class="curation-button curate-add" is="add-button"></button> <button class="curation-button curate-approve" is="mwmbl-validate-button"></button>
<button class="curation-button curate-add" is="mwmbl-add-button"></button>
</div>
<div class="result-link">
<a href="{{result.url}}">
<p class='link'>{{result.url}}</p>
<p class='title'>{{result.title|strengthen}}</p>
<p class='extract'>{{result.extract|strengthen}}</p>
</a>
</div>
</div> </div>
<div class="result-link"> </li>
<a href="{{result.url}}"> {% endfor %}
<p class='link'>{{result.url}}}</p>
<p class='title'>{{result.title}}</p>
<p class='extract'>{{result.extract}}</p>
</a>
</div>
</div>
{% end for %}

View file

View file

@ -0,0 +1,18 @@
from django.template import Library
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
register = Library()
@register.filter(needs_autoescape=True)
def strengthen(spans, autoescape=True):
escape = conditional_escape if autoescape else lambda x: x
strengthened = []
for span in spans:
escaped_value = escape(span["value"])
if span["is_bold"]:
strengthened.append(f"<strong>{escaped_value}</strong>")
else:
strengthened.append(escaped_value)
return mark_safe("".join(strengthened))

View file

@ -18,7 +18,7 @@ from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from mwmbl.api import api_original as api, api_v1 from mwmbl.api import api_original as api, api_v1
from mwmbl.views import profile from mwmbl.views import profile, search_results
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@ -27,4 +27,6 @@ urlpatterns = [
path('accounts/', include('allauth.urls')), path('accounts/', include('allauth.urls')),
path('accounts/profile/', profile, name='profile'), path('accounts/profile/', profile, name='profile'),
path('app/search/', search_results, name="search_results")
] ]

View file

@ -1,7 +1,15 @@
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.shortcuts import render from django.shortcuts import render
from mwmbl.search_setup import ranker
@login_required @login_required
def profile(request): def profile(request):
return render(request, 'profile.html') return render(request, 'profile.html')
def search_results(request):
query = request.GET["query"]
results = ranker.search(query)
return render(request, "results.html", {"results": results})