// import Vue from "vue"; // import debounce from 'lodash/debounce'; // import { DatasetService } from "../../services/dataset.service"; import DatasetService from "../../services/dataset.service"; import { SolrSettings } from "@/models/solr"; // PENDING USE import { OpenSettings } from "@/models/solr"; import { Component, Vue, Prop, Emit } from "vue-facing-decorator"; import { Dataset, Suggestion, SearchType } from "@/models/dataset"; import { SOLR_HOST, SOLR_CORE } from "@/constants"; import { OPEN_HOST, OPEN_CORE } from "@/constants"; // PENDING USE import { HitHighlight } from "@/models/headers"; import DOMPurify from 'dompurify'; // To sanitize the HTML content to prevent XSS attacks! @Component({ name: "VsInput", }) export default class VsInput extends Vue { // @Prop() // private title!: string; // Define the placeholder text for the input field @Prop({ default: "Search" }) readonly placeholder!: string; private display = ""; // Input display value @Prop() private propDisplay = ""; private value!: Suggestion | string; private error = ""; private results: Array = []; // Array to store search results private highlights: Array = []; private loading = false; // Loading state indicator private selectedIndex = -1; // Index of the currently selected suggestion private solr: SolrSettings = { core: SOLR_CORE, //"rdr_data", // SOLR.core; host: SOLR_HOST, //"tethys.at", }; private openSearch: OpenSettings = { core: OPEN_CORE, //"rdr_data", // SOLR.core; host: OPEN_HOST, //"tethys.at", // core: "test_data", // SOLR.core; // host: "repository.geologie.ac.at", }; // private rdrAPI!: DatasetService; itemRefs!: Array; // Array to store references to suggestion items emits = ["filter"]; // Emits filter event // Set reference for each item setItemRef(el: Element): void { this.itemRefs.push(el); } beforeUpdate(): void { this.itemRefs = []; } mounted(): void { // this.rdrAPI = new DatasetService(); } get showResults(): boolean { return this.results.length > 0; } get noResults(): boolean { return Array.isArray(this.results) && this.results.length === 0; } get isLoading(): boolean { return this.loading === true; } get hasError(): boolean { return this.error !== null; } // Computed property to generate suggestions based on search results get suggestions(): Suggestion[] { const suggestions = new Array(); console.log("getSuggestions > Display:", this.display); // console.log("results:", this.results ); // console.log("highlights:", this.highlights); //The method checks if there are any highlighted titles in the highlight object. If found, it joins the highlighted fragments into a single string // Generate suggestions based on search results this.results.forEach((dataset, index) => { const highlight = this.highlights[index]; // console.log("get suggestions:id", dataset.id); // console.log("get suggestions:title_output", dataset.title_output); // console.log("get suggestions:author", dataset.author); // console.log("get suggestions:subjects", dataset.subjects); // Checks if a suggestion with the same title and type already exists in the suggestions array. If not, it creates a new Suggestion object and adds it to the suggestions array. if (highlight.title && highlight.title.length > 0) { /** This line checks if the highlight object has a title property and if that property is an array with at least one element. * The highlight object contains highlighted fragments of the search term in various fields (e.g., title, author, subjects) as returned by the OpenSearch API. * This check ensures that we only process results that have highlighted titles. */ const highlightedTitle = highlight.title.join(" "); /** * The highlight.title property is an array of strings, where each string is a highlighted fragment of the title. join(" ") combines these fragments into a single string with spaces between them. * This step constructs a full highlighted title from the individual fragments. * OpenSearch can return multiple fragments of a field (like the title) in its response, especially when the field contains multiple terms that match the search query. * This can happen because OpenSearch's highlighting feature is designed to provide context around each match within the field, which can result in multiple highlighted fragments. */ const hasTitleSuggestion = suggestions.some((suggestion) => suggestion.highlight.toLowerCase() === highlightedTitle.toLowerCase() && suggestion.type == SearchType.Title); if (!hasTitleSuggestion) { const suggestion = new Suggestion(dataset.title_output, highlightedTitle, SearchType.Title); suggestions.push(suggestion); } } if (highlight.author && highlight.author.length > 0) { const highlightedAuthor = highlight.author.join(" "); const datasetAuthor = this.find(dataset.author, this.display.toLowerCase()); const hasAuthorSuggestion = suggestions.some((suggestion) => suggestion.highlight.toLowerCase() === highlightedAuthor.toLowerCase() && suggestion.type == SearchType.Author); if (!hasAuthorSuggestion) { const suggestion = new Suggestion(datasetAuthor, highlightedAuthor, SearchType.Author); suggestions.push(suggestion); } } if (highlight.subjects && highlight.subjects.length > 0) { const highlightedSubject = highlight.subjects.join(" "); const datasetSubject = this.find(dataset.subjects, this.display.toLowerCase()); const hasSubjectSuggestion = suggestions.some((suggestion) => suggestion.highlight.toLowerCase() === highlightedSubject.toLowerCase() && suggestion.type == SearchType.Subject); if (!hasSubjectSuggestion) { const suggestion = new Suggestion(datasetSubject, highlightedSubject, SearchType.Subject); suggestions.push(suggestion); } } // ORIGINAL SOLR =================================================================================================== // if (dataset.title_output.toLowerCase().includes(this.display.toLowerCase())) { // const title = dataset.title_output; // // Check if there is already a suggestion with this title and type // const hasTitleSuggestion = suggestions.some((suggestion) => suggestion.value === title && suggestion.type == SearchType.Title); // if (!hasTitleSuggestion) { // // If there is no such suggestion, create a new one and add it to the suggestions array // const suggestion = new Suggestion(title, SearchType.Title); // suggestions.push(suggestion); // } // } // if (this.find(dataset.author, this.display.toLowerCase()) !== "") { // const author = this.find(dataset.author, this.display.toLowerCase()); // // Check if there is already a suggestion with this author and type // const hasAuthorSuggestion = suggestions.some((suggestion) => suggestion.value === author && suggestion.type == SearchType.Author); // if (!hasAuthorSuggestion) { // const suggestion = new Suggestion(author, SearchType.Author); // suggestions.push(suggestion); // } // } // if (this.find(dataset.subjects, this.display.toLowerCase()) != "") { // const subject = this.find(dataset.subjects, this.display.toLowerCase()); // const hasSubjectSuggestion = suggestions.some((suggestion) => suggestion.value === subject && suggestion.type == SearchType.Subject); // if (!hasSubjectSuggestion) { // const suggestion = new Suggestion(subject, SearchType.Subject); // suggestions.push(suggestion); // } // } }); return suggestions; } /** * This method combines the suggestion value and type into a single HTML string. It also sanitizes the HTML content using DOMPurify to prevent XSS attacks. * The vue file uses the v-html directive to bind the combined HTML string to the label element. This ensures that the HTML content (e.g., Wien) is rendered correctly in the browser. */ formatSuggestion(result: Suggestion): string { const sanitizedValue = DOMPurify.sanitize(result.highlight); // Replacing the predefined format for highlights given by OpenSearch from emphasys to bold const replacedValue = sanitizedValue.replace(//g, '').replace(/<\/em>/g, ''); return `${replacedValue} | ${result.type}`; } /** * Clear all values, results and errors **/ clear(): void { console.log("clear"); this.display = ""; // this.value = null; this.results = []; this.error = ""; // this.$emit("clear"); } /* When the search button is clicked or the search input is changed, it updates the value property of the component with the current value of display, and emits a search-change event with the current value of display as the argument. */ @Emit("search-change") search(): string { console.log("search"); this.results = []; // this.$emit("search", this.display) this.value = this.display; //(obj["title_output"]) ? obj["title_output"] : obj.id return this.display; } // Handler for search input change searchChanged(): void { // console.log("Search changed!"); this.selectedIndex = -1; // Let's warn the parent that a change was made // this.$emit("input", this.display); if (this.display.length >= 2) { this.loading = true; this.resourceSearch(); } else { this.results = []; } } // Perform the search request private resourceSearch() { // console.log("resourceSearch"); if (!this.display) { this.results = []; return; } this.loading = true; // this.setEventListener(); this.request(); } // Make the API request to search for datasets private request(): void { console.log("request()"); // DatasetService.searchTerm(this.display, this.solr.core, this.solr.host).subscribe({ DatasetService.searchTerm(this.display, this.openSearch.core, this.openSearch.host).subscribe({ // next: (res: Dataset[]) => this.dataHandler(res), next: (res: { datasets: Dataset[], highlights: HitHighlight[] }) => this.dataHandler(res.datasets, res.highlights), error: (error: string) => this.errorHandler(error), complete: () => (this.loading = false), }); } // Handle the search results private dataHandler(datasets: Dataset[], highlights: HitHighlight[]): void { this.results = datasets; this.highlights = highlights; // Store highlights // console.log(datasets); } // Handle errors from the search request private errorHandler(err: string): void { this.error = err; } /** * Is this item selected? * @param {Object} * @return {Boolean} */ isSelected(key: number): boolean { return key === this.selectedIndex; } // Handle arrow down key press to navigate suggestions onArrowDown(ev: Event): void { console.log("onArrowDown"); ev.preventDefault(); if (this.selectedIndex === -1) { this.selectedIndex = 0; return; } this.selectedIndex = this.selectedIndex === this.suggestions.length - 1 ? 0 : this.selectedIndex + 1; this.fixScrolling(); } // Scroll the selected suggestion into view private fixScrolling() { const currentElement = this.itemRefs[this.selectedIndex]; currentElement.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start", }); } // Handle arrow up key press to navigate suggestions onArrowUp(ev: Event): void { console.log("onArrowUp"); ev.preventDefault(); if (this.selectedIndex === -1) { this.selectedIndex = this.suggestions.length - 1; return; } this.selectedIndex = this.selectedIndex === 0 ? this.suggestions.length - 1 : this.selectedIndex - 1; this.fixScrolling(); } // Handle enter key press to select a suggestion onEnter(): void { console.log("onEnter"); if (this.selectedIndex === -1) { // this.$emit("nothingSelected", this.display); this.display && this.search(); } else { this.select(this.suggestions[this.selectedIndex]); } // this.$emit("enter", this.display); } @Emit("search-change") private select(obj: Suggestion): Suggestion { console.log("select:"); this.value = obj; console.log(obj); this.display = obj.value; this.close(); return this.value; } // Find a search term in an array private find(myarray: Array, searchterm: string): string { for (let i = 0, len = myarray.length; i < len; i += 1) { if (typeof myarray[i] === "string" && myarray[i].toLowerCase().indexOf(searchterm) !== -1) { // print or whatever return myarray[i]; } } return ""; } /** * Close the results list. If nothing was selected clear the search */ close(): void { console.log("close"); if (!this.value) { this.clear(); } this.results = []; this.error = ""; } }