@ng-prime-extensions/px-searchable-accordion
Version:
PrimeNG searchable accordion extension
230 lines (225 loc) • 13.3 kB
JavaScript
import * as i0 from '@angular/core';
import { DOCUMENT, Input, Inject, ViewEncapsulation, Component } from '@angular/core';
import { Accordion, AccordionPanel, AccordionHeader, AccordionContent } from 'primeng/accordion';
import { NgTemplateOutlet, AsyncPipe } from '@angular/common';
import { BehaviorSubject, debounceTime } from 'rxjs';
import * as i1 from '@angular/router';
class PxSearchableAccordionComponent {
constructor(router, activatedRoute, changeDetector, document) {
this.router = router;
this.activatedRoute = activatedRoute;
this.changeDetector = changeDetector;
this.document = document;
/**
* Search delay time in milliseconds.
*/
this.searchDebounceTime = 250;
/**
* Weather searched words will be highlighted.
*/
this.highlightWords = true;
/**
* The css class applied to the highlighted words.
*/
this.highlightClass = 'px-searchable-accordion-highlight';
/**
* If true, url fragments will be generated based on the set entry id.
*/
this.allowUrlFragments = true;
this.filteredEntries = new BehaviorSubject([]);
this.activeEntryIndex = 0;
this.lastSearchString = '';
this.searchQueryRegex = /[^a-zA-Z0-9\s]/g;
}
ngOnChanges(changes) {
if (changes['searchString']) {
this.lastSearchString = '';
this.searchSub?.unsubscribe();
delete this.searchSub;
if (typeof this.searchString === "string") {
this.search(this.searchString).then();
}
else {
this.search(this.lastSearchString).then();
this.searchSub = this.searchString
.pipe(debounceTime(this.searchDebounceTime))
.subscribe(searchStr => this.search(searchStr).then());
}
}
else if (changes['entries']) {
if (typeof this.searchString === "string") {
this.search(this.searchString).then();
}
else {
this.search(this.lastSearchString).then();
}
}
}
ngAfterViewInit() {
if (this.allowUrlFragments) {
this.routeFragmentsSub = this.activatedRoute.fragment
.subscribe((fragment) => {
if (fragment) {
const activeEntries = this.filteredEntries.getValue();
const activeEntriesLen = activeEntries.length;
for (let i = 0; i < activeEntriesLen; i++) {
const entry = activeEntries[i];
if (entry.id === fragment && this.activeEntryIndex !== i) {
this.activeEntryIndex = i;
this.changeDetector.detectChanges();
this.document.getElementById(entry.id)?.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
break;
}
}
}
});
}
}
/**
* Searches the available entries for the given search string
* @param searchStr
*/
async search(searchStr) {
searchStr = searchStr.replace(this.searchQueryRegex, '').trim().toLowerCase();
if (searchStr === '') {
this.filteredEntries.next(this.entries);
return;
}
else if (searchStr === this.lastSearchString) {
return;
}
this.lastSearchString = searchStr;
const entries = this.entries;
const entriesLen = entries.length;
const filteredEntries = [];
for (let entryIndex = 0; entryIndex < entriesLen; ++entryIndex) {
const entry = entries[entryIndex];
const lowerCaseHeader = entry.header.toLowerCase();
const lowerCaseContent = entry.content.toLowerCase();
const headerIncludesStr = lowerCaseHeader.indexOf(searchStr) !== -1;
const contentIncludesStr = lowerCaseContent.indexOf(searchStr) !== -1;
if (!(headerIncludesStr || contentIncludesStr)) {
continue;
}
filteredEntries.push(this.highlightWords ? {
header: headerIncludesStr ? this.highlightString(entry.header, lowerCaseHeader, searchStr) : entry.header,
content: contentIncludesStr ? this.highlightString(entry.content, lowerCaseContent, searchStr) : entry.content,
data: entry.data,
} : entry);
}
this.filteredEntries.next(filteredEntries);
}
activeIndexChanged() {
let activeIndex;
if (Array.isArray(this.activeEntryIndex)) {
activeIndex = this.activeEntryIndex[0];
}
else {
activeIndex = this.activeEntryIndex;
}
if (!(activeIndex && this.allowUrlFragments)) {
return;
}
const entries = this.filteredEntries.getValue();
if (!entries.length) {
return;
}
const activeEntry = entries[activeIndex];
if (!activeEntry.id) {
return;
}
this.router.navigate([], {
fragment: activeEntry.id,
}).then();
}
highlightString(sourceString, sourceStringLower, searchStr) {
const destString = [];
const sourceStringLen = sourceString.length;
const highlightClass = this.highlightClass;
let highlightedChars = [];
let inHtmlTag = false;
for (let i = 0; i < sourceStringLen; ++i) {
let char = sourceString[i];
if (inHtmlTag) {
if (char === '>') {
inHtmlTag = false;
}
}
else if (char === '<') {
inHtmlTag = true;
}
else if (searchStr.indexOf(sourceStringLower[i]) !== -1) {
highlightedChars.push(char);
continue;
}
else if (highlightedChars.length) {
if (highlightedChars.length === searchStr.length) {
char = '<span class="' + highlightClass + '">' + highlightedChars.join('') + '</span>' + char;
}
else {
char = highlightedChars.join('') + char;
}
highlightedChars = [];
}
destString.push(char);
}
if (highlightedChars.length) {
if (highlightedChars.length === searchStr.length) {
destString.push('<span class="' + highlightClass + '">' + highlightedChars.join('') + '</span>');
}
else {
destString.push(highlightedChars.join(''));
}
}
return destString.join('');
}
ngOnDestroy() {
this.searchSub?.unsubscribe();
this.filteredEntries.complete();
this.routeFragmentsSub?.unsubscribe();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PxSearchableAccordionComponent, deps: [{ token: i1.Router }, { token: i1.ActivatedRoute }, { token: i0.ChangeDetectorRef }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: PxSearchableAccordionComponent, isStandalone: true, selector: "px-searchable-accordion", inputs: { entries: "entries", searchString: "searchString", headerTemplate: "headerTemplate", contentTemplate: "contentTemplate", noResultsTemplate: "noResultsTemplate", searchDebounceTime: "searchDebounceTime", highlightWords: "highlightWords", highlightClass: "highlightClass", allowUrlFragments: "allowUrlFragments" }, usesOnChanges: true, ngImport: i0, template: "<p-accordion [(value)]=\"activeEntryIndex\" (valueChange)=\"activeIndexChanged()\">\n @if(!(filteredEntries | async)?.length)\n {\n @if(noResultsTemplate)\n {\n <ng-container *ngTemplateOutlet=\"noResultsTemplate ? noResultsTemplate : defaultNoResultsTemplate\"></ng-container>\n }\n }\n\n @for(entry of filteredEntries | async; track entry.id || $index)\n {\n <p-accordion-panel [value]=\"$index\" [attr.id]=\"entry.id\">\n <p-accordion-header>\n <ng-container *ngTemplateOutlet=\"headerTemplate ? headerTemplate : defaultHeader; context: { $implicit: entry }\"></ng-container>\n </p-accordion-header>\n\n <p-accordion-content>\n <ng-container *ngTemplateOutlet=\"contentTemplate ? contentTemplate : defaultContent; context: { $implicit: entry }\"></ng-container>\n </p-accordion-content>\n </p-accordion-panel>\n }\n</p-accordion>\n\n<ng-template #defaultHeader let-entry>\n <span [innerHTML]=\"entry.header\"></span>\n</ng-template>\n<ng-template #defaultContent let-entry>\n <p [innerHTML]=\"entry.content\"></p>\n</ng-template>\n<ng-template #defaultNoResultsTemplate>\n\n</ng-template>\n", styles: [".px-searchable-accordion-highlight{padding:0;margin:0;background:var(--p-highlight-background);color:var(--p-highlight-color);font-weight:700}\n"], dependencies: [{ kind: "component", type: Accordion, selector: "p-accordion", inputs: ["value", "multiple", "styleClass", "expandIcon", "collapseIcon", "selectOnFocus", "transitionOptions"], outputs: ["valueChange", "onClose", "onOpen"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: AccordionPanel, selector: "p-accordion-panel, p-accordionpanel", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: AccordionHeader, selector: "p-accordion-header, p-accordionheader" }, { kind: "component", type: AccordionContent, selector: "p-accordion-content, p-accordioncontent" }, { kind: "pipe", type: AsyncPipe, name: "async" }], encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PxSearchableAccordionComponent, decorators: [{
type: Component,
args: [{ selector: 'px-searchable-accordion', imports: [
Accordion,
AsyncPipe,
NgTemplateOutlet,
AccordionPanel,
AccordionHeader,
AccordionContent
], encapsulation: ViewEncapsulation.None, template: "<p-accordion [(value)]=\"activeEntryIndex\" (valueChange)=\"activeIndexChanged()\">\n @if(!(filteredEntries | async)?.length)\n {\n @if(noResultsTemplate)\n {\n <ng-container *ngTemplateOutlet=\"noResultsTemplate ? noResultsTemplate : defaultNoResultsTemplate\"></ng-container>\n }\n }\n\n @for(entry of filteredEntries | async; track entry.id || $index)\n {\n <p-accordion-panel [value]=\"$index\" [attr.id]=\"entry.id\">\n <p-accordion-header>\n <ng-container *ngTemplateOutlet=\"headerTemplate ? headerTemplate : defaultHeader; context: { $implicit: entry }\"></ng-container>\n </p-accordion-header>\n\n <p-accordion-content>\n <ng-container *ngTemplateOutlet=\"contentTemplate ? contentTemplate : defaultContent; context: { $implicit: entry }\"></ng-container>\n </p-accordion-content>\n </p-accordion-panel>\n }\n</p-accordion>\n\n<ng-template #defaultHeader let-entry>\n <span [innerHTML]=\"entry.header\"></span>\n</ng-template>\n<ng-template #defaultContent let-entry>\n <p [innerHTML]=\"entry.content\"></p>\n</ng-template>\n<ng-template #defaultNoResultsTemplate>\n\n</ng-template>\n", styles: [".px-searchable-accordion-highlight{padding:0;margin:0;background:var(--p-highlight-background);color:var(--p-highlight-color);font-weight:700}\n"] }]
}], ctorParameters: () => [{ type: i1.Router }, { type: i1.ActivatedRoute }, { type: i0.ChangeDetectorRef }, { type: Document, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }], propDecorators: { entries: [{
type: Input,
args: [{ required: true }]
}], searchString: [{
type: Input,
args: [{ required: true }]
}], headerTemplate: [{
type: Input
}], contentTemplate: [{
type: Input
}], noResultsTemplate: [{
type: Input
}], searchDebounceTime: [{
type: Input
}], highlightWords: [{
type: Input
}], highlightClass: [{
type: Input
}], allowUrlFragments: [{
type: Input
}] } });
/*
* Public API Surface of px-searchable-accordion
*/
/**
* Generated bundle index. Do not edit.
*/
export { PxSearchableAccordionComponent };
//# sourceMappingURL=ng-prime-extensions-px-searchable-accordion.mjs.map