From e289cd8d77dff2cb16ba9aa025a4a89d509582bc Mon Sep 17 00:00:00 2001 From: Svyatoslav Zaytsev Date: Fri, 3 Nov 2023 10:57:00 +0600 Subject: [PATCH] feat(kit): highlight improvements 1. Adding support for multiple occurrences 2. Adding support for multiple highlights 3. Adding support for case-sensitive mode 4. Adding support for regexp --- projects/cdk/types/array-or-value.ts | 1 + projects/cdk/types/index.ts | 1 + projects/cdk/utils/miscellaneous/index.ts | 1 + projects/cdk/utils/miscellaneous/to-array.ts | 5 + .../highlight/examples/1/index.html | 11 +- .../highlight/examples/1/index.less | 3 + .../highlight/examples/2/index.html | 26 ++ .../highlight/examples/2/index.less | 20 + .../directives/highlight/examples/2/index.ts | 34 ++ .../highlight/examples/3/index.html | 20 + .../highlight/examples/3/index.less | 20 + .../directives/highlight/examples/3/index.ts | 25 ++ .../highlight/examples/4/index.html | 20 + .../highlight/examples/4/index.less | 20 + .../directives/highlight/examples/4/index.ts | 26 ++ .../highlight/examples/import/template.md | 2 +- .../modules/directives/highlight/index.html | 25 +- projects/demo/src/utils/setup.component.ts | 3 + .../highlight/highlight-occurrence.ts | 4 + .../highlight/highlight.component.ts | 10 + .../highlight/highlight.directive.ts | 174 ++++++--- .../directives/highlight/highlight.style.less | 5 + .../kit/directives/highlight/highlight.ts | 46 +++ .../highlight.directive.spec.ts.snap | 362 ++++++++++++++++++ .../test/__snapshots__/highlight.spec.ts.snap | 19 + .../test/highlight.directive.spec.ts | 114 +++--- .../highlight/test/highlight.spec.ts | 48 +++ projects/kit/pipes/index.ts | 1 + projects/kit/pipes/to-regexp/index.ts | 1 + projects/kit/pipes/to-regexp/ng-package.json | 5 + .../kit/pipes/to-regexp/to-regexp.pipe.ts | 41 ++ 31 files changed, 971 insertions(+), 122 deletions(-) create mode 100644 projects/cdk/types/array-or-value.ts create mode 100644 projects/cdk/utils/miscellaneous/to-array.ts create mode 100644 projects/demo/src/modules/directives/highlight/examples/2/index.html create mode 100644 projects/demo/src/modules/directives/highlight/examples/2/index.less create mode 100644 projects/demo/src/modules/directives/highlight/examples/2/index.ts create mode 100644 projects/demo/src/modules/directives/highlight/examples/3/index.html create mode 100644 projects/demo/src/modules/directives/highlight/examples/3/index.less create mode 100644 projects/demo/src/modules/directives/highlight/examples/3/index.ts create mode 100644 projects/demo/src/modules/directives/highlight/examples/4/index.html create mode 100644 projects/demo/src/modules/directives/highlight/examples/4/index.less create mode 100644 projects/demo/src/modules/directives/highlight/examples/4/index.ts create mode 100644 projects/kit/directives/highlight/highlight-occurrence.ts create mode 100644 projects/kit/directives/highlight/highlight.component.ts create mode 100644 projects/kit/directives/highlight/highlight.style.less create mode 100644 projects/kit/directives/highlight/highlight.ts create mode 100644 projects/kit/directives/highlight/test/__snapshots__/highlight.directive.spec.ts.snap create mode 100644 projects/kit/directives/highlight/test/__snapshots__/highlight.spec.ts.snap create mode 100644 projects/kit/directives/highlight/test/highlight.spec.ts create mode 100644 projects/kit/pipes/to-regexp/index.ts create mode 100644 projects/kit/pipes/to-regexp/ng-package.json create mode 100644 projects/kit/pipes/to-regexp/to-regexp.pipe.ts diff --git a/projects/cdk/types/array-or-value.ts b/projects/cdk/types/array-or-value.ts new file mode 100644 index 000000000000..f2d6aa9f3393 --- /dev/null +++ b/projects/cdk/types/array-or-value.ts @@ -0,0 +1 @@ +export type TuiArrayOrValue = T | readonly T[]; diff --git a/projects/cdk/types/index.ts b/projects/cdk/types/index.ts index 178814544708..f05bf4f3586f 100644 --- a/projects/cdk/types/index.ts +++ b/projects/cdk/types/index.ts @@ -1,3 +1,4 @@ +export * from './array-or-value'; export * from './context'; export * from './deep-partial'; export * from './event-with'; diff --git a/projects/cdk/utils/miscellaneous/index.ts b/projects/cdk/utils/miscellaneous/index.ts index 46aaae25ce83..5ba748430f84 100644 --- a/projects/cdk/utils/miscellaneous/index.ts +++ b/projects/cdk/utils/miscellaneous/index.ts @@ -24,5 +24,6 @@ export * from './provide'; export * from './provide-options'; export * from './pure'; export * from './px'; +export * from './to-array'; export * from './uniq-by'; export * from './with-styles'; diff --git a/projects/cdk/utils/miscellaneous/to-array.ts b/projects/cdk/utils/miscellaneous/to-array.ts new file mode 100644 index 000000000000..0b1024abc890 --- /dev/null +++ b/projects/cdk/utils/miscellaneous/to-array.ts @@ -0,0 +1,5 @@ +import type {TuiArrayOrValue} from '@taiga-ui/cdk/types'; + +export function tuiToArray(value: TuiArrayOrValue): readonly T[] { + return value instanceof Array ? value : [value]; +} diff --git a/projects/demo/src/modules/directives/highlight/examples/1/index.html b/projects/demo/src/modules/directives/highlight/examples/1/index.html index b87ce09c9912..f567f43d4c84 100644 --- a/projects/demo/src/modules/directives/highlight/examples/1/index.html +++ b/projects/demo/src/modules/directives/highlight/examples/1/index.html @@ -4,7 +4,10 @@ > Search - +
@@ -14,11 +17,7 @@ - diff --git a/projects/demo/src/modules/directives/highlight/examples/1/index.less b/projects/demo/src/modules/directives/highlight/examples/1/index.less index d8b678d81bab..b110def0707f 100644 --- a/projects/demo/src/modules/directives/highlight/examples/1/index.less +++ b/projects/demo/src/modules/directives/highlight/examples/1/index.less @@ -1,5 +1,8 @@ :host { display: block; + --tui-highlight-background: var(--tui-primary); + --tui-highlight-radius: 5px; + --tui-highlight-color: var(--tui-base-01); } table { diff --git a/projects/demo/src/modules/directives/highlight/examples/2/index.html b/projects/demo/src/modules/directives/highlight/examples/2/index.html new file mode 100644 index 000000000000..5e7d97ba1d6b --- /dev/null +++ b/projects/demo/src/modules/directives/highlight/examples/2/index.html @@ -0,0 +1,26 @@ + + Search + +
Member
+ {{ cell }}
+ + + + + + + + + + + + +
MemberNicknameFate
+ {{ cell }} +
diff --git a/projects/demo/src/modules/directives/highlight/examples/2/index.less b/projects/demo/src/modules/directives/highlight/examples/2/index.less new file mode 100644 index 000000000000..b110def0707f --- /dev/null +++ b/projects/demo/src/modules/directives/highlight/examples/2/index.less @@ -0,0 +1,20 @@ +:host { + display: block; + --tui-highlight-background: var(--tui-primary); + --tui-highlight-radius: 5px; + --tui-highlight-color: var(--tui-base-01); +} + +table { + width: 100%; + border-spacing: 0; +} + +th, +td { + text-align: left; + border: 1px solid var(--tui-base-03); + height: 3.375rem; + padding: 0 1rem; + vertical-align: middle; +} diff --git a/projects/demo/src/modules/directives/highlight/examples/2/index.ts b/projects/demo/src/modules/directives/highlight/examples/2/index.ts new file mode 100644 index 000000000000..9e12a2685d12 --- /dev/null +++ b/projects/demo/src/modules/directives/highlight/examples/2/index.ts @@ -0,0 +1,34 @@ +import {NgForOf} from '@angular/common'; +import {Component} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {changeDetection} from '@demo/emulate/change-detection'; +import {encapsulation} from '@demo/emulate/encapsulation'; +import {TuiTextfieldControllerModule} from '@taiga-ui/core'; +import {TuiHighlightDirective} from '@taiga-ui/kit'; +import {TuiInputModule} from '@taiga-ui/legacy'; + +@Component({ + standalone: true, + imports: [ + TuiInputModule, + TuiTextfieldControllerModule, + FormsModule, + NgForOf, + TuiHighlightDirective, + ], + templateUrl: './index.html', + styleUrls: ['./index.less'], + encapsulation, + changeDetection, +}) +export default class ExampleComponent { + protected search = ''; + + protected readonly rows = [ + ['King Arthur', '-', 'Arrested'], + ['Sir Bedevere', 'The Wise', 'Arrested'], + ['Sir Lancelot', 'The Brave', 'Arrested'], + ['Sir Galahad', 'The Chaste', 'Killed'], + ['Sir Robin', 'The Not-Quite-So-Brave-As-Sir-Lancelot', 'Killed'], + ]; +} diff --git a/projects/demo/src/modules/directives/highlight/examples/3/index.html b/projects/demo/src/modules/directives/highlight/examples/3/index.html new file mode 100644 index 000000000000..5b4c69e9b426 --- /dev/null +++ b/projects/demo/src/modules/directives/highlight/examples/3/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + +
MemberNicknameFate
+ {{ cell }} +
diff --git a/projects/demo/src/modules/directives/highlight/examples/3/index.less b/projects/demo/src/modules/directives/highlight/examples/3/index.less new file mode 100644 index 000000000000..b110def0707f --- /dev/null +++ b/projects/demo/src/modules/directives/highlight/examples/3/index.less @@ -0,0 +1,20 @@ +:host { + display: block; + --tui-highlight-background: var(--tui-primary); + --tui-highlight-radius: 5px; + --tui-highlight-color: var(--tui-base-01); +} + +table { + width: 100%; + border-spacing: 0; +} + +th, +td { + text-align: left; + border: 1px solid var(--tui-base-03); + height: 3.375rem; + padding: 0 1rem; + vertical-align: middle; +} diff --git a/projects/demo/src/modules/directives/highlight/examples/3/index.ts b/projects/demo/src/modules/directives/highlight/examples/3/index.ts new file mode 100644 index 000000000000..a53178494d26 --- /dev/null +++ b/projects/demo/src/modules/directives/highlight/examples/3/index.ts @@ -0,0 +1,25 @@ +import {NgForOf} from '@angular/common'; +import {Component} from '@angular/core'; +import {changeDetection} from '@demo/emulate/change-detection'; +import {encapsulation} from '@demo/emulate/encapsulation'; +import {TuiHighlightDirective} from '@taiga-ui/kit'; + +@Component({ + standalone: true, + imports: [NgForOf, TuiHighlightDirective], + templateUrl: './index.html', + styleUrls: ['./index.less'], + encapsulation, + changeDetection, +}) +export default class ExampleComponent { + protected readonly rows = [ + ['King Arthur', '-', 'Arrested'], + ['Sir Bedevere', 'The Wise', 'Arrested'], + ['Sir Lancelot', 'The Brave', 'Arrested'], + ['Sir Galahad', 'The Chaste', 'Killed'], + ['Sir Robin', 'The Not-Quite-So-Brave-As-Sir-Lancelot', 'Killed'], + ]; + + protected readonly regexp = [/S[a-z]+/g, /A[a-z]+/g]; +} diff --git a/projects/demo/src/modules/directives/highlight/examples/4/index.html b/projects/demo/src/modules/directives/highlight/examples/4/index.html new file mode 100644 index 000000000000..8784cb049c71 --- /dev/null +++ b/projects/demo/src/modules/directives/highlight/examples/4/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + +
MemberNicknameFate
+ {{ cell }} +
diff --git a/projects/demo/src/modules/directives/highlight/examples/4/index.less b/projects/demo/src/modules/directives/highlight/examples/4/index.less new file mode 100644 index 000000000000..b110def0707f --- /dev/null +++ b/projects/demo/src/modules/directives/highlight/examples/4/index.less @@ -0,0 +1,20 @@ +:host { + display: block; + --tui-highlight-background: var(--tui-primary); + --tui-highlight-radius: 5px; + --tui-highlight-color: var(--tui-base-01); +} + +table { + width: 100%; + border-spacing: 0; +} + +th, +td { + text-align: left; + border: 1px solid var(--tui-base-03); + height: 3.375rem; + padding: 0 1rem; + vertical-align: middle; +} diff --git a/projects/demo/src/modules/directives/highlight/examples/4/index.ts b/projects/demo/src/modules/directives/highlight/examples/4/index.ts new file mode 100644 index 000000000000..c4abd6d0f541 --- /dev/null +++ b/projects/demo/src/modules/directives/highlight/examples/4/index.ts @@ -0,0 +1,26 @@ +import {NgForOf} from '@angular/common'; +import {Component} from '@angular/core'; +import {changeDetection} from '@demo/emulate/change-detection'; +import {encapsulation} from '@demo/emulate/encapsulation'; +import {TuiHighlightDirective, TuiToRegexpPipe} from '@taiga-ui/kit'; + +@Component({ + standalone: true, + imports: [NgForOf, TuiHighlightDirective, TuiToRegexpPipe], + templateUrl: './index.html', + styleUrls: ['./index.less'], + encapsulation, + changeDetection, +}) +export default class ExampleComponent { + protected readonly rows = [ + ['King Arthur', '-', 'Arrested'], + ['Sir Bedevere', 'The Wise', 'Arrested'], + ['Sir Lancelot', 'The Brave', 'Arrested'], + ['Sir Galahad', 'The Chaste', 'Killed'], + ['Sir Robin', 'The Not-Quite-So-Brave-As-Sir-Lancelot', 'Killed'], + ]; + + /* cspell:disable-next-line */ + protected readonly search = ['Sir', 'Arrested', 'killed']; +} diff --git a/projects/demo/src/modules/directives/highlight/examples/import/template.md b/projects/demo/src/modules/directives/highlight/examples/import/template.md index ec00b0cd8e7c..79dd9e9d6780 100644 --- a/projects/demo/src/modules/directives/highlight/examples/import/template.md +++ b/projects/demo/src/modules/directives/highlight/examples/import/template.md @@ -1,7 +1,7 @@ ```html
...
diff --git a/projects/demo/src/modules/directives/highlight/index.html b/projects/demo/src/modules/directives/highlight/index.html index 629e6e8aa2fb..315490795b54 100644 --- a/projects/demo/src/modules/directives/highlight/index.html +++ b/projects/demo/src/modules/directives/highlight/index.html @@ -7,11 +7,32 @@

Directive is used to highlight text in element

+ + + + + + diff --git a/projects/demo/src/utils/setup.component.ts b/projects/demo/src/utils/setup.component.ts index 43eda6f01964..7b8e9e427143 100644 --- a/projects/demo/src/utils/setup.component.ts +++ b/projects/demo/src/utils/setup.component.ts @@ -35,6 +35,9 @@ export class TuiSetupComponent { @Input() public template: TuiRawLoaderContent = ''; + @Input() + public styles: TuiRawLoaderContent = ''; + @tuiPure protected get computedImport(): TuiRawLoaderContent { return ( diff --git a/projects/kit/directives/highlight/highlight-occurrence.ts b/projects/kit/directives/highlight/highlight-occurrence.ts new file mode 100644 index 000000000000..95ee41e85e91 --- /dev/null +++ b/projects/kit/directives/highlight/highlight-occurrence.ts @@ -0,0 +1,4 @@ +export interface TuiHighlightOccurrence { + index: number; + length: number; +} diff --git a/projects/kit/directives/highlight/highlight.component.ts b/projects/kit/directives/highlight/highlight.component.ts new file mode 100644 index 000000000000..bfb2af60fa36 --- /dev/null +++ b/projects/kit/directives/highlight/highlight.component.ts @@ -0,0 +1,10 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +@Component({ + standalone: true, + selector: 'mark[tuiHighlightMark]', + template: '', + styleUrls: ['./highlight.style.less'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TuiHighlightComponent {} diff --git a/projects/kit/directives/highlight/highlight.directive.ts b/projects/kit/directives/highlight/highlight.directive.ts index eade45ebaada..ea267d49025d 100644 --- a/projects/kit/directives/highlight/highlight.directive.ts +++ b/projects/kit/directives/highlight/highlight.directive.ts @@ -1,101 +1,151 @@ -import {DOCUMENT} from '@angular/common'; +import {DOCUMENT, isPlatformBrowser} from '@angular/common'; import type {OnChanges} from '@angular/core'; -import {Directive, inject, Input, Renderer2} from '@angular/core'; +import { + booleanAttribute, + Directive, + ElementRef, + EnvironmentInjector, + inject, + Input, + PLATFORM_ID, +} from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {ResizeObserverService} from '@ng-web-apis/resize-observer'; -import {svgNodeFilter, tuiInjectElement, tuiPx} from '@taiga-ui/cdk'; +import type {TuiArrayOrValue} from '@taiga-ui/cdk'; +import {svgNodeFilter, tuiIsNumber, tuiToArray} from '@taiga-ui/cdk'; +import {TuiToRegexpPipe} from '@taiga-ui/kit/pipes'; +import {mergeAll, Subject, switchMap, tap} from 'rxjs'; + +import {TuiHighlight} from './highlight'; +import type {TuiHighlightOccurrence} from './highlight-occurrence'; @Directive({ standalone: true, selector: '[tuiHighlight]', - providers: [ResizeObserverService], - host: { - '[style.position]': '"relative"', - '[style.zIndex]': '0', - }, }) export class TuiHighlightDirective implements OnChanges { - private readonly el = tuiInjectElement(); - private readonly renderer = inject(Renderer2); + private patterns: readonly RegExp[] = []; + private readonly highlight$ = new Subject(); + private readonly resetHighlights$ = new Subject(); + private readonly toRegexpPipe = inject(TuiToRegexpPipe); + private readonly environmentInjector = inject(EnvironmentInjector); private readonly doc = inject(DOCUMENT); - private readonly highlight: HTMLElement = this.setUpHighlight(); + private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); private readonly treeWalker = this.doc.createTreeWalker( - this.el, + inject(ElementRef).nativeElement, NodeFilter.SHOW_TEXT, svgNodeFilter, ); - @Input() - public tuiHighlight = ''; - - @Input() - public tuiHighlightColor = 'var(--tui-selection)'; + @Input({ + transform: booleanAttribute, + }) + public tuiHighlightMultiOccurrences = false; constructor() { - inject(ResizeObserverService) - .pipe(takeUntilDestroyed()) - .subscribe(() => this.updateStyles()); + this.resetHighlights$ + .pipe( + switchMap(() => + this.highlight$.pipe( + mergeAll(), + tap(node => { + this.treeWalker.currentNode = node; + }), + ), + ), + takeUntilDestroyed(), + ) + .subscribe(); } - public ngOnChanges(): void { - this.updateStyles(); - } + @Input() + public set tuiHighlight(value: TuiArrayOrValue) { + this.patterns = tuiToArray(value).map(item => { + if (item instanceof RegExp) { + // Only global regexp's can be used in String.prototype.mathAll method + if (!item.global) { + return new RegExp(item.source, `${item.flags}g`); + } + + return item; + } - protected get match(): boolean { - return this.indexOf(this.el.textContent) !== -1; + return this.toRegexpPipe.transform(item, 'gi'); + }); } - private updateStyles(): void { - this.highlight.style.display = 'none'; + public get tuiHighlight(): readonly RegExp[] { + return this.patterns; + } - if (!this.match) { + public ngOnChanges(): void { + if (!this.isBrowser) { return; } - this.treeWalker.currentNode = this.el; + queueMicrotask(() => this.createHighlights()); + } - do { - const index = this.indexOf(this.treeWalker.currentNode.nodeValue); + private createHighlights(): void { + this.resetHighlights$.next(); - if (index === -1) { - continue; - } + for (const node of this.getNodes()) { + const occurrence = this.getFirstOccurrence(node.nodeValue); - const range = this.doc.createRange(); + if (occurrence) { + this.highlight$.next( + new TuiHighlight( + this.environmentInjector, + this.createRange(node, occurrence), + ), + ); - range.setStart(this.treeWalker.currentNode, index); - range.setEnd(this.treeWalker.currentNode, index + this.tuiHighlight.length); + if (!this.tuiHighlightMultiOccurrences) { + return; + } + } + } + } - const hostRect = this.el.getBoundingClientRect(); - const {left, top, width, height} = range.getBoundingClientRect(); - const {style} = this.highlight; + private createRange(node: Node, {index, length}: TuiHighlightOccurrence): Range { + const range = this.doc.createRange(); - style.background = this.tuiHighlightColor; - style.left = tuiPx(left - hostRect.left); - style.top = tuiPx(top - hostRect.top); - style.width = tuiPx(width); - style.height = tuiPx(height); - style.display = 'block'; + range.setStart(node, index); + range.setEnd(node, index + length); - return; - } while (this.treeWalker.nextNode()); + return range; } - private indexOf(source: string | null): number { - return !source || !this.tuiHighlight - ? -1 - : source.toLowerCase().indexOf(this.tuiHighlight.toLowerCase()); - } + private getFirstOccurrence(source: string | null): TuiHighlightOccurrence | null { + if (!source) { + return null; + } - private setUpHighlight(): HTMLElement { - const highlight = this.renderer.createElement('div'); - const {style} = highlight; + let lastApprovedOccurrence: TuiHighlightOccurrence | null = null; + + for (const item of this.tuiHighlight) { + const [match] = source.matchAll(item); + + if ( + match && + tuiIsNumber(match.index) && + match[0].length && + (!lastApprovedOccurrence || lastApprovedOccurrence.index > match.index) + ) { + lastApprovedOccurrence = { + index: match.index, + length: match[0].length, + }; + } + } - style.background = this.tuiHighlightColor; - style.zIndex = '-1'; - style.position = 'absolute'; - this.renderer.appendChild(this.el, highlight); + return lastApprovedOccurrence; + } - return highlight; + private *getNodes(): Generator { + this.treeWalker.currentNode = this.treeWalker.root; + + while (this.treeWalker.nextNode()) { + yield this.treeWalker.currentNode; + } } } diff --git a/projects/kit/directives/highlight/highlight.style.less b/projects/kit/directives/highlight/highlight.style.less new file mode 100644 index 000000000000..13254ddeee33 --- /dev/null +++ b/projects/kit/directives/highlight/highlight.style.less @@ -0,0 +1,5 @@ +:host { + background: var(--tui-highlight-background, var(--tui-selection)); + border-radius: var(--tui-highlight-radius, 0); + color: var(--tui-highlight-color, currentColor); +} diff --git a/projects/kit/directives/highlight/highlight.ts b/projects/kit/directives/highlight/highlight.ts new file mode 100644 index 000000000000..6ac148d370ad --- /dev/null +++ b/projects/kit/directives/highlight/highlight.ts @@ -0,0 +1,46 @@ +import type {ElementRef, EnvironmentInjector} from '@angular/core'; +import {createComponent} from '@angular/core'; +import {Observable} from 'rxjs'; + +import {TuiHighlightComponent} from './highlight.component'; + +export class TuiHighlight extends Observable { + constructor(environmentInjector: EnvironmentInjector, range: Range) { + super(observer => { + const component = createComponent(TuiHighlightComponent, { + environmentInjector, + }); + const {nativeElement} = component.location as ElementRef; + + range.surroundContents(nativeElement); + observer.next(nativeElement.firstChild!); + + return () => { + const parentNode = nativeElement.parentNode; + + if (parentNode) { + while (nativeElement.firstChild) { + parentNode.insertBefore(nativeElement.firstChild, nativeElement); + } + + parentNode.removeChild(nativeElement); + + const firstNode = parentNode.firstChild; + + if (firstNode instanceof Text) { + while (firstNode.nextSibling) { + if (firstNode.nextSibling instanceof Text) { + firstNode.data += firstNode.nextSibling.data; + parentNode.removeChild(firstNode.nextSibling); + } else { + break; + } + } + } + } + + component.destroy(); + }; + }); + } +} diff --git a/projects/kit/directives/highlight/test/__snapshots__/highlight.directive.spec.ts.snap b/projects/kit/directives/highlight/test/__snapshots__/highlight.directive.spec.ts.snap new file mode 100644 index 000000000000..e3b11da7702b --- /dev/null +++ b/projects/kit/directives/highlight/test/__snapshots__/highlight.directive.spec.ts.snap @@ -0,0 +1,362 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TuiHighlight directive in multi occurrences mode with one regExp variant Highlight should shown properly 1`] = ` + +
+

+ + + Kin + + g + + Arthur +

+

+ + Kin + + g Arthur +

+

+ + Kin + + g Arthur +

+

+ + + Kin + + g + + Arthur +

+
+
+`; + +exports[`TuiHighlight directive in multi occurrences mode with one string variant Highlight should shown properly 1`] = ` + +
+

+ + King + + Arthur +

+

+ Ki + + ng Ar + + thur +

+

+ Ki + + ng Ar + + thur +

+

+ + King + + Arthur +

+
+
+`; + +exports[`TuiHighlight directive in multi occurrences mode with several regExp variants Highlight should shown properly 1`] = ` + +
+

+ + + Kin + + g + + + A + + rthur +

+

+ + Kin + + g + + A + + rthur +

+

+ + Kin + + g + + A + + rthur +

+

+ + + Kin + + g + + + A + + rthur +

+
+
+`; + +exports[`TuiHighlight directive in multi occurrences mode with several string variants Highlight should shown properly 1`] = ` + +
+

+ + Ki + + ng + + + + Ar + + thur +

+

+ Ki + + ng + + + Ar + + thur +

+

+ Ki + + ng + + + Ar + + thur +

+

+ + Ki + + ng + + + + Ar + + thur +

+
+
+`; + +exports[`TuiHighlight directive in single occurrence mode with one regExp variant Highlight should shown properly 1`] = ` + +
+

+ + + Kin + + g + + Arthur +

+

+ King Arthur +

+

+ King Arthur +

+

+ + King + + Arthur +

+
+
+`; + +exports[`TuiHighlight directive in single occurrence mode with one string variant Highlight should shown properly 1`] = ` + +
+

+ + King + + Arthur +

+

+ Ki + + ng Ar + + thur +

+

+ King Arthur +

+

+ + King + + Arthur +

+
+
+`; + +exports[`TuiHighlight directive in single occurrence mode with several regExp variants Highlight should shown properly 1`] = ` + +
+

+ + + Kin + + g + + Arthur +

+

+ King Arthur +

+

+ King Arthur +

+

+ + King + + Arthur +

+
+
+`; + +exports[`TuiHighlight directive in single occurrence mode with several string variants Highlight should shown properly 1`] = ` + +
+

+ + Ki + + ng + + + Arthur +

+

+ King Arthur +

+

+ King Arthur +

+

+ + King + + Arthur +

+
+
+`; diff --git a/projects/kit/directives/highlight/test/__snapshots__/highlight.spec.ts.snap b/projects/kit/directives/highlight/test/__snapshots__/highlight.spec.ts.snap new file mode 100644 index 000000000000..e89fa068bbbf --- /dev/null +++ b/projects/kit/directives/highlight/test/__snapshots__/highlight.spec.ts.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TuiHighlight should clean up the DOM and component on unsubscribe 1`] = ` +
+ Hello, world! +
+`; + +exports[`TuiHighlight should create a component and insert it into a DOM Range 1`] = ` +
+ + + Hello + + , world! +
+`; diff --git a/projects/kit/directives/highlight/test/highlight.directive.spec.ts b/projects/kit/directives/highlight/test/highlight.directive.spec.ts index 5512980de1db..c70ff8dfde76 100644 --- a/projects/kit/directives/highlight/test/highlight.directive.spec.ts +++ b/projects/kit/directives/highlight/test/highlight.directive.spec.ts @@ -1,61 +1,73 @@ import {Component} from '@angular/core'; +import type {ComponentFixture} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing'; import {TuiHighlightDirective} from '@taiga-ui/kit'; -import {NG_EVENT_PLUGINS} from '@tinkoff/ng-event-plugins'; describe('TuiHighlight directive', () => { - @Component({ - template: ` -
- HAPICA -
-
- ding -
-
- aaabbb -
- `, - }) - class TestComponent {} + let fixture: ComponentFixture; - beforeEach(async () => { - TestBed.configureTestingModule({ - imports: [TuiHighlightDirective], - declarations: [TestComponent], - providers: [NG_EVENT_PLUGINS], - }); - await TestBed.compileComponents(); - const fixture = TestBed.createComponent(TestComponent); - - fixture.detectChanges(); - }); - - it('Highlight is shown', () => { - const element = document.querySelector('#ica')?.firstElementChild as HTMLElement; - - expect(element.style.display).toBe('block'); - }); - - it('Highlight is not shown', () => { - const element = document.querySelector('#dong')?.firstElementChild as HTMLElement; + describe.each([ + { + name: 'in single occurrence mode', + isMulti: false, + }, + { + name: 'in multi occurrences mode', + isMulti: true, + }, + ])('$name', ({isMulti}) => { + describe.each([ + { + name: 'with one string variant', + value: 'ng Ar', + }, + { + name: 'with several string variants', + value: ['Ar', 'Ng'], + }, + { + name: 'with one regExp variant', + value: /[ink]+/gi, + }, + { + name: 'with several regExp variants', + value: [/a/gi, /[ink]+/gi], + }, + ])('$name', ({value}) => { + @Component({ + standalone: true, + imports: [TuiHighlightDirective], + template: ` +
+

