From bbc449f419f8a1254f540e20be3e13be5f8e1280 Mon Sep 17 00:00:00 2001 From: fraxken Date: Thu, 6 Nov 2025 00:25:35 +0100 Subject: [PATCH] refactor(search-view): use HTML5 template and cleanup JS implementation --- public/components/views/search/search.html | 23 ++ public/components/views/search/search.js | 324 +++++++++++---------- public/main.js | 3 +- views/index.html | 17 +- 4 files changed, 193 insertions(+), 174 deletions(-) create mode 100644 public/components/views/search/search.html diff --git a/public/components/views/search/search.html b/public/components/views/search/search.html new file mode 100644 index 00000000..5859489f --- /dev/null +++ b/public/components/views/search/search.html @@ -0,0 +1,23 @@ + diff --git a/public/components/views/search/search.js b/public/components/views/search/search.js index 14d39b70..b03867e8 100644 --- a/public/components/views/search/search.js +++ b/public/components/views/search/search.js @@ -29,171 +29,210 @@ export class SearchView { this.secureDataSet = secureDataSet; this.nsn = nsn; + this.mount(); this.initialize(); } + mount() { + const template = document.getElementById("search-view-template"); + /** @type {HTMLTemplateElement} */ + const clone = document.importNode(template.content, true); + + const view = document.getElementById("search--view"); + view.innerHTML = ""; + view.appendChild(clone); + } + initialize() { - this.searchContainer = document.querySelector("#search--view .container"); this.searchForm = document.querySelector("#search--view form"); - const formGroup = this.searchForm.querySelector(".form-group"); - const input = this.searchForm.querySelector("input"); - const lang = currentLang(); + this.searchForm.addEventListener("submit", (event) => { + event.preventDefault(); + }); + const input = this.searchForm.querySelector("input"); input.addEventListener("input", debounce(async() => { - document.querySelector(".result-container")?.remove(); - this.searchForm.querySelector(".hint")?.remove(); + await this.#handleSearchInput(input.value); + }, 500)); - const packageName = input.value; - if (packageName.length === 0) { - return; - } - else if (packageName.length < kMinPackageNameLength || packageName.length > kMaxPackageNameLength) { - const hintElement = document.createElement("div"); - hintElement.classList.add("hint"); - hintElement.textContent = window.i18n[lang].search.packageLengthErr; - this.searchForm.appendChild(hintElement); + this.#initializePackages( + ".cache-packages", + window.scannedPackageCache + ); + this.#initializePackages( + ".recent-packages", + window.recentPackageCache + ); + } - return; - } + #initializePackages( + selector, + specs + ) { + const packagesElement = document.querySelector(`#search--view .container ${selector}`); + if (!packagesElement) { + return; + } - const loaderElement = createDOMElement("div", { - classList: ["spinner-small", "search-spinner"] - }); - formGroup.appendChild(loaderElement); + packagesElement.classList.toggle( + "hidden", + specs.length === 0 + ); + const fragment = document.createDocumentFragment(); + for (const spec of specs) { + fragment.appendChild(this.#cachePackageElement(spec)); + } + packagesElement.appendChild(fragment); + } - const { result, count } = await getJSON(`/search/${encodeURIComponent(packageName)}`); + async #handleSearchInput( + packageName + ) { + const lang = currentLang(); + const formGroup = this.searchForm.querySelector(".form-group"); - this.searchForm.querySelector(".spinner-small").remove(); + document.querySelector(".result-container")?.remove(); + this.searchForm.querySelector(".hint")?.remove(); - const divResultContainer = document.createElement("div"); - divResultContainer.classList.add("result-container"); + if (packageName.length === 0) { + return; + } + else if ( + packageName.length < kMinPackageNameLength || + packageName.length > kMaxPackageNameLength + ) { + const hintElement = document.createElement("div"); + hintElement.classList.add("hint"); + hintElement.textContent = window.i18n[lang].search.packageLengthErr; + this.searchForm.appendChild(hintElement); - if (count === 0) { - const divResultElement = document.createElement("div"); - divResultElement.classList.add("result-not-found"); - divResultElement.textContent = window.i18n[lang].search.noPackageFound; - divResultContainer.appendChild(divResultElement); - this.searchForm.appendChild(divResultContainer); + return; + } - return; - } + const loaderElement = createDOMElement("div", { + classList: ["spinner-small", "search-spinner"] + }); + formGroup.appendChild(loaderElement); - for (const { name, version, description } of result) { - const divResultElement = document.createElement("div"); - divResultElement.classList.add("result"); - if (packageName === name) { - divResultElement.classList.add("exact"); - } + const { result, count } = await getJSON(`/search/${encodeURIComponent(packageName)}`); - const pkgElement = document.createElement("div"); - pkgElement.classList.add("package-result"); - const pkgSpanElement = document.createElement("span"); - pkgSpanElement.textContent = name; - pkgSpanElement.addEventListener("click", async() => { - const packageVersion = divResultElement.querySelector("select option:checked"); - await this.fetchPackage(name, packageVersion.value); - }, { once: true }); - pkgElement.appendChild(pkgSpanElement); - const pkgDescriptionElement = document.createElement("p"); - pkgDescriptionElement.textContent = description; - pkgDescriptionElement.classList.add("description"); - pkgElement.appendChild(pkgDescriptionElement); - divResultElement.appendChild(pkgElement); - - const selectElement = document.createElement("select"); - const optionElement = document.createElement("option"); - optionElement.value = version; - optionElement.textContent = version; - selectElement.appendChild(optionElement); - selectElement.addEventListener("click", async() => { - const spinnerOption = ""; - selectElement.insertAdjacentHTML("beforeend", spinnerOption); - - function spinnerOptionSpin() { - const spinnerOptionElement = selectElement.querySelector(".spinner-option"); - spinnerOptionElement.textContent += "."; - if (spinnerOptionElement.textContent.length > 3) { - spinnerOptionElement.textContent = "."; - } - } + this.searchForm.querySelector(".spinner-small").remove(); - const spinIntervalId = setInterval(spinnerOptionSpin, 180); + this.#displaySearchResults({ results: result, count, packageName, lang }); + } - try { - const versions = await this.fetchPackageVersions(name); + #displaySearchResults({ results, count, packageName, lang }) { + const divResultContainer = document.createElement("div"); + divResultContainer.classList.add("result-container"); - clearInterval(spinIntervalId); + if (count === 0) { + const divResultElement = document.createElement("div"); + divResultElement.classList.add("result-not-found"); + divResultElement.textContent = window.i18n[lang].search.noPackageFound; + divResultContainer.appendChild(divResultElement); + this.searchForm.appendChild(divResultContainer); - selectElement.querySelector(".spinner-option").remove(); + return; + } - for (const pkgVersion of versions) { - if (pkgVersion === version) { - continue; - } - const optionElement = document.createElement("option"); - optionElement.value = pkgVersion; - optionElement.textContent = pkgVersion; - selectElement.appendChild(optionElement); - } - } - catch { - clearInterval(spinIntervalId); - selectElement.querySelector(".spinner-option").remove(); - } - }, { once: true }); - divResultElement.appendChild(selectElement); - divResultContainer.appendChild(divResultElement); - } - this.searchForm.parentNode.insertBefore(divResultContainer, this.searchForm.nextSibling); - }, 500)); + for (const { name, version, description } of results) { + const divResultElement = this.#createSearchResultElement({ + name, + version, + description, + packageName + }); + divResultContainer.appendChild(divResultElement); + } - this.searchForm.addEventListener("submit", (event) => { - event.preventDefault(); - }); + this.searchForm.parentNode.insertBefore(divResultContainer, this.searchForm.nextSibling); + } - const cachePackagesElement = this.searchContainer.querySelector(".cache-packages"); - if (cachePackagesElement === null) { - return; + #createSearchResultElement({ name, version, description, packageName }) { + const divResultElement = document.createElement("div"); + divResultElement.classList.add("result"); + if (packageName === name) { + divResultElement.classList.add("exact"); } - if (window.scannedPackageCache.length > 0) { - cachePackagesElement.classList.remove("hidden"); - const h1Element = document.createElement("h1"); - h1Element.textContent = window.i18n[lang].search.packagesCache; - cachePackagesElement.appendChild(h1Element); - - for (const pkg of window.scannedPackageCache) { - cachePackagesElement.appendChild(this.#cachePackageElement(pkg)); + + const pkgElement = document.createElement("div"); + pkgElement.classList.add("package-result"); + const pkgSpanElement = document.createElement("span"); + pkgSpanElement.textContent = name; + pkgSpanElement.addEventListener("click", () => { + const packageVersion = divResultElement.querySelector("select option:checked"); + this.fetchPackage(name, packageVersion.value); + }, { once: true }); + pkgElement.appendChild(pkgSpanElement); + + const pkgDescriptionElement = document.createElement("p"); + pkgDescriptionElement.textContent = description; + pkgDescriptionElement.classList.add("description"); + pkgElement.appendChild(pkgDescriptionElement); + divResultElement.appendChild(pkgElement); + + const selectElement = this.#createVersionSelect(name, version); + divResultElement.appendChild(selectElement); + + return divResultElement; + } + + #createVersionSelect(name, version) { + const selectElement = document.createElement("select"); + const optionElement = document.createElement("option"); + optionElement.value = version; + optionElement.textContent = version; + selectElement.appendChild(optionElement); + + selectElement.addEventListener("click", async() => { + const spinnerOption = ""; + selectElement.insertAdjacentHTML("beforeend", spinnerOption); + + function spinnerOptionSpin() { + const spinnerOptionElement = selectElement.querySelector(".spinner-option"); + spinnerOptionElement.textContent += "."; + if (spinnerOptionElement.textContent.length > 3) { + spinnerOptionElement.textContent = "."; + } } - } - else { - cachePackagesElement.classList.add("hidden"); - } - const recentPackagesElement = this.searchContainer.querySelector(".recent-packages"); - if (window.recentPackageCache.length > 0) { - recentPackagesElement.classList.remove("hidden"); - const h1Element = document.createElement("h1"); - h1Element.textContent = window.i18n[lang].search.recentPackages; - recentPackagesElement.appendChild(h1Element); + const spinIntervalId = setInterval(spinnerOptionSpin, 180); - for (const pkg of window.recentPackageCache) { - recentPackagesElement.appendChild(this.#cachePackageElement(pkg)); + try { + const versions = await this.fetchPackageVersions(name); + + clearInterval(spinIntervalId); + selectElement.querySelector(".spinner-option").remove(); + + for (const pkgVersion of versions) { + if (pkgVersion === version) { + continue; + } + const optionElement = document.createElement("option"); + optionElement.value = pkgVersion; + optionElement.textContent = pkgVersion; + selectElement.appendChild(optionElement); + } } - } - else { - recentPackagesElement.classList.add("hidden"); - } + catch { + clearInterval(spinIntervalId); + selectElement.querySelector(".spinner-option").remove(); + } + }, { once: true }); + + return selectElement; } #cachePackageElement(pkg) { const { name, version, local } = parseNpmSpec(pkg); const pkgElement = document.createElement("div"); pkgElement.classList.add("package-cache-result"); + const pkgSpanElement = document.createElement("span"); pkgSpanElement.innerHTML = `${name}@${version}${local ? " local" : ""}`; pkgSpanElement.addEventListener("click", () => { window.socket.send(JSON.stringify({ action: "SEARCH", pkg })); }, { once: true }); + const removeButton = createDOMElement("button", { classList: ["remove"], text: "x" @@ -202,12 +241,13 @@ export class SearchView { event.stopPropagation(); window.socket.send(JSON.stringify({ action: "REMOVE", pkg })); }, { once: true }); + pkgElement.append(pkgSpanElement, removeButton); return pkgElement; } - async fetchPackage(packageName, version) { + fetchPackage(packageName, version) { const pkg = `${packageName}@${version}`; window.socket.send(JSON.stringify({ action: "SEARCH", pkg })); @@ -219,39 +259,9 @@ export class SearchView { return versions.reverse(); } - reset() { - const lang = currentLang(); - - const searchViewContainer = document.querySelector("#search--view .container"); - searchViewContainer.innerHTML = ""; - const form = document.createElement("form"); - const formGroup = document.createElement("div"); - formGroup.classList.add("form-group"); - const iconSearch = document.createElement("i"); - iconSearch.classList.add("icon-search"); - const input = document.createElement("input"); - input.type = "text"; - input.placeholder = window.i18n[lang].search.registryPlaceholder; - input.name = "package"; - input.id = "package"; - formGroup.appendChild(iconSearch); - formGroup.appendChild(input); - form.appendChild(formGroup); - searchViewContainer.appendChild(form); - - const cachePackagesElement = document.createElement("div"); - cachePackagesElement.classList.add("cache-packages", "hidden"); - searchViewContainer.appendChild(cachePackagesElement); - const recentPackagesElement = document.createElement("div"); - recentPackagesElement.classList.add("recent-packages", "hidden"); - searchViewContainer.appendChild(recentPackagesElement); - - this.initialize(); - } - onScan(pkg) { const searchViewForm = document.querySelector("#search--view form"); - searchViewForm.remove(); + searchViewForm?.remove(); const containerResult = document.querySelector("#search--view .result-container"); containerResult?.remove(); diff --git a/public/main.js b/public/main.js index 48537b4d..97fea7f6 100644 --- a/public/main.js +++ b/public/main.js @@ -72,7 +72,8 @@ document.addEventListener("DOMContentLoaded", async() => { nsn, secureDataSet } }); - searchview.reset(); + searchview.mount(); + searchview.initialize(); const nsnActivePackage = secureDataSet.linker.get(0); const nsnRootPackage = nsnActivePackage ? `${nsnActivePackage.name}@${nsnActivePackage.version}` : null; if (data.status === "RELOAD" && nsnRootPackage !== null && nsnRootPackage !== window.activePackage) { diff --git a/views/index.html b/views/index.html index 9a24cd0c..6b7b41d8 100644 --- a/views/index.html +++ b/views/index.html @@ -89,22 +89,7 @@ - +