diff --git a/tabby-terminal/src/components/baseTerminalTab.component.pug b/tabby-terminal/src/components/baseTerminalTab.component.pug index 62cae54e0..97eccee4f 100644 --- a/tabby-terminal/src/components/baseTerminalTab.component.pug +++ b/tabby-terminal/src/components/baseTerminalTab.component.pug @@ -1,7 +1,8 @@ .terminal-toolbar-spacer .content(#content, [style.opacity]='frontendIsReady ? 1 : 0') search-panel( - *ngIf='showSearchPanel && hasFocus && frontend', + *ngIf='showSearchPanel && frontend', + [class.d-none]='!hasFocus', @toolbarSlide, [@.disabled]='!config.store.accessibility.animations', [frontend]='frontend', diff --git a/tabby-terminal/src/components/searchPanel.component.pug b/tabby-terminal/src/components/searchPanel.component.pug index 55820df1e..a8fee1f1d 100644 --- a/tabby-terminal/src/components/searchPanel.component.pug +++ b/tabby-terminal/src/components/searchPanel.component.pug @@ -5,7 +5,9 @@ (ngModelChange)='onQueryChange()', [class.text-danger]='state.resultCount == 0', (click)='$event.stopPropagation()', - (keyup.enter)='findPrevious()', + (keydown.enter)='onKeyDown($event)', + (keyup.enter)='onKeyUp($event)', + (blur)='onBlur()', (keyup.up)='findPrevious()', (keyup.down)='findNext()', (keyup.esc)='close.emit()', diff --git a/tabby-terminal/src/components/searchPanel.component.ts b/tabby-terminal/src/components/searchPanel.component.ts index 83c840476..91fcdcf70 100644 --- a/tabby-terminal/src/components/searchPanel.component.ts +++ b/tabby-terminal/src/components/searchPanel.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core' +import { Component, Input, Output, EventEmitter, NgZone, OnDestroy } from '@angular/core' import { Subject, debounceTime } from 'rxjs' import { Frontend, SearchOptions, SearchState } from '../frontends/frontend' import { ConfigService, NotificationsService, TranslateService } from 'tabby-core' @@ -8,8 +8,8 @@ import { ConfigService, NotificationsService, TranslateService } from 'tabby-cor templateUrl: './searchPanel.component.pug', styleUrls: ['./searchPanel.component.scss'], }) -export class SearchPanelComponent { - @Input() query: string +export class SearchPanelComponent implements OnDestroy { + query = '' @Input() frontend: Frontend state: SearchState = { resultCount: 0 } options: SearchOptions = { @@ -20,6 +20,11 @@ export class SearchPanelComponent { @Output() close = new EventEmitter() private queryChanged = new Subject() + private holdTimer: ReturnType | null = null + private repeatTimer: ReturnType | null = null + + private readonly HOLD_DELAY = 400 + private readonly REPEAT_INTERVAL = 100 icons = { 'case': require('../icons/case.svg'), @@ -34,6 +39,7 @@ export class SearchPanelComponent { private notifications: NotificationsService, private translate: TranslateService, public config: ConfigService, + private ngZone: NgZone, ) { this.queryChanged.pipe(debounceTime(250)).subscribe(() => { this.findPrevious(true) @@ -45,6 +51,44 @@ export class SearchPanelComponent { this.queryChanged.next(this.query) } + onKeyDown (event: KeyboardEvent): void { + if (event.key !== 'Enter' || event.repeat) { return } + if (this.holdTimer ?? this.repeatTimer) { return } + + this.holdTimer = setTimeout(() => { + this.holdTimer = null + this.ngZone.runOutsideAngular(() => { + this.repeatTimer = setInterval(() => { + this.ngZone.run(() => { + this.findPrevious() + if (!this.state.resultCount) { + // nothing to cycle through - one notice is enough + this.stopRepeat() + } + }) + }, this.REPEAT_INTERVAL) + }) + }, this.HOLD_DELAY) + } + + private stopRepeat (fireOnTap = false): void { + if (this.holdTimer) { + clearTimeout(this.holdTimer) + this.holdTimer = null + if (fireOnTap) { this.findPrevious() } + } + if (this.repeatTimer) { clearInterval(this.repeatTimer); this.repeatTimer = null } + } + + onKeyUp (event: KeyboardEvent): void { + if (event.key !== 'Enter') { return } + this.stopRepeat(true) + } + + onBlur (): void { + this.stopRepeat(false) + } + findNext (incremental = false): void { if (!this.query) { return @@ -75,5 +119,6 @@ export class SearchPanelComponent { ngOnDestroy (): void { this.queryChanged.complete() + this.stopRepeat() } }