ngx-bootstrap
Version:
Angular Bootstrap
326 lines (325 loc) • 15.6 kB
JavaScript
import { ChangeDetectorRef, Component, ElementRef, HostListener, QueryList, Renderer2, ViewChild, ViewChildren, Output, EventEmitter } from '@angular/core';
import { isBs3, Utils } from 'ngx-bootstrap/utils';
import { PositioningService } from 'ngx-bootstrap/positioning';
import { Subscription } from 'rxjs';
import { latinize } from './typeahead-utils';
import { typeaheadAnimation } from './typeahead-animations';
let nextWindowId = 0;
export class TypeaheadContainerComponent {
constructor(positionService, renderer, element, changeDetectorRef) {
var _a;
this.positionService = positionService;
this.renderer = renderer;
this.element = element;
this.changeDetectorRef = changeDetectorRef;
// eslint-disable-next-line @angular-eslint/no-output-rename
this.activeChangeEvent = new EventEmitter();
this.isFocused = false;
this.positionServiceSubscription = new Subscription();
this.height = 0;
this.popupId = `ngb-typeahead-${nextWindowId++}`;
this._matches = [];
this.renderer.setAttribute(this.element.nativeElement, 'id', this.popupId);
this.positionServiceSubscription.add((_a = this.positionService.event$) === null || _a === void 0 ? void 0 : _a.subscribe(() => {
if (this.isAnimated) {
this.animationState = this.isTopPosition ? 'animated-up' : 'animated-down';
this.changeDetectorRef.detectChanges();
return;
}
this.animationState = 'unanimated';
this.changeDetectorRef.detectChanges();
}));
}
get isBs4() {
return !isBs3();
}
get typeaheadTemplateMethods() {
return {
selectMatch: this.selectMatch.bind(this),
selectActive: this.selectActive.bind(this),
isActive: this.isActive.bind(this)
};
}
get active() {
return this._active;
}
set active(active) {
this._active = active;
this.activeChanged();
}
get matches() {
return this._matches;
}
set matches(value) {
var _a;
this.positionService.setOptions({
modifiers: { flip: { enabled: this.adaptivePosition } },
allowedPositions: ['top', 'bottom']
});
this._matches = value;
this.needScrollbar = this.typeaheadScrollable && this.typeaheadOptionsInScrollableView < this.matches.length;
if (this.typeaheadScrollable) {
setTimeout(() => {
this.setScrollableMode();
});
}
if (this.typeaheadIsFirstItemActive && this._matches.length > 0) {
this.setActive(this._matches[0]);
if ((_a = this._active) === null || _a === void 0 ? void 0 : _a.isHeader()) {
this.nextActiveMatch();
}
}
if (this._active && !this.typeaheadIsFirstItemActive) {
const concurrency = this._matches.find(match => { var _a; return match.value === ((_a = this._active) === null || _a === void 0 ? void 0 : _a.value); });
if (concurrency) {
this.selectActive(concurrency);
return;
}
this.active = void 0;
}
}
get isTopPosition() {
return this.element.nativeElement.classList.contains('top');
}
get optionsListTemplate() {
return this.parent ? this.parent.optionsListTemplate : undefined;
}
get isAnimated() {
return this.parent ? this.parent.isAnimated : false;
}
get adaptivePosition() {
return this.parent ? this.parent.adaptivePosition : false;
}
get typeaheadScrollable() {
return this.parent ? this.parent.typeaheadScrollable : false;
}
get typeaheadOptionsInScrollableView() {
return this.parent ? this.parent.typeaheadOptionsInScrollableView : 5;
}
get typeaheadIsFirstItemActive() {
return this.parent ? this.parent.typeaheadIsFirstItemActive : true;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get itemTemplate() {
return this.parent ? this.parent.typeaheadItemTemplate : undefined;
}
selectActiveMatch(isActiveItemChanged) {
var _a, _b;
if (this._active && ((_a = this.parent) === null || _a === void 0 ? void 0 : _a.typeaheadSelectFirstItem)) {
this.selectMatch(this._active);
}
if (!((_b = this.parent) === null || _b === void 0 ? void 0 : _b.typeaheadSelectFirstItem) && isActiveItemChanged) {
this.selectMatch(this._active);
}
}
activeChanged() {
if (!this._active) {
return;
}
const index = this.matches.indexOf(this._active);
this.activeChangeEvent.emit(`${this.popupId}-${index}`);
}
prevActiveMatch() {
if (!this._active) {
return;
}
const index = this.matches.indexOf(this._active);
this.setActive(this.matches[index - 1 < 0 ? this.matches.length - 1 : index - 1]);
if (this._active.isHeader()) {
this.prevActiveMatch();
}
if (this.typeaheadScrollable) {
this.scrollPrevious(index);
}
}
nextActiveMatch() {
var _a;
const index = this._active ? this.matches.indexOf(this._active) : -1;
this.setActive(this.matches[index + 1 > this.matches.length - 1 ? 0 : index + 1]);
if ((_a = this._active) === null || _a === void 0 ? void 0 : _a.isHeader()) {
this.nextActiveMatch();
}
if (this.typeaheadScrollable) {
this.scrollNext(index);
}
}
selectActive(value) {
this.isFocused = true;
this.setActive(value);
}
highlight(match, query) {
let itemStr = match.value;
let itemStrHelper = (this.parent && this.parent.typeaheadLatinize
? latinize(itemStr)
: itemStr).toLowerCase();
let startIdx;
let tokenLen;
// Replaces the capture string with the same string inside of a "strong" tag
if (typeof query === 'object') {
const queryLen = query.length;
for (let i = 0; i < queryLen; i += 1) {
// query[i] is already latinized and lower case
startIdx = itemStrHelper.indexOf(query[i]);
tokenLen = query[i].length;
if (startIdx >= 0 && tokenLen > 0) {
itemStr =
`${itemStr.substring(0, startIdx)}<strong>${itemStr.substring(startIdx, startIdx + tokenLen)}</strong>` +
`${itemStr.substring(startIdx + tokenLen)}`;
itemStrHelper =
`${itemStrHelper.substring(0, startIdx)} ${' '.repeat(tokenLen)} ` +
`${itemStrHelper.substring(startIdx + tokenLen)}`;
}
}
}
else if (query) {
// query is already latinized and lower case
startIdx = itemStrHelper.indexOf(query);
tokenLen = query.length;
if (startIdx >= 0 && tokenLen > 0) {
itemStr =
`${itemStr.substring(0, startIdx)}<strong>${itemStr.substring(startIdx, startIdx + tokenLen)}</strong>` +
`${itemStr.substring(startIdx + tokenLen)}`;
}
}
return itemStr;
}
focusLost() {
this.isFocused = false;
this.setActive(void 0);
}
isActive(value) {
return this.active === value;
}
selectMatch(value, event) {
var _a;
if (event) {
event.stopPropagation();
event.preventDefault();
}
(_a = this.parent) === null || _a === void 0 ? void 0 : _a.changeModel(value);
setTimeout(() => { var _a; return (_a = this.parent) === null || _a === void 0 ? void 0 : _a.typeaheadOnSelect.emit(value); }, 0);
return false;
}
setScrollableMode() {
var _a;
if (!this.ulElement) {
this.ulElement = this.element;
}
if ((_a = this.liElements) === null || _a === void 0 ? void 0 : _a.first) {
const ulStyles = Utils.getStyles(this.ulElement.nativeElement);
const liStyles = Utils.getStyles(this.liElements.first.nativeElement);
const ulPaddingBottom = parseFloat((ulStyles['padding-bottom'] ? ulStyles['padding-bottom'] : '')
.replace('px', ''));
const ulPaddingTop = parseFloat((ulStyles['padding-top'] ? ulStyles['padding-top'] : '0')
.replace('px', ''));
const optionHeight = parseFloat((liStyles.height ? liStyles.height : '0')
.replace('px', ''));
const height = this.typeaheadOptionsInScrollableView * optionHeight;
this.guiHeight = `${height + ulPaddingTop + ulPaddingBottom}px`;
}
this.renderer.setStyle(this.element.nativeElement, 'visibility', 'visible');
}
scrollPrevious(index) {
if (index === 0) {
this.scrollToBottom();
return;
}
if (this.liElements && this.ulElement) {
const liElement = this.liElements.toArray()[index - 1];
if (liElement && !this.isScrolledIntoView(liElement.nativeElement)) {
this.ulElement.nativeElement.scrollTop = liElement.nativeElement.offsetTop;
}
}
}
scrollNext(index) {
if (index + 1 > this.matches.length - 1) {
this.scrollToTop();
return;
}
if (this.liElements && this.ulElement) {
const liElement = this.liElements.toArray()[index + 1];
if (liElement && !this.isScrolledIntoView(liElement.nativeElement)) {
this.ulElement.nativeElement.scrollTop =
liElement.nativeElement.offsetTop -
Number(this.ulElement.nativeElement.offsetHeight) +
Number(liElement.nativeElement.offsetHeight);
}
}
}
ngOnDestroy() {
this.positionServiceSubscription.unsubscribe();
}
setActive(value) {
var _a;
this._active = value;
let preview;
if (!(this._active == null || this._active.isHeader())) {
preview = value;
}
(_a = this.parent) === null || _a === void 0 ? void 0 : _a.typeaheadOnPreview.emit(preview);
}
isScrolledIntoView(elem) {
if (!this.ulElement) {
return false;
}
const containerViewTop = this.ulElement.nativeElement.scrollTop;
const containerViewBottom = containerViewTop + Number(this.ulElement.nativeElement.offsetHeight);
const elemTop = elem.offsetTop;
const elemBottom = elemTop + elem.offsetHeight;
return ((elemBottom <= containerViewBottom) && (elemTop >= containerViewTop));
}
;
scrollToBottom() {
var _a;
if (!((_a = this.ulElement) === null || _a === void 0 ? void 0 : _a.nativeElement)) {
return;
}
this.ulElement.nativeElement.scrollTop = this.ulElement.nativeElement.scrollHeight;
}
scrollToTop() {
var _a;
if (!((_a = this.ulElement) === null || _a === void 0 ? void 0 : _a.nativeElement)) {
return;
}
this.ulElement.nativeElement.scrollTop = 0;
}
}
TypeaheadContainerComponent.decorators = [
{ type: Component, args: [{
selector: 'typeahead-container',
template: "<!-- inject options list template -->\n<ng-template [ngTemplateOutlet]=\"optionsListTemplate || (isBs4 ? bs4Template : bs3Template)\"\n [ngTemplateOutletContext]=\"{\n matches: matches,\n itemTemplate: itemTemplate || bsItemTemplate,\n query: query,\n $implicit: typeaheadTemplateMethods\n }\">\n</ng-template>\n\n<!-- default options item template -->\n<ng-template #bsItemTemplate let-match=\"match\" let-query=\"query\">\n <span [innerHtml]=\"highlight(match, query)\"></span>\n</ng-template>\n\n<!-- Bootstrap 3 options list template -->\n<ng-template #bs3Template>\n <ul class=\"dropdown-menu\"\n #ulElement\n role=\"listbox\"\n [style.overflow-y]=\"needScrollbar ? 'scroll': 'auto'\"\n [style.height]=\"needScrollbar ? guiHeight: 'auto'\">\n <ng-template ngFor let-match let-i=\"index\" [ngForOf]=\"matches\">\n <li #liElements *ngIf=\"match.isHeader()\" class=\"dropdown-header\">{{ match }}</li>\n <li #liElements\n *ngIf=\"!match.isHeader()\"\n [id]=\"popupId + '-' + i\"\n role=\"option\"\n [@typeaheadAnimation]=\"animationState\"\n [class.active]=\"isActive(match)\"\n (mouseenter)=\"selectActive(match)\">\n\n <a href=\"#\" (click)=\"selectMatch(match, $event)\" tabindex=\"-1\">\n <ng-template [ngTemplateOutlet]=\"itemTemplate || bsItemTemplate\"\n [ngTemplateOutletContext]=\"{item: match.item, index: i, match: match, query: query}\">\n </ng-template>\n </a>\n </li>\n </ng-template>\n </ul>\n</ng-template>\n\n<!-- Bootstrap 4 options list template -->\n<ng-template #bs4Template>\n <ng-template ngFor let-match let-i=\"index\" [ngForOf]=\"matches\">\n <h6 *ngIf=\"match.isHeader()\" class=\"dropdown-header\">{{ match }}</h6>\n <ng-template [ngIf]=\"!match.isHeader()\">\n <button #liElements\n [id]=\"popupId + '-' + i\"\n role=\"option\"\n [@typeaheadAnimation]=\"animationState\"\n class=\"dropdown-item\"\n (click)=\"selectMatch(match, $event)\"\n (mouseenter)=\"selectActive(match)\"\n [class.active]=\"isActive(match)\">\n <ng-template [ngTemplateOutlet]=\"itemTemplate || bsItemTemplate\"\n [ngTemplateOutletContext]=\"{item: match.item, index: i, match: match, query: query}\">\n </ng-template>\n </button>\n </ng-template>\n </ng-template>\n</ng-template>\n",
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: {
class: 'dropdown open bottom',
'[class.dropdown-menu]': 'isBs4',
'[style.height]': `isBs4 && needScrollbar ? guiHeight: 'auto'`,
'[style.visibility]': `'inherit'`,
'[class.dropup]': 'dropup',
style: 'position: absolute;display: block;',
'[attr.role]': `isBs4 ? 'listbox' : null `
},
animations: [typeaheadAnimation],
styles: [`
:host.dropdown {
z-index: 1000;
}
:host.dropdown-menu, .dropdown-menu {
overflow-y: auto;
height: 100px;
}
`]
},] }
];
TypeaheadContainerComponent.ctorParameters = () => [
{ type: PositioningService },
{ type: Renderer2 },
{ type: ElementRef },
{ type: ChangeDetectorRef }
];
TypeaheadContainerComponent.propDecorators = {
activeChangeEvent: [{ type: Output, args: ['activeChange',] }],
ulElement: [{ type: ViewChild, args: ['ulElement', { static: false },] }],
liElements: [{ type: ViewChildren, args: ['liElements',] }],
focusLost: [{ type: HostListener, args: ['mouseleave',] }, { type: HostListener, args: ['blur',] }]
};
//# sourceMappingURL=typeahead-container.component.js.map