import * as debug from 'debug' import { Subject } from 'rxjs' import { debounceTime, distinctUntilChanged } from 'rxjs/operators' import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { ActivatedRoute, Params, Router } from '@angular/router' import { RestService } from '@app/core' export type AdvancedInputFilter = { title: string children: AdvancedInputFilterChild[] } export type AdvancedInputFilterChild = { label: string value: string } const debugLogger = debug('peertube:AdvancedInputFilterComponent') @Component({ selector: 'my-advanced-input-filter', templateUrl: './advanced-input-filter.component.html', styleUrls: [ './advanced-input-filter.component.scss' ] }) export class AdvancedInputFilterComponent implements OnInit, AfterViewInit { @Input() filters: AdvancedInputFilter[] = [] @Input() emitOnInit = true @Output() search = new EventEmitter() searchValue: string private enabledFilters = new Set() private searchStream: Subject private viewInitialized = false private emitSearchAfterViewInit = false constructor ( private route: ActivatedRoute, private restService: RestService, private router: Router ) { } ngOnInit () { this.initSearchStream() this.listenToRouteSearchChange() } ngAfterViewInit () { this.viewInitialized = true // Init after view init to not send an event too early if (this.emitOnInit && this.emitSearchAfterViewInit) this.emitSearch() } onInputSearch (event: Event) { this.scheduleSearchUpdate((event.target as HTMLInputElement).value) } onResetTableFilter () { this.immediateSearchUpdate('') } hasFilters () { return this.filters && this.filters.length !== 0 } isFilterEnabled (filter: AdvancedInputFilterChild) { return this.enabledFilters.has(filter.value) } onFilterClick (filter: AdvancedInputFilterChild) { const newSearch = this.isFilterEnabled(filter) ? this.removeFilterToSearch(this.searchValue, filter) : this.addFilterToSearch(this.searchValue, filter) this.router.navigate([ '.' ], { relativeTo: this.route, queryParams: { search: newSearch.trim() } }) } private scheduleSearchUpdate (value: string) { this.searchValue = value this.searchStream.next(this.searchValue) } private immediateSearchUpdate (value: string) { this.searchValue = value this.setQueryParams(this.searchValue) this.parseFilters(this.searchValue) this.emitSearch() } private listenToRouteSearchChange () { this.route.queryParams .subscribe(params => { const search = params.search || '' debugLogger('On route search change "%s".', search) if (this.searchValue === search) return this.searchValue = search this.parseFilters(this.searchValue) this.emitSearch() }) } private initSearchStream () { this.searchStream = new Subject() this.searchStream .pipe( debounceTime(300), distinctUntilChanged() ) .subscribe(() => { this.setQueryParams(this.searchValue) this.parseFilters(this.searchValue) this.emitSearch() }) } private emitSearch () { if (!this.viewInitialized) { this.emitSearchAfterViewInit = true return } debugLogger('On search "%s".', this.searchValue) this.search.emit(this.searchValue) } private setQueryParams (search: string) { const queryParams: Params = {} if (search) Object.assign(queryParams, { search }) this.router.navigate([ ], { queryParams }) } private removeFilterToSearch (search: string, removedFilter: AdvancedInputFilterChild) { return search.replace(removedFilter.value, '') } private addFilterToSearch (search: string, newFilter: AdvancedInputFilterChild) { const prefix = newFilter.value.split(':').shift() // Tokenize search and remove a potential existing filter const tokens = this.restService.tokenizeString(search) .filter(t => !t.startsWith(prefix)) tokens.push(newFilter.value) return tokens.join(' ') } private parseFilters (search: string) { const tokens = this.restService.tokenizeString(search) this.enabledFilters = new Set() for (const group of this.filters) { for (const filter of group.children) { if (tokens.includes(filter.value)) { this.enabledFilters.add(filter.value) } } } } }