ng2-completer
Version:
angular autocomplete/typeahead component
1,610 lines (1,596 loc) • 55.9 kB
JavaScript
/**
* @license ng2-completer
* MIT license
*/
import { EventEmitter, Injectable, Output, Directive, HostListener, Host, ElementRef, NgZone, Input, TemplateRef, ViewContainerRef, ChangeDetectorRef, Renderer2, Component, forwardRef, ViewChild, NgModule } from '@angular/core';
import { Subject, Observable, timer } from 'rxjs';
import { catchError, map, take } from 'rxjs/operators';
import { __decorate, __metadata, __param } from 'tslib';
import { HttpClient } from '@angular/common/http';
import { NgModel, NG_VALUE_ACCESSOR, FormControl, FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
const MAX_CHARS = 524288; // the default max length per the html maxlength attribute
const MIN_SEARCH_LENGTH = 3;
const PAUSE = 10;
const TEXT_SEARCHING = "Searching...";
const TEXT_NO_RESULTS = "No results found";
const CLEAR_TIMEOUT = 50;
function isNil(value) {
return typeof value === "undefined" || value === null;
}
class CompleterBaseData extends Subject {
constructor() {
super();
this._searchFields = null;
this._titleField = null;
this._descriptionField = undefined;
this._imageField = undefined;
}
cancel() {
return;
}
searchFields(searchFields) {
this._searchFields = searchFields;
return this;
}
titleField(titleField) {
this._titleField = titleField;
return this;
}
descriptionField(descriptionField) {
this._descriptionField = descriptionField;
return this;
}
imageField(imageField) {
this._imageField = imageField;
return this;
}
convertToItem(data) {
let image = null;
let formattedText;
let formattedDesc = null;
if (this._titleField) {
formattedText = this.extractTitle(data);
}
else {
formattedText = data;
}
if (typeof formattedText !== "string") {
formattedText = JSON.stringify(formattedText);
}
if (this._descriptionField) {
formattedDesc = this.extractValue(data, this._descriptionField);
}
if (this._imageField) {
image = this.extractValue(data, this._imageField);
}
if (isNil(formattedText)) {
return null;
}
return {
description: formattedDesc,
image,
originalObject: data,
title: formattedText
};
}
extractMatches(data, term) {
let matches = [];
const searchFields = this._searchFields ? this._searchFields.split(",") : null;
if (this._searchFields !== null && this._searchFields !== undefined && term !== "") {
matches = data.filter((item) => {
const values = searchFields ? this.extractBySearchFields(searchFields, item) : [item];
return values.some((value) => value
.toString()
.toLowerCase()
.indexOf(term.toString().toLowerCase()) >= 0);
});
}
else {
matches = data;
}
return matches;
}
extractTitle(item) {
// split title fields and run extractValue for each and join with ' '
if (!this._titleField) {
return "";
}
return this._titleField.split(",")
.map((field) => {
return this.extractValue(item, field);
})
.reduce((acc, titlePart) => acc ? `${acc} ${titlePart}` : titlePart);
}
extractValue(obj, key) {
let keys;
let result;
if (key) {
keys = key.split(".");
result = obj;
for (key of keys) {
if (result) {
result = result[key];
}
}
}
else {
result = obj;
}
return result;
}
processResults(matches) {
let i;
const results = [];
if (matches && matches.length > 0) {
for (i = 0; i < matches.length; i++) {
const item = this.convertToItem(matches[i]);
if (item) {
results.push(item);
}
}
}
return results;
}
extractBySearchFields(searchFields, item) {
return searchFields
.map((searchField) => this.extractValue(item, searchField)).filter((value) => !!value);
}
}
class LocalData extends CompleterBaseData {
constructor() {
super();
this.dataSourceChange = new EventEmitter();
this._data = [];
this.savedTerm = null;
}
data(data) {
if (data instanceof Observable) {
const data$ = data;
data$
.pipe(catchError(() => []))
.subscribe((res) => {
this._data = res;
if (this.savedTerm) {
this.search(this.savedTerm);
}
this.dataSourceChange.emit();
});
}
else {
this._data = data;
}
this.dataSourceChange.emit();
return this;
}
search(term) {
if (!this._data) {
this.savedTerm = term;
}
else {
this.savedTerm = null;
const matches = this.extractMatches(this._data, term);
this.next(this.processResults(matches));
}
}
convertToItem(data) {
return super.convertToItem(data);
}
}
class RemoteData extends CompleterBaseData {
constructor(http) {
super();
this.http = http;
this.dataSourceChange = new EventEmitter();
this._remoteUrl = null;
this.remoteSearch = null;
this._urlFormater = null;
this._dataField = null;
}
remoteUrl(remoteUrl) {
this._remoteUrl = remoteUrl;
this.dataSourceChange.emit();
return this;
}
urlFormater(urlFormater) {
this._urlFormater = urlFormater;
}
dataField(dataField) {
this._dataField = dataField;
}
requestOptions(requestOptions) {
this._requestOptions = requestOptions;
}
search(term) {
this.cancel();
// let params = {};
let url = "";
if (this._urlFormater) {
url = this._urlFormater(term);
}
else {
url = this._remoteUrl + encodeURIComponent(term);
}
this.remoteSearch = this.http
.get(url, Object.assign({}, this._requestOptions))
.pipe(map((data) => {
const matches = this.extractValue(data, this._dataField);
return this.extractMatches(matches, term);
}), catchError(() => []))
.subscribe((matches) => {
const results = this.processResults(matches);
this.next(results);
});
}
cancel() {
if (this.remoteSearch) {
this.remoteSearch.unsubscribe();
}
}
convertToItem(data) {
return super.convertToItem(data);
}
}
let LocalDataFactory = class LocalDataFactory {
create() {
return new LocalData();
}
};
LocalDataFactory = __decorate([
Injectable()
], LocalDataFactory);
let RemoteDataFactory = class RemoteDataFactory {
constructor(http) {
this.http = http;
}
create() {
return new RemoteData(this.http);
}
};
RemoteDataFactory = __decorate([
Injectable(),
__metadata("design:paramtypes", [HttpClient])
], RemoteDataFactory);
let CompleterService = class CompleterService {
constructor(localDataFactory, // Using any instead of () => LocalData because of AoT errors
remoteDataFactory // Using any instead of () => LocalData because of AoT errors
) {
this.localDataFactory = localDataFactory;
this.remoteDataFactory = remoteDataFactory;
}
local(data, searchFields = "", titleField = "") {
const localData = this.localDataFactory.create();
return localData
.data(data)
.searchFields(searchFields)
.titleField(titleField);
}
remote(url, searchFields = "", titleField = "") {
const remoteData = this.remoteDataFactory.create();
return remoteData
.remoteUrl(url)
.searchFields(searchFields)
.titleField(titleField);
}
};
CompleterService = __decorate([
Injectable(),
__metadata("design:paramtypes", [LocalDataFactory,
RemoteDataFactory // Using any instead of () => LocalData because of AoT errors
])
], CompleterService);
let CtrCompleter = class CtrCompleter {
constructor() {
this.selected = new EventEmitter();
this.highlighted = new EventEmitter();
this.opened = new EventEmitter();
this.dataSourceChange = new EventEmitter();
this.list = null;
this.dropdown = null;
this._hasHighlighted = false;
this._hasSelected = false;
this._cancelBlur = false;
this._isOpen = false;
this._autoHighlightIndex = null;
}
registerList(list) {
this.list = list;
}
registerDropdown(dropdown) {
this.dropdown = dropdown;
}
onHighlighted(item) {
this.highlighted.emit(item);
this._hasHighlighted = !!item;
}
onSelected(item, clearList = true) {
this.selected.emit(item);
if (item) {
this._hasSelected = true;
}
if (clearList) {
this.clear();
}
}
onDataSourceChange() {
if (this.hasSelected) {
this.selected.emit(null);
this._hasSelected = false;
}
this.dataSourceChange.emit();
}
search(term) {
if (this._hasSelected) {
this.selected.emit(null);
this._hasSelected = false;
}
if (this.list) {
this.list.search(term);
}
}
clear() {
this._hasHighlighted = false;
this.isOpen = false;
if (this.dropdown) {
this.dropdown.clear();
}
if (this.list) {
this.list.clear();
}
}
selectCurrent() {
if (this.dropdown) {
this.dropdown.selectCurrent();
}
}
nextRow() {
if (this.dropdown) {
this.dropdown.nextRow();
}
}
prevRow() {
if (this.dropdown) {
this.dropdown.prevRow();
}
}
hasHighlighted() {
return this._hasHighlighted;
}
cancelBlur(cancel) {
this._cancelBlur = cancel;
}
isCancelBlur() {
return this._cancelBlur;
}
open() {
if (!this._isOpen && !!this.list) {
this.isOpen = true;
this.list.open();
}
}
get isOpen() {
return this._isOpen;
}
set isOpen(open) {
this._isOpen = open;
this.opened.emit(this._isOpen);
if (this.list) {
this.list.isOpen(open);
}
}
get autoHighlightIndex() {
return this._autoHighlightIndex;
}
set autoHighlightIndex(index) {
this._autoHighlightIndex = index;
if (this.dropdown) {
this.dropdown.highlightRow(this._autoHighlightIndex);
}
}
get hasSelected() {
return this._hasSelected;
}
};
__decorate([
Output(),
__metadata("design:type", Object)
], CtrCompleter.prototype, "selected", void 0);
__decorate([
Output(),
__metadata("design:type", Object)
], CtrCompleter.prototype, "highlighted", void 0);
__decorate([
Output(),
__metadata("design:type", Object)
], CtrCompleter.prototype, "opened", void 0);
__decorate([
Output(),
__metadata("design:type", Object)
], CtrCompleter.prototype, "dataSourceChange", void 0);
CtrCompleter = __decorate([
Directive({
selector: "[ctrCompleter]",
})
], CtrCompleter);
class CtrRowItem {
constructor(row, index) {
this.row = row;
this.index = index;
}
}
let CtrDropdown = class CtrDropdown {
constructor(completer, el, zone) {
this.completer = completer;
this.el = el;
this.zone = zone;
this.rows = [];
this.isScrollOn = false;
this._rowMouseDown = false;
this.completer.registerDropdown(this);
}
ngOnDestroy() {
this.completer.registerDropdown(null);
}
ngAfterViewInit() {
const css = getComputedStyle(this.el.nativeElement);
const autoHighlightIndex = this.completer.autoHighlightIndex;
this.isScrollOn = !!css.maxHeight && css.overflowY === "auto";
if (autoHighlightIndex) {
this.zone.run(() => {
this.highlightRow(autoHighlightIndex);
});
}
}
onMouseDown(event) {
// Support for canceling blur on IE (issue #158)
if (!this._rowMouseDown) {
this.completer.cancelBlur(true);
this.zone.run(() => {
this.completer.cancelBlur(false);
});
}
else {
this._rowMouseDown = false;
}
}
registerRow(row) {
const arrIndex = this.rows.findIndex(_row => _row.index === row.index);
if (arrIndex >= 0) {
this.rows[arrIndex] = row;
}
else {
this.rows.push(row);
}
}
unregisterRow(rowIndex) {
const arrIndex = this.rows.findIndex(_row => _row.index === rowIndex);
this.rows.splice(arrIndex, 1);
if (this.currHighlighted && rowIndex === this.currHighlighted.index) {
this.highlightRow(null);
}
}
highlightRow(index) {
const highlighted = this.rows.find(row => row.index === index);
if (isNil(index) || index < 0) {
if (this.currHighlighted) {
this.currHighlighted.row.setHighlighted(false);
}
this.currHighlighted = undefined;
this.completer.onHighlighted(null);
return;
}
if (!highlighted) {
return;
}
if (this.currHighlighted) {
this.currHighlighted.row.setHighlighted(false);
}
this.currHighlighted = highlighted;
this.currHighlighted.row.setHighlighted(true);
this.completer.onHighlighted(this.currHighlighted.row.getDataItem());
if (this.isScrollOn && this.currHighlighted) {
const rowTop = this.dropdownRowTop();
if (!rowTop) {
return;
}
if (rowTop < 0) {
this.dropdownScrollTopTo(rowTop - 1);
}
else {
const row = this.currHighlighted.row.getNativeElement();
if (this.dropdownHeight() < row.getBoundingClientRect().bottom) {
this.dropdownScrollTopTo(this.dropdownRowOffsetHeight(row));
if (this.el.nativeElement.getBoundingClientRect().bottom - this.dropdownRowOffsetHeight(row) < row.getBoundingClientRect().top) {
this.dropdownScrollTopTo(row.getBoundingClientRect().top - (this.el.nativeElement.getBoundingClientRect().top + parseInt(getComputedStyle(this.el.nativeElement).paddingTop, 10)));
}
}
}
}
}
clear() {
this.rows = [];
}
onSelected(item) {
this.completer.onSelected(item);
}
rowMouseDown() {
this._rowMouseDown = true;
}
selectCurrent() {
if (!!this.currHighlighted && !!this.currHighlighted.row) {
this.onSelected(this.currHighlighted.row.getDataItem());
}
else if (this.rows.length > 0) {
this.onSelected(this.rows[0].row.getDataItem());
}
}
nextRow() {
let nextRowIndex = 0;
if (this.currHighlighted) {
nextRowIndex = this.currHighlighted.index + 1;
}
this.highlightRow(nextRowIndex);
}
prevRow() {
let nextRowIndex = -1;
if (this.currHighlighted) {
nextRowIndex = this.currHighlighted.index - 1;
}
this.highlightRow(nextRowIndex);
}
dropdownScrollTopTo(offset) {
this.el.nativeElement.scrollTop = this.el.nativeElement.scrollTop + offset;
}
dropdownRowTop() {
if (!this.currHighlighted) {
return;
}
return this.currHighlighted.row.getNativeElement().getBoundingClientRect().top -
(this.el.nativeElement.getBoundingClientRect().top +
parseInt(getComputedStyle(this.el.nativeElement).paddingTop, 10));
}
dropdownHeight() {
return this.el.nativeElement.getBoundingClientRect().top +
parseInt(getComputedStyle(this.el.nativeElement).maxHeight, 10);
}
dropdownRowOffsetHeight(row) {
const css = getComputedStyle(row.parentElement);
return row.parentElement.offsetHeight +
parseInt(css.marginTop, 10) + parseInt(css.marginBottom, 10);
}
};
__decorate([
HostListener("mousedown", ["$event"]),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CtrDropdown.prototype, "onMouseDown", null);
CtrDropdown = __decorate([
Directive({
selector: "[ctrDropdown]",
}),
__param(0, Host()),
__metadata("design:paramtypes", [CtrCompleter, ElementRef, NgZone])
], CtrDropdown);
// keyboard events
const KEY_DW = 40;
const KEY_RT = 39;
const KEY_UP = 38;
const KEY_LF = 37;
const KEY_ES = 27;
const KEY_EN = 13;
const KEY_TAB = 9;
const KEY_BK = 8;
const KEY_SH = 16;
const KEY_CL = 20;
const KEY_F1 = 112;
const KEY_F12 = 123;
let CtrInput = class CtrInput {
constructor(completer, ngModel, el) {
this.completer = completer;
this.ngModel = ngModel;
this.el = el;
this.clearSelected = false;
this.clearUnselected = false;
this.overrideSuggested = false;
this.fillHighlighted = true;
this.openOnFocus = false;
this.openOnClick = false;
this.selectOnClick = false;
this.selectOnFocus = false;
this.ngModelChange = new EventEmitter();
this._searchStr = "";
this._displayStr = "";
this.blurTimer = null;
this.completer.selected.subscribe((item) => {
if (!item) {
return;
}
if (this.clearSelected) {
this.searchStr = "";
}
else {
this.searchStr = item.title;
}
this.ngModelChange.emit(this.searchStr);
});
this.completer.highlighted.subscribe((item) => {
if (this.fillHighlighted) {
if (item) {
this._displayStr = item.title;
this.ngModelChange.emit(item.title);
}
else {
this._displayStr = this.searchStr;
this.ngModelChange.emit(this.searchStr);
}
}
});
this.completer.dataSourceChange.subscribe(() => {
this.completer.search(this.searchStr);
});
if (this.ngModel.valueChanges) {
this.ngModel.valueChanges.subscribe((value) => {
if (!isNil(value) && this._displayStr !== value) {
if (this.searchStr !== value) {
this.completer.search(value);
}
this.searchStr = value;
}
});
}
}
keyupHandler(event) {
if (event.keyCode === KEY_LF || event.keyCode === KEY_RT || event.keyCode === KEY_TAB) {
// do nothing
return;
}
if (event.keyCode === KEY_UP || event.keyCode === KEY_EN) {
event.preventDefault();
}
else if (event.keyCode === KEY_DW) {
event.preventDefault();
this.completer.search(this.searchStr);
}
else if (event.keyCode === KEY_ES) {
if (this.completer.isOpen) {
this.restoreSearchValue();
this.completer.clear();
event.stopPropagation();
event.preventDefault();
}
}
}
pasteHandler(event) {
this.completer.open();
}
keydownHandler(event) {
const keyCode = event.keyCode || event.which;
if (keyCode === KEY_EN) {
if (this.completer.hasHighlighted()) {
event.preventDefault();
}
this.handleSelection();
}
else if (keyCode === KEY_DW) {
event.preventDefault();
this.completer.open();
this.completer.nextRow();
}
else if (keyCode === KEY_UP) {
event.preventDefault();
this.completer.prevRow();
}
else if (keyCode === KEY_TAB) {
this.handleSelection();
}
else if (keyCode === KEY_BK) {
this.completer.open();
}
else if (keyCode === KEY_ES) {
// This is very specific to IE10/11 #272
// without this, IE clears the input text
event.preventDefault();
if (this.completer.isOpen) {
event.stopPropagation();
}
}
else {
if (keyCode !== 0 && keyCode !== KEY_SH && keyCode !== KEY_CL &&
(keyCode <= KEY_F1 || keyCode >= KEY_F12) &&
!event.ctrlKey && !event.metaKey && !event.altKey) {
this.completer.open();
}
}
}
onBlur(event) {
// Check if we need to cancel Blur for IE
if (this.completer.isCancelBlur()) {
setTimeout(() => {
// get the focus back
this.el.nativeElement.focus();
}, 0);
return;
}
if (this.completer.isOpen) {
this.blurTimer = timer(200).pipe(take(1)).subscribe(() => this.doBlur());
}
}
onfocus() {
if (this.blurTimer) {
this.blurTimer.unsubscribe();
this.blurTimer = null;
}
if (this.selectOnFocus) {
this.el.nativeElement.select();
}
if (this.openOnFocus) {
this.completer.open();
}
}
onClick(event) {
if (this.selectOnClick) {
this.el.nativeElement.select();
}
if (this.openOnClick) {
if (this.completer.isOpen) {
this.completer.clear();
}
else {
this.completer.open();
}
}
}
get searchStr() {
return this._searchStr;
}
set searchStr(term) {
this._searchStr = term;
this._displayStr = term;
}
handleSelection() {
if (this.completer.hasHighlighted()) {
this._searchStr = "";
this.completer.selectCurrent();
}
else if (this.overrideSuggested) {
this.completer.onSelected({ title: this.searchStr, originalObject: null });
}
else {
if (this.clearUnselected && !this.completer.hasSelected) {
this.searchStr = "";
this.ngModelChange.emit(this.searchStr);
}
this.completer.clear();
}
}
restoreSearchValue() {
if (this.fillHighlighted) {
if (this._displayStr !== this.searchStr) {
this._displayStr = this.searchStr;
this.ngModelChange.emit(this.searchStr);
}
}
}
doBlur() {
if (this.blurTimer) {
this.blurTimer.unsubscribe();
this.blurTimer = null;
}
if (this.overrideSuggested) {
this.completer.onSelected({ title: this.searchStr, originalObject: null });
}
else {
if (this.clearUnselected && !this.completer.hasSelected) {
this.searchStr = "";
this.ngModelChange.emit(this.searchStr);
}
else {
this.restoreSearchValue();
}
}
this.completer.clear();
}
};
__decorate([
Input("clearSelected"),
__metadata("design:type", Object)
], CtrInput.prototype, "clearSelected", void 0);
__decorate([
Input("clearUnselected"),
__metadata("design:type", Object)
], CtrInput.prototype, "clearUnselected", void 0);
__decorate([
Input("overrideSuggested"),
__metadata("design:type", Object)
], CtrInput.prototype, "overrideSuggested", void 0);
__decorate([
Input("fillHighlighted"),
__metadata("design:type", Object)
], CtrInput.prototype, "fillHighlighted", void 0);
__decorate([
Input("openOnFocus"),
__metadata("design:type", Object)
], CtrInput.prototype, "openOnFocus", void 0);
__decorate([
Input("openOnClick"),
__metadata("design:type", Object)
], CtrInput.prototype, "openOnClick", void 0);
__decorate([
Input("selectOnClick"),
__metadata("design:type", Object)
], CtrInput.prototype, "selectOnClick", void 0);
__decorate([
Input("selectOnFocus"),
__metadata("design:type", Object)
], CtrInput.prototype, "selectOnFocus", void 0);
__decorate([
Output(),
__metadata("design:type", EventEmitter)
], CtrInput.prototype, "ngModelChange", void 0);
__decorate([
HostListener("keyup", ["$event"]),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CtrInput.prototype, "keyupHandler", null);
__decorate([
HostListener("paste", ["$event"]),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CtrInput.prototype, "pasteHandler", null);
__decorate([
HostListener("keydown", ["$event"]),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CtrInput.prototype, "keydownHandler", null);
__decorate([
HostListener("blur", ["$event"]),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CtrInput.prototype, "onBlur", null);
__decorate([
HostListener("focus", []),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], CtrInput.prototype, "onfocus", null);
__decorate([
HostListener("click", ["$event"]),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CtrInput.prototype, "onClick", null);
CtrInput = __decorate([
Directive({
selector: "[ctrInput]",
}),
__param(0, Host()),
__metadata("design:paramtypes", [CtrCompleter, NgModel, ElementRef])
], CtrInput);
class CtrListContext {
constructor(results, searching, searchInitialized, isOpen) {
this.results = results;
this.searching = searching;
this.searchInitialized = searchInitialized;
this.isOpen = isOpen;
}
}
let CtrList = class CtrList {
constructor(completer, templateRef, viewContainer, cd, zone) {
this.completer = completer;
this.templateRef = templateRef;
this.viewContainer = viewContainer;
this.cd = cd;
this.zone = zone;
this.ctrListMinSearchLength = MIN_SEARCH_LENGTH;
this.ctrListPause = PAUSE;
this.ctrListAutoMatch = false;
this.ctrListAutoHighlight = false;
this.ctrListDisplaySearching = true;
this._dataService = null;
// private results: CompleterItem[] = [];
this.term = null;
// private searching = false;
this.searchTimer = null;
this.clearTimer = null;
this.ctx = new CtrListContext([], false, false, false);
this._initialValue = null;
this.viewRef = null;
}
ngOnInit() {
this.completer.registerList(this);
this.viewRef = this.viewContainer.createEmbeddedView(this.templateRef, new CtrListContext([], false, false, false));
}
set dataService(newService) {
this._dataService = newService;
this.dataServiceSubscribe();
}
set initialValue(value) {
if (this._dataService &&
typeof this._dataService.convertToItem === "function") {
this.zone.run(() => {
const initialItem = this._dataService && this._dataService.convertToItem(value);
if (initialItem) {
this.completer.onSelected(initialItem, false);
}
});
}
else if (!this._dataService) {
this._initialValue = value;
}
}
search(term) {
if (!isNil(term) &&
term.length >= this.ctrListMinSearchLength &&
this.term !== term) {
if (this.searchTimer) {
this.searchTimer.unsubscribe();
this.searchTimer = null;
}
if (!this.ctx.searching) {
if (this.ctrListDisplaySearching) {
this.ctx.results = [];
}
this.ctx.searching = true;
this.ctx.searchInitialized = true;
this.refreshTemplate();
}
if (this.clearTimer) {
this.clearTimer.unsubscribe();
}
this.searchTimer = timer(this.ctrListPause)
.pipe(take(1))
.subscribe(() => {
this.searchTimerComplete(term);
});
}
else if (!isNil(term) && term.length < this.ctrListMinSearchLength) {
this.clear();
this.term = "";
}
}
clear() {
if (this.searchTimer) {
this.searchTimer.unsubscribe();
}
this.clearTimer = timer(CLEAR_TIMEOUT)
.pipe(take(1))
.subscribe(() => {
this._clear();
});
}
open() {
if (!this.ctx.searchInitialized) {
this.search("");
}
this.refreshTemplate();
}
isOpen(open) {
this.ctx.isOpen = open;
}
_clear() {
if (this.searchTimer) {
this.searchTimer.unsubscribe();
this.searchTimer = null;
}
if (this.dataService) {
this.dataService.cancel();
}
this.viewContainer.clear();
this.viewRef = null;
}
searchTimerComplete(term) {
// Begin the search
if (isNil(term) || term.length < this.ctrListMinSearchLength) {
this.ctx.searching = false;
return;
}
this.term = term;
if (this._dataService) {
this._dataService.search(term);
}
}
refreshTemplate() {
// create the template if it doesn't exist
if (!this.viewRef) {
this.viewRef = this.viewContainer.createEmbeddedView(this.templateRef, this.ctx);
}
else if (!this.viewRef.destroyed) {
// refresh the template
this.viewRef.context.isOpen = this.ctx.isOpen;
this.viewRef.context.results = this.ctx.results;
this.viewRef.context.searching = this.ctx.searching;
this.viewRef.context.searchInitialized = this.ctx.searchInitialized;
this.viewRef.detectChanges();
}
this.cd.markForCheck();
}
getBestMatchIndex() {
if (!this.ctx.results || !this.term) {
return null;
}
// First try to find the exact term
let bestMatch = this.ctx.results.findIndex((item) => item.title.toLowerCase() === this.term.toLocaleLowerCase());
// If not try to find the first item that starts with the term
if (bestMatch < 0) {
bestMatch = this.ctx.results.findIndex((item) => item.title.toLowerCase().startsWith(this.term.toLocaleLowerCase()));
}
// If not try to find the first item that includes the term
if (bestMatch < 0) {
bestMatch = this.ctx.results.findIndex((item) => item.title.toLowerCase().includes(this.term.toLocaleLowerCase()));
}
return bestMatch < 0 ? null : bestMatch;
}
dataServiceSubscribe() {
if (this._dataService) {
this._dataService.subscribe((results) => {
this.ctx.searchInitialized = true;
this.ctx.searching = false;
this.ctx.results = results;
if (this.ctrListAutoMatch &&
results &&
results.length === 1 &&
results[0].title &&
!isNil(this.term) &&
results[0].title.toLocaleLowerCase() ===
this.term.toLocaleLowerCase()) {
// Do automatch
this.completer.onSelected(results[0]);
return;
}
this.refreshTemplate();
if (this.ctrListAutoHighlight) {
this.completer.autoHighlightIndex = this.getBestMatchIndex();
}
}, (error) => {
// tslint:disable-next-line:no-console
console.error(error);
// tslint:disable-next-line:no-console
console.error("Unexpected error in dataService: errors should be handled by the dataService Observable");
return [];
});
if (this._dataService.dataSourceChange) {
this._dataService.dataSourceChange.subscribe(() => {
this.term = null;
this.ctx.searchInitialized = false;
this.ctx.searching = false;
this.ctx.results = [];
this.refreshTemplate();
this.completer.onDataSourceChange();
});
}
}
}
};
__decorate([
Input(),
__metadata("design:type", Object)
], CtrList.prototype, "ctrListMinSearchLength", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CtrList.prototype, "ctrListPause", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CtrList.prototype, "ctrListAutoMatch", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CtrList.prototype, "ctrListAutoHighlight", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CtrList.prototype, "ctrListDisplaySearching", void 0);
__decorate([
Input("ctrList"),
__metadata("design:type", Object),
__metadata("design:paramtypes", [Object])
], CtrList.prototype, "dataService", null);
__decorate([
Input("ctrListInitialValue"),
__metadata("design:type", Object),
__metadata("design:paramtypes", [Object])
], CtrList.prototype, "initialValue", null);
CtrList = __decorate([
Directive({
selector: "[ctrList]",
}),
__param(0, Host()),
__metadata("design:paramtypes", [CtrCompleter,
TemplateRef,
ViewContainerRef,
ChangeDetectorRef,
NgZone])
], CtrList);
let CtrRow = class CtrRow {
constructor(el, renderer, dropdown) {
this.el = el;
this.renderer = renderer;
this.dropdown = dropdown;
this.selected = false;
this._rowIndex = 0;
this._item = null;
}
ngOnDestroy() {
if (this._rowIndex) {
this.dropdown.unregisterRow(this._rowIndex);
}
}
set ctrRow(index) {
this._rowIndex = index;
this.dropdown.registerRow(new CtrRowItem(this, this._rowIndex));
}
set dataItem(item) {
this._item = item;
}
onClick(event) {
this.dropdown.onSelected(this._item);
}
onMouseEnter(event) {
this.dropdown.highlightRow(this._rowIndex);
}
onMouseDown(event) {
this.dropdown.rowMouseDown();
}
setHighlighted(selected) {
this.selected = selected;
if (this.selected) {
this.renderer.addClass(this.el.nativeElement, "completer-selected-row");
}
else {
this.renderer.removeClass(this.el.nativeElement, "completer-selected-row");
}
}
getNativeElement() {
return this.el.nativeElement;
}
getDataItem() {
return this._item;
}
};
__decorate([
Input(),
__metadata("design:type", Number),
__metadata("design:paramtypes", [Number])
], CtrRow.prototype, "ctrRow", null);
__decorate([
Input(),
__metadata("design:type", Object),
__metadata("design:paramtypes", [Object])
], CtrRow.prototype, "dataItem", null);
__decorate([
HostListener("click", ["$event"]),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CtrRow.prototype, "onClick", null);
__decorate([
HostListener("mouseenter", ["$event"]),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CtrRow.prototype, "onMouseEnter", null);
__decorate([
HostListener("mousedown", ["$event"]),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CtrRow.prototype, "onMouseDown", null);
CtrRow = __decorate([
Directive({
selector: "[ctrRow]",
}),
__param(2, Host()),
__metadata("design:paramtypes", [ElementRef, Renderer2, CtrDropdown])
], CtrRow);
let CompleterListItemCmp = class CompleterListItemCmp {
constructor() {
this.text = "";
this.searchStr = "";
this.matchClass = "";
this.type = "";
this.parts = [];
}
ngOnInit() {
if (!this.searchStr) {
this.parts.push({ isMatch: false, text: this.text });
return;
}
const matchStr = this.text.toLowerCase();
let matchPos = matchStr.indexOf(this.searchStr.toLowerCase());
let startIndex = 0;
while (matchPos >= 0) {
const matchText = this.text.slice(matchPos, matchPos + this.searchStr.length);
if (matchPos === 0) {
this.parts.push({ isMatch: true, text: matchText });
startIndex += this.searchStr.length;
}
else if (matchPos > 0) {
const matchPart = this.text.slice(startIndex, matchPos);
this.parts.push({ isMatch: false, text: matchPart });
this.parts.push({ isMatch: true, text: matchText });
startIndex += this.searchStr.length + matchPart.length;
}
matchPos = matchStr.indexOf(this.searchStr.toLowerCase(), startIndex);
}
if (startIndex < this.text.length) {
this.parts.push({ isMatch: false, text: this.text.slice(startIndex, this.text.length) });
}
}
};
__decorate([
Input(),
__metadata("design:type", String)
], CompleterListItemCmp.prototype, "text", void 0);
__decorate([
Input(),
__metadata("design:type", String)
], CompleterListItemCmp.prototype, "searchStr", void 0);
__decorate([
Input(),
__metadata("design:type", String)
], CompleterListItemCmp.prototype, "matchClass", void 0);
__decorate([
Input(),
__metadata("design:type", String)
], CompleterListItemCmp.prototype, "type", void 0);
CompleterListItemCmp = __decorate([
Component({
selector: "completer-list-item",
template: `<span class="completer-list-item-holder" [ngClass]= "{'completer-title': type === 'title', 'completer-description': type === 'description'}" >
<span class="completer-list-item" *ngFor="let part of parts" [ngClass]= "part.isMatch ? matchClass : null">{{part.text}}</span>
</span>`
})
], CompleterListItemCmp);
const noop = () => {
return;
};
const COMPLETER_CONTROL_VALUE_ACCESSOR = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CompleterCmp),
};
let CompleterCmp = class CompleterCmp {
constructor(completerService, cdr) {
this.completerService = completerService;
this.cdr = cdr;
this.inputName = "";
this.inputId = "";
this.pause = PAUSE;
this.minSearchLength = MIN_SEARCH_LENGTH;
this.maxChars = MAX_CHARS;
this.overrideSuggested = false;
this.clearSelected = false;
this.clearUnselected = false;
this.fillHighlighted = true;
this.placeholder = "";
this.autoMatch = false;
this.disableInput = false;
this.autofocus = false;
this.openOnFocus = false;
this.openOnClick = false;
this.selectOnClick = false;
this.selectOnFocus = false;
this.autoHighlight = false;
this.selected = new EventEmitter();
this.highlighted = new EventEmitter();
this.blurEvent = new EventEmitter();
this.click = new EventEmitter();
this.focusEvent = new EventEmitter();
this.opened = new EventEmitter();
this.keyup = new EventEmitter();
this.keydown = new EventEmitter();
this.control = new FormControl("");
this.displaySearching = true;
this.displayNoResults = true;
this._textNoResults = TEXT_NO_RESULTS;
this._textSearching = TEXT_SEARCHING;
this._onTouchedCallback = noop;
this._onChangeCallback = noop;
this._focus = false;
this._open = false;
this._searchStr = "";
}
get value() { return this.searchStr; }
;
set value(v) {
if (v !== this.searchStr) {
this.searchStr = v;
}
// Propagate the change in any case
this._onChangeCallback(v);
}
get searchStr() {
return this._searchStr;
}
set searchStr(value) {
if (typeof value === "string" || isNil(value)) {
this._searchStr = value;
}
else {
this._searchStr = JSON.stringify(value);
}
}
ngAfterViewInit() {
if (this.autofocus) {
this._focus = true;
}
if (!this.completer) {
return;
}
this.completer.selected.subscribe((item) => {
this.selected.emit(item);
});
this.completer.highlighted.subscribe((item) => {
this.highlighted.emit(item);
});
this.completer.opened.subscribe((isOpen) => {
this._open = isOpen;
this.opened.emit(isOpen);
});
}
ngAfterViewChecked() {
if (this._focus) {
setTimeout(() => {
if (!!this.ctrInput) {
this.ctrInput.nativeElement.focus();
this._focus = false;
}
}, 0);
}
}
onTouched() {
this._onTouchedCallback();
}
writeValue(value) {
this.searchStr = value;
}
registerOnChange(fn) {
this._onChangeCallback = fn;
}
registerOnTouched(fn) {
this._onTouchedCallback = fn;
}
setDisabledState(isDisabled) {
this.disableInput = isDisabled;
}
set datasource(source) {
if (source) {
if (source instanceof Array) {
this.dataService = this.completerService.local(source);
}
else if (typeof (source) === "string") {
this.dataService = this.completerService.remote(source);
}
else {
this.dataService = source;
}
}
}
set textNoResults(text) {
if (this._textNoResults !== text) {
this._textNoResults = text;
this.displayNoResults = !!this._textNoResults && this._textNoResults !== "false";
}
}
set textSearching(text) {
if (this._textSearching !== text) {
this._textSearching = text;
this.displaySearching = !!this._textSearching && this._textSearching !== "false";
}
}
onBlur() {
this.blurEvent.emit();
this.onTouched();
this.cdr.detectChanges();
}
onFocus() {
this.focusEvent.emit();
this.onTouched();
}
onClick(event) {
this.click.emit(event);
this.onTouched();
}
onKeyup(event) {
this.keyup.emit(event);
event.stopPropagation();
}
onKeydown(event) {
this.keydown.emit(event);
event.stopPropagation();
}
onChange(value) {
this.value = value;
}
open() {
if (!this.completer) {
return;
}
this.completer.open();
}
close() {
if (!this.completer) {
return;
}
this.completer.clear();
}
focus() {
if (this.ctrInput) {
this.ctrInput.nativeElement.focus();
}
else {
this._focus = true;
}
}
blur() {
if (this.ctrInput) {
this.ctrInput.nativeElement.blur();
}
else {
this._focus = false;
}
}
isOpen() {
return this._open;
}
};
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "dataService", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "inputName", void 0);
__decorate([
Input(),
__metadata("design:type", String)
], CompleterCmp.prototype, "inputId", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "pause", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "minSearchLength", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "maxChars", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "overrideSuggested", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "clearSelected", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "clearUnselected", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "fillHighlighted", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "placeholder", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "matchClass", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "fieldTabindex", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "autoMatch", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "disableInput", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "inputClass", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "autofocus", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "openOnFocus", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "openOnClick", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "selectOnClick", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "selectOnFocus", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "initialValue", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "autoHighlight", void 0);
__decorate([
Output(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "selected", void 0);
__decorate([
Output(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "highlighted", void 0);
__decorate([
Output("blur"),
__metadata("design:type", Object)
], CompleterCmp.prototype, "blurEvent", void 0);
__decorate([
Output(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "click", void 0);
__decorate([
Output("focus"),
__metadata("design:type", Object)
], CompleterCmp.prototype, "focusEvent", void 0);
__decorate([
Output(),
__metadata("design:type", Object)
], CompleterCmp.prototype, "opened", void 0);
__decorate([
Output(),
__metadata("design:type", EventEmitter)
], CompleterCmp.prototype, "keyup", void 0);
__decorate([
Output(),
__metadata("design:type", EventEmitter)
], CompleterCmp.prototype, "keydown", void 0);
__decorate([
ViewChild(CtrCompleter, { static: false }),
__metadata("design:type", Object)
], CompleterCmp.prototype, "completer", void 0);
__decorate([
ViewChild("ctrInput", { static: false }),
__metadata("design:type", Object)
], CompleterCmp.prototype, "ctrInput", void 0);
__decorate([
Input(),
__metadata("design:type", Object),
__metadata("design:paramtypes", [Object])
], CompleterCmp.prototype, "datasource", null);
__decorate([
Input(),
__metadata("design:type", String),
__metadata("design:paramtypes", [String])
], CompleterCmp.prototype, "textNoResults", null);
__decorate([
Input(),
__metadata("design:type", String),
__metadata("design:paramtypes", [String])
], CompleterCmp.prototype, "textSearching", null);
CompleterCmp = __decorate([
Component({
selector: "ng2-completer",
template: `
<div class="completer-holder" ctrCompleter>
<input #ctrInput [attr.id]="inputId.length > 0 ? inputId : null" type="search"
class="comp