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