• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.44
/projects/igniteui-angular/src/lib/directives/text-highlight/text-highlight.directive.ts
1
import {
2
    AfterViewInit,
3
    Directive,
4
    ElementRef,
5
    Input,
6
    OnChanges,
7
    OnDestroy,
8
    Renderer2,
9
    SimpleChanges,
10
    AfterViewChecked,
11
} from '@angular/core';
12
import { takeUntil } from 'rxjs/operators';
13
import { Subject } from 'rxjs';
14
import { compareMaps } from '../../core/utils';
15
import { IgxTextHighlightService } from './text-highlight.service';
16

17
export interface IBaseSearchInfo {
18
    searchText: string;
19
    caseSensitive: boolean;
20
    exactMatch: boolean;
21
    matchCount: number;
22
    content: string;
23
}
24

25
/**
26
 * An interface describing information for the active highlight.
27
 */
28
export interface IActiveHighlightInfo {
29
    /**
30
     * The row of the highlight.
31
     */
32
    row?: any;
33
    /**
34
     * The column of the highlight.
35
     */
36
    column?: any;
37
    /**
38
     * The index of the highlight.
39
     */
40
    index: number;
41
    /**
42
     * Additional, custom checks to perform prior an element highlighting.
43
     */
44
    metadata?: Map<string, any>;
45
}
46

