236 lines
6.2 KiB
TypeScript
Raw Normal View History

2018-04-02 05:34:16 +00:00
import AnimeNotifier from "../AnimeNotifier"
2018-04-02 14:07:52 +00:00
import { delay, requestIdleCallback, findAllInside } from "../Utils"
2017-10-17 09:27:15 +00:00
2018-03-17 19:41:18 +00:00
// Search page reference
var emptySearchHTML = ""
var searchPage: HTMLElement
2018-03-17 23:35:38 +00:00
var searchPageTitle: HTMLElement
2018-03-17 19:41:18 +00:00
var correctResponseRendered = {
"anime": false,
"character": false,
"forum": false,
"soundtrack": false,
2018-03-17 21:09:17 +00:00
"user": false,
"company": false
2018-03-17 19:41:18 +00:00
}
// Save old term to compare
var oldTerm = ""
2018-03-17 19:41:18 +00:00
// Containers for all the search results
var animeSearchResults: HTMLElement
var characterSearchResults: HTMLElement
var forumSearchResults: HTMLElement
var soundtrackSearchResults: HTMLElement
var userSearchResults: HTMLElement
2018-03-17 21:09:17 +00:00
var companySearchResults: HTMLElement
2018-03-17 19:41:18 +00:00
// Delay before a request is sent
2018-04-02 14:07:52 +00:00
const searchDelay = 140
2018-03-29 08:49:53 +00:00
// Fetch options
const fetchOptions: RequestInit = {
credentials: "same-origin"
}
2018-04-20 18:55:40 +00:00
// Speech recognition
let recognition: SpeechRecognition
2017-10-17 09:27:15 +00:00
// Search
2018-04-20 18:16:59 +00:00
export async function search(arn: AnimeNotifier, search: HTMLInputElement, evt?: KeyboardEvent) {
if(evt && (evt.ctrlKey || evt.altKey)) {
2017-10-17 09:27:15 +00:00
return
}
2018-03-22 00:32:04 +00:00
// Determine if we're already seeing the search page
let searchPageActivated = (searchPage === arn.app.content.children[0])
// Check if the search term really changed
2018-03-17 19:41:18 +00:00
let term = search.value.trim()
2018-03-22 00:32:04 +00:00
if(term === oldTerm && searchPageActivated) {
return
}
oldTerm = term
2018-03-17 19:41:18 +00:00
// Reset
correctResponseRendered.anime = false
correctResponseRendered.character = false
correctResponseRendered.forum = false
correctResponseRendered.soundtrack = false
correctResponseRendered.user = false
2018-03-29 08:49:53 +00:00
correctResponseRendered.company = false
2018-03-17 19:41:18 +00:00
// Set browser URL
let url = "/search/" + term
2018-03-17 23:35:38 +00:00
document.title = "Search: " + term
2018-03-17 19:41:18 +00:00
arn.app.currentPath = url
// Unmount mountables to improve visual responsiveness on key press
arn.unmountMountables()
// Show loading spinner
arn.loading(true)
try {
// Fetch empty search frame if needed
if(emptySearchHTML === "") {
let response = await fetch("/_/empty-search")
emptySearchHTML = await response.text()
}
if(!searchPageActivated) {
if(!searchPage) {
searchPage = document.createElement("div")
searchPage.innerHTML = emptySearchHTML
}
arn.app.content.innerHTML = ""
arn.app.content.appendChild(searchPage)
2018-03-29 08:49:53 +00:00
history.pushState(url, document.title, url)
} else {
history.replaceState(url, document.title, url)
2018-03-17 19:41:18 +00:00
2018-04-02 14:07:52 +00:00
// Delay
await delay(searchDelay)
}
2018-03-29 11:43:17 +00:00
if(term !== search.value.trim()) {
arn.mountMountables()
return
}
2018-03-17 19:41:18 +00:00
if(!animeSearchResults) {
animeSearchResults = document.getElementById("anime-search-results")
characterSearchResults = document.getElementById("character-search-results")
forumSearchResults = document.getElementById("forum-search-results")
soundtrackSearchResults = document.getElementById("soundtrack-search-results")
userSearchResults = document.getElementById("user-search-results")
2018-03-17 21:09:17 +00:00
companySearchResults = document.getElementById("company-search-results")
2018-03-17 23:35:38 +00:00
searchPageTitle = document.getElementsByTagName("h1")[0]
2018-03-17 19:41:18 +00:00
}
2018-06-28 06:30:24 +00:00
searchPageTitle.textContent = document.title
2018-03-17 23:35:38 +00:00
2018-03-17 19:41:18 +00:00
if(!term || term.length < 1) {
await arn.innerHTML(searchPage, emptySearchHTML)
arn.app.emit("DOMContentLoaded")
return
}
// Start searching
2018-03-17 19:53:18 +00:00
fetch("/_/anime-search/" + term, fetchOptions)
2018-03-17 19:41:18 +00:00
.then(showResponseInElement(arn, url, "anime", animeSearchResults))
.catch(console.error)
2018-04-02 05:34:16 +00:00
requestIdleCallback(() => {
fetch("/_/character-search/" + term, fetchOptions)
.then(showResponseInElement(arn, url, "character", characterSearchResults))
.catch(console.error)
2018-03-17 19:41:18 +00:00
fetch("/_/forum-search/" + term, fetchOptions)
.then(showResponseInElement(arn, url, "forum", forumSearchResults))
.catch(console.error)
2018-03-17 19:41:18 +00:00
fetch("/_/soundtrack-search/" + term, fetchOptions)
.then(showResponseInElement(arn, url, "soundtrack", soundtrackSearchResults))
.catch(console.error)
2018-03-17 19:41:18 +00:00
fetch("/_/user-search/" + term, fetchOptions)
.then(showResponseInElement(arn, url, "user", userSearchResults))
.catch(console.error)
2018-03-17 21:09:17 +00:00
fetch("/_/company-search/" + term, fetchOptions)
.then(showResponseInElement(arn, url, "company", companySearchResults))
.catch(console.error)
})
2018-03-17 19:41:18 +00:00
} catch(err) {
console.error(err)
} finally {
arn.loading(false)
2017-10-17 09:27:15 +00:00
}
2018-03-17 19:41:18 +00:00
}
2017-10-17 09:27:15 +00:00
2018-03-17 19:41:18 +00:00
function showResponseInElement(arn: AnimeNotifier, url: string, typeName: string, element: HTMLElement) {
return async (response: Response) => {
2018-03-17 19:41:18 +00:00
let html = await response.text()
if(arn.app.currentPath !== url) {
// Return if this result would overwrite the already arrived correct result
if(correctResponseRendered[typeName]) {
return
}
} else {
correctResponseRendered[typeName] = true
}
await arn.innerHTML(element, html)
2018-04-16 22:02:24 +00:00
showSearchResults(arn, element)
2018-03-17 19:41:18 +00:00
}
2017-10-19 19:43:42 +00:00
}
2018-04-16 22:02:24 +00:00
export function showSearchResults(arn: AnimeNotifier, element: HTMLElement) {
// Do the same as for the content loaded event,
// except here we are limiting it to the element.
arn.app.ajaxify(element.getElementsByTagName("a"))
arn.lazyLoad(findAllInside("lazy", element))
arn.mountMountables(findAllInside("mountable", element))
2018-04-18 18:40:45 +00:00
arn.assignTooltipOffsets(findAllInside("tip", element))
2018-04-16 22:02:24 +00:00
}
2018-04-20 18:16:59 +00:00
export function searchBySpeech(arn: AnimeNotifier, element: HTMLElement) {
2018-04-20 18:55:40 +00:00
if(recognition) {
recognition.stop()
return
}
2018-04-20 18:16:59 +00:00
let searchInput = document.getElementById("search") as HTMLInputElement
let oldPlaceholder = searchInput.placeholder
let SpeechRecognition: SpeechRecognitionStatic = window["SpeechRecognition"] || window["webkitSpeechRecognition"]
2018-04-20 18:55:40 +00:00
recognition = new SpeechRecognition()
2018-04-20 18:16:59 +00:00
recognition.continuous = false
recognition.interimResults = false
recognition.lang = "en-US"
recognition.onresult = evt => {
if(evt.results.length > 0) {
let result = evt.results.item(0).item(0)
let term = result.transcript
if(term !== "") {
searchInput.value = term
arn.sideBar.hide()
search(arn, searchInput)
}
}
recognition.stop()
}
recognition.onerror = e => {
recognition.stop()
}
recognition.onend = e => {
searchInput.placeholder = oldPlaceholder
element.classList.remove("speech-listening")
2018-04-20 18:55:40 +00:00
recognition = null
2018-04-20 18:16:59 +00:00
}
// Focus search field
searchInput.placeholder = "Listening..."
searchInput.value = ""
searchInput.focus()
searchInput.select()
// Highlight microphone icon
element.classList.add("speech-listening")
// Start voice recognition
recognition.start()
}