diff --git a/index.html b/index.html index b586344..12529b4 100644 --- a/index.html +++ b/index.html @@ -105,10 +105,11 @@
+
-

-
...
+
+
Type to search...
diff --git a/main.js b/main.js index a307cbc..a75fc12 100644 --- a/main.js +++ b/main.js @@ -7,6 +7,10 @@ let loading = 0 /** @type {PlistAndImageComboDeal} */ let currentCombo = null +let autocompleteResults = [] +let autocompleteIndex = -1 +let spriteNameCache = [] + /** @type {PlistDict} */ let currentDict = null @@ -243,6 +247,11 @@ function updateCanvasSize() { async function updateCurrentCombo() { currentDict = null currentCombo = await autoGetImageAndPlist() + if (currentCombo) { + spriteNameCache = currentCombo.plist.map(p => p.key) + } else { + spriteNameCache = [] + } } function draw() { @@ -632,6 +641,78 @@ function searchCurrentCombo(name, exact = false) { } } +function fuzzyScore(query, target) { + query = query.toLowerCase() + target = target.toLowerCase() + + if (target.startsWith(query)) return 1000 - target.length + + let qi = 0 + let score = 0 + for (let i = 0; i < target.length && qi < query.length; i++) { + if (target[i] === query[qi]) { + score += 10 + qi++ + } + } + if (qi !== query.length) return -1 + return score - target.length +} + +function updateAutocomplete(query) { + const container = document.querySelector("#autocomplete") + if (!query || !currentCombo) { + container.classList.add("autocomplete-hidden") + return + } + + let scored = spriteNameCache + .map(name => ({ name, score: fuzzyScore(query, name) })) + .filter(x => x.score >= 0) + .sort((a, b) => b.score - a.score) + .slice(0, 20) + + autocompleteResults = scored.map(x => x.name) + autocompleteIndex = -1 + + if (autocompleteResults.length === 0) { + container.classList.add("autocomplete-hidden") + return + } + + container.innerHTML = "" + for (let name of autocompleteResults) { + const div = document.createElement("div") + div.className = "autocomplete-item" + div.textContent = name + div.addEventListener("mousedown", () => { + selectAutocomplete(name) + }) + container.appendChild(div) + } + + container.classList.remove("autocomplete-hidden") +} + +function selectAutocomplete(name) { + const searchBar = document.querySelector("#search-bar") + searchBar.value = name + document.querySelector("#autocomplete").classList.add("autocomplete-hidden") + + currentDictLocked = true + currentDict = searchCurrentCombo(name, true).result + if (currentDict) updateInfoAndPreview() +} + +function moveAutocomplete(dir) { + const items = document.querySelectorAll(".autocomplete-item") + if (items.length === 0) return + + autocompleteIndex = (autocompleteIndex + dir + items.length) % items.length + items.forEach(el => el.classList.remove("active")) + items[autocompleteIndex].classList.add("active") +} + function populateSelectionFromURL() { let hash = new URL(window.location.href).hash if (hash == "") return @@ -761,6 +842,7 @@ function populateSelectionFromURL() { if (searchTerm == "") { currentDictLocked = false document.querySelector("#search-info").innerText = "Type to search..." + updateAutocomplete("") return } @@ -776,9 +858,31 @@ function populateSelectionFromURL() { document.querySelector("#search-info").innerText = `No results!` } } + + updateAutocomplete(searchTerm) }) window.addEventListener("keydown", event => { + const container = document.querySelector("#autocomplete") + + if (!container.classList.contains("autocomplete-hidden")) { + if (event.code === "ArrowDown") { + event.preventDefault() + moveAutocomplete(1) + return + } + if (event.code === "ArrowUp") { + event.preventDefault() + moveAutocomplete(-1) + return + } + if (event.code === "Enter" && autocompleteIndex >= 0) { + event.preventDefault() + selectAutocomplete(autocompleteResults[autocompleteIndex]) + return + } + } + if (event.code == "KeyF" && event.ctrlKey) { event.preventDefault() searchBar.value = "" diff --git a/style.css b/style.css index 99fd573..5ef2188 100644 --- a/style.css +++ b/style.css @@ -159,7 +159,7 @@ canvas#preview, canvas#preview2 { #search-wrap { position: fixed; - top: -70px; + top: 5px; right: 5px; transition: translate .2s linear; @@ -167,9 +167,6 @@ canvas#preview, canvas#preview2 { background-color: #ffffff8a; } -#search-wrap:has(#search-bar:focus) { - translate: 0px 75px; -} #search-bar { width: 250px; @@ -187,6 +184,30 @@ label { user-select: none; } +#autocomplete { + width: 250px; + max-height: calc(100vh - 120px); + overflow-y: auto; + background: #ffffffee; + border: 1px solid #ccc; + font-family: monospace; + font-size: 14px; +} + +.autocomplete-item { + padding: 4px 6px; + cursor: pointer; +} + +.autocomplete-item.active { + background-color: #287dff; + color: white; +} + +.autocomplete-hidden { + display: none; +} + #physical-buttons { display: flex; flex-direction: row;