Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add autocomplete in search bar #165

Merged
merged 16 commits into from
Jun 20, 2024
4 changes: 4 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ The project is currently composed of several widgets
* searchalicious-button-transparent is a transparent button with defined style
* it can be used to replace the default button
* searchalicious-chart renders vega chart, currently only for distribution. Requires [vega](https://vega.github.io/).
* searchalicious-icon-cross is a cross icon
* it can be used to delete actions
* searchalicious-suggestion-entry is a suggestion entry
* it can be used to display a suggestion in searchalicious-bar

You can give a specific `name` attribute to your search bar.
Then all other component that needs to connect with this search must use the same value in `search-name` attribute
Expand Down
2 changes: 1 addition & 1 deletion frontend/public/off.html
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
<li class="search-li">
<div class="row collapse postfix-round search-bar">
<div class="columns">
<searchalicious-bar name="off" page-size="24" taxonomies="brand,category"></searchalicious-bar>
<searchalicious-bar name="off" page-size="24" suggestions="brand,category"></searchalicious-bar>
</div>
<div class="columns">
<searchalicious-button search-name="off">
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/mixins/search-ctl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,11 @@ export const SearchaliciousSearchMixin = <T extends Constructor<LitElement>>(
@state()
_count?: number;

/** list of facets containers */
_facetsParentNode() {
Kout95 marked this conversation as resolved.
Show resolved Hide resolved
return document.querySelectorAll('searchalicious-facets');
return document.querySelectorAll(
`searchalicious-facets[search-name=${this.name}]`
);
Comment on lines +122 to +124
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not work with default name, but I will fix this with a commit before testing.

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@ import {property} from 'lit/decorators.js';
import {DebounceMixin, DebounceMixinInterface} from './debounce';

/**
* Interface for the Autocomplete mixin.
* Interface for the Suggestion Selection mixin.
*/
export interface AutocompleteMixinInterface extends DebounceMixinInterface {
export interface SuggestionSelectionMixinInterface
extends DebounceMixinInterface {
inputName: string;
options: AutocompleteOption[];
options: SuggestionSelectionOption[];
value: string;
currentIndex: number;
getOptionIndex: number;
visible: boolean;
isLoading: boolean;
currentOption: AutocompleteOption | undefined;
currentOption: SuggestionSelectionOption | undefined;

onInput(event: InputEvent): void;
handleInput(value: string): void;
blurInput(): void;
resetInput(): void;
submit(isSuggestion?: boolean): void;
getAutocompleteValueByIndex(index: number): string;
handleArrowKey(direction: 'up' | 'down'): void;
handleEnter(event: KeyboardEvent): void;
handleEscape(): void;
Expand All @@ -31,33 +31,39 @@ export interface AutocompleteMixinInterface extends DebounceMixinInterface {
onBlur(): void;
}
/**
* Type for autocomplete option.
* Type for suggestion option.
*/
export type AutocompleteOption = {
export type SuggestionSelectionOption = {
value: string;
label: string;
};
/**
* Type for autocomplete result.
* Type for suggestion result.
*/
export type AutocompleteResult = {
export type SuggestionSelectionResult = {
value: string;
label?: string;
};

export const AutocompleteMixin = <T extends Constructor<LitElement>>(
/**
* This mixin handles the logic of having a list of suggestion,
* and letting the user choose on suggestion.
*
* It factors the interaction logic but does not deal with the rendering.
*/
export const SuggestionSelectionMixin = <T extends Constructor<LitElement>>(
superClass: T
): Constructor<AutocompleteMixinInterface> & T => {
class AutocompleteMixinClass extends DebounceMixin(superClass) {
): Constructor<SuggestionSelectionMixinInterface> & T => {
class SuggestionSelectionMixinClass extends DebounceMixin(superClass) {
@property({attribute: 'input-name'})
inputName = 'autocomplete';
inputName = 'suggestion';

/**
* The options for the autocomplete.
* The options for the suggestion.
* It is provided by the parent component.
*/
@property({attribute: false, type: Array})
options: AutocompleteOption[] = [];
options: SuggestionSelectionOption[] = [];

// selected values
@property()
Expand Down Expand Up @@ -86,7 +92,7 @@ export const AutocompleteMixin = <T extends Constructor<LitElement>>(
}

/**
* Handles the input event on the autocomplete and dispatch custom event : "autocomplete-input".
* Handles the input event on the suggestion and dispatch custom event : "suggestion-input".
* @param {InputEvent} event - The input event.
*/
onInput(event: InputEvent) {
Expand All @@ -96,7 +102,9 @@ export const AutocompleteMixin = <T extends Constructor<LitElement>>(
}

handleInput(value: string) {
throw new Error(`handleInput method must be implemented with ${value}`);
throw new Error(
`handleInput method must be implemented for ${this} with ${value}`
);
}
/**
* This method is used to remove focus from the input element.
Expand Down Expand Up @@ -125,7 +133,9 @@ export const AutocompleteMixin = <T extends Constructor<LitElement>>(
* @param {boolean} isSuggestion - A boolean value to check if the value is a suggestion.
*/
submit(isSuggestion = false) {
throw new Error(`submit method must be implemented with ${isSuggestion}`);
throw new Error(
`submit method must be implemented for ${this} with ${isSuggestion}`
);
}

/**
Expand All @@ -146,15 +156,15 @@ export const AutocompleteMixin = <T extends Constructor<LitElement>>(
* @param event
*/
handleEnter(event: KeyboardEvent) {
let isAutoComplete = false;
let isSuggestion = false;
if (this.currentIndex) {
isAutoComplete = true;
isSuggestion = true;
this.value = this.currentOption!.value;
} else {
const value = (event.target as HTMLInputElement).value;
this.value = value;
}
this.submit(isAutoComplete);
this.submit(isSuggestion);
}

/**
Expand Down Expand Up @@ -186,7 +196,7 @@ export const AutocompleteMixin = <T extends Constructor<LitElement>>(
}

/**
* On a click on the autocomplete option, we select it as value and submit it.
* On a click on the suggestion option, we select it as value and submit it.
* @param index
*/
onClick(index: number) {
Expand All @@ -200,15 +210,15 @@ export const AutocompleteMixin = <T extends Constructor<LitElement>>(

/**
* This method is used to handle the focus event on the input element.
* It is used to show the autocomplete options when the input is focused.
* It is used to show the suggestion options when the input is focused.
*/
onFocus() {
this.visible = true;
}

/**
* This method is used to handle the blur event on the input element.
* It is used to hide the autocomplete options when the input is blurred.
* It is used to hide the suggestion options when the input is blurred.
* It is debounced to avoid to quit before select with click.
*/
onBlur() {
Expand All @@ -218,5 +228,6 @@ export const AutocompleteMixin = <T extends Constructor<LitElement>>(
}
}

return AutocompleteMixinClass as Constructor<AutocompleteMixinInterface> & T;
return SuggestionSelectionMixinClass as Constructor<SuggestionSelectionMixinInterface> &
T;
};
3 changes: 2 additions & 1 deletion frontend/src/search-a-licious.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export {SearchaliciousAutocomplete} from './search-autocomplete';
export {SearchaliciousSecondaryButton} from './secondary-button';
export {SearchaliciousButtonTransparent} from './button-transparent';
export {SearchaliciousIconCross} from './icons/cross';
export {SearchaliciousTermLine} from './search-term-line';
export {SearchaliciousChart} from './search-chart';
export {SearchaliciousSuggestionEntry} from './search-suggestion-entry';
17 changes: 11 additions & 6 deletions frontend/src/search-autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import {AutocompleteMixin, AutocompleteResult} from './mixins/autocomplete';
import {
SuggestionSelectionMixin,
SuggestionSelectionResult,
} from './mixins/suggestion-selection';
import {SearchaliciousEvents} from './utils/enums';

/**
Expand All @@ -13,7 +16,9 @@ import {SearchaliciousEvents} from './utils/enums';
* @slot - This slot is for the button contents, default to "Search" string.
*/
@customElement('searchalicious-autocomplete')
export class SearchaliciousAutocomplete extends AutocompleteMixin(LitElement) {
export class SearchaliciousAutocomplete extends SuggestionSelectionMixin(
LitElement
) {
static override styles = css`
.search-autocomplete {
position: relative;
Expand Down Expand Up @@ -60,7 +65,7 @@ export class SearchaliciousAutocomplete extends AutocompleteMixin(LitElement) {
* Renders the possiblibility as list for user to select from
* @returns {import('lit').TemplateResult<1>} The HTML template for the possible terms.
*/
_renderPossibility() {
_renderSuggestions() {
return this.options.length
? this.options.map(
(option, index) => html` <li
Expand Down Expand Up @@ -94,7 +99,7 @@ export class SearchaliciousAutocomplete extends AutocompleteMixin(LitElement) {
/**
* This method is used to submit the input value.
* It is used to submit the input value after selecting an option.
* @param {boolean} isSuggestion - A boolean value to check if the value is a suggestion.
* @param {boolean} isSuggestion - A boolean value to check if the value is a suggestion or a free input from the user.
*/
override submit(isSuggestion = false) {
if (!this.value) return;
Expand All @@ -106,7 +111,7 @@ export class SearchaliciousAutocomplete extends AutocompleteMixin(LitElement) {
detail: {
value: this.value,
label: isSuggestion ? this.currentOption!.label : undefined,
} as AutocompleteResult,
} as SuggestionSelectionResult,
bubbles: true,
composed: true,
}
Expand Down Expand Up @@ -136,7 +141,7 @@ export class SearchaliciousAutocomplete extends AutocompleteMixin(LitElement) {
<ul class=${classMap({visible: this.visible && this.value.length})}>
${this.isLoading
? html`<li>Loading...</li>`
: this._renderPossibility()}
: this._renderSuggestions()}
</ul>
</span>
`;
Expand Down
28 changes: 14 additions & 14 deletions frontend/src/search-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {SearchaliciousSearchMixin} from './mixins/search-ctl';
import {SearchaliciousTermsMixin} from './mixins/suggestions-ctl';
import {AutocompleteMixin} from './mixins/autocomplete';
import {SuggestionSelectionMixin} from './mixins/suggestion-selection';
import {classMap} from 'lit/directives/class-map.js';
import {removeLangFromTermId} from './utils/taxonomies';

Expand All @@ -13,7 +13,7 @@ import {removeLangFromTermId} from './utils/taxonomies';
* and it also manage all the search thanks to SearchaliciousSearchMixin inheritance.
*/
@customElement('searchalicious-bar')
export class SearchaliciousBar extends AutocompleteMixin(
export class SearchaliciousBar extends SuggestionSelectionMixin(
SearchaliciousTermsMixin(SearchaliciousSearchMixin(LitElement))
) {
static override styles = css`
Expand All @@ -26,6 +26,7 @@ export class SearchaliciousBar extends AutocompleteMixin(
position: relative;
}

/* Search suggestions list */
.search-bar ul {
Kout95 marked this conversation as resolved.
Show resolved Hide resolved
--left-offset: 8px;
position: absolute;
Expand Down Expand Up @@ -54,10 +55,10 @@ export class SearchaliciousBar extends AutocompleteMixin(
`;

/**
* The selected taxonomies
* Taxonomies we want to use for suggestions
*/
@property({type: String, attribute: 'taxonomies'})
taxonomies = '';
@property({type: String, attribute: 'suggestions'})
suggestions = '';

/**
* Place holder in search bar
Expand All @@ -66,10 +67,10 @@ export class SearchaliciousBar extends AutocompleteMixin(
placeholder = 'Search...';

/**
* It parses the string taxonomies attribute and returns an array
* It parses the string suggestions attribute and returns an array
*/
get parsedTaxonomies() {
return this.taxonomies.split(',');
get parsedSuggestions() {
return this.suggestions.split(',');
}

/**
Expand All @@ -80,7 +81,7 @@ export class SearchaliciousBar extends AutocompleteMixin(
override handleInput(value: string) {
this.query = value;
this.debounce(() => {
this.getTaxonomiesTerms(value, this.parsedTaxonomies).then(() => {
this.getTaxonomiesTerms(value, this.parsedSuggestions).then(() => {
this.options = this.terms.map((term) => ({
value: term.text,
label: term.text,
Expand All @@ -90,10 +91,9 @@ export class SearchaliciousBar extends AutocompleteMixin(
}

/**
* Submit the search
* Submit - might either be selecting a suggestion or submitting a search expression
*/
override submit(isSuggestion?: boolean) {
console.log(this.query, this.value, isSuggestion);
// If the value is a suggestion, select the term and reset the input otherwise search
if (isSuggestion) {
this.selectTermByTaxonomy(
Expand Down Expand Up @@ -126,9 +126,9 @@ export class SearchaliciousBar extends AutocompleteMixin(
class=${classMap({selected: index + 1 === this.currentIndex})}
@click=${this.onClick(index)}
>
<searchalicious-term-line
<searchalicious-suggestion-entry
.term=${term}
></searchalicious-term-line>
></searchalicious-suggestion-entry>
</li>
`
)}
Expand All @@ -138,7 +138,7 @@ export class SearchaliciousBar extends AutocompleteMixin(

override render() {
return html`
<div class="search-bar">
<div class="search-bar" part="wrapper">
<input
type="text"
name="q"
Expand Down
Loading