@@ -36,7 +36,6 @@ export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null;
3636type MonitoredElementInfo = {
3737 unlisten : Function ,
3838 checkChildren : boolean ,
39- renderer : Renderer2 ,
4039 subject : Subject < FocusOrigin >
4140} ;
4241
@@ -62,22 +61,53 @@ export class FocusMonitor {
6261 /** Weak map of elements being monitored to their info. */
6362 private _elementInfo = new WeakMap < Element , MonitoredElementInfo > ( ) ;
6463
65- constructor ( private _ngZone : NgZone , private _platform : Platform ) {
66- this . _ngZone . runOutsideAngular ( ( ) => this . _registerDocumentEvents ( ) ) ;
64+ /** A map of global objects to lists of current listeners. */
65+ private _unregisterGlobalListeners = ( ) => { } ;
66+
67+ /** The number of elements currently being monitored. */
68+ private get _monitoredElementCount ( ) {
69+ return this . _monitoredElementCountGetterSetterBacking ;
70+ }
71+ private set _monitoredElementCount ( n ) {
72+ // Register global listeners when first element is monitored.
73+ if ( ! this . _monitoredElementCountGetterSetterBacking && n ) {
74+ this . _registerGlobalListeners ( ) ;
75+ }
76+ // Unregister global listeners when last element is unmonitored.
77+ else if ( this . _monitoredElementCountGetterSetterBacking && ! n ) {
78+ this . _unregisterGlobalListeners ( ) ;
79+ this . _unregisterGlobalListeners = ( ) => { } ;
80+ }
81+ this . _monitoredElementCountGetterSetterBacking = n ;
6782 }
83+ private _monitoredElementCountGetterSetterBacking = 0 ;
6884
85+ constructor ( private _ngZone : NgZone , private _platform : Platform ) { }
86+
87+ /**
88+ * @docs -private
89+ * @deprecated renderer param no longer needed.
90+ */
91+ monitor ( element : HTMLElement , renderer : Renderer2 , checkChildren : boolean ) :
92+ Observable < FocusOrigin > ;
6993 /**
7094 * Monitors focus on an element and applies appropriate CSS classes.
7195 * @param element The element to monitor
72- * @param renderer The renderer to use to apply CSS classes to the element.
7396 * @param checkChildren Whether to count the element as focused when its children are focused.
7497 * @returns An observable that emits when the focus state of the element changes.
7598 * When the element is blurred, null will be emitted.
7699 */
100+ monitor ( element : HTMLElement , checkChildren : boolean ) : Observable < FocusOrigin > ;
77101 monitor (
78102 element : HTMLElement ,
79- renderer : Renderer2 ,
80- checkChildren : boolean ) : Observable < FocusOrigin > {
103+ renderer : Renderer2 | boolean ,
104+ checkChildren ?: boolean ) : Observable < FocusOrigin > {
105+ // TODO(mmalerba): clean up after deprecated signature is removed.
106+ if ( ! ( renderer instanceof Renderer2 ) ) {
107+ checkChildren = renderer ;
108+ }
109+ checkChildren = ! ! checkChildren ;
110+
81111 // Do nothing if we're not on the browser platform.
82112 if ( ! this . _platform . isBrowser ) {
83113 return observableOf ( null ) ;
@@ -93,10 +123,10 @@ export class FocusMonitor {
93123 let info : MonitoredElementInfo = {
94124 unlisten : ( ) => { } ,
95125 checkChildren : checkChildren ,
96- renderer : renderer ,
97126 subject : new Subject < FocusOrigin > ( )
98127 } ;
99128 this . _elementInfo . set ( element , info ) ;
129+ this . _monitoredElementCount ++ ;
100130
101131 // Start listening. We need to listen in capture phase since focus events don't bubble.
102132 let focusListener = ( event : FocusEvent ) => this . _onFocus ( event , element ) ;
@@ -128,6 +158,7 @@ export class FocusMonitor {
128158
129159 this . _setClasses ( element ) ;
130160 this . _elementInfo . delete ( element ) ;
161+ this . _monitoredElementCount -- ;
131162 }
132163 }
133164
@@ -142,49 +173,69 @@ export class FocusMonitor {
142173 }
143174
144175 /** Register necessary event listeners on the document and window. */
145- private _registerDocumentEvents ( ) {
176+ private _registerGlobalListeners ( ) {
146177 // Do nothing if we're not on the browser platform.
147178 if ( ! this . _platform . isBrowser ) {
148179 return ;
149180 }
150181
151- // Note: we listen to events in the capture phase so we can detect them even if the user stops
152- // propagation.
153-
154182 // On keydown record the origin and clear any touch event that may be in progress.
155- document . addEventListener ( 'keydown' , ( ) => {
183+ let documentKeydownListener = ( ) => {
156184 this . _lastTouchTarget = null ;
157185 this . _setOriginForCurrentEventQueue ( 'keyboard' ) ;
158- } , true ) ;
186+ } ;
159187
160188 // On mousedown record the origin only if there is not touch target, since a mousedown can
161189 // happen as a result of a touch event.
162- document . addEventListener ( 'mousedown' , ( ) => {
190+ let documentMousedownListener = ( ) => {
163191 if ( ! this . _lastTouchTarget ) {
164192 this . _setOriginForCurrentEventQueue ( 'mouse' ) ;
165193 }
166- } , true ) ;
194+ } ;
167195
168196 // When the touchstart event fires the focus event is not yet in the event queue. This means
169197 // we can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to
170198 // see if a focus happens.
171- document . addEventListener ( 'touchstart' , ( event : TouchEvent ) => {
199+ let documentTouchstartListener = ( event : TouchEvent ) => {
172200 if ( this . _touchTimeout != null ) {
173201 clearTimeout ( this . _touchTimeout ) ;
174202 }
175203 this . _lastTouchTarget = event . target ;
176204 this . _touchTimeout = setTimeout ( ( ) => this . _lastTouchTarget = null , TOUCH_BUFFER_MS ) ;
177-
178- // Note that we need to cast the event options to `any`, because at the time of writing
179- // (TypeScript 2.5), the built-in types don't support the `addEventListener` options param.
180- } , supportsPassiveEventListeners ( ) ? ( { passive : true , capture : true } as any ) : true ) ;
205+ } ;
181206
182207 // Make a note of when the window regains focus, so we can restore the origin info for the
183208 // focused element.
184- window . addEventListener ( 'focus' , ( ) => {
209+ let windowFocusListener = ( ) => {
185210 this . _windowFocused = true ;
186211 setTimeout ( ( ) => this . _windowFocused = false , 0 ) ;
212+ } ;
213+
214+ // Note: we listen to events in the capture phase so we can detect them even if the user stops
215+ // propagation.
216+ this . _ngZone . runOutsideAngular ( ( ) => {
217+ document . addEventListener ( 'keydown' , documentKeydownListener , true ) ;
218+ document . addEventListener ( 'mousedown' , documentMousedownListener , true ) ;
219+ document . addEventListener ( 'touchstart' , documentTouchstartListener ,
220+ supportsPassiveEventListeners ( ) ? ( { passive : true , capture : true } as any ) : true ) ;
221+ window . addEventListener ( 'focus' , windowFocusListener ) ;
187222 } ) ;
223+
224+ this . _unregisterGlobalListeners = ( ) => {
225+ document . removeEventListener ( 'keydown' , documentKeydownListener , true ) ;
226+ document . removeEventListener ( 'mousedown' , documentMousedownListener , true ) ;
227+ document . removeEventListener ( 'touchstart' , documentTouchstartListener ,
228+ supportsPassiveEventListeners ( ) ? ( { passive : true , capture : true } as any ) : true ) ;
229+ window . removeEventListener ( 'focus' , windowFocusListener ) ;
230+ } ;
231+ }
232+
233+ private _toggleClass ( element : Element , className : string , shouldSet : boolean ) {
234+ if ( shouldSet ) {
235+ element . classList . add ( className ) ;
236+ } else {
237+ element . classList . remove ( className ) ;
238+ }
188239 }
189240
190241 /**
@@ -196,16 +247,11 @@ export class FocusMonitor {
196247 const elementInfo = this . _elementInfo . get ( element ) ;
197248
198249 if ( elementInfo ) {
199- const toggleClass = ( className : string , shouldSet : boolean ) => {
200- shouldSet ? elementInfo . renderer . addClass ( element , className ) :
201- elementInfo . renderer . removeClass ( element , className ) ;
202- } ;
203-
204- toggleClass ( 'cdk-focused' , ! ! origin ) ;
205- toggleClass ( 'cdk-touch-focused' , origin === 'touch' ) ;
206- toggleClass ( 'cdk-keyboard-focused' , origin === 'keyboard' ) ;
207- toggleClass ( 'cdk-mouse-focused' , origin === 'mouse' ) ;
208- toggleClass ( 'cdk-program-focused' , origin === 'program' ) ;
250+ this . _toggleClass ( element , 'cdk-focused' , ! ! origin ) ;
251+ this . _toggleClass ( element , 'cdk-touch-focused' , origin === 'touch' ) ;
252+ this . _toggleClass ( element , 'cdk-keyboard-focused' , origin === 'keyboard' ) ;
253+ this . _toggleClass ( element , 'cdk-mouse-focused' , origin === 'mouse' ) ;
254+ this . _toggleClass ( element , 'cdk-program-focused' , origin === 'program' ) ;
209255 }
210256 }
211257
@@ -235,7 +281,7 @@ export class FocusMonitor {
235281 // result, this code will still consider it to have been caused by the touch event and will
236282 // apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
237283 // relatively small edge-case that can be worked around by using
238- // focusVia(parentEl, renderer, 'program') to focus the parent element.
284+ // focusVia(parentEl, 'program') to focus the parent element.
239285 //
240286 // If we decide that we absolutely must handle this case correctly, we can do so by listening
241287 // for the first focus event after the touchstart, and then the first blur event after that
@@ -323,10 +369,9 @@ export class CdkMonitorFocus implements OnDestroy {
323369 private _monitorSubscription : Subscription ;
324370 @Output ( ) cdkFocusChange = new EventEmitter < FocusOrigin > ( ) ;
325371
326- constructor ( private _elementRef : ElementRef , private _focusMonitor : FocusMonitor ,
327- renderer : Renderer2 ) {
372+ constructor ( private _elementRef : ElementRef , private _focusMonitor : FocusMonitor ) {
328373 this . _monitorSubscription = this . _focusMonitor . monitor (
329- this . _elementRef . nativeElement , renderer ,
374+ this . _elementRef . nativeElement ,
330375 this . _elementRef . nativeElement . hasAttribute ( 'cdkMonitorSubtreeFocus' ) )
331376 . subscribe ( origin => this . cdkFocusChange . emit ( origin ) ) ;
332377 }
0 commit comments