|
@@ -22,93 +22,111 @@
|
|
|
|
|
|
|
|
|
import { Component } from '../component';
|
|
|
-import { IAugmentedJQuery, ICompileService, IDocumentService, IPromise, IScope } from 'angular';
|
|
|
+import { IAttributes, IAugmentedJQuery, IDocumentService, IPromise, IScope } from 'angular';
|
|
|
|
|
|
@Component({
|
|
|
bindings: {
|
|
|
- 'search': '&',
|
|
|
+ 'onSearchTextChange': '&',
|
|
|
'itemSelected': '&',
|
|
|
'item': '@',
|
|
|
'itemText': '@',
|
|
|
- 'query': '='
|
|
|
+ 'searchFunction': '&search',
|
|
|
+ 'searchText': '<'
|
|
|
},
|
|
|
- templateUrl: require('ux/auto-complete.component.html'),
|
|
|
- transclude: true,
|
|
|
+ template: [
|
|
|
+ '$element',
|
|
|
+ '$attrs',
|
|
|
+ ($element: IAugmentedJQuery, $attrs: IAttributes): string => {
|
|
|
+ // Remove content template from dom
|
|
|
+ const contentTemplate: IAugmentedJQuery = $element.find('content-template');
|
|
|
+ contentTemplate.detach();
|
|
|
+
|
|
|
+ return `
|
|
|
+ <mf-search-bar search-text="$ctrl.searchText"
|
|
|
+ on-search-text-change="$ctrl.onSearchBarTextChange(value)"
|
|
|
+ on-key-down="$ctrl.onSearchBarKeyDown($event)"
|
|
|
+ ng-click="$ctrl.onSearchBarClick($event)"
|
|
|
+ auto-focus></mf-search-bar>
|
|
|
+ <ul class="results" ng-if="$ctrl.show" ng-click="$event.stopPropagation()">
|
|
|
+ <li ng-repeat="item in $ctrl.items"
|
|
|
+ ng-click="$ctrl.selectItem(item)"
|
|
|
+ ng-class="{ \'selected\': $index == $ctrl.selectedIndex }\">` +
|
|
|
+ contentTemplate.html().replace(new RegExp($attrs['item'], 'g'), 'item') +
|
|
|
+ `</li>
|
|
|
+ <li class="search-message" ng-if="$ctrl.show && $ctrl.searchText && !$ctrl.items.length">
|
|
|
+ <span translate="Display_SearchResultsNone"></span>
|
|
|
+ </li>
|
|
|
+ </ul>`;
|
|
|
+ }],
|
|
|
stylesheetUrl: require('ux/auto-complete.component.scss')
|
|
|
})
|
|
|
export default class AutoCompleteComponent {
|
|
|
item: string;
|
|
|
items: any[];
|
|
|
itemSelected: (item: any) => void;
|
|
|
- query: string;
|
|
|
- search: (query: any) => IPromise<any[]>;
|
|
|
+ onSearchTextChange: Function;
|
|
|
+ searchText: string;
|
|
|
+ searchFunction: (query: any) => IPromise<any[]>;
|
|
|
searchMessage: string;
|
|
|
selectedIndex: number;
|
|
|
show: boolean;
|
|
|
|
|
|
- static $inject = [ '$compile', '$document', '$element', '$scope' ];
|
|
|
- constructor(private $compile: ICompileService,
|
|
|
- private $document: IDocumentService,
|
|
|
+ static $inject = [ '$document', '$element', '$scope' ];
|
|
|
+ constructor(private $document: IDocumentService,
|
|
|
private $element: IAugmentedJQuery,
|
|
|
private $scope: IScope) {
|
|
|
}
|
|
|
|
|
|
+ $onDestroy(): void {
|
|
|
+ this.$document.off('click', this.onDocumentClick.bind(this));
|
|
|
+ }
|
|
|
+
|
|
|
$onInit(): void {
|
|
|
- const self = this;
|
|
|
+ this.selectedIndex = -1;
|
|
|
|
|
|
- this.$scope.$watch('$ctrl.query', () => {
|
|
|
- self.search({ query: self.query })
|
|
|
- .then((results: any[]) => {
|
|
|
- self.items = results;
|
|
|
- self.resetSelection();
|
|
|
- self.showAutoCompleteResults();
|
|
|
- });
|
|
|
- });
|
|
|
+ if (this.searchText) {
|
|
|
+ this.fetchAutoCompleteData(this.searchText);
|
|
|
+ }
|
|
|
|
|
|
- this.selectedIndex = -1;
|
|
|
+ this.hideResults();
|
|
|
+ }
|
|
|
+
|
|
|
+ $postLink(): void {
|
|
|
+ var self = this;
|
|
|
|
|
|
// Listen for clicks outside of the auto-complete component
|
|
|
- this.$document.on('click', (event: Event) => {
|
|
|
- if (self.show) {
|
|
|
- self.hideAutoCompleteResults();
|
|
|
- self.$scope.$apply();
|
|
|
- }
|
|
|
- });
|
|
|
+ // Implemented as a click event instead of a blur event, so the results list can be clicked
|
|
|
+ this.$document.on('click', this.onDocumentClick.bind(this));
|
|
|
}
|
|
|
|
|
|
- $postLink(): void {
|
|
|
- // Remove content template from dom
|
|
|
- const contentTemplate: IAugmentedJQuery = this.$element.find('content-template');
|
|
|
- // noinspection TypeScriptUnresolvedFunction
|
|
|
- contentTemplate.remove();
|
|
|
-
|
|
|
- const autoCompleteHtml =
|
|
|
- '<ul class="results" ng-if="$ctrl.show" ng-click="$event.stopPropagation()">' +
|
|
|
- ' <li ng-repeat="item in $ctrl.items"' +
|
|
|
- ' ng-click="$ctrl.selectItem(item)"' +
|
|
|
- ' ng-class="{ \'selected\': $index == $ctrl.selectedIndex }\">' +
|
|
|
- contentTemplate.html().replace(new RegExp(this.item, 'g'), 'item') +
|
|
|
- ' </li>' +
|
|
|
- ' <li class="search-message" ng-if="$ctrl.show && $ctrl.query && !$ctrl.items.length">' +
|
|
|
- ' <span translate="Display_SearchResultsNone"></span>' +
|
|
|
- ' </li>' +
|
|
|
- '</ul>';
|
|
|
- const compiledElement = this.$compile(autoCompleteHtml)(this.$scope);
|
|
|
-
|
|
|
- this.$element.append(compiledElement);
|
|
|
+ onSearchBarClick(event: Event): void {
|
|
|
+ event.stopImmediatePropagation();
|
|
|
}
|
|
|
|
|
|
onSearchBarFocus(): void {
|
|
|
if (this.hasItems()) {
|
|
|
- this.showAutoCompleteResults();
|
|
|
+ this.showResults();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ onSearchBarTextChange(value: string): void {
|
|
|
+ this.searchText = value;
|
|
|
+ this.fetchAutoCompleteData(value);
|
|
|
+ this.showResults();
|
|
|
+
|
|
|
+ this.onSearchTextChange({ value: value });
|
|
|
+ }
|
|
|
+
|
|
|
onSearchBarKeyDown(event: KeyboardEvent): void {
|
|
|
switch (event.keyCode) {
|
|
|
case 40: // ArrowDown
|
|
|
- this.selectNextItem();
|
|
|
- event.preventDefault();
|
|
|
+ if (this.hasItems() && !this.show) {
|
|
|
+ this.showResults();
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ this.selectNextItem();
|
|
|
+ event.preventDefault();
|
|
|
+ }
|
|
|
break;
|
|
|
case 38: // ArrowUp
|
|
|
this.selectPreviousItem();
|
|
@@ -116,10 +134,10 @@ export default class AutoCompleteComponent {
|
|
|
break;
|
|
|
case 27: // Escape
|
|
|
if (!this.show || !this.hasItems()) {
|
|
|
- this.clearAutoCompleteResults();
|
|
|
+ this.clearResults();
|
|
|
}
|
|
|
else {
|
|
|
- this.hideAutoCompleteResults();
|
|
|
+ this.hideResults();
|
|
|
}
|
|
|
|
|
|
break;
|
|
@@ -130,7 +148,7 @@ export default class AutoCompleteComponent {
|
|
|
}
|
|
|
break;
|
|
|
case 9: // Tab
|
|
|
- if (!this.query) {
|
|
|
+ if (!this.searchText || !this.show) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
@@ -147,19 +165,35 @@ export default class AutoCompleteComponent {
|
|
|
}
|
|
|
|
|
|
selectItem(item: any): void {
|
|
|
- this.hideAutoCompleteResults();
|
|
|
+ this.clearResults();
|
|
|
|
|
|
const data = {};
|
|
|
data[this.item] = item;
|
|
|
this.itemSelected(data);
|
|
|
}
|
|
|
|
|
|
- private clearAutoCompleteResults(): void {
|
|
|
+ private clearResults(): void {
|
|
|
this.resetSelection();
|
|
|
- this.query = null;
|
|
|
+ this.searchText = null;
|
|
|
this.items = [];
|
|
|
}
|
|
|
|
|
|
+ private onDocumentClick(): void {
|
|
|
+ if (this.show) {
|
|
|
+ this.hideResults();
|
|
|
+ this.$scope.$apply();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fetchAutoCompleteData(value: string): void {
|
|
|
+ var self = this;
|
|
|
+ this.searchFunction({ query: value })
|
|
|
+ .then((results: any[]) => {
|
|
|
+ self.items = results;
|
|
|
+ self.resetSelection();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
private getSelectedItem(): any {
|
|
|
return this.items[this.selectedIndex];
|
|
|
}
|
|
@@ -168,7 +202,7 @@ export default class AutoCompleteComponent {
|
|
|
return this.items && !!this.items.length;
|
|
|
}
|
|
|
|
|
|
- private hideAutoCompleteResults(): void {
|
|
|
+ private hideResults(): void {
|
|
|
this.show = false;
|
|
|
}
|
|
|
|
|
@@ -188,9 +222,7 @@ export default class AutoCompleteComponent {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private showAutoCompleteResults(): void {
|
|
|
- if (this.hasItems() || !/^\s*$/.test(this.query)) {
|
|
|
- this.show = true;
|
|
|
- }
|
|
|
+ private showResults(): void {
|
|
|
+ this.show = true;
|
|
|
}
|
|
|
}
|