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

View File

@ -7,15 +7,22 @@ import emptyResult from '../molecules/empty-result.js';
import home from './home.js';
import escapeString from '../../utils/escapeString.js';
const template = () => /*html*/`
<ul class='results'>
<li is='${home}'></li>
</ul>
`;
// const template = () => /*html*/`
// <ul class='results'>
// <li is='${home}'></li>
// </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() {
super();
this.results = null;
this.oldIndex = null;
this.curating = false;
@ -23,50 +30,16 @@ export default define('results', class extends HTMLElement {
}
__setup() {
this.innerHTML = template();
this.results = this.querySelector('.results');
// this.innerHTML = template();
this.__events();
}
__events() {
globalBus.on('search', (e) => {
this.results.innerHTML = '';
let resultsHTML = '';
if (!e.detail.error) {
// If there is no details the input is empty
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;
document.body.addEventListener('htmx:load', e => {
// });
//
// globalBus.on('search', (e) => {
this.results = document.querySelector('.results');
// Allow the user to re-order search results
$(".results").sortable({
@ -236,4 +209,7 @@ export default define('results', class extends HTMLElement {
});
globalBus.dispatch(curationMoveEvent);
}
});
}
const resultsHandler = new ResultsHandler();

View File

@ -48,6 +48,8 @@
<!-- <mwmbl-register></mwmbl-register>-->
<mwmbl-app></mwmbl-app>
<noscript>
<!-- https://stackoverflow.com/a/431554 -->
<style> .jsonly { display: none } </style>
<main class="noscript">
<img class="brand-icon" src="/static/images/logo.svg" width="40" height="40" alt="mwmbl logo">
<h1>
@ -63,8 +65,46 @@
</p>
</main>
</noscript>
<!-- Javasript entrypoint -->
<script src="https://unpkg.com/htmx.org@1.9.6"></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>
</html>

View File

@ -14,7 +14,7 @@
if (!redirected) {
// Load components only after redirects are checked.
import('./components/app.js');
// import('./components/app.js');
import('./components/login.js');
import('./components/register.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.security import django_auth
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.search_setup import queued_batches, index_path, ranker, batch_cache
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):

View File

@ -13,7 +13,7 @@ class MwmblConfig(AppConfig):
def ready(self):
# 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.indexer.paths import INDEX_NAME
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 %}
<div class="result-container">
<div class="curation-buttons">
<button class="curation-button curate-delete" is="delete-button"></button>
<button class="curation-button curate-approve" is="validate-button"></button>
<button class="curation-button curate-add" is="add-button"></button>
<li class="result" is="mwmbl-result">
<div class="result-container">
<div class="curation-buttons">
<button class="curation-button curate-delete" is="mwmbl-delete-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 class="result-link">
<a href="{{result.url}}">
<p class='link'>{{result.url}}}</p>
<p class='title'>{{result.title}}</p>
<p class='extract'>{{result.extract}}</p>
</a>
</div>
</div>
{% end for %}
</li>
{% endfor %}

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 mwmbl.api import api_original as api, api_v1
from mwmbl.views import profile
from mwmbl.views import profile, search_results
urlpatterns = [
path('admin/', admin.site.urls),
@ -27,4 +27,6 @@ urlpatterns = [
path('accounts/', include('allauth.urls')),
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.shortcuts import render
from mwmbl.search_setup import ranker
@login_required
def profile(request):
return render(request, 'profile.html')
def search_results(request):
query = request.GET["query"]
results = ranker.search(query)
return render(request, "results.html", {"results": results})