|
|
|
|
@@ -13,6 +13,10 @@ export class SearchWrapper extends BaseComponent {
|
|
|
|
|
#arePropertiesSuggestionsEnabled = false;
|
|
|
|
|
/** @type {"start"|"end"} */
|
|
|
|
|
#propertiesSuggestionsPosition = "start";
|
|
|
|
|
/** @type {HTMLElement|null} */
|
|
|
|
|
#cachedAutocompleteContainer = null;
|
|
|
|
|
/** @type {TermToken|QuotedTermToken|null} */
|
|
|
|
|
#lastTermToken = null;
|
|
|
|
|
|
|
|
|
|
build() {
|
|
|
|
|
this.#searchField = this.container.querySelector('input[name=q]');
|
|
|
|
|
@@ -94,6 +98,7 @@ export class SearchWrapper extends BaseComponent {
|
|
|
|
|
let searchValue = this.#searchField.value;
|
|
|
|
|
|
|
|
|
|
if (!searchValue) {
|
|
|
|
|
this.#lastTermToken = null;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -103,16 +108,37 @@ export class SearchWrapper extends BaseComponent {
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (token instanceof TermToken) {
|
|
|
|
|
this.#lastTermToken = token;
|
|
|
|
|
return token.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (token instanceof QuotedTermToken) {
|
|
|
|
|
this.#lastTermToken = token;
|
|
|
|
|
return token.decodedValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.#lastTermToken = null;
|
|
|
|
|
return searchValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolve the autocomplete container from the document. Once resolved, it can be safely reused without breaking
|
|
|
|
|
* anything. Assuming refactored autocomplete handler is still implemented the way it is.
|
|
|
|
|
*
|
|
|
|
|
* This means, that properties will only be suggested once actual autocomplete logic was activated.
|
|
|
|
|
*
|
|
|
|
|
* @return {HTMLElement|null} Resolved element or nothing.
|
|
|
|
|
*/
|
|
|
|
|
#resolveAutocompleteContainer() {
|
|
|
|
|
if (this.#cachedAutocompleteContainer) {
|
|
|
|
|
return this.#cachedAutocompleteContainer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.#cachedAutocompleteContainer = document.querySelector('.autocomplete');
|
|
|
|
|
|
|
|
|
|
return this.#cachedAutocompleteContainer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render the list of suggestions into the existing popup or create and populate a new one.
|
|
|
|
|
* @param {string[]} suggestions List of suggestion to render the popup from.
|
|
|
|
|
@@ -121,12 +147,24 @@ export class SearchWrapper extends BaseComponent {
|
|
|
|
|
#renderSuggestions(suggestions, targetInput) {
|
|
|
|
|
/** @type {HTMLElement[]} */
|
|
|
|
|
const suggestedListItems = suggestions
|
|
|
|
|
.map(suggestedTerm => SearchWrapper.#renderTermSuggestion(suggestedTerm));
|
|
|
|
|
.map(suggestedTerm => this.#renderTermSuggestion(suggestedTerm));
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
const autocompleteContainer = document.querySelector('.autocomplete') ?? SearchWrapper.#renderAutocompleteContainer();
|
|
|
|
|
const autocompleteContainer = this.#resolveAutocompleteContainer();
|
|
|
|
|
|
|
|
|
|
for (let existingTerm of autocompleteContainer.querySelectorAll('.autocomplete__item--property')) {
|
|
|
|
|
if (!autocompleteContainer) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Since the autocomplete popup was refactored to re-use the same element over and over again, we need to remove
|
|
|
|
|
// the options from the popup manually when autocomplete was removed from the DOM, since site is not doing that.
|
|
|
|
|
const termsToRemove = autocompleteContainer.isConnected
|
|
|
|
|
// Only removing properties when element is still connected to the DOM (popup is used by the website)
|
|
|
|
|
? autocompleteContainer.querySelectorAll('.autocomplete__item--property')
|
|
|
|
|
// Remove everything if popup was disconnected from the DOM.
|
|
|
|
|
: autocompleteContainer.querySelectorAll('.autocomplete__item')
|
|
|
|
|
|
|
|
|
|
for (let existingTerm of termsToRemove) {
|
|
|
|
|
existingTerm.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -239,29 +277,12 @@ export class SearchWrapper extends BaseComponent {
|
|
|
|
|
return suggestionsList;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render a new autocomplete container similar to the one generated by website. Might be sensitive to the updates
|
|
|
|
|
* made to the Philomena.
|
|
|
|
|
* @return {HTMLElement}
|
|
|
|
|
*/
|
|
|
|
|
static #renderAutocompleteContainer() {
|
|
|
|
|
const autocompleteContainer = document.createElement('div');
|
|
|
|
|
autocompleteContainer.className = 'autocomplete';
|
|
|
|
|
|
|
|
|
|
const innerListContainer = document.createElement('ul');
|
|
|
|
|
innerListContainer.className = 'autocomplete__list';
|
|
|
|
|
|
|
|
|
|
autocompleteContainer.append(innerListContainer);
|
|
|
|
|
|
|
|
|
|
return autocompleteContainer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render a single suggestion item and connect required events to interact with the user.
|
|
|
|
|
* @param {string} suggestedTerm Term to use for suggestion item.
|
|
|
|
|
* @return {HTMLElement} Resulting element.
|
|
|
|
|
*/
|
|
|
|
|
static #renderTermSuggestion(suggestedTerm) {
|
|
|
|
|
#renderTermSuggestion(suggestedTerm) {
|
|
|
|
|
/** @type {HTMLElement} */
|
|
|
|
|
const suggestionItem = document.createElement('li');
|
|
|
|
|
suggestionItem.classList.add('autocomplete__item', 'autocomplete__item--property');
|
|
|
|
|
@@ -269,17 +290,43 @@ export class SearchWrapper extends BaseComponent {
|
|
|
|
|
suggestionItem.innerText = suggestedTerm;
|
|
|
|
|
|
|
|
|
|
suggestionItem.addEventListener('mouseover', () => {
|
|
|
|
|
this.#findAndResetSelectedSuggestion(suggestionItem);
|
|
|
|
|
SearchWrapper.#findAndResetSelectedSuggestion(suggestionItem);
|
|
|
|
|
suggestionItem.classList.add('autocomplete__item--selected');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
suggestionItem.addEventListener('mouseout', () => {
|
|
|
|
|
this.#findAndResetSelectedSuggestion(suggestionItem);
|
|
|
|
|
})
|
|
|
|
|
SearchWrapper.#findAndResetSelectedSuggestion(suggestionItem);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
suggestionItem.addEventListener('click', () => {
|
|
|
|
|
this.#replaceLastActiveTokenWithSuggestion(suggestedTerm);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return suggestionItem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Automatically replace the last active token stored in the variable with the new value.
|
|
|
|
|
* @param {string} suggestedTerm Term to replace the value with.
|
|
|
|
|
*/
|
|
|
|
|
#replaceLastActiveTokenWithSuggestion(suggestedTerm) {
|
|
|
|
|
if (!this.#lastTermToken) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const searchQuery = this.#searchField.value;
|
|
|
|
|
const beforeToken = searchQuery.substring(0, this.#lastTermToken.index);
|
|
|
|
|
const afterToken = searchQuery.substring(this.#lastTermToken.index + this.#lastTermToken.value.length);
|
|
|
|
|
|
|
|
|
|
let replacementValue = suggestedTerm;
|
|
|
|
|
|
|
|
|
|
if (replacementValue.includes('"')) {
|
|
|
|
|
replacementValue = `"${QuotedTermToken.encode(replacementValue)}"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.#searchField.value = beforeToken + replacementValue + afterToken;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find the selected suggestion item(s) and unselect them. Similar to the logic implemented by the Philomena's
|
|
|
|
|
* front-end.
|
|
|
|
|
|