forked from cadence/breezewiki
		
	Add search suggestions
This commit is contained in:
		
							parent
							
								
									71705d6e74
								
							
						
					
					
						commit
						07db44e732
					
				
					 5 changed files with 187 additions and 4 deletions
				
			
		| 
						 | 
					@ -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,11 @@
 | 
				
			||||||
     ,@(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))))
 | 
				
			||||||
 | 
					     (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 +108,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))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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"))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
									
								
							
							
						
						
									
										21
									
								
								static/preact.js
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										93
									
								
								static/search-suggestions.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								static/search-suggestions.js
									
										
									
									
									
										Normal 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)
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue