Skip to content

Commit 282b63b

Browse files
authored
Fix search highlight functionality (#29)
fixes #18
1 parent 69bc72b commit 282b63b

File tree

7 files changed

+160
-54
lines changed

7 files changed

+160
-54
lines changed

src/ng-generate/table/generators/components/table/files/__name@dasherize__.component.html.template

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107

108108
<% if(options.hasSearchBar) { %>
109109
<ng-container
110-
[ngTemplateOutlet]="highlightConfig?.selected && ((<%= cellContent %>) | searchString: highlightString) ? searchedWordExists : normal"
110+
[ngTemplateOutlet]="highlightCell"
111111
[ngTemplateOutletContext]="{ value: <%= cellContent %> }"></ng-container>
112112
<% } else { %>
113113
{{<%= cellContent %>}}
@@ -185,15 +185,9 @@
185185

186186
<% if (options.hasSearchBar) { %>
187187
<!-- Highlighting search values -->
188-
<ng-template #normal let-value="value">{{ value === null ? '-' : value }}</ng-template>
189-
<ng-template #searchedWordExists let-value="value">
190-
<ng-container *ngFor="let letter of value.toString().split(''); let i = index">
191-
<ng-container [ngTemplateOutlet]="shouldHighlight(value, letter) ? highlight : notHighlighted"
192-
[ngTemplateOutletContext]="{ $implicit: letter }"></ng-container>
193-
</ng-container>
188+
<ng-template #highlightCell let-value="value">
189+
<span [highlight]="highlightString" [highlightSource]="value" [highlightColor]="highlightConfig?.color">
190+
{{ value === null ? '-' : value }}
191+
</span>
194192
</ng-template>
195-
<ng-template #highlight let-letter>
196-
<mark [style.background-color]="highlightConfig?.color">{{ letter }}</mark>
197-
</ng-template>
198-
<ng-template #notHighlighted let-letter>{{ letter }}</ng-template>
199193
<% } %>

src/ng-generate/table/generators/components/table/files/__name@dasherize__.component.ts.template

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -600,13 +600,6 @@ export class <%= classify(name) %>Component implements OnInit, AfterViewInit, Af
600600
setConfiguration(configs: Array<Config>): void {
601601
this.configs = [...configs];
602602
}
603-
604-
shouldHighlight(name: string, letter: string): boolean {
605-
const highlightLetters = [...new Set(this.highlightString.join().split(''))].join();
606-
const index = name.toString().indexOf(letter);
607-
608-
return index !== -1 && highlightLetters.includes(name.toString()[index]);
609-
}
610603
<% } %>
611604

612605
setDisplayedColumns(columns: Array<Column>): void {
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/** <%= options.enerationDisclaimerText %> **/
2+
import { Directive, ElementRef, Input, OnChanges, OnInit, SecurityContext, SimpleChange, SimpleChanges } from '@angular/core';
3+
import { DomSanitizer } from '@angular/platform-browser';
4+
5+
interface HighlightSimpleChanges extends SimpleChanges {
6+
highlight: SimpleChange;
7+
caseSensitive: SimpleChange;
8+
}
9+
10+
interface HighlightRange {
11+
from: number;
12+
to: number;
13+
}
14+
15+
const threeDigitsRegExp = new RegExp("^#?\\d{3}$");
16+
const sixDigitsRegExp = new RegExp("^#?\\d{6}$");
17+
18+
@Directive({
19+
selector: '[highlight]'
20+
})
21+
export class <%= classify(name) %>Directive implements OnChanges, OnInit {
22+
@Input() highlightSource: string | null = null;
23+
@Input() set highlight(value: string | string[]) {
24+
this._highlight = Array.isArray(value) ? value : [value];
25+
}
26+
27+
@Input() set highlightColor(value: string | undefined) {
28+
if (value) {
29+
if (value.match(threeDigitsRegExp) || value.match(sixDigitsRegExp)) {
30+
this._color = value.padStart(value.length + 1, '#');
31+
} else {
32+
this._color = value;
33+
}
34+
}
35+
}
36+
37+
@Input() set caseSensitive(value: boolean) {
38+
this.regExpFlags = value ? 'g' : 'gi';
39+
}
40+
41+
private regExpFlags = 'gi';
42+
private _highlight: string[] = [];
43+
private _color: string | undefined = undefined;
44+
45+
constructor(private el: ElementRef, private sanitizer: DomSanitizer) {}
46+
47+
ngOnChanges(changes: HighlightSimpleChanges) {
48+
if ((changes.highlight && !changes.highlight.firstChange) || (changes.caseSensitive && !changes.caseSensitive.firstChange)) {
49+
this.transformText();
50+
}
51+
}
52+
53+
ngOnInit(): void {
54+
this.transformText();
55+
}
56+
57+
private transformText(): void {
58+
if (this.canHighlightText()) {
59+
const allRanges = this.calcRangesToReplace();
60+
const rangesToHighlight = this.mergeRangeIntersections(allRanges);
61+
const content = this.sanitizer.sanitize(SecurityContext.STYLE, this.replaceHighlights(rangesToHighlight));
62+
63+
if (content?.length) {
64+
(this.el.nativeElement as HTMLElement).innerHTML = content;
65+
}
66+
}
67+
}
68+
69+
private canHighlightText(): boolean {
70+
return this.el?.nativeElement && this._highlight && typeof this.highlightSource === 'string' && this.highlightSource.length > 0 && !!this._color;
71+
}
72+
73+
private calcRangesToReplace(): HighlightRange[] {
74+
return this._highlight
75+
.map(highlightString => {
76+
const len = highlightString.length;
77+
const matches = this.highlightSource?.matchAll(new RegExp(highlightString.toLowerCase(), this.regExpFlags));
78+
if (!matches) {
79+
return [];
80+
}
81+
82+
const matchIndexes = [...matches]
83+
.filter((a: RegExpMatchArray) => a && a.index !== undefined)
84+
.map<HighlightRange>(a => ({from: a.index!, to: a.index! + len}));
85+
86+
return matchIndexes;
87+
})
88+
.filter(match => match.length > 0)
89+
.flat()
90+
.sort((a, b) => a.from - b.from);
91+
}
92+
93+
private mergeRangeIntersections(allRanges: HighlightRange[]): HighlightRange[] {
94+
if (allRanges.length === 0) {
95+
return [];
96+
}
97+
98+
const rangesToHighlight = [allRanges[0]];
99+
100+
allRanges.forEach(range => {
101+
const currentRange = rangesToHighlight[rangesToHighlight.length - 1];
102+
103+
if (range.from <= currentRange.from) {
104+
currentRange.from = range.from;
105+
106+
if (range.to > currentRange.to) {
107+
currentRange.to = range.to;
108+
}
109+
} else if (range.from <= currentRange.to && range.to >= currentRange.to) {
110+
currentRange.to = range.to;
111+
} else {
112+
rangesToHighlight.push(range);
113+
}
114+
});
115+
116+
return rangesToHighlight;
117+
}
118+
119+
private replaceHighlights(rangesToHighlight: HighlightRange[]): string {
120+
if (!this.highlightSource?.length || rangesToHighlight.length === 0) {
121+
return '';
122+
}
123+
124+
const resultStringParts: string[] = [];
125+
let index = 0;
126+
127+
rangesToHighlight.forEach(({from, to}) => {
128+
if (from > index) {
129+
resultStringParts.push(this.highlightSource!.substring(index, from));
130+
}
131+
132+
const strPart = this.highlightSource!.substring(from, to);
133+
resultStringParts.push('<mark style="background-color: ' + this._color + ';">' + strPart + '</mark>');
134+
index = to;
135+
});
136+
137+
if (index < this.highlightSource!.length) {
138+
resultStringParts.push(this.highlightSource!.substring(index));
139+
}
140+
141+
return resultStringParts.join('');
142+
}
143+
}

src/ng-generate/table/generators/pipes/search-string/index.ts renamed to src/ng-generate/table/generators/directives/highlight/index.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* SPDX-License-Identifier: MPL-2.0
1212
*/
1313

14+
import { strings } from "@angular-devkit/core";
1415
import {
1516
apply,
1617
applyTemplates,
@@ -19,27 +20,24 @@ import {
1920
move,
2021
noop,
2122
Rule,
22-
SchematicContext,
23-
Tree,
2423
url
2524
} from '@angular-devkit/schematics';
26-
import {strings} from "@angular-devkit/core";
2725

28-
export function generateSearchStringPipe(options: any): Rule {
29-
return (tree: Tree, _context: SchematicContext) => {
26+
export function generateHighlightDirective(options: any): Rule {
27+
return () => {
3028
if (!options.hasSearchBar) {
3129
return noop;
3230
}
33-
31+
3432
return mergeWith(
35-
apply(url('./generators/pipes/search-string/files'), [
33+
apply(url('./generators/directives/highlight/files'), [
3634
applyTemplates({
3735
classify: strings.classify,
3836
dasherize: strings.dasherize,
3937
options: options,
40-
name: 'search-string',
38+
name: 'highlight',
4139
}),
42-
move('src/app/shared/pipes'),
40+
move('src/app/shared/directives'),
4341
]),
4442
options.overwrite? MergeStrategy.Overwrite : MergeStrategy.Error
4543
);

src/ng-generate/table/generators/pipes/search-string/files/__name@dasherize__.pipe.ts.template

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/ng-generate/table/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ import {generateCustomService} from "./generators/services/custom/index";
4646
import {generateResizeDirective} from "./generators/directives/resize/index";
4747
import {generateValidateInputDirective} from "./generators/directives/validate-input/index";
4848
import {generateHorizontalOverflowDirective} from "./generators/directives/horizontal-overflow/index";
49+
import {generateHighlightDirective} from "./generators/directives/highlight/index";
4950
import {generateShowDescriptionPipe} from "./generators/pipes/show-description/index";
50-
import {generateSearchStringPipe} from "./generators/pipes/search-string/index";
5151
import {generateStorageService} from "./generators/services/storage/index";
5252
import {genrateTableStyle} from "./generators/styles/table/index";
5353
import {APP_SHARED_MODULES, COMPONENT_MODULES, updateSharedModule} from "../../utils/modules";
@@ -141,8 +141,8 @@ export function generate(options: Schema): Rule {
141141
generateResizeDirective(options),
142142
generateValidateInputDirective(options),
143143
generateHorizontalOverflowDirective(options),
144+
generateHighlightDirective(options),
144145
generateShowDescriptionPipe(options),
145-
generateSearchStringPipe(options),
146146
updateSharedModule(options),
147147
formatAllFiles(options)
148148
]);

src/utils/modules.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,11 @@ export function updateSharedModule(options: Schema) {
144144
processItem('component', 'export-confirmation-dialog');
145145

146146
['horizontal-overflow', 'resize-column', 'validate-input'].forEach(directive => processItem('directive', directive));
147-
148-
const pipes = ['show-description'];
149147
if (options.templateHelper.hasSearchBar(options)) {
150-
pipes.push('search-string');
148+
processItem('directive', 'highlight');
151149
}
152-
pipes.forEach(pipe => processItem('pipe', pipe));
150+
151+
processItem('pipe', 'show-description');
153152

154153
return tree;
155154
}

0 commit comments

Comments
 (0)