import './app-search.component.scss' ; import angular , { IScope } from 'angular' ; import { OnInit } from 'angular-ts-decorators' ; import autobind from 'autobind-decorator' ; import { AndroidAppHelperService } from '../../android/android-app/shared/android-app-helper/android-app-helper.service' ; import { AlertService } from '../../shared/alert/alert.service' ; import { Bookmark , BookmarkSearchQuery } from '../../shared/bookmark/bookmark.interface' ; import { BookmarkHelperService } from '../../shared/bookmark/bookmark-helper/bookmark-helper.service' ; import { ExceptionHandler } from '../../shared/errors/errors.interface' ; import Globals from '../../shared/global-shared.constants' ; import { PlatformService } from '../../shared/global-shared.interface' ; import { SettingsService } from '../../shared/settings/settings.service' ; import { UtilityService } from '../../shared/utility/utility.service' ; import { WorkingService } from '../../shared/working/working.service' ; import { KeyCode , RoutePath } from '../app.enum' ; import { AppHelperService } from '../shared/app-helper/app-helper.service' ; import { BookmarkSearchResult , BookmarkTreeItem } from './app-search.interface' ; @ autobind export abstract class AppSearchComponent implements OnInit { Strings = require ( '../../../../res/strings/en.json' ) ; $exceptionHandler : ExceptionHandler ; $q : ng . IQService ; $scope : IScope ; $timeout : ng . ITimeoutService ; alertSvc : AlertService ; appHelperSvc : AppHelperService ; bookmarkHelperSvc : BookmarkHelperService ; platformSvc : PlatformService ; settingsSvc : SettingsService ; utilitySvc : UtilityService ; workingSvc : WorkingService ; alternateSearchBarPosition : boolean ; RoutePath = RoutePath ; batchResultsNum = 10 ; bookmarkTree : BookmarkTreeItem [ ] ; cachedBookmarks : Bookmark [ ] ; currentUrlBookmarked : boolean ; disableQueryWatch : ( ) => void ; displayFolderView : boolean ; lastWord : string ; lookahead : string ; query : string ; queryMeasure : string ; results : BookmarkSearchResult [ ] ; resultsDisplayed = 10 ; selectedBookmarkId : number ; scrollDisplayMoreEnabled = true ; static $inject = [ '$exceptionHandler' , '$q' , '$scope' , '$timeout' , 'AlertService' , 'AppHelperService' , 'BookmarkHelperService' , 'PlatformService' , 'SettingsService' , 'UtilityService' , 'WorkingService' ] ; constructor ( $exceptionHandler , $q , $scope , $timeout , AlertSvc , AppHelperSvc , BookmarkHelperSvc , PlatformSvc , SettingsSvc , UtilitySvc , WorkingSvc ) { this . $exceptionHandler = $exceptionHandler ; this . $q = $q ; this . $scope = $scope ; this . $timeout = $timeout ; this . alertSvc = AlertSvc ; this . appHelperSvc = AppHelperSvc ; this . bookmarkHelperSvc = BookmarkHelperSvc ; this . platformSvc = PlatformSvc ; this . settingsSvc = SettingsSvc ; this . utilitySvc = UtilitySvc ; this . workingSvc = WorkingSvc ; this . enableQueryWatch ( ) ; } addBookmark ( ) { this . appHelperSvc . switchView ( RoutePath . Bookmark ) ; } bookmarksAreEquivalent ( b1 , b2 ) { const getAllBookmarkProps = ( bookmarks ) => { const allProps = [ ] ; this . bookmarkHelperSvc . eachBookmark ( ( bookmark ) => { const props = Object . entries ( bookmark ) . filter ( ( resultProps ) => { const [ propertyName ] = resultProps ; return ( propertyName !== '$$hashKey' && propertyName !== 'children' && propertyName !== 'displayChildren' && propertyName !== 'open' ) ; } ) ; allProps . push ( props ) ; } , bookmarks ) ; return allProps ; } ; const b1Props = getAllBookmarkProps ( b1 ) ; const b2Props = getAllBookmarkProps ( b2 ) ; return angular . equals ( b1Props , b2Props ) ; } clearSearch ( ) { this . clearSearchQuery ( ) ; this . displayFolderView = false ; this . results = this . results ?? [ ] ; this . refreshBookmarks ( ) ; this . appHelperSvc . focusOnElement ( 'input[name=txtSearch]' ) ; } clearSearchQuery ( ) { this . disableQueryWatch ( ) ; this . query = null ; this . queryMeasure = null ; this . lookahead = null ; this . enableQueryWatch ( ) ; } displayMoreSearchResults ( ) { if ( this . results ?. length && this . scrollDisplayMoreEnabled ) { this . resultsDisplayed += this . batchResultsNum ; } } displaySearchResults ( results ) { this . scrollDisplayMoreEnabled = false ; this . resultsDisplayed = this . batchResultsNum ; this . results = results ; this . $timeout ( ( ) => { this . scrollDisplayMoreEnabled = true ; const resultsPanel = document . querySelector ( '.search-results-container' ) ; if ( resultsPanel ) { resultsPanel . scrollTop = 0 ; } } , Globals . InterfaceReadyTimeout ) ; } editBookmark ( event , bookmarkToUpdate ) { this . utilitySvc . stopEventPropagation ( event ) ; if ( this . utilitySvc . isMobilePlatform ( this . platformSvc . platformName ) ) { this . $timeout ( ( ) => { this . appHelperSvc . switchView ( `${ RoutePath . Bookmark }/${ bookmarkToUpdate . id }` ) ; } , 500 ) ; } else { this . appHelperSvc . switchView ( `${ RoutePath . Bookmark }/${ bookmarkToUpdate . id }` ) ; } } enableQueryWatch ( ) { this . disableQueryWatch = this . $scope . $watch ( ( ) => this . query , ( newVal , oldVal ) => { if ( ( newVal ?? null ) !== ( oldVal ?? null ) ) { this . searchTextChanged ( ) ; } } ) ; } getKeywords ( text ) { return this . platformSvc . getCurrentLocale ( ) . then ( ( currentLocale ) => this . utilitySvc . splitTextIntoWords ( text , currentLocale ) ) . then ( ( words ) => words . filter ( ( x ) => x . length > Globals . LookaheadMinChars ) ) ; } getSearchResults ( ) { let queryText = this . query ; return ( this . $q . resolve ( ) . then ( ( ) => { const searchQuery = { url : undefined , keywords : [ ] } ; if ( ! queryText ) { return searchQuery ; } const urlRegex = new RegExp ( `^${ Globals . URL . ValidUrlRegex }` , 'i' ) ; const url = queryText . match ( urlRegex ) ?. find ( Boolean ) ; if ( url ) { searchQuery . url = url ; queryText = queryText . replace ( urlRegex , '' ) . trim ( ) ; } return this . getKeywords ( queryText ) . then ( ( keywords ) => { searchQuery . keywords = keywords ; return searchQuery ; } ) ; } ) . then ( ( searchQuery ) => this . bookmarkHelperSvc . searchBookmarks ( searchQuery ) ) ) ; } ngOnInit ( ) { return this . settingsSvc . all ( ) . then ( ( settings ) => { this . alternateSearchBarPosition = settings . alternateSearchBarPosition ; this . displayFolderView = settings . defaultToFolderView ; this . refreshBookmarks ( ) ; } ) ; } refreshBookmarks ( ) { return this . bookmarkHelperSvc . getCachedBookmarks ( ) . then ( ( cachedBookmarks ) => { const doRefresh = ! angular . equals ( cachedBookmarks , this . cachedBookmarks ) || ( this . displayFolderView ? ! this . bookmarkTree : ! this . results ) ; if ( doRefresh ) { this . selectedBookmarkId = undefined ; this . cachedBookmarks = cachedBookmarks ; if ( this . displayFolderView ) { const bookmarkTreeItems = angular . copy ( cachedBookmarks ) . sort ( ( x , y ) => this . bookmarkHelperSvc . getBookmarkTitleForDisplay ( x ) . localeCompare ( this . bookmarkHelperSvc . getBookmarkTitleForDisplay ( y ) ) ) as BookmarkTreeItem [ ] ; this . bookmarkTree = bookmarkTreeItems ; } } return doRefresh ; } ) ; } searchBookmarks ( ) { return this . getSearchResults ( ) . then ( this . displaySearchResults ) ; } searchBoxKeyDown ( event ) { switch ( event . keyCode ) { case KeyCode . Enter : ( document . activeElement as HTMLInputElement ) . blur ( ) ; this . displayFolderView = false ; this . searchBookmarks ( ) ; this . appHelperSvc . focusOnElement ( 'input[name=txtSearch]' ) ; break ; case KeyCode . ArrowDown : if ( ! this . results ?. length ) { break ; } event . preventDefault ( ) ; Array . prototype . slice . call ( document . querySelectorAll ( '.search-results-container bookmark' ) ) . every ( ( searchResult ) => { searchResult . focus ( ) ; return false ; } ) ; break ; case KeyCode . Tab : case KeyCode . ArrowRight : if ( ! this . lookahead ) { break ; } event . preventDefault ( ) ; this . selectLookahead ( ) ; break ; default : } } searchResultsKeyDown ( event ) { let currentIndex ; let newIndex ; let elementToFocus ; const target = event . target as HTMLDivElement ; switch ( event . keyCode ) { case KeyCode . Enter : ( target . querySelector ( '.bookmark-content' ) as HTMLDivElement ) . click ( ) ; break ; case KeyCode . ArrowUp : if ( target . previousElementSibling ) { elementToFocus = target . previousElementSibling ; } else { elementToFocus = document . querySelector ( 'input[name=txtSearch]' ) ; } break ; case KeyCode . ArrowDown : if ( target . nextElementSibling ) { elementToFocus = target . nextElementSibling ; } break ; case KeyCode . PageUp : currentIndex = [ ... target . parentElement . children ] . indexOf ( target ) ; newIndex = currentIndex - 10 ; if ( newIndex < 0 ) { elementToFocus = target . parentElement . firstElementChild ; } else { elementToFocus = target . parentElement . children [ newIndex ] ; } break ; case KeyCode . PageDown : currentIndex = [ ... target . parentElement . children ] . indexOf ( target ) ; newIndex = currentIndex + 10 ; if ( target . parentElement . children . length <= newIndex ) { elementToFocus = target . parentElement . lastElementChild ; } else { elementToFocus = target . parentElement . children [ newIndex ] ; } break ; case KeyCode . Home : elementToFocus = target . parentElement . firstElementChild ; break ; case KeyCode . End : elementToFocus = target . parentElement . lastElementChild ; break ; case KeyCode . Backspace : case KeyCode . Space : elementToFocus = document . querySelector ( 'input[name=txtSearch]' ) ; break ; default : if ( event . keyCode > 47 && event . keyCode < 106 ) { elementToFocus = document . querySelector ( 'input[name=txtSearch]' ) ; } } if ( elementToFocus ) { event . preventDefault ( ) ; ( elementToFocus as HTMLDivElement ) . focus ( ) ; } } searchTextChanged ( ) { this . alertSvc . clearCurrentAlert ( ) ; if ( ! this . query ?. trim ( ) ) { this . refreshBookmarks ( ) ; return ; } const queryWords = this . query . split ( /[\s]+/ ) ; const lastWord = queryWords . slice ( - 1 ) . find ( Boolean ) ; if ( ( lastWord ?? [ ] ) . length < Globals . LookaheadMinChars ) { this . lookahead = null ; return ; } this . platformSvc . getCurrentLocale ( ) . then ( ( currentLocale ) => { return this . bookmarkHelperSvc . getLookahead ( lastWord . toLocaleLowerCase ( currentLocale ) , queryWords . length > 1 ? this . results : undefined ) . then ( ( results ) => { if ( ! results ) { this . lookahead = null ; return ; } let lookahead = results [ 0 ] ; const word = results [ 1 ] ; if ( queryWords . findIndex ( ( x ) => this . utilitySvc . stringsAreEquivalent ( x , lookahead , currentLocale ) ) >= 0 ) { this . lookahead = null ; return ; } if ( lookahead && this . utilitySvc . stringsAreEquivalent ( word , lastWord , currentLocale ) ) { lookahead = lookahead ? lookahead . substring ( word . length ) : undefined ; this . queryMeasure = this . query . replace ( /\s/g , '&nbsp;' ) ; this . lookahead = lookahead . replace ( /\s/g , '&nbsp;' ) ; } } ) . then ( ( ) => { this . displayFolderView = false ; return this . searchBookmarks ( ) ; } ) ; } ) ; } selectBookmark ( event , bookmarkId ) { this . utilitySvc . stopEventPropagation ( event ) ; if ( ! this . utilitySvc . isMobilePlatform ( this . platformSvc . platformName ) ) { return ; } this . selectedBookmarkId = bookmarkId ; } selectLookahead ( ) { this . query = `${ this . query }${ this . lookahead }` ; this . searchTextChanged ( ) ; this . appHelperSvc . focusOnElement ( 'input[name=txtSearch]' ) ; } shareBookmark ( event , bookmarkToShare ) { this . utilitySvc . stopEventPropagation ( event ) ; ( this . appHelperSvc as AndroidAppHelperService ) . shareBookmark ( bookmarkToShare ) ; } switchToBookmarkView ( ) { this . appHelperSvc . switchView ( RoutePath . Bookmark ) ; } switchToSettingsView ( ) { this . appHelperSvc . switchView ( RoutePath . Settings ) ; } toggleBookmarkTreeView ( ) { this . clearSearchQuery ( ) ; this . displayFolderView = ! this . displayFolderView ; return this . refreshBookmarks ( ) . then ( ( ) => { if ( ! this . displayFolderView ) { return this . getSearchResults ( ) . then ( ( results ) => this . displaySearchResults ( results ) ) ; } } ) ; } }