@@ -16,22 +16,17 @@ import {
16
16
SPACE ,
17
17
TAB ,
18
18
UP_ARROW ,
19
- A ,
20
- Z ,
21
- ZERO ,
22
- NINE ,
23
19
} from '@angular/cdk/keycodes' ;
24
20
import { InjectionToken , QueryList } from '@angular/core' ;
25
- import { of as observableOf , isObservable , Observable , Subject , Subscription } from 'rxjs' ;
26
- import { debounceTime , filter , map , take , tap } from 'rxjs/operators' ;
21
+ import { Observable , Subject , Subscription , isObservable , of as observableOf } from 'rxjs' ;
22
+ import { take } from 'rxjs/operators' ;
27
23
import {
28
24
TreeKeyManagerFactory ,
29
25
TreeKeyManagerItem ,
30
26
TreeKeyManagerOptions ,
31
27
TreeKeyManagerStrategy ,
32
28
} from './tree-key-manager-strategy' ;
33
-
34
- const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS = 200 ;
29
+ import { Typeahead } from './typeahead' ;
35
30
36
31
function coerceObservable < T > ( data : T | Observable < T > ) : Observable < T > {
37
32
if ( ! isObservable ( data ) ) {
@@ -50,8 +45,6 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
50
45
private _activeItem : T | null = null ;
51
46
private _activationFollowsFocus = false ;
52
47
private _horizontal : 'ltr' | 'rtl' = 'ltr' ;
53
- private readonly _letterKeyStream = new Subject < string > ( ) ;
54
- private _typeaheadSubscription = Subscription . EMPTY ;
55
48
56
49
/**
57
50
* Predicate function that can be used to check whether an item should be skipped
@@ -62,11 +55,11 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
62
55
/** Function to determine equivalent items. */
63
56
private _trackByFn : ( item : T ) => unknown = ( item : T ) => item ;
64
57
65
- /** Buffer for the letters that the user has pressed when the typeahead option is turned on. */
66
- private _pressedLetters : string [ ] = [ ] ;
67
-
68
58
private _items : T [ ] = [ ] ;
69
59
60
+ private _typeahead ?: Typeahead < T > ;
61
+ private _typeaheadSubscription = Subscription . EMPTY ;
62
+
70
63
constructor ( items : Observable < T [ ] > | QueryList < T > | T [ ] , config : TreeKeyManagerOptions < T > ) {
71
64
// We allow for the items to be an array or Observable because, in some cases, the consumer may
72
65
// not have access to a QueryList of the items they want to manage (e.g. when the
@@ -75,11 +68,13 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
75
68
this . _items = items . toArray ( ) ;
76
69
items . changes . subscribe ( ( newItems : QueryList < T > ) => {
77
70
this . _items = newItems . toArray ( ) ;
71
+ this . _typeahead ?. setItems ( this . _items ) ;
78
72
this . _updateActiveItemIndex ( this . _items ) ;
79
73
} ) ;
80
74
} else if ( isObservable ( items ) ) {
81
75
items . subscribe ( newItems => {
82
76
this . _items = newItems ;
77
+ this . _typeahead ?. setItems ( newItems ) ;
83
78
this . _updateActiveItemIndex ( newItems ) ;
84
79
} ) ;
85
80
} else {
@@ -99,18 +94,20 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
99
94
this . _trackByFn = config . trackBy ;
100
95
}
101
96
if ( typeof config . typeAheadDebounceInterval !== 'undefined' ) {
102
- const typeAheadInterval =
103
- typeof config . typeAheadDebounceInterval === 'number'
104
- ? config . typeAheadDebounceInterval
105
- : DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS ;
106
-
107
- this . _setTypeAhead ( typeAheadInterval ) ;
97
+ this . _setTypeAhead ( config . typeAheadDebounceInterval ) ;
108
98
}
109
99
}
110
100
111
101
/** Stream that emits any time the focused item changes. */
112
102
readonly change = new Subject < T | null > ( ) ;
113
103
104
+ /** Cleans up the key manager. */
105
+ destroy ( ) {
106
+ this . _typeaheadSubscription . unsubscribe ( ) ;
107
+ this . _typeahead ?. destroy ( ) ;
108
+ this . change . complete ( ) ;
109
+ }
110
+
114
111
/**
115
112
* Handles a keyboard event on the tree.
116
113
* @param event Keyboard event that represents the user interaction with the tree.
@@ -160,21 +157,14 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
160
157
break ;
161
158
}
162
159
163
- // Attempt to use the `event.key` which also maps it to the user's keyboard language,
164
- // otherwise fall back to resolving alphanumeric characters via the keyCode.
165
- if ( event . key && event . key . length === 1 ) {
166
- this . _letterKeyStream . next ( event . key . toLocaleUpperCase ( ) ) ;
167
- } else if ( ( keyCode >= A && keyCode <= Z ) || ( keyCode >= ZERO && keyCode <= NINE ) ) {
168
- this . _letterKeyStream . next ( String . fromCharCode ( keyCode ) ) ;
169
- }
170
-
171
- // NB: return here, in order to avoid preventing the default action of non-navigational
160
+ this . _typeahead ?. handleKey ( event ) ;
161
+ // Return here, in order to avoid preventing the default action of non-navigational
172
162
// keys or resetting the buffer of pressed letters.
173
163
return ;
174
164
}
175
165
176
166
// Reset the typeahead since the user has used a navigational key.
177
- this . _pressedLetters = [ ] ;
167
+ this . _typeahead ?. reset ( ) ;
178
168
event . preventDefault ( ) ;
179
169
}
180
170
@@ -247,6 +237,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
247
237
248
238
this . _activeItem = activeItem ?? null ;
249
239
this . _activeItemIndex = index ;
240
+ this . _typeahead ?. setCurrentSelectedItemIndex ( index ) ;
250
241
251
242
if ( options . emitChangeEvent ) {
252
243
// Emit to `change` stream as required by TreeKeyManagerStrategy interface.
@@ -270,50 +261,19 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
270
261
271
262
if ( newIndex > - 1 && newIndex !== this . _activeItemIndex ) {
272
263
this . _activeItemIndex = newIndex ;
264
+ this . _typeahead ?. setCurrentSelectedItemIndex ( newIndex ) ;
273
265
}
274
266
}
275
267
276
- private _setTypeAhead ( debounceInterval : number ) {
277
- this . _typeaheadSubscription . unsubscribe ( ) ;
278
-
279
- if (
280
- ( typeof ngDevMode === 'undefined' || ngDevMode ) &&
281
- this . _items . length &&
282
- this . _items . some ( item => typeof item . getLabel !== 'function' )
283
- ) {
284
- throw new Error (
285
- 'TreeKeyManager items in typeahead mode must implement the `getLabel` method.' ,
286
- ) ;
287
- }
288
-
289
- // Debounce the presses of non-navigational keys, collect the ones that correspond to letters
290
- // and convert those letters back into a string. Afterwards find the first item that starts
291
- // with that string and select it.
292
- this . _typeaheadSubscription = this . _letterKeyStream
293
- . pipe (
294
- tap ( letter => this . _pressedLetters . push ( letter ) ) ,
295
- debounceTime ( debounceInterval ) ,
296
- filter ( ( ) => this . _pressedLetters . length > 0 ) ,
297
- map ( ( ) => this . _pressedLetters . join ( '' ) . toLocaleUpperCase ( ) ) ,
298
- )
299
- . subscribe ( inputString => {
300
- // Start at 1 because we want to start searching at the item immediately
301
- // following the current active item.
302
- for ( let i = 1 ; i < this . _items . length + 1 ; i ++ ) {
303
- const index = ( this . _activeItemIndex + i ) % this . _items . length ;
304
- const item = this . _items [ index ] ;
305
-
306
- if (
307
- ! this . _skipPredicateFn ( item ) &&
308
- item . getLabel ?.( ) . toLocaleUpperCase ( ) . trim ( ) . indexOf ( inputString ) === 0
309
- ) {
310
- this . focusItem ( index ) ;
311
- break ;
312
- }
313
- }
268
+ private _setTypeAhead ( debounceInterval : number | boolean ) {
269
+ this . _typeahead = new Typeahead ( this . _items , {
270
+ debounceInterval : typeof debounceInterval === 'number' ? debounceInterval : undefined ,
271
+ skipPredicate : item => this . _skipPredicateFn ( item ) ,
272
+ } ) ;
314
273
315
- this . _pressedLetters = [ ] ;
316
- } ) ;
274
+ this . _typeaheadSubscription = this . _typeahead . selectedItem . subscribe ( item => {
275
+ this . focusItem ( item ) ;
276
+ } ) ;
317
277
}
318
278
319
279
private _findNextAvailableItemIndex ( startingIndex : number ) {
@@ -364,10 +324,6 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
364
324
if ( ! this . _isCurrentItemExpanded ( ) ) {
365
325
this . _activeItem . expand ( ) ;
366
326
} else {
367
- const children = this . _activeItem . getChildren ( ) ;
368
-
369
- const children2 = isObservable ( children ) ? children : observableOf ( children ) ;
370
-
371
327
coerceObservable ( this . _activeItem . getChildren ( ) )
372
328
. pipe ( take ( 1 ) )
373
329
. subscribe ( children => {
0 commit comments