UNPKG

ngx-bootstrap

Version:
326 lines (325 loc) 15.6 kB
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