import { Atom } from "@web-atoms/core/dist/Atom"; import Bind from "@web-atoms/core/dist/core/Bind"; import { BindableProperty } from "@web-atoms/core/dist/core/BindableProperty"; import { CancelToken, IClassOf } from "@web-atoms/core/dist/core/types"; import XNode from "@web-atoms/core/dist/core/XNode"; import { NavigationService } from "@web-atoms/core/dist/services/NavigationService"; import ReferenceService from "@web-atoms/core/dist/services/ReferenceService"; import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl"; import { AtomTemplateControl } from "@web-atoms/core/dist/web/controls/AtomTemplateControl"; import PopupService from "@web-atoms/core/dist/web/services/PopupService"; import { WindowService } from "@web-atoms/core/dist/web/services/WindowService"; import AppComboBoxViewModel from "./AppComboBoxViewModel"; import AutoCompleteBoxStyle from "./AutoCompleteBoxStyle"; import ItemHost from "./ItemHost"; export interface IFetchEvent { search?: string; value?: any; cancel?: CancelToken; } enum KeyCodes { Enter = "Enter", ArrowDown = "ArrowDown", ArrowUp = "ArrowUp", ArrowLeft = "ArrowLeft", ArrowRight = "ArrowRight", End = "End", Home = "Home", Escape = "Escape" } export default class AutoCompleteBox extends AtomControl { public static itemTemplate = XNode.prepare("itemTemplate", true, true); public itemTemplate: IClassOf; @BindableProperty public selectedItem: any; public previousItem: any; @BindableProperty public value: any; /** * Label is displayed in place holder * @type {*} * @memberof AutoCompleteBox */ public label: any; public items: any[]; public itemsSource: (fetchEvent: IFetchEvent) => Promise | any[]; public valueFunc: ((item: any) => any); public itemHostTemplate: IClassOf; public searchText: string; public isPopupOpen: boolean; private lastSearchText: string; private lastValue: any; private cancelToken: CancelToken; private windowViewModel: AppComboBoxViewModel; private created: boolean; private isUpdating: boolean; public onPropertyChanged(name: string): void { switch (name) { case "searchText": case "value": this.onSearchTextChanged(true); break; case "selectedItem": const si = this.selectedItem; const vf = this.valueFunc; if (vf && si) { this.value = vf (si); } break; } } public onUpdateUI(): void { super.onUpdateUI(); if (this.created) { return; } const openPopup = () => this.app.runAsync( async () => { await Atom.delay(200); await this.openPopup(true); }); this.render(
({ [this.controlStyle.name]: 1, "popup-open": this.isPopupOpen || !this.value }))}> this.searchText, ["keyup", "keypress", "keydown"])} placeholder={Bind.oneWay(() => this.label)} eventKeydown={(e) => this.onKey(e)} eventBlur={() => setTimeout(() => this.cancelToken?.cancel(), 100)}/> this.itemTemplate)} data={Bind.oneWay(() => this.selectedItem)} />
); } public hasProperty(name: string): boolean { if (/^(data|selectedItem|itemTemplate|isPopupOpen|value|localViewModel|viewModel)$/.test(name)) { return true; } return super.hasProperty(name); } protected onSearchTextChanged(force: boolean = false): void { if (this.isUpdating) { return; } if (!force) { if (this.lastSearchText === this.searchText) { return; } this.lastSearchText = this.searchText; } setTimeout(() => { this.app.runAsync(() => this.openPopup()); }, 10); if (!this.itemsSource) { // tslint:disable-next-line:no-console console.warn("No itemsSource defined"); return; } const s = this.searchText || this.value; if (!s) { return; } this.cancelToken?.cancel(); const c = new CancelToken(); this.cancelToken = c; this.runAsync(async () => { await Atom.delay(250, c); if (c.cancelled) { return; } const result = this.itemsSource({ search: this.searchText, cancel: c, value: this.value }); if (!result) { return; } const rp = result as Promise; if (rp.then && rp.catch) { try { const items = await rp; if (c.cancelled) { return; } this.items.replace(items || []); } catch (ex) { c.cancel(); } } else { const items = result as any[]; if (c.cancelled) { return; } this.items.replace(items || []); } if (!this.selectedItem) { // this.selectedItem = this.items[0]; const first = this.items[0]; if (first) { const v1 = this.valueFunc ? this.valueFunc(first) : first; // tslint:disable-next-line: triple-equals if (v1 == this.value) { this.selectedItem = first; this.element.dispatchEvent(new CustomEvent("item-selected", { cancelable: false, bubbles: false, detail: this.selectedItem })); } } } }); } protected runAsync(fx: () => Promise) { this.app.runAsync(async () => { try { await fx(); } catch (e) { const s = e.message || e.toString(); if (s === "cancelled") { return; } // tslint:disable-next-line: no-console console.error(e); } }); } protected preCreate(): void { this.itemTemplate = null; this.items = []; this.itemsSource = null; this.label = null; this.lastSearchText = null; this.lastValue = null; this.value = null; this.isPopupOpen = false; this.valueFunc = null; this.itemHostTemplate = ItemHost; this.searchText = null; this.lastSearchText = null; this.lastValue = null; this.created = false; this.isUpdating = false; this.defaultControlStyle = AutoCompleteBoxStyle; } // tslint:disable-next-line: no-empty protected create(): void { } protected onKey(e: KeyboardEvent): void { if (!this.isPopupOpen) { return; } switch (e.key) { case KeyCodes.Enter: if (this.windowViewModel) { this.element.dispatchEvent(new CustomEvent("item-selected", { cancelable: false, bubbles: false, detail: this.selectedItem })); this.windowViewModel.close(this.selectedItem); this.windowViewModel = null; } break; case KeyCodes.Escape: if (this.windowViewModel) { this.windowViewModel.cancel(); this.windowViewModel = null; } break; case KeyCodes.ArrowUp: this.moveSelection(-1); break; case KeyCodes.ArrowDown: this.moveSelection(+1); break; } } protected moveSelection(n: number): void { try { this.isUpdating = true; if (!this.items) { return; } const index = this.items.indexOf(this.selectedItem); if (index === -1) { if (n > 0 && this.items.length) { this.selectedItem = this.items[0]; } return; } const newIndex = index + n; if (newIndex < 0 || newIndex >= this.items.length) { return; } this.selectedItem = this.items[newIndex]; } finally { this.isUpdating = false; } } protected async openPopup(force: boolean = false): Promise { if (this.isPopupOpen) { return; } try { if (!force) { if (!this.searchText) { return; } } this.isPopupOpen = true; this.onSearchTextChanged(); const rs = this.app.get(ReferenceService); const ws = this.app.get(WindowService); const iht = rs.put(this.itemHostTemplate); const parent = rs.put(this); const ns = this.app.resolve(NavigationService) as NavigationService; this.previousItem = this.selectedItem; const value = await ns.openPage(`app://class/${iht.key}`, { "ref:comboBox": parent }); this.selectedItem = value; } catch (e) { this.selectedItem = this.previousItem; // tslint:disable-next-line:triple-equals if (e != "cancelled") { // tslint:disable-next-line:no-console console.warn(e); } else { this.searchText = ""; } // throw e; } finally { await Atom.delay(100); this.isPopupOpen = false; } } }