Merge pull request #4028 from mempool/nymkappa/search-autofocus
[search bar] only autofocus when in `/`, `/mining` and `/lightning`
This commit is contained in:
		
						commit
						8ee9f52634
					
				| @ -1,6 +1,8 @@ | |||||||
| import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||||
| import { SeoService } from '../../services/seo.service'; | import { SeoService } from '../../services/seo.service'; | ||||||
| import { WebsocketService } from '../../services/websocket.service'; | import { WebsocketService } from '../../services/websocket.service'; | ||||||
|  | import { StateService } from '../../services/state.service'; | ||||||
|  | import { EventType, NavigationStart, Router } from '@angular/router'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-mining-dashboard', |   selector: 'app-mining-dashboard', | ||||||
| @ -8,10 +10,12 @@ import { WebsocketService } from '../../services/websocket.service'; | |||||||
|   styleUrls: ['./mining-dashboard.component.scss'], |   styleUrls: ['./mining-dashboard.component.scss'], | ||||||
|   changeDetection: ChangeDetectionStrategy.OnPush, |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
| }) | }) | ||||||
| export class MiningDashboardComponent implements OnInit { | export class MiningDashboardComponent implements OnInit, AfterViewInit { | ||||||
|   constructor( |   constructor( | ||||||
|     private seoService: SeoService, |     private seoService: SeoService, | ||||||
|     private websocketService: WebsocketService, |     private websocketService: WebsocketService, | ||||||
|  |     private stateService: StateService, | ||||||
|  |     private router: Router | ||||||
|   ) { |   ) { | ||||||
|     this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`); |     this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`); | ||||||
|   } |   } | ||||||
| @ -19,4 +23,15 @@ export class MiningDashboardComponent implements OnInit { | |||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); |     this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   ngAfterViewInit(): void { | ||||||
|  |     this.stateService.focusSearchInputDesktop(); | ||||||
|  |     this.router.events.subscribe((e: NavigationStart) => { | ||||||
|  |       if (e.type === EventType.NavigationStart) { | ||||||
|  |         if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
 | ||||||
|  |           this.stateService.focusSearchInputDesktop();  | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate> | <form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate> | ||||||
|   <div class="d-flex"> |   <div class="d-flex"> | ||||||
|     <div class="search-box-container mr-2"> |     <div class="search-box-container mr-2"> | ||||||
|       <input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem"> |       <input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem"> | ||||||
|       <app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results> |       <app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results> | ||||||
|     </div> |     </div> | ||||||
|     <div> |     <div> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core'; | import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core'; | ||||||
| import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; | import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; | ||||||
| import { Router } from '@angular/router'; | import { EventType, NavigationStart, Router } from '@angular/router'; | ||||||
| import { AssetsService } from '../../services/assets.service'; | import { AssetsService } from '../../services/assets.service'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs'; | import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs'; | ||||||
| @ -47,6 +47,8 @@ export class SearchFormComponent implements OnInit { | |||||||
|     this.handleKeyDown($event); |     this.handleKeyDown($event); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   @ViewChild('searchInput') searchInput: ElementRef; | ||||||
|  | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     private formBuilder: UntypedFormBuilder, |     private formBuilder: UntypedFormBuilder, | ||||||
|     private router: Router, |     private router: Router, | ||||||
| @ -55,12 +57,27 @@ export class SearchFormComponent implements OnInit { | |||||||
|     private electrsApiService: ElectrsApiService, |     private electrsApiService: ElectrsApiService, | ||||||
|     private apiService: ApiService, |     private apiService: ApiService, | ||||||
|     private relativeUrlPipe: RelativeUrlPipe, |     private relativeUrlPipe: RelativeUrlPipe, | ||||||
|     private elementRef: ElementRef, |     private elementRef: ElementRef | ||||||
|   ) { } |   ) { | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.stateService.networkChanged$.subscribe((network) => this.network = network); |     this.stateService.networkChanged$.subscribe((network) => this.network = network); | ||||||
|      |      | ||||||
|  |     this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
 | ||||||
|  |       if (this.searchInput && e.type === EventType.NavigationStart) { | ||||||
|  |         this.searchInput.nativeElement.blur(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.stateService.searchFocus$.subscribe(() => { | ||||||
|  |       if (!this.searchInput) { // Try again a bit later once the view is properly initialized
 | ||||||
|  |         setTimeout(() => this.searchInput.nativeElement.focus(), 100); | ||||||
|  |       } else if (this.searchInput) { | ||||||
|  |         this.searchInput.nativeElement.focus(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     this.searchForm = this.formBuilder.group({ |     this.searchForm = this.formBuilder.group({ | ||||||
|       searchText: ['', Validators.required], |       searchText: ['', Validators.required], | ||||||
|     }); |     }); | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; | import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; | ||||||
| import { combineLatest, merge, Observable, of, Subscription } from 'rxjs'; | import { combineLatest, merge, Observable, of, Subscription } from 'rxjs'; | ||||||
| import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; | import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; | ||||||
| import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; | import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; | ||||||
| import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface'; | import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface'; | ||||||
| import { ApiService } from '../services/api.service'; | import { ApiService } from '../services/api.service'; | ||||||
| import { StateService } from '../services/state.service'; | import { StateService } from '../services/state.service'; | ||||||
| @ -31,7 +31,7 @@ interface MempoolStatsData { | |||||||
|   styleUrls: ['./dashboard.component.scss'], |   styleUrls: ['./dashboard.component.scss'], | ||||||
|   changeDetection: ChangeDetectionStrategy.OnPush |   changeDetection: ChangeDetectionStrategy.OnPush | ||||||
| }) | }) | ||||||
| export class DashboardComponent implements OnInit, OnDestroy { | export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | ||||||
|   featuredAssets$: Observable<any>; |   featuredAssets$: Observable<any>; | ||||||
|   network$: Observable<string>; |   network$: Observable<string>; | ||||||
|   mempoolBlocksData$: Observable<MempoolBlocksData>; |   mempoolBlocksData$: Observable<MempoolBlocksData>; | ||||||
| @ -57,6 +57,10 @@ export class DashboardComponent implements OnInit, OnDestroy { | |||||||
|     private seoService: SeoService |     private seoService: SeoService | ||||||
|   ) { } |   ) { } | ||||||
| 
 | 
 | ||||||
|  |   ngAfterViewInit(): void { | ||||||
|  |     this.stateService.focusSearchInputDesktop(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   ngOnDestroy(): void { |   ngOnDestroy(): void { | ||||||
|     this.currencySubscription.unsubscribe(); |     this.currencySubscription.unsubscribe(); | ||||||
|     this.websocketService.stopTrackRbfSummary(); |     this.websocketService.stopTrackRbfSummary(); | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; | ||||||
| import { Observable } from 'rxjs'; | import { Observable } from 'rxjs'; | ||||||
| import { share } from 'rxjs/operators'; | import { share } from 'rxjs/operators'; | ||||||
| import { INodesRanking } from '../../interfaces/node-api.interface'; | import { INodesRanking } from '../../interfaces/node-api.interface'; | ||||||
| @ -12,7 +12,7 @@ import { LightningApiService } from '../lightning-api.service'; | |||||||
|   styleUrls: ['./lightning-dashboard.component.scss'], |   styleUrls: ['./lightning-dashboard.component.scss'], | ||||||
|   changeDetection: ChangeDetectionStrategy.OnPush, |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
| }) | }) | ||||||
| export class LightningDashboardComponent implements OnInit { | export class LightningDashboardComponent implements OnInit, AfterViewInit { | ||||||
|   statistics$: Observable<any>; |   statistics$: Observable<any>; | ||||||
|   nodesRanking$: Observable<INodesRanking>; |   nodesRanking$: Observable<INodesRanking>; | ||||||
|   officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; |   officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; | ||||||
| @ -30,4 +30,7 @@ export class LightningDashboardComponent implements OnInit { | |||||||
|     this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); |     this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   ngAfterViewInit(): void { | ||||||
|  |     this.stateService.focusSearchInputDesktop(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ import { Router, NavigationStart } from '@angular/router'; | |||||||
| import { isPlatformBrowser } from '@angular/common'; | import { isPlatformBrowser } from '@angular/common'; | ||||||
| import { filter, map, scan, shareReplay } from 'rxjs/operators'; | import { filter, map, scan, shareReplay } from 'rxjs/operators'; | ||||||
| import { StorageService } from './storage.service'; | import { StorageService } from './storage.service'; | ||||||
|  | import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; | ||||||
| 
 | 
 | ||||||
| export interface MarkBlockState { | export interface MarkBlockState { | ||||||
|   blockHeight?: number; |   blockHeight?: number; | ||||||
| @ -139,6 +140,8 @@ export class StateService { | |||||||
|   fiatCurrency$: BehaviorSubject<string>; |   fiatCurrency$: BehaviorSubject<string>; | ||||||
|   rateUnits$: BehaviorSubject<string>; |   rateUnits$: BehaviorSubject<string>; | ||||||
| 
 | 
 | ||||||
|  |   searchFocus$: Subject<boolean> = new Subject<boolean>(); | ||||||
|  | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     @Inject(PLATFORM_ID) private platformId: any, |     @Inject(PLATFORM_ID) private platformId: any, | ||||||
|     @Inject(LOCALE_ID) private locale: string, |     @Inject(LOCALE_ID) private locale: string, | ||||||
| @ -356,4 +359,10 @@ export class StateService { | |||||||
|     this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT); |     this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT); | ||||||
|     this.blocksSubject$.next(this.blocks); |     this.blocksSubject$.next(this.blocks); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   focusSearchInputDesktop() { | ||||||
|  |     if (!hasTouchScreen()) { | ||||||
|  |       this.searchFocus$.next(true); | ||||||
|  |     }     | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -309,3 +309,28 @@ export function takeWhile(input: any[], predicate: CollectionPredicate) { | |||||||
|   return takeUntil(input, (item: any, index: number | undefined, collection: any[] | undefined) => |   return takeUntil(input, (item: any, index: number | undefined, collection: any[] | undefined) => | ||||||
|     !predicate(item, index, collection)); |     !predicate(item, index, collection)); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
 | ||||||
|  | export function hasTouchScreen(): boolean { | ||||||
|  |   let hasTouchScreen = false; | ||||||
|  |   if ('maxTouchPoints' in navigator) { | ||||||
|  |     hasTouchScreen = navigator.maxTouchPoints > 0; | ||||||
|  |   } else if ('msMaxTouchPoints' in navigator) { | ||||||
|  |     // @ts-ignore
 | ||||||
|  |     hasTouchScreen = navigator.msMaxTouchPoints > 0; | ||||||
|  |   } else { | ||||||
|  |     const mQ = matchMedia?.('(pointer:coarse)'); | ||||||
|  |     if (mQ?.media === '(pointer:coarse)') { | ||||||
|  |       hasTouchScreen = !!mQ.matches; | ||||||
|  |     } else if ('orientation' in window) { | ||||||
|  |       hasTouchScreen = true; // deprecated, but good fallback
 | ||||||
|  |     } else { | ||||||
|  |       // @ts-ignore - Only as a last resort, fall back to user agent sniffing
 | ||||||
|  |     const UA = navigator.userAgent; | ||||||
|  |       hasTouchScreen = | ||||||
|  |         /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || | ||||||
|  |         /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return hasTouchScreen; | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user