import { browser } from 'webextension-polyfill-ts' ; import { SUGGESTION_CONTAINER_QUERY } from './constants' ; import { Operator , searchOperators } from './operators' ; export class Opelete { /** Matched suggestions */ private suggestions : Operator [ ] = [ ] ; /** Exclude operator's ids */ private blacklist : string [ ] = [ ] ; /** Index of suggestion which user focused */ private focusedSuggestionIndex = 0 ; /** Google's input element of search form  */ private inputNode : HTMLInputElement ; /** Google's suggesntion element below of search form */ private suggestionNode : HTMLDivElement ; /** Container of root node */ private opeleteNode : HTMLDivElement | null = null ; param inputNode param suggestionNode constructor ( inputNode , suggestionNode ) { this . inputNode = inputNode ; this . suggestionNode = suggestionNode ; this . inputNode . addEventListener ( 'input' , this . handleInput , true ) ; this . inputNode . addEventListener ( 'keydown' , this . handleKeyDown , true ) ; document . addEventListener ( 'keydown' , this . handleKeyDown , true ) ; this . initializeNodes ( ) ; } return private initializeNodes = async ( ) : Promise < void > => { if ( ! this . suggestionNode ) { return ; } const { hide_descriptions , operator_blacklist } = await browser . storage . sync . get ( ) ; const node = document . createElement ( 'div' ) ; node . classList . add ( 'opelete' ) ; node . setAttribute ( 'dir' , 'ltr' ) ; if ( hide_descriptions ) { node . classList . add ( 'opelete--hide-descriptions' ) ; } if ( operator_blacklist ) { this . blacklist = operator_blacklist ; } this . opeleteNode = this . suggestionNode . insertBefore ( node , this . suggestionNode . firstChild ) ; } ; param e return private handleKeyDown = ( e ) => { if ( this . suggestions . length === 0 ) { return ; } switch ( e . key ) { case 'ArrowDown' : e . preventDefault ( ) ; e . stopImmediatePropagation ( ) ; this . focusedSuggestionIndex = Math . min ( this . focusedSuggestionIndex + 1 , this . suggestions . length - 1 ) ; this . updateSuggestion ( ) ; break ; case 'ArrowUp' : e . preventDefault ( ) ; e . stopImmediatePropagation ( ) ; this . focusedSuggestionIndex = Math . max ( this . focusedSuggestionIndex - 1 , 0 ) ; this . updateSuggestion ( ) ; break ; case 'Enter' : e . preventDefault ( ) ; e . stopImmediatePropagation ( ) ; const focusedSuggestion = this . suggestions [ this . focusedSuggestionIndex ] ; this . handleSelect ( focusedSuggestion ) ; break ; case 'Escape' : this . clearSuggestion ( ) ; this . disableForceShowSuggestion ( ) ; break ; default : break ; } } ; param e private handleInput = async ( e ) => { const target = e . target as HTMLInputElement ; if ( target && target . value === '' || /\s$/ . test ( target . value ) ) { this . clearSuggestion ( ) ; this . disableForceShowSuggestion ( ) ; return ; } const [ , operator ] = target . value . match ( /([^\s\n]+?)$/ ) as string [ ] ; if ( ! operator ) { return ; } this . suggestions = await searchOperators ( operator ) ; this . updateSuggestion ( ) ; this . enableForceShowSuggestion ( ) ; } ; param focusedSuggestion return private handleSelect = ( focusedSuggestion ) => { if ( ! this . inputNode ) { return ; } this . inputNode . value = this . inputNode . value . replace ( /([^\s\n]+?)$/ , focusedSuggestion . operator ) ; if ( focusedSuggestion . cursorPosition ) { const cursorPosition = this . inputNode . value . length - focusedSuggestion . operator . length + focusedSuggestion . cursorPosition ; this . inputNode . setSelectionRange ( cursorPosition , cursorPosition ) ; } if ( focusedSuggestion . insertWhiteSpace ) { this . inputNode . value += ' ' ; } this . clearSuggestion ( ) ; this . disableForceShowSuggestion ( ) ; } ; return private updateSuggestion = ( ) : void => { const suggestion = document . createElement ( 'ul' ) ; suggestion . classList . add ( 'opelete-list' ) ; this . suggestions . forEach ( ( operator , i ) => { if ( this . blacklist . indexOf ( operator . id ) !== - 1 ) { return ; } const listItem = document . createElement ( 'li' ) ; listItem . classList . add ( 'opelete-list-item' ) ; if ( i === this . focusedSuggestionIndex ) { listItem . classList . add ( 'opelete-list-item--focused' ) ; } const operatorNode = document . createElement ( 'code' ) ; operatorNode . classList . add ( 'opelete-list-item__operator' ) ; operatorNode . textContent = operator . operator ; listItem . appendChild ( operatorNode ) ; const descriptionNode = document . createElement ( 'p' ) ; descriptionNode . classList . add ( 'opelete-list-item__description' ) ; descriptionNode . textContent = operator . description ; listItem . appendChild ( descriptionNode ) ; const appendedItem = suggestion . appendChild ( listItem ) ; appendedItem . addEventListener ( 'click' , ( ) => { this . handleSelect ( operator ) ; } ) ; } ) ; if ( this . opeleteNode && this . opeleteNode . firstChild ) { this . opeleteNode . removeChild ( this . opeleteNode . firstChild ) ; } if ( this . opeleteNode ) { this . opeleteNode . appendChild ( suggestion ) ; } } ; return private clearSuggestion = ( ) : void => { this . suggestions = [ ] ; this . focusedSuggestionIndex = 0 ; this . updateSuggestion ( ) ; } ; return private enableForceShowSuggestion = ( ) : void => { if ( ! this . suggestionNode ) { return ; } this . suggestionNode . classList . add ( 'opelete-force-show' ) ; const suggestionContainer = document . querySelector ( SUGGESTION_CONTAINER_QUERY ) ; if ( suggestionContainer ) { suggestionContainer . classList . add ( 'opelete-force-show' ) ; } } ; return private disableForceShowSuggestion = ( ) : void => { if ( ! this . suggestionNode ) { return ; } this . suggestionNode . classList . add ( 'opelete-force-show' ) ; const suggestionContainer = document . querySelector ( SUGGESTION_CONTAINER_QUERY ) ; if ( suggestionContainer ) { suggestionContainer . classList . remove ( 'opelete-force-show' ) ; } } ; }