354 lines
14 KiB
TypeScript
354 lines
14 KiB
TypeScript
// 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<Dataset> = []; // Array to store search results
|
|
private highlights: Array<HitHighlight> = [];
|
|
|
|
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<Element>; // 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<Suggestion>();
|
|
|
|
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., <em>Wien</em>) 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 <em> emphasys to <b> bold
|
|
const replacedValue = sanitizedValue.replace(/<em>/g, '<b>').replace(/<\/em>/g, '</b>');
|
|
return `${replacedValue} <em>| ${result.type}</em>`;
|
|
}
|
|
|
|
/**
|
|
* 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<string>, 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 = "";
|
|
}
|
|
}
|