added autocomplete
This commit is contained in:
		
							
								
								
									
										225
									
								
								api/v1/ac.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								api/v1/ac.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,225 @@ | ||||
| <?php | ||||
|  | ||||
| new autocomplete(); | ||||
|  | ||||
| class autocomplete{ | ||||
| 	 | ||||
| 	public function __construct(){ | ||||
| 		 | ||||
| 		header("Content-Type: application/json"); | ||||
| 		 | ||||
| 		$this->scrapers = [ | ||||
| 			"brave" => "https://search.brave.com/api/suggest?q={searchTerms}", | ||||
| 			"ddg" => "https://duckduckgo.com/ac/?q={searchTerms}&type=list", | ||||
| 			"yandex" => "https://suggest.yandex.com/suggest-ff.cgi?part={searchTerms}&uil=en&v=3&sn=5&lr=21276&yu=4861394161661655015", | ||||
| 			"google" => "https://www.google.com/complete/search?client=mobile-gws-lite&q={searchTerms}", | ||||
| 			"qwant" => "https://api.qwant.com/v3/suggest/?q={searchTerms}&client=opensearch", | ||||
| 			"yep" => "https://api.yep.com/ac/?query={searchTerms}", | ||||
| 			"marginalia" => "https://search.marginalia.nu/suggest/?partial={searchTerms}", | ||||
| 			"yt" => "https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&q={searchTerms}", | ||||
| 			"sc" => "https://api-v2.soundcloud.com/search/queries?q={searchTerms}&client_id=iMxZgT5mfGstBj8GWJbYMvpzelS8ne0E&limit=10&offset=0&linked_partitioning=1&app_version=1693487844&app_locale=en" | ||||
| 		]; | ||||
| 		 | ||||
| 		/* | ||||
| 			Sanitize input | ||||
| 		*/ | ||||
| 		if(!isset($_GET["s"])){ | ||||
| 			 | ||||
| 			$this->do404("Missing search(s) parameter"); | ||||
| 		} | ||||
| 		 | ||||
| 		if(is_string($_GET["s"]) === false){ | ||||
| 			 | ||||
| 			$this->do404("Invalid search(s) parameter"); | ||||
| 		} | ||||
| 		 | ||||
| 		if(strlen($_GET["s"]) > 500){ | ||||
| 			 | ||||
| 			$this->do404("Search(s) exceeds the 500 char length"); | ||||
| 		} | ||||
| 		 | ||||
| 		if( | ||||
| 			isset($_GET["scraper"]) && | ||||
| 			is_string($_GET["scraper"]) === false | ||||
| 		){ | ||||
| 			 | ||||
| 			$_GET["scraper"] = "brave"; // default option | ||||
| 		} | ||||
| 		 | ||||
| 		/* | ||||
| 			Get $scraper | ||||
| 		*/ | ||||
| 		if(!isset($_GET["scraper"])){ | ||||
| 			 | ||||
| 			if(isset($_COOKIE["scraper_ac"])){ | ||||
| 				 | ||||
| 				$scraper = $_COOKIE["scraper_ac"]; | ||||
| 			}else{ | ||||
| 				 | ||||
| 				$scraper = "brave"; // default option | ||||
| 			} | ||||
| 		}else{ | ||||
| 			 | ||||
| 			$scraper = $_GET["scraper"]; | ||||
| 		} | ||||
| 		 | ||||
| 		if($scraper == "disabled"){ | ||||
| 			 | ||||
| 			// this shouldnt happen, but let's handle it anyways | ||||
| 			$this->doempty(); | ||||
| 		} | ||||
| 		 | ||||
| 		// make sure it exists | ||||
| 		if(!isset($this->scrapers[$scraper])){ | ||||
| 			 | ||||
| 			$scraper = "brave"; // default option | ||||
| 		} | ||||
| 		 | ||||
| 		// return results | ||||
| 		 | ||||
| 		switch($scraper){ | ||||
| 			 | ||||
| 			case "google": | ||||
| 			case "yt": | ||||
| 				// handle google cause they want to be a special snowflake :( | ||||
| 				$js = $this->get($this->scrapers[$scraper], $_GET["s"]); | ||||
| 				 | ||||
| 				preg_match( | ||||
| 					'/\((\[.*\])\)/', | ||||
| 					$js, | ||||
| 					$js | ||||
| 				); | ||||
| 				 | ||||
| 				if(!isset($js[1])){ | ||||
| 					 | ||||
| 					$this->doempty(); | ||||
| 				} | ||||
| 				 | ||||
| 				$js = json_decode($js[1]); | ||||
| 				$json = []; | ||||
| 				 | ||||
| 				foreach($js[1] as $item){ | ||||
| 					 | ||||
| 					$json[] = strip_tags($item[0]); | ||||
| 				} | ||||
| 				 | ||||
| 				echo json_encode( | ||||
| 					[ | ||||
| 						$_GET["s"], | ||||
| 						$json | ||||
| 					] | ||||
| 				); | ||||
| 				break; | ||||
| 			 | ||||
| 			case "sc": | ||||
| 				// soundcloud | ||||
| 				$js = $this->get($this->scrapers[$scraper], $_GET["s"]); | ||||
| 				 | ||||
| 				$js = json_decode($js, true); | ||||
| 				 | ||||
| 				if(!isset($js["collection"])){ | ||||
| 					 | ||||
| 					$this->doempty(); | ||||
| 				} | ||||
| 				 | ||||
| 				$json = []; | ||||
| 				foreach($js["collection"] as $item){ | ||||
| 					 | ||||
| 					$json[] = $item["query"]; | ||||
| 				} | ||||
| 				 | ||||
| 				echo json_encode( | ||||
| 					[ | ||||
| 						$_GET["s"], | ||||
| 						$json | ||||
| 					] | ||||
| 				); | ||||
| 				break; | ||||
| 			 | ||||
| 			case "marginalia": | ||||
| 				$json = $this->get($this->scrapers[$scraper], $_GET["s"]); | ||||
| 				 | ||||
| 				$json = json_decode($json, true); | ||||
| 				if($json === null){ | ||||
| 					 | ||||
| 					 | ||||
| 					$this->doempty(); | ||||
| 				} | ||||
| 				 | ||||
| 				echo json_encode( | ||||
| 					[ | ||||
| 						$_GET["s"], | ||||
| 						$json | ||||
| 					] | ||||
| 				); | ||||
| 				break; | ||||
| 			 | ||||
| 			default: | ||||
| 				// if it respects the openSearch protocol | ||||
| 				$json = json_decode($this->get($this->scrapers[$scraper], $_GET["s"]), true); | ||||
| 				 | ||||
| 				echo json_encode( | ||||
| 					[ | ||||
| 						$_GET["s"], | ||||
| 						$json[1] // ensure it contains valid key 0 | ||||
| 					] | ||||
| 				); | ||||
| 				break; | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	private function get($url, $query){ | ||||
| 		 | ||||
| 		$curlproc = curl_init(); | ||||
| 		 | ||||
| 		$url = str_replace("{searchTerms}", urlencode($query), $url); | ||||
| 		 | ||||
| 		curl_setopt($curlproc, CURLOPT_URL, $url); | ||||
| 		 | ||||
| 		curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding | ||||
| 		curl_setopt($curlproc, CURLOPT_HTTPHEADER, | ||||
| 			["User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0", | ||||
| 			"Accept: application/json, text/javascript, */*; q=0.01", | ||||
| 			"Accept-Language: en-US,en;q=0.5", | ||||
| 			"Accept-Encoding: gzip", | ||||
| 			"DNT: 1", | ||||
| 			"Connection: keep-alive", | ||||
| 			"Sec-Fetch-Dest: empty", | ||||
| 			"Sec-Fetch-Mode: cors", | ||||
| 			"Sec-Fetch-Site: same-site"] | ||||
| 		); | ||||
| 		 | ||||
| 		curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true); | ||||
| 		curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2); | ||||
| 		curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true); | ||||
| 		curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30); | ||||
| 		curl_setopt($curlproc, CURLOPT_TIMEOUT, 30); | ||||
| 		 | ||||
| 		$data = curl_exec($curlproc); | ||||
| 		 | ||||
| 		if(curl_errno($curlproc)){ | ||||
| 			 | ||||
| 			throw new Exception(curl_error($curlproc)); | ||||
| 		} | ||||
| 		 | ||||
| 		curl_close($curlproc); | ||||
| 		return $data; | ||||
| 	} | ||||
| 	 | ||||
| 	private function do404($error){ | ||||
| 		 | ||||
| 		echo json_encode(["error" => $error]); | ||||
| 		die(); | ||||
| 	} | ||||
| 	 | ||||
| 	private function doempty(){ | ||||
| 		 | ||||
| 		echo json_encode( | ||||
| 			[ | ||||
| 				$_GET["s"], | ||||
| 				[] | ||||
| 			] | ||||
| 		); | ||||
| 		die(); | ||||
| 	} | ||||
| } | ||||
| @@ -574,8 +574,6 @@ class brave{ | ||||
| 						} | ||||
| 					} | ||||
| 					 | ||||
| 					echo "test"; | ||||
| 					 | ||||
| 					if($rating !== null){ | ||||
| 						 | ||||
| 						$table["Rating"] = $rating; | ||||
|   | ||||
| @@ -1616,21 +1616,23 @@ class google{ | ||||
| 					$imgvl | ||||
| 				); | ||||
| 				 | ||||
| 				$imgvl = $imgvl[1]; | ||||
| 				 | ||||
| 				$params["async"] = "_id:islrg_c,_fmt:html"; | ||||
| 				$params["asearch"] = "ichunklite"; | ||||
| 				$params["ved"] = $ved; | ||||
| 				$params["vet"] = "1" . $ved . "..i"; | ||||
| 				$params["start"] = 100; | ||||
| 				$params["ijn"] = 1; | ||||
| 				$params["imgvl"] = $imgvl; | ||||
| 				 | ||||
| 				$out["npt"] = | ||||
| 					$this->nextpage->store( | ||||
| 						json_encode($params), | ||||
| 						"images" | ||||
| 					); | ||||
| 				if(isset($imgvl[1])){ | ||||
| 					$imgvl = $imgvl[1]; | ||||
| 					 | ||||
| 					$params["async"] = "_id:islrg_c,_fmt:html"; | ||||
| 					$params["asearch"] = "ichunklite"; | ||||
| 					$params["ved"] = $ved; | ||||
| 					$params["vet"] = "1" . $ved . "..i"; | ||||
| 					$params["start"] = 100; | ||||
| 					$params["ijn"] = 1; | ||||
| 					$params["imgvl"] = $imgvl; | ||||
| 					 | ||||
| 					$out["npt"] = | ||||
| 						$this->nextpage->store( | ||||
| 							json_encode($params), | ||||
| 							"images" | ||||
| 						); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
|   | ||||
| @@ -288,7 +288,7 @@ class sc{ | ||||
| 					 | ||||
| 					if(count($description) != 0){ | ||||
| 						 | ||||
| 						$description = $count . " songs. " . implode(", ", $description); | ||||
| 						$description = trim($count . " songs. " . implode(", ", $description)); | ||||
| 					} | ||||
| 					 | ||||
| 					if( | ||||
| @@ -320,7 +320,7 @@ class sc{ | ||||
| 					 | ||||
| 					$out["playlist"][] = [ | ||||
| 						"title" => $item["title"], | ||||
| 						"description" => $description, | ||||
| 						"description" => $this->limitstrlen($description), | ||||
| 						"author" => [ | ||||
| 							"name" => $item["user"]["username"], | ||||
| 							"url" => $item["user"]["permalink_url"], | ||||
| @@ -385,13 +385,14 @@ class sc{ | ||||
| 				"\n", | ||||
| 				wordwrap( | ||||
| 					str_replace( | ||||
| 						"\n", | ||||
| 						["\n\r", "\r\n", "\n", "\r"], | ||||
| 						" ", | ||||
| 						$text | ||||
| 					), | ||||
| 					300, | ||||
| 					"\n" | ||||
| 				) | ||||
| 				), | ||||
| 				2 | ||||
| 			)[0]; | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										91
									
								
								settings.php
									
									
									
									
									
								
							
							
						
						
									
										91
									
								
								settings.php
									
									
									
									
									
								
							| @@ -58,6 +58,56 @@ $settings = [ | ||||
| 	[ | ||||
| 		"name" => "Scrapers to use", | ||||
| 		"settings" => [ | ||||
| 			[ | ||||
| 				"description" => "Autocomplete<br><i>Picking <div class=\"code-inline\">Auto</div> changes the source dynamically depending of the page's scraper<br>Picking <div class=\"code-inline\">Disabled</div> disables this feature</i>", | ||||
| 				"parameter" => "scraper_ac", | ||||
| 				"options" => [ | ||||
| 					[ | ||||
| 						"value" => "disabled", | ||||
| 						"text" => "Disabled" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "auto", | ||||
| 						"text" => "Auto" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "brave", | ||||
| 						"text" => "Brave" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "ddg", | ||||
| 						"text" => "DuckDuckGo" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "yandex", | ||||
| 						"text" => "Yandex" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "google", | ||||
| 						"text" => "Google" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "qwant", | ||||
| 						"text" => "Qwant" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "yep", | ||||
| 						"text" => "Yep" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "marginalia", | ||||
| 						"text" => "Marginalia" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "yt", | ||||
| 						"text" => "YouTube" | ||||
| 					], | ||||
| 					[ | ||||
| 						"value" => "sc", | ||||
| 						"text" => "SoundCloud" | ||||
| 					] | ||||
| 				] | ||||
| 			], | ||||
| 			[ | ||||
| 				"description" => "Web", | ||||
| 				"parameter" => "scraper_web", | ||||
| @@ -183,8 +233,13 @@ $settings = [ | ||||
| if($_POST){ | ||||
|  | ||||
| 	$loop = &$_POST; | ||||
| }else{ | ||||
| }elseif(count($_GET) !== 0){ | ||||
| 	 | ||||
| 	// redirect user to front page | ||||
| 	$loop = &$_GET; | ||||
| 	header("Location: /"); | ||||
| 	 | ||||
| }else{ | ||||
| 	// refresh cookie dates | ||||
| 	$loop = &$_COOKIE; | ||||
| } | ||||
| @@ -245,7 +300,7 @@ echo | ||||
| 		'<head>' . | ||||
| 			'<meta http-equiv="Content-Type" content="text/html;charset=utf-8">' . | ||||
| 			'<title>Settings</title>' . | ||||
| 			'<link rel="stylesheet" href="/static/style.css?v3">' . | ||||
| 			'<link rel="stylesheet" href="/static/style.css?v4">' . | ||||
| 			'<meta name="viewport" content="width=device-width,initial-scale=1">' . | ||||
| 			'<meta name="robots" content="index,follow">' . | ||||
| 			'<link rel="icon" type="image/x-icon" href="/favicon.ico">' . | ||||
| @@ -260,14 +315,14 @@ $left = | ||||
| 		'By clicking <div class="code-inline">Update settings!</div>, a plaintext <div class="code-inline">key=value</div> cookie will be stored on your browser. When selecting a default setting, the parameter is removed from your cookies.'; | ||||
|  | ||||
| $c = count($_COOKIE); | ||||
| $code = ""; | ||||
|  | ||||
| if($c !== 0){ | ||||
| 	 | ||||
| 	$left .= | ||||
| 		'<br><br>Your current cookie looks like this:' . | ||||
| 		'<div class="code">'; | ||||
| 	 | ||||
| 	$code = ""; | ||||
| 	 | ||||
| 	$ca = 0; | ||||
| 	foreach($_COOKIE as $key => $value){ | ||||
| 		 | ||||
| @@ -326,17 +381,23 @@ $left .= | ||||
| 	'</div>' . | ||||
| 	'<div class="settings-submit">' . | ||||
| 		'<input type="submit" value="Update settings!">' . | ||||
| 		'<a href="../">< Return to main page</a>' . | ||||
| 		'<a href="../">< Return to front page</a>' . | ||||
| 	'</div>' . | ||||
| 	'</form>'; | ||||
|  | ||||
| echo | ||||
| 	$frontend->load( | ||||
| 		"search.html", | ||||
| 		[ | ||||
| 			"class" => "", | ||||
| 			"right-left" => "", | ||||
| 			"right-right" => "", | ||||
| 			"left" => $left | ||||
| 		] | ||||
| 	); | ||||
| if(count($_GET) === 0){ | ||||
|  | ||||
| 	echo | ||||
| 		$frontend->load( | ||||
| 			"search.html", | ||||
| 			[ | ||||
| 				"class" => "", | ||||
| 				"right-left" =>			 | ||||
| 					'<div class="infobox"><h2>Preference link</h2>Follow this link to auto-apply all cookies. Useful if your browser clears out cookies after a browsing session. Following this link will redirect you to the front page, unless no settings are set.<br><br>' . | ||||
| 					'<a href="settings' . rtrim("?" . str_replace("; ", "&", $code), "?") . '">Bookmark me!</a>' . | ||||
| 					'</div>', | ||||
| 				"right-right" => "", | ||||
| 				"left" => $left | ||||
| 			] | ||||
| 		); | ||||
| } | ||||
|   | ||||
							
								
								
									
										266
									
								
								static/client.js
									
									
									
									
									
								
							
							
						
						
									
										266
									
								
								static/client.js
									
									
									
									
									
								
							| @@ -660,15 +660,16 @@ function changeimage(event){ | ||||
| 	centerpopup(); | ||||
| } | ||||
|  | ||||
| /* | ||||
| 	Shortcuts | ||||
| */ | ||||
| var searchbox_wrapper = document.getElementsByClassName("searchbox"); | ||||
|  | ||||
| if(searchbox_wrapper.length !== 0){ | ||||
| 		 | ||||
| 	searchbox_wrapper = searchbox_wrapper[0]; | ||||
| 	var searchbox = searchbox_wrapper.getElementsByTagName("input")[1]; | ||||
|  | ||||
| 	 | ||||
| 	/* | ||||
| 		Textarea shortcuts | ||||
| 	*/ | ||||
| 	document.addEventListener("keydown", function(key){ | ||||
| 		 | ||||
| 		switch(key.keyCode){ | ||||
| @@ -695,4 +696,261 @@ if(searchbox_wrapper.length !== 0){ | ||||
| 				break; | ||||
| 		} | ||||
| 	}); | ||||
| 	 | ||||
| 	/* | ||||
| 		Autocompleter | ||||
| 	*/ | ||||
| 	if( // make sure the user wants it | ||||
| 		document.cookie.includes("scraper_ac=") && | ||||
| 		document.cookie.includes("scraper_ac=disabled") === false | ||||
| 	){ | ||||
| 		 | ||||
| 		var autocomplete_cache = []; | ||||
| 		var focuspos = -1; | ||||
| 		var list = []; | ||||
| 		var autocomplete_div = document.getElementsByClassName("autocomplete")[0]; | ||||
| 		 | ||||
| 		if( | ||||
| 			document.cookie.includes("scraper_ac=auto") && | ||||
| 			typeof scraper_dropdown != "undefined" | ||||
| 		){ | ||||
| 			 | ||||
| 			var ac_req_appendix = "&scraper=" + scraper_dropdown.value; | ||||
| 		}else{ | ||||
| 			 | ||||
| 			var ac_req_appendix = ""; | ||||
| 		} | ||||
| 		 | ||||
| 		function getsearchboxtext(){ | ||||
| 			 | ||||
| 			var value = | ||||
| 				searchbox.value | ||||
| 				.trim() | ||||
| 				.replace( | ||||
| 					/ +/g, | ||||
| 					" " | ||||
| 				) | ||||
| 				.toLowerCase(); | ||||
| 			 | ||||
| 			return value; | ||||
| 		} | ||||
| 		 | ||||
| 		searchbox.addEventListener("input", async function(){ | ||||
| 			 | ||||
| 			// ratelimit on input only | ||||
| 			// dont ratelimit if we already have res | ||||
| 			if(typeof autocomplete_cache[getsearchboxtext()] != "undefined"){ | ||||
| 				 | ||||
| 				await getac(); | ||||
| 			}else{ | ||||
| 				 | ||||
| 				await getac_ratelimit(); | ||||
| 			} | ||||
| 		}); | ||||
| 		 | ||||
| 		async function getac(){ | ||||
| 			 | ||||
| 			var curvalue = getsearchboxtext(); | ||||
| 			 | ||||
| 			if(curvalue == ""){ | ||||
| 				 | ||||
| 				// hide autocompleter | ||||
| 				autocomplete_div.style.display = "none"; | ||||
| 				return; | ||||
| 			} | ||||
| 			 | ||||
| 			if(typeof autocomplete_cache[curvalue] == "undefined"){ | ||||
| 				 | ||||
| 				/* | ||||
| 					Fetch autocomplete | ||||
| 				*/ | ||||
| 				// make sure we dont fetch same thing twice | ||||
| 				autocomplete_cache[curvalue] = []; | ||||
| 				 | ||||
| 				var res = await fetch("/api/v1/ac?s=" + encodeURIComponent(curvalue) + ac_req_appendix); | ||||
| 				var json = await res.json(); | ||||
| 				 | ||||
| 				autocomplete_cache[curvalue] = json[1]; | ||||
| 				 | ||||
| 				if(curvalue == getsearchboxtext()){ | ||||
| 					 | ||||
| 					render_ac(curvalue, autocomplete_cache[curvalue]); | ||||
| 				} | ||||
| 				return; | ||||
| 			} | ||||
| 			 | ||||
| 			render_ac(curvalue, autocomplete_cache[curvalue]);	 | ||||
| 		} | ||||
| 		 | ||||
| 		var ac_func = null; | ||||
| 		function getac_ratelimit(){ | ||||
| 			 | ||||
| 			return new Promise(async function(resolve, reject){ | ||||
| 				 | ||||
| 				if(ac_func !== null){ | ||||
| 					 | ||||
| 					clearTimeout(ac_func); | ||||
| 				}//else{ | ||||
| 					 | ||||
| 					// no ratelimits | ||||
| 					//getac(); | ||||
| 				//} | ||||
| 				 | ||||
| 				ac_func = | ||||
| 					setTimeout(function(){ | ||||
| 						 | ||||
| 						ac_func = null; | ||||
| 						getac(); // get results after 100ms of no keystroke | ||||
| 						resolve(); | ||||
| 					}, 300); | ||||
| 			}); | ||||
| 		} | ||||
| 		 | ||||
| 		function render_ac(query, list){ | ||||
| 			 | ||||
| 			if(list.length === 0){ | ||||
| 				 | ||||
| 				autocomplete_div.style.display = "none"; | ||||
| 				return; | ||||
| 			} | ||||
| 			 | ||||
| 			html = ""; | ||||
| 			 | ||||
| 			// prepare regex | ||||
| 			var highlight = query.split(" "); | ||||
| 			var regex = []; | ||||
| 			 | ||||
| 			for(var k=0; k<highlight.length; k++){ | ||||
| 				 | ||||
| 				// espace regex | ||||
| 				regex.push( | ||||
| 					highlight[k].replace(/[.*+?^${}()|[\]\\]/g, "\\$&") | ||||
| 				); | ||||
| 			} | ||||
| 			 | ||||
| 			regex = new RegExp(highlight.join("|"), "gi"); | ||||
| 			 | ||||
| 			for(var i=0; i<list.length; i++){ | ||||
| 				 | ||||
| 				html += | ||||
| 					'<div tabindex="0" class="entry" onclick="handle_entry_click(this);">' + | ||||
| 						htmlspecialchars( | ||||
| 							list[i] | ||||
| 						).replace( | ||||
| 							regex, | ||||
| 							'<u>$&</u>' | ||||
| 						) + | ||||
| 					'</div>'; | ||||
| 			} | ||||
| 			 | ||||
| 			autocomplete_div.innerHTML = html; | ||||
| 			autocomplete_div.style.display = "block"; | ||||
| 		} | ||||
| 		 | ||||
| 		var should_focus = false; | ||||
| 		document.addEventListener("keydown", function(event){ | ||||
| 			 | ||||
| 			if(event.key == "Escape"){ | ||||
| 				 | ||||
| 				document.activeElement.blur(); | ||||
| 				focuspos = -1; | ||||
| 				autocomplete_div.style.display = "none"; | ||||
| 				return; | ||||
| 			} | ||||
| 			 | ||||
| 			if( | ||||
| 				is_click_within(event.target, "searchbox") === false || | ||||
| 				typeof autocomplete_cache[getsearchboxtext()] == "undefined" | ||||
| 			){ | ||||
| 				 | ||||
| 				return; | ||||
| 			} | ||||
| 			 | ||||
| 			switch(event.key){ | ||||
| 				 | ||||
| 				case "ArrowUp": | ||||
| 					event.preventDefault(); | ||||
| 					focuspos--; | ||||
| 					if(focuspos === -2){ | ||||
| 						 | ||||
| 						focuspos = autocomplete_cache[getsearchboxtext()].length - 1; | ||||
| 					} | ||||
| 					break; | ||||
| 				 | ||||
| 				case "ArrowDown": | ||||
| 				case "Tab": | ||||
| 					event.preventDefault(); | ||||
| 					 | ||||
| 					focuspos++; | ||||
| 					if(focuspos >= autocomplete_cache[getsearchboxtext()].length){ | ||||
| 						 | ||||
| 						focuspos = -1; | ||||
| 					} | ||||
| 					break; | ||||
| 				 | ||||
| 				case "Enter": | ||||
| 					should_focus = true; | ||||
| 					 | ||||
| 					if(focuspos !== -1){ | ||||
| 						 | ||||
| 						// replace input content | ||||
| 						event.preventDefault(); | ||||
| 						searchbox.value = | ||||
| 							autocomplete_div.getElementsByClassName("entry")[focuspos].innerText; | ||||
| 						break; | ||||
| 					} | ||||
| 					break; | ||||
| 				 | ||||
| 				default: | ||||
| 					focuspos = -1; | ||||
| 					break; | ||||
| 			} | ||||
| 			 | ||||
| 			if(focuspos === -1){ | ||||
| 				 | ||||
| 				searchbox.focus(); | ||||
| 				return; | ||||
| 			} | ||||
| 			 | ||||
| 			autocomplete_div.getElementsByClassName("entry")[focuspos].focus(); | ||||
| 		}); | ||||
| 		 | ||||
| 		window.addEventListener("blur", function(){ | ||||
| 			 | ||||
| 			autocomplete_div.style.display = "none"; | ||||
| 		}); | ||||
| 		 | ||||
| 		document.addEventListener("keyup", function(event){ | ||||
| 			 | ||||
| 			// handle ENTER key on entry | ||||
| 			if(should_focus){ | ||||
| 				 | ||||
| 				should_focus = false; | ||||
| 				searchbox.focus(); | ||||
| 			} | ||||
| 		}); | ||||
| 		 | ||||
| 		document.addEventListener("mousedown", function(event){ | ||||
| 			 | ||||
| 			// hide input if click is outside | ||||
| 			if(is_click_within(event.target, "searchbox") === false){ | ||||
| 				 | ||||
| 				autocomplete_div.style.display = "none"; | ||||
| 				return; | ||||
| 			} | ||||
| 		}); | ||||
| 		 | ||||
| 		function handle_entry_click(event){ | ||||
| 			 | ||||
| 			searchbox.value = event.innerText; | ||||
| 			focuspos = -1; | ||||
| 			searchbox.focus(); | ||||
| 		} | ||||
| 		 | ||||
| 		searchbox.addEventListener("focus", function(){ | ||||
| 			 | ||||
| 			focuspos = -1; | ||||
| 			getac(); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -149,31 +149,27 @@ h3,h4,h5,h6{ | ||||
| 	left:-1px; | ||||
| 	right:-1px; | ||||
| 	background:var(--282828); | ||||
| 	border:1px solid var(--504945); | ||||
| 	border:1px solid var(--928374); | ||||
| 	border-top:none; | ||||
| 	border-radius:0 0 2px 2px; | ||||
| 	z-index:10; | ||||
| 	overflow:hidden; | ||||
| } | ||||
|  | ||||
| .autocomplete .entry{ | ||||
| 	overflow:hidden; | ||||
| 	padding:4px 10px; | ||||
| 	cursor:pointer; | ||||
| 	outline:none; | ||||
| 	user-select:none; | ||||
| } | ||||
|  | ||||
| .autocomplete .entry:hover{ | ||||
| 	background:var(--3c3836); | ||||
| } | ||||
|  | ||||
| .autocomplete .title{ | ||||
| 	float:left; | ||||
| } | ||||
|  | ||||
| .autocomplete .subtext{ | ||||
| 	float:right; | ||||
| 	font-size:14px; | ||||
| 	color:var(--928374); | ||||
| 	margin-left:7px; | ||||
| .autocomplete .entry:focus{ | ||||
| 	background:var(--3c3836); | ||||
| } | ||||
|  | ||||
| /* Tabs */ | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| 	<head> | ||||
| 		<meta http-equiv="Content-Type" content="text/html;charset=utf-8"> | ||||
| 		<title>{%title%}</title> | ||||
| 		<link rel="stylesheet" href="/static/style.css?v3"> | ||||
| 		<link rel="stylesheet" href="/static/style.css?v4"> | ||||
| 		<meta name="viewport" content="width=device-width,initial-scale=1"> | ||||
| 		<meta name="robots" content="{%index%}index,{%index%}follow"> | ||||
| 		<link rel="icon" type="image/x-icon" href="/favicon.ico"> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 		<meta http-equiv="Content-Type" content="text/html;charset=utf-8"> | ||||
| 		<title>4get</title> | ||||
| 		<meta name="viewport" content="width=device-width,initial-scale=1"> | ||||
| 		<link rel="stylesheet" href="/static/style.css?v3"> | ||||
| 		<link rel="stylesheet" href="/static/style.css?v4"> | ||||
| 		<meta name="robots" content="index,follow"> | ||||
| 		<link rel="icon" type="image/x-icon" href="/favicon.ico"> | ||||
| 		<meta name="description" content="4get.ca: They live in our walls!"> | ||||
| @@ -33,6 +33,6 @@ | ||||
| 				Report a problem: <a href="https://lolcat.ca">lolcat.ca</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<script src="/static/client.js?v3"></script> | ||||
| 		<script src="/static/client.js?v4"></script> | ||||
| 	</body> | ||||
| </html> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user