mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 02:24:11 +00:00
53881: facet search box
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="filters">
|
<div class="filters py-2">
|
||||||
<a *ngFor="let value of selectedValues" class="d-block"
|
<a *ngFor="let value of selectedValues" class="d-block"
|
||||||
[routerLink]="[getSearchLink()]"
|
[routerLink]="[getSearchLink()]"
|
||||||
[queryParams]="getRemoveParams(value)" queryParamsHandling="merge">
|
[queryParams]="getRemoveParams(value)" queryParamsHandling="merge">
|
||||||
@@ -28,11 +28,15 @@
|
|||||||
| translate}}</a>
|
| translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="add-filter"
|
<ds-input-suggestions [suggestions]="(filterSearchResults | async)"
|
||||||
[action]="getCurrentUrl()">
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
|
||||||
<input type="text" [(ngModel)]="filter" [name]="filterConfig.paramName" class="form-control"
|
[action]="getCurrentUrl()"
|
||||||
aria-label="New filter input"
|
[name]="filterConfig.paramName"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate" [ngModelOptions]="{standalone: true}"/>
|
[(ngModel)]="filter"
|
||||||
<input type="submit" class="d-none"/>
|
(submitSuggestion)="onSubmit($event)"
|
||||||
</form>
|
(clickSuggestion)="clickFilter($event)"
|
||||||
|
(findSuggestions)="findSuggestions($event)"
|
||||||
|
[getDisplayValue]="getDisplayValue"
|
||||||
|
ngDefaultControl
|
||||||
|
></ds-input-suggestions>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,11 +2,9 @@
|
|||||||
@import '../../../../../styles/mixins.scss';
|
@import '../../../../../styles/mixins.scss';
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
margin-top: $spacer/2;
|
|
||||||
margin-bottom: $spacer/2;
|
|
||||||
a {
|
a {
|
||||||
color: $body-color;
|
color: $body-color;
|
||||||
&:hover {
|
&:hover, &focus {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,3 +14,4 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,12 @@
|
|||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit, QueryList,
|
||||||
|
ViewChild,
|
||||||
|
ViewChildren
|
||||||
|
} from '@angular/core';
|
||||||
import { FacetValue } from '../../../search-service/facet-value.model';
|
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -11,6 +19,7 @@ import { SearchService } from '../../../search-service/search.service';
|
|||||||
import { SearchOptions } from '../../../search-options.model';
|
import { SearchOptions } from '../../../search-options.model';
|
||||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
|
import { EmphasizePipe } from '../../../../shared/utils/emphasize.pipe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -34,6 +43,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
filter: string;
|
filter: string;
|
||||||
pageChange = false;
|
pageChange = false;
|
||||||
sub: Subscription;
|
sub: Subscription;
|
||||||
|
filterSearchResults: Observable<string[]> = Observable.of([]);
|
||||||
|
|
||||||
constructor(private searchService: SearchService, private filterService: SearchFilterService, private router: Router) {
|
constructor(private searchService: SearchService, private filterService: SearchFilterService, private router: Router) {
|
||||||
}
|
}
|
||||||
@@ -94,11 +104,17 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.filter = '';
|
this.filter = '';
|
||||||
}
|
}
|
||||||
|
this.filterSearchResults = Observable.of([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
clickFilter(data: string) {
|
||||||
|
this.onSubmit({ [this.filterConfig.paramName]: data });
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValue(o: any): boolean {
|
hasValue(o: any): boolean {
|
||||||
return hasValue(o);
|
return hasValue(o);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRemoveParams(value: string) {
|
getRemoveParams(value: string) {
|
||||||
return {
|
return {
|
||||||
[this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value),
|
[this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value),
|
||||||
@@ -122,4 +138,26 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.sub.unsubscribe();
|
this.sub.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findSuggestions(data): void {
|
||||||
|
if (isNotEmpty(data)) {
|
||||||
|
this.filterService.getSearchOptions().first().subscribe(
|
||||||
|
(options) => {
|
||||||
|
this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase())
|
||||||
|
.first()
|
||||||
|
.map(
|
||||||
|
(rd: RemoteData<PaginatedList<FacetValue>>) => {
|
||||||
|
return rd.payload.page.map((facet) => facet.value)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.filterSearchResults = Observable.of([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayValue(value: string, searchValue: string) {
|
||||||
|
return new EmphasizePipe().transform(value, searchValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
:host {
|
:host {
|
||||||
border: 1px solid map-get($theme-colors, light);
|
border: 1px solid map-get($theme-colors, light);
|
||||||
|
>div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.search-filter-wrapper {
|
.search-filter-wrapper {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@@ -175,13 +175,18 @@ export class SearchService implements OnDestroy {
|
|||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
|
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions): Observable<RemoteData<PaginatedList<FacetValue>>> {
|
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions, filterQuery?: string): Observable<RemoteData<PaginatedList<FacetValue>>> {
|
||||||
const requestObs = this.halService.getEndpoint(this.facetValueLinkPathPrefix + filterConfig.name).pipe(
|
const requestObs = this.halService.getEndpoint(this.facetValueLinkPathPrefix + filterConfig.name).pipe(
|
||||||
map((url: string) => {
|
map((url: string) => {
|
||||||
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
|
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
|
||||||
|
if (hasValue(filterQuery)) {
|
||||||
|
// args.push(`${filterConfig.paramName}=${filterQuery},query`);
|
||||||
|
args.push(`prefix=${filterQuery}`);
|
||||||
|
}
|
||||||
if (hasValue(searchOptions)) {
|
if (hasValue(searchOptions)) {
|
||||||
url = searchOptions.toRestUrl(url, args);
|
url = searchOptions.toRestUrl(url, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||||
return Object.assign(request, {
|
return Object.assign(request, {
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
@@ -16,7 +16,7 @@ export class SearchResponseParsingService implements ResponseParsingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
const payload = data.payload;
|
const payload = data.payload._embedded.searchResult;
|
||||||
const hitHighlights = payload._embedded.objects
|
const hitHighlights = payload._embedded.objects
|
||||||
.map((object) => object.hitHighlights)
|
.map((object) => object.hitHighlights)
|
||||||
.map((hhObject) => {
|
.map((hhObject) => {
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
<form #form="ngForm" (ngSubmit)="submitSuggestion.emit(form.value)"
|
||||||
|
[action]="action" (keydown)="onKeydown($event)"
|
||||||
|
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||||
|
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||||
|
(dsClickOutside)="close()">
|
||||||
|
<input #inputField type="text" [(ngModel)]="ngModel" [name]="name"
|
||||||
|
class="form-control"
|
||||||
|
[dsDebounce]="debounceTime" (onDebounce)="findSuggestions.emit($event)"
|
||||||
|
[placeholder]="placeholder"
|
||||||
|
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
||||||
|
<input type="submit" class="d-none"/>
|
||||||
|
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li *ngFor="let suggestionOption of suggestions">
|
||||||
|
<a href="#" class="d-block" class="dropdown-item" (click)="onClickSuggestion(suggestionOption)" #suggestion>
|
||||||
|
<span [innerHTML]="getDisplayValue(suggestionOption, inputField.value)"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</form>
|
@@ -0,0 +1,7 @@
|
|||||||
|
.autocomplete {
|
||||||
|
width: 100%;
|
||||||
|
.dropdown-item {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef, EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
QueryList, SimpleChanges,
|
||||||
|
ViewChild,
|
||||||
|
ViewChildren
|
||||||
|
} from '@angular/core';
|
||||||
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders a simple item page.
|
||||||
|
* The route parameter 'id' is used to request the item it represents.
|
||||||
|
* All fields of the item that should be displayed, are defined in its template.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-input-suggestions',
|
||||||
|
styleUrls: ['./input-suggestions.component.scss'],
|
||||||
|
templateUrl: './input-suggestions.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class InputSuggestionsComponent {
|
||||||
|
@Input() suggestions: string[] = [];
|
||||||
|
@Input() debounceTime = 500;
|
||||||
|
@Input() placeholder = '';
|
||||||
|
@Input() action;
|
||||||
|
@Input() name;
|
||||||
|
@Input() ngModel;
|
||||||
|
@Output() ngModelChange = new EventEmitter();
|
||||||
|
@Output() submitSuggestion = new EventEmitter();
|
||||||
|
@Output() clickSuggestion = new EventEmitter();
|
||||||
|
@Output() findSuggestions = new EventEmitter();
|
||||||
|
show = new BehaviorSubject<boolean>(false);
|
||||||
|
selectedIndex = -1;
|
||||||
|
@ViewChild('inputField') queryInput: ElementRef;
|
||||||
|
@ViewChildren('suggestion') resultViews: QueryList<ElementRef>;
|
||||||
|
@Input() getDisplayValue: (value: string, query: string) => string = (value: string, query: string) => value;
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
|
if (hasValue(changes.suggestions)) {
|
||||||
|
this.show.next(isNotEmpty(changes.suggestions.currentValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shiftFocusUp(event: KeyboardEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.selectedIndex > 0) {
|
||||||
|
this.selectedIndex--;
|
||||||
|
this.selectedIndex = (this.selectedIndex + this.resultViews.length) % this.resultViews.length; // Prevent negative modulo outcome
|
||||||
|
} else {
|
||||||
|
this.selectedIndex = this.resultViews.length - 1;
|
||||||
|
}
|
||||||
|
this.changeFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
shiftFocusDown(event: KeyboardEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.selectedIndex >= 0) {
|
||||||
|
this.selectedIndex++;
|
||||||
|
this.selectedIndex %= this.resultViews.length;
|
||||||
|
} else {
|
||||||
|
this.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
this.changeFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFocus() {
|
||||||
|
if (this.resultViews.length > 0) {
|
||||||
|
this.resultViews.toArray()[this.selectedIndex].nativeElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== 'Enter') {
|
||||||
|
this.queryInput.nativeElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.show.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
isNotEmpty(data) {
|
||||||
|
return isNotEmpty(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickSuggestion(data) {
|
||||||
|
this.clickSuggestion.emit(data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -40,14 +40,16 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
|
|||||||
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
||||||
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
|
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
|
||||||
import { VarDirective } from './utils/var.directive';
|
import { VarDirective } from './utils/var.directive';
|
||||||
import { NotificationComponent } from './notifications/notification/notification.component';
|
|
||||||
import { NotificationsBoardComponent } from './notifications/notifications-board/notifications-board.component';
|
|
||||||
import { DragClickDirective } from './utils/drag-click.directive';
|
import { DragClickDirective } from './utils/drag-click.directive';
|
||||||
import { TruncatePipe } from './utils/truncate.pipe';
|
import { TruncatePipe } from './utils/truncate.pipe';
|
||||||
import { TruncatableComponent } from './truncatable/truncatable.component';
|
import { TruncatableComponent } from './truncatable/truncatable.component';
|
||||||
import { TruncatableService } from './truncatable/truncatable.service';
|
import { TruncatableService } from './truncatable/truncatable.service';
|
||||||
import { TruncatablePartComponent } from './truncatable/truncatable-part/truncatable-part.component';
|
import { TruncatablePartComponent } from './truncatable/truncatable-part/truncatable-part.component';
|
||||||
import { MockAdminGuard } from './mocks/mock-admin-guard.service';
|
import { MockAdminGuard } from './mocks/mock-admin-guard.service';
|
||||||
|
import { DebounceDirective } from './utils/debounce.directive';
|
||||||
|
import { ClickOutsideDirective } from './utils/click-outside.directive';
|
||||||
|
import { EmphasizePipe } from './utils/emphasize.pipe';
|
||||||
|
import { InputSuggestionsComponent } from './input-suggestions/input-suggestions.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -65,7 +67,8 @@ const PIPES = [
|
|||||||
EnumKeysPipe,
|
EnumKeysPipe,
|
||||||
FileSizePipe,
|
FileSizePipe,
|
||||||
SafeUrlPipe,
|
SafeUrlPipe,
|
||||||
TruncatePipe
|
TruncatePipe,
|
||||||
|
EmphasizePipe
|
||||||
];
|
];
|
||||||
|
|
||||||
const COMPONENTS = [
|
const COMPONENTS = [
|
||||||
@@ -89,6 +92,7 @@ const COMPONENTS = [
|
|||||||
ViewModeSwitchComponent,
|
ViewModeSwitchComponent,
|
||||||
TruncatableComponent,
|
TruncatableComponent,
|
||||||
TruncatablePartComponent,
|
TruncatablePartComponent,
|
||||||
|
InputSuggestionsComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -110,7 +114,9 @@ const PROVIDERS = [
|
|||||||
|
|
||||||
const DIRECTIVES = [
|
const DIRECTIVES = [
|
||||||
VarDirective,
|
VarDirective,
|
||||||
DragClickDirective
|
DragClickDirective,
|
||||||
|
DebounceDirective,
|
||||||
|
ClickOutsideDirective
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
20
src/app/shared/utils/click-outside.directive.ts
Normal file
20
src/app/shared/utils/click-outside.directive.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import {Directive, ElementRef, Output, EventEmitter, HostListener} from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[dsClickOutside]'
|
||||||
|
})
|
||||||
|
export class ClickOutsideDirective {
|
||||||
|
constructor(private _elementRef: ElementRef) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public dsClickOutside = new EventEmitter();
|
||||||
|
|
||||||
|
@HostListener('document:click', ['$event.target'])
|
||||||
|
public onClick(targetElement) {
|
||||||
|
const clickedInside = this._elementRef.nativeElement.contains(targetElement);
|
||||||
|
if (!clickedInside) {
|
||||||
|
this.dsClickOutside.emit(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
src/app/shared/utils/debounce.directive.ts
Normal file
43
src/app/shared/utils/debounce.directive.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Directive, Input, Output, EventEmitter, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { NgControl } from '@angular/forms';
|
||||||
|
import 'rxjs/add/operator/debounceTime';
|
||||||
|
import 'rxjs/add/operator/distinctUntilChanged';
|
||||||
|
import 'rxjs/add/operator/takeUntil';
|
||||||
|
import { Subject } from 'rxjs/Subject';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[ngModel][dsDebounce]',
|
||||||
|
})
|
||||||
|
export class DebounceDirective implements OnInit, OnDestroy {
|
||||||
|
@Output()
|
||||||
|
public onDebounce = new EventEmitter<any>();
|
||||||
|
|
||||||
|
@Input('dsDebounce')
|
||||||
|
public debounceTime = 500;
|
||||||
|
|
||||||
|
private isFirstChange = true;
|
||||||
|
private ngUnsubscribe: Subject<void> = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(public model: NgControl) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.model.valueChanges
|
||||||
|
.takeUntil(this.ngUnsubscribe)
|
||||||
|
.debounceTime(this.debounceTime)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.subscribe((modelValue) => {
|
||||||
|
if (this.isFirstChange) {
|
||||||
|
this.isFirstChange = false;
|
||||||
|
} else {
|
||||||
|
this.onDebounce.emit(modelValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.ngUnsubscribe.next();
|
||||||
|
this.ngUnsubscribe.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
36
src/app/shared/utils/emphasize.pipe.ts
Normal file
36
src/app/shared/utils/emphasize.pipe.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({ name: 'dsEmphasize' })
|
||||||
|
export class EmphasizePipe implements PipeTransform {
|
||||||
|
specials = [
|
||||||
|
// order matters for these
|
||||||
|
'-'
|
||||||
|
, '['
|
||||||
|
, ']'
|
||||||
|
// order doesn't matter for any of these
|
||||||
|
, '/'
|
||||||
|
, '{'
|
||||||
|
, '}'
|
||||||
|
, '('
|
||||||
|
, ')'
|
||||||
|
, '*'
|
||||||
|
, '+'
|
||||||
|
, '?'
|
||||||
|
, '.'
|
||||||
|
, '\\'
|
||||||
|
, '^'
|
||||||
|
, '$'
|
||||||
|
, '|'
|
||||||
|
];
|
||||||
|
regex = RegExp('[' + this.specials.join('\\') + ']', 'g');
|
||||||
|
|
||||||
|
transform(haystack, needle): any {
|
||||||
|
const escaped = this.escapeRegExp(needle);
|
||||||
|
const reg = new RegExp(escaped, 'gi');
|
||||||
|
return haystack.replace(reg, '<em>$&</em>');
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeRegExp(str) {
|
||||||
|
return str.replace(this.regex, '\\$&');
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user