WIP: implement search using htmx
This commit is contained in:
parent
ff212d6e15
commit
fb27053295
12 changed files with 153 additions and 105 deletions
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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");
|
||||
|
|
20
mwmbl/api.py
20
mwmbl/api.py
|
@ -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):
|
||||
|
|
|
@ -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
19
mwmbl/search_setup.py
Normal 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)
|
|
@ -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 %}
|
||||
|
|
0
mwmbl/templatetags/__init__.py
Normal file
0
mwmbl/templatetags/__init__.py
Normal file
18
mwmbl/templatetags/result_filters.py
Normal file
18
mwmbl/templatetags/result_filters.py
Normal 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))
|
|
@ -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")
|
||||
|
||||
]
|
||||
|
|
|
@ -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})
|
||||
|
|
Loading…
Add table
Reference in a new issue