@universal-material/web
Version:
Material web components
362 lines • 11.5 kB
JavaScript
import { __decorate } from "tslib";
import { html, LitElement } from 'lit';
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import { customElement, property, query, queryAll, state } from 'lit/decorators.js';
import { MenuFieldNavigationController } from '../shared/menu-field/menu-field-navigation-controller.js';
import { normalizeText } from '../shared/normalize-text.js';
import { styles } from './typeahead.styles.js';
import '../menu/menu.js';
import '../menu/menu-item.js';
import './highlight.js';
let UmTypeahead = class UmTypeahead extends LitElement {
static { this.formAssociated = true; }
static { this.styles = styles; }
#targetId;
#connected;
#documentMutationObserver;
#navigationController;
#termNormalized;
#debounceTimeout;
#value;
#elementInternals;
get form() {
return this.#elementInternals.form;
}
/**
* Gets or sets the current value of the typeahead.
*/
get value() {
return this.#value;
}
set value(value) {
this.#value = value;
this.#elementInternals.setFormValue(value);
if (this.#connected) {
this.#setValueOnTarget();
}
}
focus() {
this.target?.focus();
}
clear() {
if (!this.target) {
return;
}
this.#termNormalized = '';
this.setTargetValue('');
}
/**
* The id of the target element to attach the typeahead.
*/
get targetId() {
return this.#targetId;
}
set targetId(value) {
this.#targetId = value;
if (this.#connected) {
this.#attach();
}
}
get _menuItems() {
return Array.from(this.menuItems);
}
constructor() {
super();
this.#connected = false;
this.target = null;
this.#documentMutationObserver = null;
this.#navigationController = new MenuFieldNavigationController(this);
this.#termNormalized = '';
this.#debounceTimeout = null;
/**
* The time in milliseconds before triggering an update in the results.
*/
this.debounce = 300;
/**
* The number of suggestions to show
*/
this.limit = 10;
/**
* How many characters must be typed before show suggestions
*
* _Note:_ Not used when the source is a `Promise`
*/
this.minLength = 2;
/**
* Whether the menu will be show when the target get focus.
*
* _Note:_ The `minLength` will still be applied
*/
this.openOnFocus = false;
/**
* If `true`, model values will not be restricted only to items selected from the menu.
*/
this.editable = false;
/**
* The value for the `autocomplete` attribute for the target element.
*/
this.autocomplete = 'off';
/**
* The value for the `spellcheck` attribute for the target element.
*/
this.spellcheck = false;
this.#handleFocus = async () => {
if (this.openOnFocus) {
await this.#updateResults();
}
};
this.#handleInput = () => {
if (this.#debounceTimeout) {
clearTimeout(this.#debounceTimeout);
}
this.#setValueAndDispatchEvents(this.editable ? this.getTargetValue() : null, true);
this.#debounceTimeout = setTimeout(async () => await this.#updateResults(true), this.debounce);
};
this.#elementInternals = this.attachInternals();
}
attributeChangedCallback(name, _old, value) {
super.attributeChangedCallback(name, _old, value);
if (!this.target) {
return;
}
if (name === 'autocomplete') {
this.target.autocomplete = value;
}
if (name === 'spellcheck') {
this.target.spellcheck = value === 'true';
}
}
connectedCallback() {
super.connectedCallback();
this.#connected = true;
this.#attach();
this.#documentMutationObserver = new MutationObserver(() => this.#attach());
this.#documentMutationObserver.observe(document, {
attributes: true,
childList: true,
});
}
disconnectedCallback() {
super.disconnectedCallback();
this.#connected = false;
this.#detach();
this.#documentMutationObserver.disconnect();
this.#documentMutationObserver = null;
}
#attach() {
if (!this.targetId) {
this.#detach();
return;
}
const newTarget = document.getElementById(this.targetId);
if (newTarget === this.target) {
return;
}
this.#detach();
if (!newTarget) {
return;
}
// @ts-ignore
this.target = newTarget;
newTarget.role = 'combobox';
newTarget.autocomplete = this.autocomplete;
newTarget.spellcheck = this.spellcheck;
newTarget.autocapitalize = 'off';
newTarget.addEventListener('click', this.#handleClick);
newTarget.addEventListener('input', this.#handleInput);
this.#navigationController.attach(newTarget);
newTarget.addEventListener('focus', this.#handleFocus);
if (this.value) {
this.#setValueOnTarget();
}
}
#detach() {
this.target?.removeEventListener('click', this.#handleClick);
this.target?.removeEventListener('input', this.#handleInput);
this.#navigationController.detach();
this.target?.removeEventListener('focus', this.#handleFocus);
}
#handleItemMouseDown(e) {
e.preventDefault();
}
#handleClick(e) {
e.stopPropagation();
}
#handleFocus;
#handleInput;
#getItemClickHandler(data) {
return () => {
const selectedEvent = new CustomEvent('selected', {
cancelable: true,
detail: data.value,
});
this.dispatchEvent(selectedEvent);
if (selectedEvent.defaultPrevented) {
return;
}
this.#setValueAndDispatchEvents(data.value);
};
}
#setValueAndDispatchEvents(value, direct = false) {
if (!direct) {
this.value = value;
}
else {
this.#value = value;
this.#elementInternals.setFormValue(value);
}
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
this.dispatchEvent(new Event('change', { bubbles: true }));
}
render() {
if (!this.results?.length) {
return html ``;
}
setTimeout(() => {
this._menu.anchorElement = this.getMenuAnchor();
this._menu.open = true;
this.#navigationController.focusMenu(this._menuItems[0], 0);
});
return html `
<u-menu manualFocus manualClose anchor-corner="auto-start">
${this.results.map(result => {
const content = this.template
? unsafeHTML(this.template(this.#termNormalized, result.value))
: html `
<u-highlight .term=${this.#termNormalized} .result=${result.label}></u-highlight>
`;
return html `
<u-menu-item
=${this.#handleItemMouseDown}
=${this.#getItemClickHandler(result)}
tabindex="-1">
${content}
</u-menu-item>
`;
})}
</u-menu>
`;
}
async #updateResults(lazy = false) {
const term = this.getTargetValue();
const termNormalized = normalizeText(term).toLowerCase();
if (lazy && termNormalized === this.#termNormalized && this._menu?.open === true) {
return;
}
this.#termNormalized = termNormalized;
if (termNormalized.length < this.minLength) {
this.results = [];
return;
}
this.results = await this.#getData();
this.results = this.results.slice(0, this.limit || this.results.length);
}
async #getData() {
if (!this.source) {
return [];
}
let values;
let filter = false;
if (this.source instanceof Array) {
values = this.source;
filter = true;
}
else {
const source = this.source;
values = await source(this.#termNormalized);
}
const result = values.map(source => ({
label: this.formatter ? this.formatter(source) : source.toString(),
value: source,
}));
if (!filter) {
return result;
}
return result.filter(t => normalizeText(t.label).toLowerCase()
.includes(this.#termNormalized));
}
#setValueOnTarget() {
if (!this.target) {
return;
}
const textValue = this.getTextValue();
this.#termNormalized = normalizeText(textValue)?.toLowerCase();
if (this.target.tagName === 'U-TEXT-FIELD') {
this.target.value = textValue;
return;
}
if (this.target.input) {
this.target.input.value = textValue;
return;
}
this.target.value = textValue;
}
getTargetValue() {
return this.target.input?.value ?? this.target.value;
}
setTargetValue(value) {
const targetInput = this.target?.input ?? this.target;
if (targetInput) {
targetInput.value = value;
}
}
getMenuAnchor() {
if (!this.target) {
return null;
}
if (this.target.tagName === 'U-CHIP-FIELD') {
return this.target.input;
}
if (this.target.tagName === 'U-TEXT-FIELD') {
return this.target.container;
}
return this.target;
}
getTextValue() {
if (!this.value) {
return '';
}
return this.formatter ? this.formatter(this.value) : this.value;
}
};
__decorate([
state()
], UmTypeahead.prototype, "results", void 0);
__decorate([
state()
], UmTypeahead.prototype, "source", void 0);
__decorate([
property({ type: Number, reflect: true })
], UmTypeahead.prototype, "debounce", void 0);
__decorate([
property({ type: Number, reflect: true })
], UmTypeahead.prototype, "limit", void 0);
__decorate([
property({ type: Number, reflect: true })
], UmTypeahead.prototype, "minLength", void 0);
__decorate([
property({ type: Boolean, attribute: 'open-on-focus', reflect: true })
], UmTypeahead.prototype, "openOnFocus", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], UmTypeahead.prototype, "editable", void 0);
__decorate([
property({ reflect: true })
], UmTypeahead.prototype, "autocomplete", void 0);
__decorate([
property({ reflect: true })
], UmTypeahead.prototype, "spellcheck", void 0);
__decorate([
property({ reflect: true, attribute: 'target-id' })
], UmTypeahead.prototype, "targetId", null);
__decorate([
query('u-menu')
], UmTypeahead.prototype, "_menu", void 0);
__decorate([
queryAll('u-menu-item')
], UmTypeahead.prototype, "menuItems", void 0);
UmTypeahead = __decorate([
customElement('u-typeahead')
], UmTypeahead);
export { UmTypeahead };
//# sourceMappingURL=typeahead.js.map