Просмотр исходного кода

WIP: implement search using htmx

Daoud Clarke 1 год назад
Родитель
Сommit
fb27053295

+ 21 - 21
front-end/src/components/molecules/result.js

@@ -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();
   }
 

+ 24 - 48
front-end/src/components/organisms/results.js

@@ -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();
+

+ 40 - 0
front-end/src/index.html

@@ -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>

+ 1 - 1
front-end/src/index.js

@@ -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");

+ 1 - 19
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):

+ 1 - 1
mwmbl/apps.py

@@ -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 - 0
mwmbl/search_setup.py

@@ -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)

+ 17 - 14
mwmbl/templates/results.html

@@ -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 - 0
mwmbl/templatetags/__init__.py


+ 18 - 0
mwmbl/templatetags/result_filters.py

@@ -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))

+ 3 - 1
mwmbl/urls.py

@@ -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")
+
 ]

+ 8 - 0
mwmbl/views.py

@@ -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})