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 | ||||
| (require racket/string | ||||
|          json | ||||
|          (prefix-in easy: net/http-easy) | ||||
|          html-writing | ||||
|          web-server/http | ||||
|  | @ -94,7 +95,11 @@ | |||
|      ,@(map (λ (url) | ||||
|               `(link (@ (rel "stylesheet") (type "text/css") (href ,url)))) | ||||
|             (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)) | ||||
|           (div (@ (class "main-container")) | ||||
|                (div (@ (class "fandom-community-header__background tileHorizontally header"))) | ||||
|  | @ -103,9 +108,12 @@ | |||
|                           (div (@ (class "custom-top")) | ||||
|                                (h1 (@ (class "page-title")) ,title) | ||||
|                                (nav (@ (class "sitesearch")) | ||||
|                                     (form (@ (action ,(format "/~a/search" wikiname))) | ||||
|                                           (label "Search " | ||||
|                                                  (input (@ (type "text") (name "q"))))))) | ||||
|                                     (form (@ (action ,(format "/~a/search" wikiname)) | ||||
|                                              (class "bw-search-form") | ||||
|                                              (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 "mw-content-text")) | ||||
|                                     ,content)) | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ | |||
| 
 | ||||
| (define hash-ext-mime-type | ||||
|   (hash #".css" #"text/css" | ||||
|         #".js" #"text/javascript" | ||||
|         #".png" #"image/png" | ||||
|         #".svg" #"image/svg+xml" | ||||
|         #".txt" #"text/plain")) | ||||
|  |  | |||
|  | @ -225,6 +225,66 @@ figcaption, .lightbox-caption, .thumbcaption { | |||
|     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 */ | ||||
| 
 | ||||
| /* 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