import { html, HTMLTemplateResult, PropertyValueMap, unsafeCSS } from "lit";
import { property, query, queryAssignedElements } from "lit/decorators.js";
import eleStyle from "./f-search.scss?inline";
import globalStyle from "./f-search-global.scss?inline";
import { FRoot } from "../../mixins/components/f-root/f-root";
import { FText } from "../f-text/f-text";
import { FDiv } from "../f-div/f-div";
import { FSelect } from "../f-select/f-select";
import { FSuggest } from "../f-suggest/f-suggest";
import { FIconButton } from "../f-icon-button/f-icon-button";
import { flowElement } from "./../../utils";
import { injectCss } from "@cldcvr/flow-core-config";
injectCss("f-search", globalStyle);
export type FSearchState = "primary" | "default" | "success" | "warning" | "danger";
export type FSearchCustomEvent = {
value: Type;
scope: string;
};
export type FSearchSuffixWhen = (value: string) => boolean;
export type FSearchSuggestionsCategory = Record;
export type FSearchOptionTemplate = {
value: Type;
template: (value?: string) => HTMLTemplateResult;
toString: () => string;
};
export type FSearchResultWhen = (
suggestion: string | FSearchOptionTemplate,
value?: string
) => boolean;
export type FSearchSuggestions = string[] | FSearchSuggestionsCategory | FSearchOptionTemplate[];
export type FSearchScope = string[] | "none";
@flowElement("f-search")
export class FSearch extends FRoot {
/**
* css loaded from scss file
*/
static styles = [
unsafeCSS(eleStyle),
unsafeCSS(globalStyle),
...FText.styles,
...FDiv.styles,
...FSuggest.styles,
...FSelect.styles,
...FIconButton.styles
];
/**
* @attribute Variants are various visual representations of an input field.
*/
@property({ reflect: true, type: String })
variant?: "curved" | "round" | "block" = "curved";
/**
* @attribute Categories are various visual representations of an input field.
*/
@property({ reflect: true, type: String })
category?: "fill" | "outline" | "transparent" = "fill";
/**
* @attribute results to show on value
*/
@property({ reflect: true, type: String })
result?: FSearchSuggestions = [];
/**
* @attribute disable showing result
*/
@property({ reflect: true, type: Boolean, attribute: "disable-result" })
disableResult = false;
/**
* @attribute States are used to communicate purpose and connotations.
*/
@property({ reflect: true, type: String })
state?: FSearchState = "default";
/**
* @attribute f-input can have 2 sizes. By default size is inherited by the parent f-field.
*/
@property({ reflect: true, type: String })
size?: "medium" | "small" = "medium";
/**
* @attribute Defines the value of a f-search.
*/
@property({ reflect: true, type: String })
value?: string;
/**
* @attribute sets the value of scope in use
*/
@property({ reflect: true, type: String, attribute: "selected-scope" })
selectedScope?: string;
/**
* for vue2 camelcase support
*/
set ["selected-scope"](val: string) {
this.selectedScope = val;
}
/**
* @attribute Defines the placeholder text
*/
@property({ reflect: true, type: String })
placeholder?: string;
/**
* @attribute Shows scope list if not none
*/
@property({ reflect: true, type: Array })
scope?: FSearchScope = "none";
/**
* @attribute Shows disabled state of input element
*/
@property({ reflect: true, type: Boolean })
disabled?: boolean = false;
/**
* @attribute Displays a close icon-button on the right side of the input that allows the user to clear the input value
*/
@property({ reflect: true, type: Boolean })
clear?: boolean = true;
/**
* @attribute Note: Provides an icon button to trigger the search query. Note: Search icon on left is not shown if search-button is true.
*/
@property({ reflect: true, type: Boolean })
["search-button"]?: boolean = false;
/**
* @attribute Loader icon .
*/
@property({ reflect: true, type: Boolean })
loading?: boolean = false;
/**
* to customize result
*/
@property({ reflect: false, type: Function, attribute: "result-when" })
resultWhen: FSearchResultWhen = (sg, value) => {
if (typeof sg === "object") {
return sg
.toString()
.toLocaleLowerCase()
.includes(value?.toLocaleLowerCase() ?? "");
}
return sg.toLocaleLowerCase().includes(value?.toLocaleLowerCase() ?? "");
};
set ["result-when"](val: FSearchResultWhen) {
this.resultWhen = val;
}
/**
* @attribute max height for options
*/
@property({ reflect: true, type: String, attribute: "result-max-height" })
resultMaxHeight?: string;
@query("slot[name='label']")
labelSlot!: HTMLElement;
@query("slot[name='description']")
descriptionSlot!: HTMLElement;
@query("slot[name='help']")
helpSlot!: HTMLElement;
@query("#header-section")
headerSection!: FDiv;
@query("#helper-text-section")
helperTextSection!: FDiv;
@query("f-suggest")
suggestElement!: FSuggest;
/**
* @attribute assigned elements inside slot label
*/
@queryAssignedElements({ slot: "label" })
_labelNodes!: NodeListOf;
/**
* @attribute assigned elements inside slot description
*/
@queryAssignedElements({ slot: "description" })
_descriptionNodes!: NodeListOf;
/**
* @attribute assigned elements inside slot help
*/
@queryAssignedElements({ slot: "help" })
_helpNodes!: NodeListOf;
/**
* has label slot
*/
get hasLabel() {
return this._labelNodes.length > 0;
}
/**
* has description slot
*/
get hasDescription() {
return this._descriptionNodes.length > 0;
}
/**
* has help slot
*/
get hasHelperText() {
return this._helpNodes.length > 0;
}
/**
* emit input custom event
*/
handleInput(e: CustomEvent<{ value: unknown }>) {
e.stopPropagation();
if (e.detail.value) {
if (typeof e.detail.value === "object") {
this.value = (e.detail.value as FSearchOptionTemplate)?.toString();
} else {
this.value = String(e.detail.value);
}
} else {
this.value = undefined;
}
this.dispatchInputEvent(e.detail.value, this.selectedScope);
}
/**
* emit input custom event for scope
*/
handleScopeSelection(e: CustomEvent<{ value: string }>) {
e.stopPropagation();
this.selectedScope = e.detail.value;
this.dispatchInputEvent(this.value ?? "", e.detail.value);
}
/**
* emit search custom event
*/
handleSearchClick(e: CustomEvent) {
e.stopPropagation();
const event = new CustomEvent("search", {
detail: {
value: this.value,
search: true
},
bubbles: true,
composed: true
});
this.dispatchEvent(event);
}
/**
* clear input value on clear icon clicked
*/
clearInputValue() {
this.value = "";
this.dispatchInputEvent("", this.selectedScope);
}
/**
*
* @param value string for value
* @param scope string for scope value
*/
dispatchInputEvent(value: any, scope = "") {
const event = new CustomEvent("input", {
detail: {
value: value,
scope: scope
},
bubbles: true,
composed: true
});
this.dispatchEvent(event);
}
/**
* set styling class
*/
get applyStyling() {
return this.scope !== "none"
? this["search-button"]
? "f-search-border-button"
: "f-search-border"
: this["search-button"]
? "f-search-suggest-button"
: "f-search-suggest";
}
/**
* conditional help section display for false spacing issue
*/
displayHelpSection() {
if (!this.hasHelperText) {
this.helperTextSection.style.display = "none";
} else {
this.helperTextSection.style.display = "";
}
}
/**
* conditional header section display for false spacing issue
*/
displayHeaderSection() {
if (!this.hasLabel && !this.hasDescription) {
this.headerSection.style.display = "none";
} else {
this.headerSection.style.display = "";
}
if (!this.hasLabel) {
this.labelSlot.style.display = "none";
}
}
render() {
return html`
${this.scope !== "none" && (this.scope as string[])?.length > 0
? html`
`
: ""}
${this["search-button"]
? html` `
: ""}
`;
}
protected updated(changedProperties: PropertyValueMap | Map) {
super.updated(changedProperties);
this.displayHelpSection();
this.displayHeaderSection();
}
}
/**
* Required for typescript
*/
declare global {
interface HTMLElementTagNameMap {
"f-search": FSearch;
}
}