From 80ba4671f51d824a345fc0a26cc5187656744bd9 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Thu, 14 Nov 2024 04:40:43 +0400 Subject: [PATCH 1/2] Fixed autocomplete popup duplication --- src/lib/components/SearchWrapper.js | 53 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/lib/components/SearchWrapper.js b/src/lib/components/SearchWrapper.js index 217c645..4e4378e 100644 --- a/src/lib/components/SearchWrapper.js +++ b/src/lib/components/SearchWrapper.js @@ -13,6 +13,8 @@ export class SearchWrapper extends BaseComponent { #arePropertiesSuggestionsEnabled = false; /** @type {"start"|"end"} */ #propertiesSuggestionsPosition = "start"; + /** @type {HTMLElement|null} */ + #cachedAutocompleteContainer = null; build() { this.#searchField = this.container.querySelector('input[name=q]'); @@ -113,6 +115,24 @@ export class SearchWrapper extends BaseComponent { 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. @@ -124,9 +144,21 @@ export class SearchWrapper extends BaseComponent { .map(suggestedTerm => SearchWrapper.#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,23 +271,6 @@ 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. From c0139d06386bb2d57fb07d5c995a2d533f8fee6d Mon Sep 17 00:00:00 2001 From: KoloMl Date: Thu, 14 Nov 2024 05:06:27 +0400 Subject: [PATCH 2/2] Fixed properties not being clickable with mouse --- src/lib/components/SearchWrapper.js | 42 +++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/lib/components/SearchWrapper.js b/src/lib/components/SearchWrapper.js index 4e4378e..43720db 100644 --- a/src/lib/components/SearchWrapper.js +++ b/src/lib/components/SearchWrapper.js @@ -15,6 +15,8 @@ export class SearchWrapper extends BaseComponent { #propertiesSuggestionsPosition = "start"; /** @type {HTMLElement|null} */ #cachedAutocompleteContainer = null; + /** @type {TermToken|QuotedTermToken|null} */ + #lastTermToken = null; build() { this.#searchField = this.container.querySelector('input[name=q]'); @@ -96,6 +98,7 @@ export class SearchWrapper extends BaseComponent { let searchValue = this.#searchField.value; if (!searchValue) { + this.#lastTermToken = null; return null; } @@ -105,13 +108,16 @@ 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; } @@ -141,7 +147,7 @@ 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 = this.#resolveAutocompleteContainer(); @@ -276,7 +282,7 @@ export class SearchWrapper extends BaseComponent { * @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'); @@ -284,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.