+ King + Arthur +

+

King Arthur

+

King Arthur

+

+ King + Arthur +

+
+ `, + }) + class TestComponent { + public readonly isMulti = isMulti; + public readonly value = value; + } - expect(element.style.display).toBe('none'); - }); + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + }); - it('Highlight color is yellow', () => { - const element = document.querySelector('#aaa')?.firstElementChild as HTMLElement; - - expect(element.style.background).toBe('yellow'); + it('Highlight should shown properly', () => { + expect(fixture).toMatchSnapshot(); + }); + }); }); }); diff --git a/projects/kit/directives/highlight/test/highlight.spec.ts b/projects/kit/directives/highlight/test/highlight.spec.ts new file mode 100644 index 000000000000..615004c968ed --- /dev/null +++ b/projects/kit/directives/highlight/test/highlight.spec.ts @@ -0,0 +1,48 @@ +import {EnvironmentInjector} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; + +import {TuiHighlight} from '../highlight'; + +describe('TuiHighlight', () => { + let environmentInjector: EnvironmentInjector; + + beforeEach(() => { + environmentInjector = TestBed.inject(EnvironmentInjector); + }); + + it('should create a component and insert it into a DOM Range', () => { + const rootElement = document.createElement('div'); + const textNode = document.createTextNode('Hello, world!'); + const range = new Range(); + + range.setStart(textNode, 0); + range.setEnd(textNode, 5); + + rootElement.appendChild(textNode); + + const highlight = new TuiHighlight(environmentInjector, range); + const subscribeFn = jest.fn(); + + highlight.subscribe(subscribeFn); + + expect(subscribeFn).toHaveBeenCalledWith(document.createTextNode('Hello')); + expect(rootElement).toMatchSnapshot(); + }); + + it('should clean up the DOM and component on unsubscribe', () => { + const rootElement = document.createElement('div'); + const textNode = document.createTextNode('Hello, world!'); + const range = new Range(); + + range.setStart(textNode, 0); + range.setEnd(textNode, 5); + + rootElement.appendChild(textNode); + + const highlight = new TuiHighlight(environmentInjector, range); + + highlight.subscribe().unsubscribe(); + + expect(rootElement).toMatchSnapshot(); + }); +}); diff --git a/projects/kit/pipes/index.ts b/projects/kit/pipes/index.ts index f4f9d9df6d88..21e329894cf7 100644 --- a/projects/kit/pipes/index.ts +++ b/projects/kit/pipes/index.ts @@ -4,3 +4,4 @@ export * from '@taiga-ui/kit/pipes/filter-by-input'; export * from '@taiga-ui/kit/pipes/sort-countries'; export * from '@taiga-ui/kit/pipes/stringify'; export * from '@taiga-ui/kit/pipes/stringify-content'; +export * from '@taiga-ui/kit/pipes/to-regexp'; diff --git a/projects/kit/pipes/to-regexp/index.ts b/projects/kit/pipes/to-regexp/index.ts new file mode 100644 index 000000000000..666bf8c8aa7d --- /dev/null +++ b/projects/kit/pipes/to-regexp/index.ts @@ -0,0 +1 @@ +export * from './to-regexp.pipe'; diff --git a/projects/kit/pipes/to-regexp/ng-package.json b/projects/kit/pipes/to-regexp/ng-package.json new file mode 100644 index 000000000000..bebf62dcb5e5 --- /dev/null +++ b/projects/kit/pipes/to-regexp/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/projects/kit/pipes/to-regexp/to-regexp.pipe.ts b/projects/kit/pipes/to-regexp/to-regexp.pipe.ts new file mode 100644 index 000000000000..4a97e37f3b7d --- /dev/null +++ b/projects/kit/pipes/to-regexp/to-regexp.pipe.ts @@ -0,0 +1,41 @@ +import type {PipeTransform} from '@angular/core'; +import {Injectable, Pipe} from '@angular/core'; +import type {TuiArrayOrValue} from '@taiga-ui/cdk'; +import {tuiIsString, tuiToArray} from '@taiga-ui/cdk'; + +/** + * Transforms a string or an array of strings into RegExp instances. + */ +@Injectable({ + providedIn: 'root', +}) +@Pipe({ + standalone: true, + name: 'tuiToRegexp', +}) +export class TuiToRegexpPipe implements PipeTransform { + /** + * Transforms a string into a RegExp instance. + * @param {string} value - The string to transform into a RegExp. + * @param {string} [flags] - Optional flags to be applied to the RegExp. + * @returns {RegExp} The transformed RegExp instance. + */ + public transform(value: string, flags?: string): RegExp; + /** + * Transforms an array of strings into an array of RegExp instances. + * @param {readonly string[]} value - The array of strings to transform into RegExp instances. + * @param {string} [flags] - Optional flags to be applied to the RegExp instances. + * @returns {readonly RegExp[]} The transformed array of RegExp instances. + */ + public transform(value: readonly string[], flags?: string): readonly RegExp[]; + public transform( + value: TuiArrayOrValue, + flags = '', + ): TuiArrayOrValue { + if (tuiIsString(value)) { + return new RegExp(value, flags); + } + + return tuiToArray(value).map(item => new RegExp(item, flags)); + } +}