Compare commits

..

2 commits

Author SHA1 Message Date
e709b3cea5
New configuration: feature_search_suggestions
Default true. Set this to false to disable search suggestions for the
whole instance.
2022-10-23 00:35:34 +13:00
07db44e732 Add search suggestions 2022-10-23 00:26:27 +13:00
6 changed files with 190 additions and 4 deletions

View file

@ -1,5 +1,6 @@
#lang racket/base #lang racket/base
(require racket/string (require racket/string
json
(prefix-in easy: net/http-easy) (prefix-in easy: net/http-easy)
html-writing html-writing
web-server/http web-server/http
@ -94,7 +95,13 @@
,@(map (λ (url) ,@(map (λ (url)
`(link (@ (rel "stylesheet") (type "text/css") (href ,url)))) `(link (@ (rel "stylesheet") (type "text/css") (href ,url))))
(required-styles (format "https://~a.fandom.com" wikiname))) (required-styles (format "https://~a.fandom.com" wikiname)))
(link (@ (rel "stylesheet") (type "text/css") (href "/static/main.css")))) (link (@ (rel "stylesheet") (type "text/css") (href "/static/main.css")))
(script "const BWData = "
,(jsexpr->string (hasheq 'wikiname wikiname
'strict_proxy (config-true? 'strict_proxy))))
,(if (config-true? 'feature_search_suggestions)
'(script (@ (type "module") (src "/static/search-suggestions.js")))
""))
(body (@ (class ,body-class)) (body (@ (class ,body-class))
(div (@ (class "main-container")) (div (@ (class "main-container"))
(div (@ (class "fandom-community-header__background tileHorizontally header"))) (div (@ (class "fandom-community-header__background tileHorizontally header")))
@ -103,9 +110,12 @@
(div (@ (class "custom-top")) (div (@ (class "custom-top"))
(h1 (@ (class "page-title")) ,title) (h1 (@ (class "page-title")) ,title)
(nav (@ (class "sitesearch")) (nav (@ (class "sitesearch"))
(form (@ (action ,(format "/~a/search" wikiname))) (form (@ (action ,(format "/~a/search" wikiname))
(label "Search " (class "bw-search-form")
(input (@ (type "text") (name "q"))))))) (id "bw-pr-search"))
(label (@ (for "bw-search-input")) "Search ")
(input (@ (type "text") (name "q") (id "bw-search-input") (autocomplete "off")))
(div (@ (class "bw-ss__container"))))))
(div (@ (id "content") #;(class "page-content")) (div (@ (id "content") #;(class "page-content"))
(div (@ (id "mw-content-text")) (div (@ (id "mw-content-text"))
,content)) ,content))

View file

@ -34,6 +34,7 @@
'((application_name . "BreezeWiki") '((application_name . "BreezeWiki")
(canonical_origin . "") (canonical_origin . "")
(debug . "false") (debug . "false")
(feature_search_suggestions . "true")
(instance_is_official . "false") ; please don't turn this on, or you will make me very upset (instance_is_official . "false") ; please don't turn this on, or you will make me very upset
(log_outgoing . "true") (log_outgoing . "true")
(port . "10416") (port . "10416")

View file

@ -19,6 +19,7 @@
(define hash-ext-mime-type (define hash-ext-mime-type
(hash #".css" #"text/css" (hash #".css" #"text/css"
#".js" #"text/javascript"
#".png" #"image/png" #".png" #"image/png"
#".svg" #"image/svg+xml" #".svg" #"image/svg+xml"
#".txt" #"text/plain")) #".txt" #"text/plain"))

View file

@ -225,6 +225,66 @@ figcaption, .lightbox-caption, .thumbcaption {
margin-left: 1.2em; margin-left: 1.2em;
} }
/* (breezewiki) search suggestions */
.bw-search-form {
display: grid;
grid-template-columns: auto 1fr;
grid-gap: 0px 5px;
}
.bw-ss__container {
grid-column: 2;
position: relative;
}
.bw-ss__list {
position: absolute;
left: 0;
right: 0;
list-style-type: none;
padding: 0;
margin: 0;
font-size: 14px;
background: white;
color: black;
border: solid #808080;
border-width: 0px 1px 1px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
z-index: 99;
display: none;
}
.bw-ss__list--focus {
display: block;
}
.bw-ss__list--loading {
background: #c0c0c0;
}
.bw-ss__input--accepted {
background: #fffbc0;
}
.bw-ss__preview {
padding: 0px 2px;
font-style: italic;
color: #555;
}
.bw-ss__item {
display: grid; /* make buttons take the full size */
}
.bw-ss__item:hover {
background-color: #ddd;
}
.bw-ss__button {
appearance: none;
-moz-appearance: none;
border: none;
margin: 0;
line-height: inherit;
background: none;
font: inherit;
cursor: pointer;
padding: 0px 2px;
text-align: left;
}
/* media queries */ /* media queries */
/* for reference, cell phone screens are generally 400 px wide, definitely less than 500 px */ /* for reference, cell phone screens are generally 400 px wide, definitely less than 500 px */

21
static/preact.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,93 @@
import {h, htm, render, useState, useEffect, createContext, useContext} from "./preact.js"
const html = htm.bind(h)
const classNames = classArr => classArr.filter(el => el).join(" ")
const form = document.getElementById("bw-pr-search")
const AcceptSuggestion = createContext(null)
const hitsPromise = new Map()
const hitsDone = new Set()
function Suggestion(props) {
const acceptSuggestion = useContext(AcceptSuggestion)
return html`<li class="bw-ss__item"><button type="button" class="bw-ss__button" onClick=${() => acceptSuggestion(props)}>${props.title}</button></li>`
}
function fetchSuggestions(query, setSuggestions) {
if (query === "") query = "\0"
if (hitsPromise.has(query)) return hitsPromise.get(query)
const url = new URL(`https://${BWData.wikiname}.fandom.com/api.php`)
url.searchParams.set("action", "opensearch")
url.searchParams.set("format", "json")
url.searchParams.set("namespace", "0") // wiki namespace, 0 is default
url.searchParams.set("origin", "*") // mediawiki api cors
url.searchParams.set("search", query)
const sendUrl = BWData.strict_proxy
? "/proxy?" + new URLSearchParams({dest: url})
: url
const promise = fetch(sendUrl).then(res => res.json()).then(root => {
hitsDone.add(query)
return Array(root[1].length).fill().map((_, i) => ({
title: root[1][i],
url: root[3][i]
}))
})
hitsPromise.set(query, promise)
return promise
}
function SuggestionList(props) {
return html`
<div class="bw-ss__container">
<ul class=${classNames(["bw-ss__list", props.focus && "bw-ss__list--focus", `bw-ss__list--${props.st}`])}>
${props.hits.map(hit => html`<${Suggestion} ...${hit} />`)}
</ul>
</div>`
}
function ControlledInput() {
const [query, setQuery] = useState("")
const [focus, setFocus] = useState(false)
const [st, setSt] = useState("ready")
const [suggestions, setSuggestions] = useState([])
useEffect(() => {
if (st === "accepted") return
setSt("loading")
fetchSuggestions(query).then(s => {
setSuggestions(s)
if (hitsDone.size === hitsPromise.size) {
setSt("ready")
}
})
}, [query])
function acceptSuggestion(suggestion) {
setQuery(suggestion.title)
setSt("accepted")
const dest = new URL(suggestion.url).pathname.match("/wiki/.*")
location = `/${BWData.wikiname}${dest}`
}
useEffect(() => {
function listener(event) {
if (event.type === "focusin") setFocus(true)
else setFocus(false)
}
form.addEventListener("focusin", listener)
form.addEventListener("focusout", listener)
return () => {
form.removeEventListener("focusin", listener)
form.removeEventListener("focusout", listener)
}
})
return html`
<${AcceptSuggestion.Provider} value=${acceptSuggestion}>
<label for="bw-search-input">Search </label>
<input type="text" name="q" id="bw-search-input" autocomplete="off" onInput=${e => setQuery(e.target.value)} value=${query} class=${classNames(["bw-ss__input", `bw-ss__input--${st}`])} />
<${SuggestionList} hits=${suggestions} focus=${focus} st=${st}/>
</${AcceptSuggestion.Provider}`
}
render(html`<${ControlledInput} />`, form)