license import { normalizeKey } from '@material/dom/keyboard' ; import { numbers } from './constants' ; import { preventDefaultEvent } from './events' ; import { MDCListTextAndIndex } from './types' ; /**
 * State of a typeahead instance.
 */ export interface TypeaheadState { typeaheadBuffer : string ; currentFirstChar : string ; sortedIndexCursor : number ; bufferClearTimeout : number ; } return export function initState ( ) { const state = { bufferClearTimeout : 0 , currentFirstChar : '' , sortedIndexCursor : 0 , typeaheadBuffer : '' , } ; return state ; } param listItemCount param getPrimaryTextByItemIndex return export function initSortedIndex ( listItemCount , getPrimaryTextByItemIndex ) { const sortedIndexByFirstChar = new Map < string , MDCListTextAndIndex [ ] > ( ) ; for ( let i = 0 ; i < listItemCount ; i ++ ) { const primaryText = getPrimaryTextByItemIndex ( i ) . trim ( ) ; if ( ! primaryText ) { continue ; } const firstChar = primaryText [ 0 ] . toLowerCase ( ) ; if ( ! sortedIndexByFirstChar . has ( firstChar ) ) { sortedIndexByFirstChar . set ( firstChar , [ ] ) ; } sortedIndexByFirstChar . get ( firstChar ) ! . push ( { text : primaryText . toLowerCase ( ) , index : i } ) ; } sortedIndexByFirstChar . forEach ( ( values ) => { values . sort ( ( first , second ) => { return first . index - second . index ; } ) ; } ) ; return sortedIndexByFirstChar ; } /**
 * Arguments for matchItem
 */ export interface TypeaheadMatchItemOpts { focusItemAtIndex : ( index : number ) => void ; nextChar : string ; focusedItemIndex : number ; sortedIndexByFirstChar : Map < string , MDCListTextAndIndex [ ] > ; skipFocus : boolean ; isItemAtIndexDisabled : ( index : number ) => boolean ; } param opts param state return export function matchItem ( opts , state ) { const { nextChar , focusItemAtIndex , sortedIndexByFirstChar , focusedItemIndex , skipFocus , isItemAtIndexDisabled , } = opts ; clearTimeout ( state . bufferClearTimeout ) ; state . bufferClearTimeout = setTimeout ( ( ) => { clearBuffer ( state ) ; } , numbers . TYPEAHEAD_BUFFER_CLEAR_TIMEOUT_MS ) ; state . typeaheadBuffer = state . typeaheadBuffer + nextChar ; let index ; if ( state . typeaheadBuffer . length === 1 ) { index = matchFirstChar ( sortedIndexByFirstChar , focusedItemIndex , isItemAtIndexDisabled , state ) ; } else { index = matchAllChars ( sortedIndexByFirstChar , isItemAtIndexDisabled , state ) ; } if ( index !== - 1 && ! skipFocus ) { focusItemAtIndex ( index ) ; } return index ; } /**
 * Matches the user's single input character in the buffer to the
 * next option that begins with such character. Wraps around if at
 * end of options. Returns -1 if no match is found.
 */ function matchFirstChar ( sortedIndexByFirstChar , focusedItemIndex , isItemAtIndexDisabled , state ) { const firstChar = state . typeaheadBuffer [ 0 ] ; const itemsMatchingFirstChar = sortedIndexByFirstChar . get ( firstChar ) ; if ( ! itemsMatchingFirstChar ) { return - 1 ; } if ( firstChar === state . currentFirstChar && itemsMatchingFirstChar [ state . sortedIndexCursor ] . index === focusedItemIndex ) { state . sortedIndexCursor = ( state . sortedIndexCursor + 1 ) % itemsMatchingFirstChar . length ; const newIndex = itemsMatchingFirstChar [ state . sortedIndexCursor ] . index ; if ( ! isItemAtIndexDisabled ( newIndex ) ) { return newIndex ; } } state . currentFirstChar = firstChar ; let newCursorPosition = - 1 ; let cursorPosition ; for ( cursorPosition = 0 ; cursorPosition < itemsMatchingFirstChar . length ; cursorPosition ++ ) { if ( ! isItemAtIndexDisabled ( itemsMatchingFirstChar [ cursorPosition ] . index ) ) { newCursorPosition = cursorPosition ; break ; } } for ( ; cursorPosition < itemsMatchingFirstChar . length ; cursorPosition ++ ) { if ( itemsMatchingFirstChar [ cursorPosition ] . index > focusedItemIndex && ! isItemAtIndexDisabled ( itemsMatchingFirstChar [ cursorPosition ] . index ) ) { newCursorPosition = cursorPosition ; break ; } } if ( newCursorPosition !== - 1 ) { state . sortedIndexCursor = newCursorPosition ; return itemsMatchingFirstChar [ state . sortedIndexCursor ] . index ; } return - 1 ; } /**
 * Attempts to find the next item that matches all of the typeahead buffer.
 * Wraps around if at end of options. Returns -1 if no match is found.
 */ function matchAllChars ( sortedIndexByFirstChar , isItemAtIndexDisabled , state ) { const firstChar = state . typeaheadBuffer [ 0 ] ; const itemsMatchingFirstChar = sortedIndexByFirstChar . get ( firstChar ) ; if ( ! itemsMatchingFirstChar ) { return - 1 ; } const startingItem = itemsMatchingFirstChar [ state . sortedIndexCursor ] ; if ( startingItem . text . lastIndexOf ( state . typeaheadBuffer , 0 ) === 0 && ! isItemAtIndexDisabled ( startingItem . index ) ) { return startingItem . index ; } let cursorPosition = ( state . sortedIndexCursor + 1 ) % itemsMatchingFirstChar . length ; let nextCursorPosition = - 1 ; while ( cursorPosition !== state . sortedIndexCursor ) { const currentItem = itemsMatchingFirstChar [ cursorPosition ] ; const matches = currentItem . text . lastIndexOf ( state . typeaheadBuffer , 0 ) === 0 ; const isEnabled = ! isItemAtIndexDisabled ( currentItem . index ) ; if ( matches && isEnabled ) { nextCursorPosition = cursorPosition ; break ; } cursorPosition = ( cursorPosition + 1 ) % itemsMatchingFirstChar . length ; } if ( nextCursorPosition !== - 1 ) { state . sortedIndexCursor = nextCursorPosition ; return itemsMatchingFirstChar [ state . sortedIndexCursor ] . index ; } return - 1 ; } param state export function isTypingInProgress ( state ) { return state . typeaheadBuffer . length > 0 ; } /**
 * Options for handleKeydown.
 */ export interface HandleKeydownOpts { event : KeyboardEvent ; isTargetListItem : boolean ; focusItemAtIndex : ( index : number ) => void ; focusedItemIndex : number ; sortedIndexByFirstChar : Map < string , MDCListTextAndIndex [ ] > ; isItemAtIndexDisabled : ( index : number ) => boolean ; } param state export function clearBuffer ( state ) { state . typeaheadBuffer = '' ; } param opts param state returns export function handleKeydown ( opts , state ) { const { event , isTargetListItem , focusedItemIndex , focusItemAtIndex , sortedIndexByFirstChar , isItemAtIndexDisabled , } = opts ; const isArrowLeft = normalizeKey ( event ) === 'ArrowLeft' ; const isArrowUp = normalizeKey ( event ) === 'ArrowUp' ; const isArrowRight = normalizeKey ( event ) === 'ArrowRight' ; const isArrowDown = normalizeKey ( event ) === 'ArrowDown' ; const isHome = normalizeKey ( event ) === 'Home' ; const isEnd = normalizeKey ( event ) === 'End' ; const isEnter = normalizeKey ( event ) === 'Enter' ; const isSpace = normalizeKey ( event ) === 'Spacebar' ; if ( event . ctrlKey || event . metaKey || isArrowLeft || isArrowUp || isArrowRight || isArrowDown || isHome || isEnd || isEnter ) { return - 1 ; } const isCharacterKey = ! isSpace && event . key . length === 1 ; if ( isCharacterKey ) { preventDefaultEvent ( event ) ; const matchItemOpts = { focusItemAtIndex , focusedItemIndex , nextChar : event . key . toLowerCase ( ) , sortedIndexByFirstChar , skipFocus : false , isItemAtIndexDisabled , } ; return matchItem ( matchItemOpts , state ) ; } if ( ! isSpace ) { return - 1 ; } if ( isTargetListItem ) { preventDefaultEvent ( event ) ; } const typeaheadOnListItem = isTargetListItem && isTypingInProgress ( state ) ; if ( typeaheadOnListItem ) { const matchItemOpts = { focusItemAtIndex , focusedItemIndex , nextChar : ' ' , sortedIndexByFirstChar , skipFocus : false , isItemAtIndexDisabled , } ; return matchItem ( matchItemOpts , state ) ; } return - 1 ; }