47
@Directive({
48
    selector: '[igxTextHighlight]',
49
    standalone: true
50
})
51
export class IgxTextHighlightDirective implements AfterViewInit, AfterViewChecked, OnDestroy, OnChanges {
2✔
52
    /**
53
     * Determines the `CSS` class of the highlight elements.
54
     * This allows the developer to provide custom `CSS` to customize the highlight.
55
     *
56
     * ```html
57
     * <div
58
     *   igxTextHighlight
59
     *   [cssClass]="myClass">
60
     * </div>
61
     * ```
62
     */
63
    @Input()
64
    public cssClass: string;
65

66
    /**
67
     * Determines the `CSS` class of the active highlight element.
68
     * This allows the developer to provide custom `CSS` to customize the highlight.
69
     *
70
     * ```html
71
     * <div
72
     *   igxTextHighlight
73
     *   [activeCssClass]="activeHighlightClass">
74
     * </div>
75
     * ```
76
     */
77
    @Input()
78
    public activeCssClass: string;
79

80
    /**
81
     * @hidden
82
     */
83
    @Input()
84
    public containerClass: string;
85

86
    /**
87
     * Identifies the highlight within a unique group.
88
     * This allows it to have several different highlight groups,
89
     * with each of them having their own active highlight.
90
     *
91
     * ```html
92
     * <div
93
     *   igxTextHighlight
94
     *   [groupName]="myGroupName">
95
     * </div>
96
     * ```
97
     */
98
    @Input()
99
    public groupName = '';
1,505✔
100

101
    /**
102
     * The underlying value of the element that will be highlighted.
103
     *
104
     * ```typescript
105
     * // get
106
     * const elementValue = this.textHighlight.value;
107
     * ```
108
     *
109
     * ```html
110
     * <!--set-->
111
     * <div
112
     *   igxTextHighlight
113
     *   [value]="newValue">
114
     * </div>
115
     * ```
116
     */
117
    @Input('value')
118
    public get value(): any {
119
        return this._value;
1,884✔
120
    }
121
    public set value(value: any) {
122
        if (value === undefined || value === null) {
1,695✔
123
            this._value = '';
296✔
124
        } else {
125
            this._value = value;
1,399✔
126
        }
127
    }
128

129
    /**
130
     * The identifier of the row on which the directive is currently on.
131
     *
132
     * ```html
133
     * <div
134
     *   igxTextHighlight
135
     *   [row]="0">
136
     * </div>
137
     * ```
138
     */
139
    @Input()
140
    public row: any;
141

142
    /**
143
     * The identifier of the column on which the directive is currently on.
144
     *
145
     * ```html
146
     * <div
147
     *   igxTextHighlight
148
     *   [column]="0">
149
     * </div>
150
     * ```
151
     */
152
    @Input()
153
    public column: any;
154

155
    /**
156
     * A map that contains all additional conditions, that you need to activate a highlighted
157
     * element. To activate the condition, you will have to add a new metadata key to
158
     * the `metadata` property of the IActiveHighlightInfo interface.
159
     *
160
     * @example
161
     * ```typescript
162
     *  // Set a property, which would disable the highlight for a given element on a certain condition
163
     *  const metadata = new Map<string, any>();
164
     *  metadata.set('highlightElement', false);
165
     * ```
166
     * ```html
167
     * <div
168
     *   igxTextHighlight
169
     *   [metadata]="metadata">
170
     * </div>
171
     * ```
172
     */
173
    @Input()
174
    public metadata: Map<string, any>;
175

176
    /**
177
     * @hidden
178
     */
179
    public get lastSearchInfo(): IBaseSearchInfo {
180
        return this._lastSearchInfo;
513✔
181
    }
182

183
    /**
184
     * @hidden
185
     */
186
    public parentElement: any;
187

188
    private _container: any;
189

190
    private destroy$ = new Subject<boolean>();
1,505✔
191
    private _value = '';
1,505✔
192
    private _lastSearchInfo: IBaseSearchInfo;
193
    private _div = null;
1,505✔
194
    private _observer: MutationObserver = null;
1,505✔
195
    private _nodeWasRemoved = false;
1,505✔
196
    private _forceEvaluation = false;
1,505✔
197
    private _activeElementIndex = -1;
1,505✔
198
    private _valueChanged: boolean;
199
    private _defaultCssClass = 'igx-highlight';
1,505✔
200
    private _defaultActiveCssClass = 'igx-highlight--active';
1,505✔
201

202
    constructor(private element: ElementRef, private service: IgxTextHighlightService, private renderer: Renderer2) {
1,505✔
203
        this.service.onActiveElementChanged.pipe(takeUntil(this.destroy$)).subscribe((groupName) => {
1,505✔
UNCOV
204
            if (this.groupName === groupName) {
×
UNCOV
205
                if (this._activeElementIndex !== -1) {
×
UNCOV
206
                    this.deactivate();
×
207
                }
UNCOV
208
                this.activateIfNecessary();
×
209
            }
210
        });
211
    }
212

213
    /**
214
     * @hidden
215
     */
216
    public ngOnDestroy() {
217
        this.clearHighlight();
1,505✔
218

219
        if (this._observer !== null) {
1,505!
220
            this._observer.disconnect();
×
221
        }
222
        this.destroy$.next(true);
1,505✔
223
        this.destroy$.complete();
1,505✔
224
    }
225

226
    /**
227
     * @hidden
228
     */
229
    public ngOnChanges(changes: SimpleChanges) {
230
        if (changes.value && !changes.value.firstChange) {
1,787✔
231
            this._valueChanged = true;
190✔
232
        } else if ((changes.row !== undefined && !changes.row.firstChange) ||
1,597!
233
            (changes.column !== undefined && !changes.column.firstChange) ||
234
            (changes.page !== undefined && !changes.page.firstChange)) {
235
            if (this._activeElementIndex !== -1) {
13!
236
                this.deactivate();
×
237
            }
238
            this.activateIfNecessary();
13✔
239
        }
240
    }
241

242
    /**
243
     * @hidden
244
     */
245
    public ngAfterViewInit() {
246
        this.parentElement = this.renderer.parentNode(this.element.nativeElement);
1,505✔
247

248
        if (this.service.highlightGroupsMap.has(this.groupName) === false) {
1,505✔
249
            this.service.highlightGroupsMap.set(this.groupName, {
37✔
250
                index: -1
251
            });
252
        }
253

254
        this._lastSearchInfo = {
1,505✔
255
            searchText: '',
256
            content: this.value,
257
            matchCount: 0,
258
            caseSensitive: false,
259
            exactMatch: false
260
        };
261

262
        this._container = this.parentElement.firstElementChild;
1,505✔
263
    }
264

265
    /**
266
     * @hidden
267
     */
268
    public ngAfterViewChecked() {
269
        if (this._valueChanged) {
1,787✔
270
            this.highlight(this._lastSearchInfo.searchText, this._lastSearchInfo.caseSensitive, this._lastSearchInfo.exactMatch);
190✔
271
            this.activateIfNecessary();
190✔
272
            this._valueChanged = false;
190✔
273
        }
274
    }
275

276
    /**
277
     * Clears the existing highlight and highlights the searched text.
278
     * Returns how many times the element contains the searched text.
279
     */
280
    public highlight(text: string, caseSensitive?: boolean, exactMatch?: boolean): number {
281
        const caseSensitiveResolved = caseSensitive ? true : false;
190!
282
        const exactMatchResolved = exactMatch ? true : false;
190!
283

284
        if (this.searchNeedsEvaluation(text, caseSensitiveResolved, exactMatchResolved)) {
190✔
285
            this._lastSearchInfo.searchText = text;
189✔
286
            this._lastSearchInfo.caseSensitive = caseSensitiveResolved;
189✔
287
            this._lastSearchInfo.exactMatch = exactMatchResolved;
189✔
288
            this._lastSearchInfo.content = this.value;
189✔
289

290
            if (text === '' || text === undefined || text === null) {
189!
291
                this.clearHighlight();
189✔
292
            } else {
UNCOV
293
                this.clearChildElements(true);
×
UNCOV
294
                this._lastSearchInfo.matchCount = this.getHighlightedText(text, caseSensitive, exactMatch);
×
295
            }
296
        } else if (this._nodeWasRemoved) {
1!
297
            this._lastSearchInfo.searchText = text;
×
298
            this._lastSearchInfo.caseSensitive = caseSensitiveResolved;
×
299
            this._lastSearchInfo.exactMatch = exactMatchResolved;
×
300
        }
301

302
        return this._lastSearchInfo.matchCount;
190✔
303
    }
304

305
    /**
306
     * Clears any existing highlight.
307
     */
308
    public clearHighlight(): void {
309
        this.clearChildElements(false);
1,694✔
310

311
        this._lastSearchInfo.searchText = '';
1,694✔
312
        this._lastSearchInfo.matchCount = 0;
1,694✔
313
    }
314

315
    /**
316
     * Activates the highlight if it is on the currently active row and column.
317
     */
318
    public activateIfNecessary(): void {
319
        const group = this.service.highlightGroupsMap.get(this.groupName);
203✔
320

321
        if (group.index >= 0 && group.column === this.column && group.row === this.row && compareMaps(this.metadata, group.metadata)) {
203!
UNCOV
322
            this.activate(group.index);
×
323
        }
324
    }
325

326
    /**
327
     * Attaches a MutationObserver to the parentElement and watches for when the container element is removed/readded to the DOM.
328
     * Should be used only when necessary as using many observers may lead to performance degradation.
329
     */
330
    public observe(): void {
331
        if (this._observer === null) {
×
332
            const callback = (mutationList) => {
×
333
                mutationList.forEach((mutation) => {
×
334
                    const removedNodes = Array.from(mutation.removedNodes);
×
335
                    removedNodes.forEach((n) => {
×
336
                        if (n === this._container) {
×
337
                            this._nodeWasRemoved = true;
×
338
                            this.clearChildElements(false);
×
339
                        }
340
                    });
341

342
                    const addedNodes = Array.from(mutation.addedNodes);
×
343
                    addedNodes.forEach((n) => {
×
344
                        if (n === this.parentElement.firstElementChild && this._nodeWasRemoved) {
×
345
                            this._container = this.parentElement.firstElementChild;
×
346
                            this._nodeWasRemoved = false;
×
347

348
                            this._forceEvaluation = true;
×
349
                            this.highlight(this._lastSearchInfo.searchText,
×
350
                                this._lastSearchInfo.caseSensitive,
351
                                this._lastSearchInfo.exactMatch);
352
                            this._forceEvaluation = false;
×
353

354
                            this.activateIfNecessary();
×
355
                            this._observer.disconnect();
×
356
                            this._observer = null;
×
357
                        }
358
                    });
359
                });
360
            };
361

362
            this._observer = new MutationObserver(callback);
×
363
            this._observer.observe(this.parentElement, {childList: true});
×
364
        }
365
    }
366

367
    private activate(index: number) {
UNCOV
368
        this.deactivate();
×
369

UNCOV
370
        if (this._div !== null) {
×
UNCOV
371
            const spans = this._div.querySelectorAll('span');
×
UNCOV
372
            this._activeElementIndex = index;
×
373

UNCOV
374
            if (spans.length <= index) {
×
375
                return;
×
376
            }
377

UNCOV
378
            const elementToActivate = spans[index];
×
UNCOV
379
            this.renderer.addClass(elementToActivate, this._defaultActiveCssClass);
×
UNCOV
380
            this.renderer.addClass(elementToActivate, this.activeCssClass);
×
381
        }
382
    }
383

384
    private deactivate() {
UNCOV
385
        if (this._activeElementIndex === -1) {
×
UNCOV
386
            return;
×
387
        }
388

UNCOV
389
        const spans = this._div.querySelectorAll('span');
×
390

UNCOV
391
        if (spans.length <= this._activeElementIndex) {
×
392
            this._activeElementIndex = -1;
×
393
            return;
×
394
        }
395

UNCOV
396
        const elementToDeactivate = spans[this._activeElementIndex];
×
UNCOV
397
        this.renderer.removeClass(elementToDeactivate, this._defaultActiveCssClass);
×
UNCOV
398
        this.renderer.removeClass(elementToDeactivate, this.activeCssClass);
×
UNCOV
399
        this._activeElementIndex = -1;
×
400
    }
401

402
    private clearChildElements(originalContentHidden: boolean): void {
403
        this.renderer.setProperty(this.element.nativeElement, 'hidden', originalContentHidden);
1,694✔
404

405
        if (this._div !== null) {
1,694!
UNCOV
406
            this.renderer.removeChild(this.parentElement, this._div);
×
407

UNCOV
408
            this._div = null;
×
UNCOV
409
            this._activeElementIndex = -1;
×
410
        }
411
    }
412

413
    private getHighlightedText(searchText: string, caseSensitive: boolean, exactMatch: boolean) {
UNCOV
414
        this.appendDiv();
×
415

UNCOV
416
        const stringValue = String(this.value);
×
UNCOV
417
        const contentStringResolved = !caseSensitive ? stringValue.toLowerCase() : stringValue;
×
UNCOV
418
        const searchTextResolved = !caseSensitive ? searchText.toLowerCase() : searchText;
×
419

UNCOV
420
        let matchCount = 0;
×
421

UNCOV
422
        if (exactMatch) {
×
UNCOV
423
            if (contentStringResolved === searchTextResolved) {
×
UNCOV
424
                this.appendSpan(`<span class="${this._defaultCssClass} ${this.cssClass ? this.cssClass : ''}">${stringValue}</span>`);
×
UNCOV
425
                matchCount++;
×
426
            } else {
UNCOV
427
                this.appendText(stringValue);
×
428
            }
429
        } else {
UNCOV
430
            let foundIndex = contentStringResolved.indexOf(searchTextResolved, 0);
×
UNCOV
431
            let previousMatchEnd = 0;
×
432

UNCOV
433
            while (foundIndex !== -1) {
×
UNCOV
434
                const start = foundIndex;
×
UNCOV
435
                const end = foundIndex + searchTextResolved.length;
×
436

UNCOV
437
                this.appendText(stringValue.substring(previousMatchEnd, start));
×
UNCOV
438
                this.appendSpan(`<span class="${this._defaultCssClass} ${this.cssClass ? this.cssClass : ''}">${stringValue.substring(start, end)}</span>`);
×
439

UNCOV
440
                previousMatchEnd = end;
×
UNCOV
441
                matchCount++;
×
442

UNCOV
443
                foundIndex = contentStringResolved.indexOf(searchTextResolved, end);
×
444
            }
445

UNCOV
446
            this.appendText(stringValue.substring(previousMatchEnd, stringValue.length));
×
447
        }
448

UNCOV
449
        return matchCount;
×
450
    }
451

452
    private appendText(text: string) {
UNCOV
453
        const textElement = this.renderer.createText(text);
×
UNCOV
454
        this.renderer.appendChild(this._div, textElement);
×
455
    }
456

457
    private appendSpan(outerHTML: string) {
UNCOV
458
        const span = this.renderer.createElement('span');
×
UNCOV
459
        this.renderer.appendChild(this._div, span);
×
UNCOV
460
        this.renderer.setProperty(span, 'outerHTML', outerHTML);
×
461
    }
462

463
    private appendDiv() {
UNCOV
464
        this._div = this.renderer.createElement('div');
×
UNCOV
465
        if ( this.containerClass) {
×
UNCOV
466
            this.renderer.addClass(this._div, this.containerClass);
×
467
        }
UNCOV
468
        this.renderer.appendChild(this.parentElement, this._div);
×
469
    }
470

471
    private searchNeedsEvaluation(text: string, caseSensitive: boolean, exactMatch: boolean): boolean {
472
        const searchedText = this._lastSearchInfo.searchText;
190✔
473

474
        return !this._nodeWasRemoved &&
190✔
475
            (searchedText === null ||
476
                searchedText !== text ||
477
                this._lastSearchInfo.content !== this.value ||
478
                this._lastSearchInfo.caseSensitive !== caseSensitive ||
479
                this._lastSearchInfo.exactMatch !== exactMatch ||
480
                this._forceEvaluation);
481
    }
482
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc