UNPKG

@universal-material/web

Version:
362 lines 11.5 kB
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 @mousedown=${this.#handleItemMouseDown} @click=${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