UNPKG

aurelia-materialize-bridge

Version:
318 lines (279 loc) 9.61 kB
import * as au from "../aurelia"; import { LookupState } from "./lookup-state"; import { ILookupOptionsFunctionParameter } from "./i-lookup-options-function-parameter"; import { DiscardablePromise, discard } from "../common/discardable-promise"; import { ConfigBuilder } from "../config-builder"; export type BlurAction = "Nothing" | "ClearOnNoMatch" | "SetOnMatch" | "Both"; @au.customElement("md-lookup") @au.autoinject export class MdLookup { constructor(private element: Element, private taskQueue: au.TaskQueue, private configBuilder: ConfigBuilder) { this.logger = au.getLogger("MdLookup"); this.controlId = `md-lookup-${MdLookup.id++}`; } static searching = Symbol("searching"); static error = Symbol("error"); errorMessage: string; static id = 0; controlId: string; dropdown: HTMLElement; dropdownUl: HTMLElement; input: HTMLInputElement; labelElement: HTMLLabelElement; logger: au.Logger; validateResults: au.ValidateResult[] = []; validationClass: string; @au.bindable({ defaultBindingMode: au.bindingMode.oneTime }) blurAction: BlurAction = "Nothing"; @au.bindable({ defaultBindingMode: au.bindingMode.twoWay }) filter: string; searchPromise: DiscardablePromise<unknown[]>; suppressFilterChanged: boolean; async filterChanged() { this.logger.debug("filterChanged"); if (!this.optionsFunction) { return; } if (this.suppressFilterChanged) { this.logger.debug("unsuppressed filter changed"); this.suppressFilterChanged = false; return; } this.setValue(undefined); discard(this.searchPromise); this.options = [MdLookup.searching]; try { this.searchPromise = new DiscardablePromise(this.getOptions({ filter: this.filter })); this.options = await this.searchPromise; this.fixDropdownSizeIfTooBig(); } catch (e) { if (e !== DiscardablePromise.discarded) { this.options = [MdLookup.error, e.message]; } } } setFilter(filter: string) { if (this.filter === filter) { return; } this.logger.debug("suppressed filter changed"); this.suppressFilterChanged = true; this.filter = filter; this.taskQueue.queueTask(() => this.updateLabel()); } @au.bindable label: string; @au.bindable({ defaultBindingMode: au.bindingMode.twoWay }) value: unknown; suppressValueChanged: boolean; async valueChanged(newValue: unknown) { this.logger.debug("valueChanged", newValue); if (this.suppressValueChanged) { this.logger.debug("unsuppressed value changed"); this.suppressValueChanged = false; return; } await this.updateFilterBasedOnValue(); } setValue(value: unknown) { if (this.value === value) { return; } this.logger.debug("suppressed value changed"); this.suppressValueChanged = true; this.value = value; } @au.bindable optionsFunction: ((p: ILookupOptionsFunctionParameter<unknown>) => Promise<unknown[]>) | unknown[]; getOptions: (p: ILookupOptionsFunctionParameter<unknown>) => Promise<unknown[]>; @au.bindable displayFieldName: ((option: unknown) => string) | string; @au.bindable valueFieldName: ((option: unknown) => unknown) | string; @au.bindable({ defaultBindingMode: au.bindingMode.twoWay }) readonly: boolean; @au.bindable placeholder: string = "Start Typing To Search"; @au.ato.bindable.numberMd debounce: number = 850; @au.bindable preloadOptions: boolean; LookupState = LookupState; // for usage from the html template state: LookupState; bindingContext: object; @au.observable options: unknown[]; optionsChanged() { this.logger.debug("optionsChanged", this.options); if (!this.options || !(this.options instanceof Array) || !this.options.length) { this.state = LookupState.noMatches; } else if (this.options[0] === MdLookup.searching) { this.state = LookupState.searching; } else if (this.options[0] === MdLookup.error) { this.state = LookupState.error; this.errorMessage = this.options.length > 1 ? this.options[1] : "Error occurred"; } else { this.state = LookupState.optionsVisible; } } isOpen: boolean; async updateFilterBasedOnValue() { this.logger.debug("updateFilterBasedOnValue", this.value); if (this.value) { this.options = await this.getOptions({ value: this.value }); } else { this.options = []; } if (this.options && this.options.length) { this.setFilter(this.getDisplayValue(this.options[0])); } else { this.setFilter(undefined); } } fixDropdownSizeIfTooBig() { this.taskQueue.queueTask(() => { if (!this.isOpen) { return; } // adjust dropdown top so it sits right below the input // doing it with CSS will not work if input margin is redefined let inputRect = this.input.getBoundingClientRect(); this.dropdown.style.top = `${inputRect.top + inputRect.height + 3}px`; this.dropdown.style.width = `${inputRect.width}px`; const rect = this.dropdown.getBoundingClientRect(); let availableSpace = window.innerHeight - rect.top + document.body.scrollTop - 5; if (this.dropdownUl.offsetHeight > availableSpace) { this.dropdown.style.height = `${availableSpace}px`; } else { this.dropdown.style.height = "auto"; } }); } open() { if (!this.readonly) { this.logger.debug("open"); this.isOpen = true; this.fixDropdownSizeIfTooBig(); } } close() { this.logger.debug("close"); this.isOpen = false; } blur() { if ((["SetOnMatch", "Both"].includes(this.blurAction)) && this.options && this.options.length === 1) { this.setValue(this.options[0]); this.setFilter(this.getDisplayValue(this.options[0])); } else if (["ClearOnNoMatch", "Both"].includes(this.blurAction) && this.optionsContainsText(this.filter)) { this.setValue(undefined); this.setFilter(undefined); } this.close(); au.fireEvent(this.element, "blur"); } optionsContainsText(txt: string) { return !this.options || !this.options.some(opt => this.getDisplayValue(opt) === txt); } focus() { this.input.focus(); au.fireEvent(this.element, "focus"); } updateLabel() { au.updateLabel(this.input, this.labelElement); } async bind(bindingContext: object, overrideContext: object) { this.bindingContext = bindingContext; if (this.optionsFunction) { if (this.optionsFunction instanceof Function) { this.getOptions = this.optionsFunction.bind(this.bindingContext); } else if (this.optionsFunction instanceof Array) { this.getOptions = (p: ILookupOptionsFunctionParameter<unknown>): Promise<unknown[]> => { const options = this.optionsFunction as unknown[]; if (p.value) { return Promise.resolve([options.find(x => this.getValue(x) === p.value)]); } else { return Promise.resolve(options.filter(x => this.getDisplayValue(x).toString().toUpperCase().includes(p.filter.toUpperCase()))); } }; } } await this.updateFilterBasedOnValue(); // restore initial value because it is set by updateFilterBasedOnValue this.suppressFilterChanged = false; } async attached() { this.logger.debug("attached"); if (this.placeholder) { this.input.setAttribute("placeholder", this.placeholder); } // we need to use queueTask because open sometimes happens before browser bubbles the click further thus closing just opened dropdown this.input.onselect = () => this.taskQueue.queueTask(() => this.open()); this.input.onclick = () => this.taskQueue.queueTask(() => this.open()); this.input.onfocus = () => this.taskQueue.queueTask(() => this.open()); this.element.mdRenderValidateResults = this.mdRenderValidateResults; this.element.mdUnrenderValidateResults = this.mdUnrenderValidateResults; if (this.preloadOptions) { this.options = await this.getOptions({ value: this.value, filter: this.filter }); } this.updateLabel(); } detached() { if (this.input) { this.input.onselect = null; this.input.onfocus = null; this.input.onblur = null; this.input.onfocus = null; } this.validateResults = []; this.validationClass = undefined; this.element.mdRenderValidateResults = null; this.element.mdUnrenderValidateResults = null; } select(option: unknown) { this.value = this.getValue(option); this.close(); au.fireEvent(this.element, "selected", { value: this.value }); } getValue(option: unknown): unknown { if (this.valueFieldName) { if (this.valueFieldName instanceof Function) { return this.valueFieldName(option); } else { return option[this.valueFieldName]; } } else { return option; } } getDisplayValue(option: unknown): string { if (option === null || option === undefined) { return null; } if (!this.displayFieldName) { return option.toString(); } else if (this.displayFieldName instanceof Function) { return this.displayFieldName(option); } else { return option[this.displayFieldName]; } } mdUnrenderValidateResults = (results: au.ValidateResult[], renderer: au.MaterializeFormValidationRenderer) => { this.validateResults = this.validateResults.filter(x => !results.find(y => y.id === x.id)); this.validationClass = undefined; } mdRenderValidateResults = (results: au.ValidateResult[], renderer: au.MaterializeFormValidationRenderer) => { this.validateResults.push(...results.filter(x => !x.valid)); this.validationClass = results.find(x => !x.valid) ? "invalid" : "valid"; } }