UNPKG

@nodesecure/cli

Version:
576 lines (486 loc) 18.3 kB
// Import Third-party Dependencies import semver from "semver"; import sizeSatisfies from "@nodesecure/size-satisfies"; // Import Internal Dependencies import { createDOMElement, vec2Distance, currentLang } from "../../common/utils.js"; // CONSTANTS const kFiltersName = new Set(["package", "version", "flag", "license", "author", "ext", "builtin", "size"]); const kHelpersTitleName = { ext: "File extensions", builtin: "Node.js core modules", license: "Available licenses", flag: "Available flags" }; const kFocusHelperBgColor = "#1976d2"; const kHelpersTemplateName = { flag: "search_helpers_flags", license(linker) { const fragment = document.createDocumentFragment(); const items = new Set(); for (const { uniqueLicenseIds = [] } of linker.values()) { uniqueLicenseIds.forEach((ext) => items.add(ext)); } [...items].forEach((value) => fragment.appendChild(createLineElement(value))); return fragment; }, ext(linker) { const fragment = document.createDocumentFragment(); const items = new Set(); for (const { composition } of linker.values()) { composition.extensions.forEach((ext) => items.add(ext)); } items.delete(""); [...items].forEach((value) => fragment.appendChild(createLineElement(value))); return fragment; }, builtin(linker) { const fragment = document.createDocumentFragment(); const items = new Set(); for (const { composition } of linker.values()) { composition.required_nodejs.forEach((ext) => items.add(ext)); } [...items].forEach((value) => fragment.appendChild(createLineElement(value))); return fragment; }, author(linker) { const fragment = document.createDocumentFragment(); const items = new Set(); for (const { author } of linker.values()) { if (author === null) { continue; } items.add(author.name); } [...items].forEach((value) => fragment.appendChild(createLineElement(value))); return fragment; } }; function createLineElement(text) { const pElement = createDOMElement("p", { text }); return createDOMElement("div", { classList: ["line"], childs: [pElement], attributes: { "data-value": text } }); } export class SearchBar { constructor(network, linker) { this.container = document.getElementById("searchbar"); this.background = document.querySelector(".search-result-background"); this.helper = document.querySelector(".search-result-pannel > .helpers"); this.input = document.getElementById("search-bar-input"); this.itemsContainer = this.container.querySelector(".search-items"); this.allSearchPackages = document.querySelectorAll(".search-result-pannel > .package"); this.network = network; this.linker = linker; this.delayOpenSearchBar = true; this.activeQuery = new Set(); this.queries = []; this.inputUpdateValue = false; this.forceNewItem = false; let confirmBackspace = false; let currentActiveQueryName = null; let helperSelectionIndex = null; this.container.addEventListener("click", () => this.open()); this.input.addEventListener("keyup", (event) => { const shownPackages = document.querySelectorAll(".search-result-pannel > .package:not(.hide)"); const shownHelpers = document.querySelectorAll(".helpers > .line:not(.hide)"); const shownItems = [...shownHelpers, ...shownPackages]; if (event.key === "ArrowDown") { if (helperSelectionIndex === null) { helperSelectionIndex = 0; } else { helperSelectionIndex++; if (helperSelectionIndex >= shownItems.length) { helperSelectionIndex = 0; } } shownItems.forEach((line, index) => { line.style.backgroundColor = "initial"; if (index === helperSelectionIndex) { line.style.backgroundColor = kFocusHelperBgColor; } }); return; } else if (event.key === "ArrowUp") { if (helperSelectionIndex === null) { helperSelectionIndex = shownItems.length - 1; } else { helperSelectionIndex--; if (helperSelectionIndex < 0) { helperSelectionIndex = shownItems.length - 1; } } shownItems.forEach((line, index) => { line.style.backgroundColor = "initial"; if (index === helperSelectionIndex) { line.style.backgroundColor = kFocusHelperBgColor; } }); return; } else if (event.key !== "Enter") { helperSelectionIndex = null; } if ((this.input.value === null || this.input.value === "")) { if (event.key === "Enter" && helperSelectionIndex !== null) { const line = shownItems[helperSelectionIndex]; if (line.classList.contains("package")) { helperSelectionIndex = null; this.close(); this.focusNodeById(line.getAttribute("data-value")); return; } this.input.value = line.getAttribute("data-value"); const [keyword] = this.input.value.split(":"); if (kFiltersName.has(keyword)) { currentActiveQueryName = keyword; this.showPannelHelper(currentActiveQueryName); helperSelectionIndex = null; } return; } // we always want to show the default helpers when the input is empty! this.showPannelHelper(); // hide all results if there is no active queries! if (this.activeQuery.size === 0) { this.allSearchPackages.forEach((element) => element.classList.add("hide")); } // if backspace is received and that we have active queries // then we want the query to be re-inserted in the input field! else if (event.key === "Backspace") { if (confirmBackspace) { this.removeSearchBarItem(); currentActiveQueryName = null; } else { confirmBackspace = true; } } else if (event.key === "Enter") { const nodeIds = []; for (const pkgElement of this.allSearchPackages) { if (!pkgElement.classList.contains("hide")) { nodeIds.push(Number(pkgElement.getAttribute("data-value"))); } } this.focusMultipleNodeIds(nodeIds); } return; } confirmBackspace = false; // if there is no active query filter name, then we want to filter the helper if (currentActiveQueryName === null) { this.showHelperByInputText(); } // inputUpdateValue is used to force a re-hydratation of the input if (event.key === ":" || this.inputUpdateValue) { if (this.inputUpdateValue) { setTimeout(() => (this.inputUpdateValue = false), 1); } // here .split is important because we may have to re-proceed complete search string like 'filter: text'. const [keyword] = this.input.value.split(":"); if (kFiltersName.has(keyword)) { currentActiveQueryName = keyword; this.showPannelHelper(currentActiveQueryName); } // TODO: we may want to implement a else here to generate an invalid keyword error! } // In case there is no active package query and the enter key is pressed, then: // - we fallback the current query to "package". let isPackageSearch = false; if (!this.activeQuery.has("package") && currentActiveQueryName === null && event.key === "Enter") { if (this.input.value.trim() === "") { return; } currentActiveQueryName = "package"; isPackageSearch = true; this.showPannelHelper(); } else if (currentActiveQueryName === null) { return; } if (event.key === "Enter" && helperSelectionIndex !== null) { const line = shownItems[helperSelectionIndex]; helperSelectionIndex = null; if (line.classList.contains("package")) { line.style.backgroundColor = "initial"; this.close(); this.focusNodeById(line.getAttribute("data-value")); } else { if (line.getAttribute("data-value").startsWith(this.input.value)) { this.input.value = line.getAttribute("data-value"); const [keyword] = this.input.value.split(":"); currentActiveQueryName = keyword; this.showPannelHelper(currentActiveQueryName); return; } this.input.value += line.getAttribute("data-value"); this.helper.classList.add("hide"); } } // fetch the search text const text = isPackageSearch ? this.input.value.trim() : this.input.value.slice(currentActiveQueryName.length + 1).trim(); this.showHelperByInputText(text); if (text.length === 0) { return; } // fetch matching result ids! const matchingIds = this.computeText(currentActiveQueryName, text); if (event.key === "Enter" || this.forceNewItem) { if (this.forceNewItem) { setTimeout(() => (this.forceNewItem = false), 1); } this.appendCancelButton(); this.addSearchBarItem(currentActiveQueryName, text, matchingIds); this.showPannelHelper(); currentActiveQueryName = null; this.input.value = ""; helperSelectionIndex = null; } this.showResultsByIds(matchingIds); }); document.addEventListener("click", (event) => { if (!this.container.contains(event.target) && this.background.classList.contains("show") && !this.inputUpdateValue && !this.forceNewItem) { helperSelectionIndex = null; this.close(); } }); this.showPannelHelper(); const self = this; for (const domElement of this.allSearchPackages) { domElement.addEventListener("click", function clikEvent() { helperSelectionIndex = null; self.focusNodeById(this.getAttribute("data-value")); }); } if (window.navigation.getAnchor() !== "network--view") { this.container.style.display = "none"; } } addNewSearchText(filterName, searchedValue) { const matchingIds = this.computeText(filterName, searchedValue); this.showResultsByIds(matchingIds); this.appendCancelButton(); this.addSearchBarItem(filterName, searchedValue, matchingIds); this.showPannelHelper(); this.open(); } appendCancelButton() { if (this.activeQuery.size > 0) { return; } const divElement = createDOMElement("div", { classList: ["cancel"], text: "x" }); divElement.addEventListener("click", () => { this.close(); setTimeout(() => this.input.focus(), 5); }); this.itemsContainer.appendChild(divElement); } showPannelHelper(filterName = null) { if (filterName !== null && !Reflect.has(kHelpersTemplateName, filterName)) { this.helper.classList.add("hide"); return; } document.querySelectorAll(".helpers > .line").forEach((element) => element.classList.add("hide")); this.helper.classList.remove("hide"); const templateName = filterName === null ? "search_helpers_default" : kHelpersTemplateName[filterName]; let clone; if (typeof templateName === "function") { clone = templateName(this.linker); } else { const templateElement = document.getElementById(templateName); clone = templateElement.content.cloneNode(true); } clone.querySelectorAll(".line").forEach((element) => { element.addEventListener("click", () => { if (filterName === null) { this.inputUpdateValue = true; this.input.value = element.getAttribute("data-value"); } else { this.forceNewItem = true; this.input.value += element.getAttribute("data-value"); this.helper.classList.add("hide"); } this.input.focus(); this.input.dispatchEvent(new Event("keyup")); }); }); const titleText = window.i18n[currentLang()].search[ Reflect.has(kHelpersTitleName, filterName) ? kHelpersTitleName[filterName] : "default" ]; // eslint-disable-next-line @stylistic/max-len this.helper.innerHTML = `<div class="title"><p>${titleText}</p><a href="https://github.com/NodeSecure/cli#searchbar-filters" rel="noopener" target="_blank"><i class="icon-attention-circled"></i></a></div>`; this.helper.appendChild(clone); } removeSearchBarItem() { const [fullQuery, divElement] = this.queries.pop(); const [filterName] = fullQuery.split(":"); this.activeQuery.delete(filterName); this.itemsContainer.removeChild(divElement); if (this.activeQuery.size === 0) { while (this.itemsContainer.firstChild) { this.itemsContainer.removeChild(this.itemsContainer.lastChild); } } this.input.value = fullQuery; this.inputUpdateValue = true; this.input.focus(); this.input.dispatchEvent(new Event("keyup")); } addSearchBarItem(filterName, text, ids) { const bElement = createDOMElement("b", { text: `${filterName}:` }); const pElement = createDOMElement("p", { text }); const element = this.itemsContainer.children[this.itemsContainer.children.length - 1]; const divElement = createDOMElement("div", { childs: [bElement, pElement] }); this.itemsContainer.insertBefore(divElement, element); this.activeQuery.add(filterName); this.queries.push([`${filterName}:${text}`, divElement, [...ids]]); } computeText(filterName, inputValue) { const matchingIds = new Set(); const storedIds = new Set(); for (const [, , ids] of this.queries) { ids.forEach((id) => storedIds.add(id)); } for (const [id, opt] of this.linker) { switch (filterName) { case "version": { if (semver.satisfies(opt.version, inputValue)) { matchingIds.add(String(id)); } break; } case "package": { if (new RegExp(inputValue, "gi").test(opt.name)) { matchingIds.add(String(id)); } break; } case "license": { const hasMatchingLicense = opt.uniqueLicenseIds.some( (value) => new RegExp(inputValue, "gi").test(value) ); if (hasMatchingLicense) { matchingIds.add(String(id)); } break; } case "ext": { const extensions = new Set(opt.composition.extensions); const wantedExtension = inputValue.startsWith(".") ? inputValue : `.${inputValue}`; if (extensions.has(wantedExtension.toLowerCase())) { matchingIds.add(String(id)); } break; } case "size": { if (sizeSatisfies(inputValue, opt.size)) { matchingIds.add(String(id)); } break; } case "builtin": { const hasMatchingBuiltin = opt.composition.required_nodejs .some((value) => new RegExp(inputValue, "gi").test(value)); if (hasMatchingBuiltin) { matchingIds.add(String(id)); } break; } case "author": { const authorRegex = new RegExp(inputValue, "gi"); if ( (typeof opt.author === "string" && authorRegex.test(opt.author)) || (opt.author !== null && "name" in opt.author && authorRegex.test(opt.author.name)) ) { matchingIds.add(String(id)); } break; } case "flag": if (opt.flags.includes(inputValue)) { matchingIds.add(String(id)); } break; } } return storedIds.size === 0 ? matchingIds : new Set([...matchingIds].filter((value) => storedIds.has(value))); } focusNodeById(nodeId) { window.navigation.setNavByName("network--view"); this.delayOpenSearchBar = false; this.network.focusNodeById(nodeId); this.close(); setTimeout(() => { this.delayOpenSearchBar = true; }, 5); } focusMultipleNodeIds(nodeIds) { window.navigation.setNavByName("network--view"); this.delayOpenSearchBar = false; this.network.highlightMultipleNodes(nodeIds); window.locker.lock(); const currentSelectedNode = window.networkNav.currentNodeParams; const moveTo = !currentSelectedNode || !nodeIds.includes(currentSelectedNode.nodes[0]); if (moveTo) { const origin = this.network.network.getViewPosition(); const closestNode = nodeIds .map((id) => { return { id, pos: this.network.network.getPosition(id) }; }) .reduce( (a, b) => (vec2Distance(origin, a.pos) < vec2Distance(origin, b.pos) ? a : b) ); const scale = nodeIds.length > 3 ? 0.25 : 0.35; this.network.network.focus(closestNode.id, { animation: true, scale }); } this.close(); setTimeout(() => { this.delayOpenSearchBar = true; }, 5); } showResultsByIds(ids = new Set()) { for (const pkgElement of this.allSearchPackages) { const isMatching = ids.has(pkgElement.getAttribute("data-value")); pkgElement.classList[isMatching ? "remove" : "add"]("hide"); } } showHelperByInputText(text = this.input.value.trim()) { const elements = document.querySelectorAll(".helpers > .line"); for (const pkgElement of elements) { const dataValue = pkgElement.getAttribute("data-value"); pkgElement.classList[dataValue.match(text) ? "remove" : "add"]("hide"); } } open() { if (!this.background.classList.contains("show") && this.delayOpenSearchBar) { this.helper.classList.remove("hide"); this.background.classList.toggle("show"); } } close() { this.background.classList.remove("show"); this.allSearchPackages.forEach((element) => element.classList.add("hide")); this.itemsContainer.innerHTML = ""; this.activeQuery = new Set(); this.queries = []; this.input.value = ""; this.input.blur(); this.helper.classList.remove("hide"); this.showPannelHelper(); } }