UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

169 lines 6.02 kB
import { TextInput, TextInputView } from "./text_input"; import { empty, display, undisplay, div } from "../../core/dom"; import { take } from "../../core/util/iterator"; import { clamp } from "../../core/util/math"; import { Enum } from "../../core/kinds"; import dropdown_css, * as dropdown from "../../styles/dropdown.css"; const SearchStrategy = Enum("starts_with", "includes"); export class AutocompleteInputView extends TextInputView { static __name__ = "AutocompleteInputView"; _open = false; _last_value = ""; _hover_index = 0; menu; stylesheets() { return [...super.stylesheets(), dropdown_css]; } render() { super.render(); this.input_el.addEventListener("focusin", () => this._toggle_menu()); this.menu = div({ class: [dropdown.menu, dropdown.below] }); this.menu.addEventListener("click", (event) => this._menu_click(event)); this.menu.addEventListener("mouseover", (event) => this._menu_hover(event)); this.shadow_el.appendChild(this.menu); undisplay(this.menu); } change_input() { if (this._open && this.menu.children.length > 0) { this.model.value = this.menu.children[this._hover_index].textContent; this.input_el.focus(); this._hide_menu(); } else if (!this.model.restrict) { super.change_input(); } } _update_completions(completions) { empty(this.menu); const { max_completions } = this.model; const selected_completions = max_completions != null ? take(completions, max_completions) : completions; for (const text of selected_completions) { const item = div(text); this.menu.append(item); } this.menu.firstElementChild?.classList.add(dropdown.active); } compute_completions(value) { const norm_function = (() => { const { case_sensitive } = this.model; return case_sensitive ? (t) => t : (t) => t.toLowerCase(); })(); const search_function = (() => { switch (this.model.search_strategy) { case "starts_with": return (t, v) => t.startsWith(v); case "includes": return (t, v) => t.includes(v); } })(); const normalized_value = norm_function(value); const completions = []; for (const text of this.model.completions) { const normalized_text = norm_function(text); if (search_function(normalized_text, normalized_value)) { completions.push(text); } } return completions; } _toggle_menu() { const { value } = this.input_el; if (value.length < this.model.min_characters) { this._hide_menu(); return; } const completions = this.compute_completions(value); this._update_completions(completions); if (completions.length == 0) { this._hide_menu(); } else { this._show_menu(); } } _show_menu() { if (!this._open) { this._open = true; this._hover_index = 0; this._last_value = this.model.value; display(this.menu); const listener = (event) => { if (!event.composedPath().includes(this.el)) { document.removeEventListener("click", listener); this._hide_menu(); } }; document.addEventListener("click", listener); } } _hide_menu() { if (this._open) { this._open = false; undisplay(this.menu); } } _menu_click(event) { if (event.target != event.currentTarget && event.target instanceof Element) { this.model.value = event.target.textContent; this.input_el.focus(); this._hide_menu(); } } _menu_hover(event) { if (event.target != event.currentTarget && event.target instanceof Element) { for (let i = 0; i < this.menu.children.length; i++) { if (this.menu.children[i].textContent == event.target.textContent) { this._bump_hover(i); break; } } } } _bump_hover(new_index) { const n_children = this.menu.children.length; if (this._open && n_children > 0) { this.menu.children[this._hover_index].classList.remove(dropdown.active); this._hover_index = clamp(new_index, 0, n_children - 1); this.menu.children[this._hover_index].classList.add(dropdown.active); } } _keyup(event) { super._keyup(event); switch (event.key) { case "Enter": { this.change_input(); break; } case "Escape": { this._hide_menu(); break; } case "ArrowUp": { this._bump_hover(this._hover_index - 1); break; } case "ArrowDown": { this._bump_hover(this._hover_index + 1); break; } default: this._toggle_menu(); } } } export class AutocompleteInput extends TextInput { static __name__ = "AutocompleteInput"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = AutocompleteInputView; this.define(({ Bool, Int, Str, List, NonNegative, Positive, Nullable }) => ({ completions: [List(Str), []], min_characters: [NonNegative(Int), 2], max_completions: [Nullable(Positive(Int)), null], case_sensitive: [Bool, true], restrict: [Bool, true], search_strategy: [SearchStrategy, "starts_with"], })); } } //# sourceMappingURL=autocomplete_input.js.map