aurelia-materialize-bridge
Version:
Aurelia interface to http://materializecss.com/
318 lines (279 loc) • 9.61 kB
text/typescript
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";
.customElement("md-lookup")
.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;
.bindable({ defaultBindingMode: au.bindingMode.oneTime })
blurAction: BlurAction = "Nothing";
.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());
}
.bindable
label: string;
.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;
}
.bindable
optionsFunction: ((p: ILookupOptionsFunctionParameter<unknown>) => Promise<unknown[]>) | unknown[];
getOptions: (p: ILookupOptionsFunctionParameter<unknown>) => Promise<unknown[]>;
.bindable
displayFieldName: ((option: unknown) => string) | string;
.bindable
valueFieldName: ((option: unknown) => unknown) | string;
.bindable({ defaultBindingMode: au.bindingMode.twoWay })
readonly: boolean;
.bindable
placeholder: string = "Start Typing To Search";
.ato.bindable.numberMd
debounce: number = 850;
.bindable
preloadOptions: boolean;
LookupState = LookupState; // for usage from the html template
state: LookupState;
bindingContext: object;
.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";
}
}