@@ -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,38 @@ 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 ( ) ) ;
67- }
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 _monitoredElementCount = 0 ;
6869
70+ constructor ( private _ngZone : NgZone , private _platform : Platform ) { }
71+
72+ /**
73+ * @docs -private
74+ * @deprecated renderer param no longer needed.
75+ */
76+ monitor ( element : HTMLElement , renderer : Renderer2 , checkChildren : boolean ) :
77+ Observable < FocusOrigin > ;
6978 /**
7079 * Monitors focus on an element and applies appropriate CSS classes.
7180 * @param element The element to monitor
72- * @param renderer The renderer to use to apply CSS classes to the element.
7381 * @param checkChildren Whether to count the element as focused when its children are focused.
7482 * @returns An observable that emits when the focus state of the element changes.
7583 * When the element is blurred, null will be emitted.
7684 */
85+ monitor ( element : HTMLElement , checkChildren : boolean ) : Observable < FocusOrigin > ;
7786 monitor (
7887 element : HTMLElement ,
79- renderer : Renderer2 ,
80- checkChildren : boolean ) : Observable < FocusOrigin > {
88+ renderer : Renderer2 | boolean ,
89+ checkChildren ?: boolean ) : Observable < FocusOrigin > {
90+ // TODO(mmalerba): clean up after deprecated signature is removed.
91+ if ( ! ( renderer instanceof Renderer2 ) ) {
92+ checkChildren = renderer ;
93+ }
94+ checkChildren = ! ! checkChildren ;
95+
8196 // Do nothing if we're not on the browser platform.
8297 if ( ! this . _platform . isBrowser ) {
8398 return observableOf ( null ) ;
@@ -93,10 +108,10 @@ export class FocusMonitor {
93108 let info : MonitoredElementInfo = {
94109 unlisten : ( ) => { } ,
95110 checkChildren : checkChildren ,
96- renderer : renderer ,
97111 subject : new Subject < FocusOrigin > ( )
98112 } ;
99113 this . _elementInfo . set ( element , info ) ;
114+ this . _incrementMonitoredElementCount ( ) ;
100115
101116 // Start listening. We need to listen in capture phase since focus events don't bubble.
102117 let focusListener = ( event : FocusEvent ) => this . _onFocus ( event , element ) ;
@@ -128,6 +143,7 @@ export class FocusMonitor {
128143
129144 this . _setClasses ( element ) ;
130145 this . _elementInfo . delete ( element ) ;
146+ this . _decrementMonitoredElementCount ( ) ;
131147 }
132148 }
133149
@@ -142,49 +158,69 @@ export class FocusMonitor {
142158 }
143159
144160 /** Register necessary event listeners on the document and window. */
145- private _registerDocumentEvents ( ) {
161+ private _registerGlobalListeners ( ) {
146162 // Do nothing if we're not on the browser platform.
147163 if ( ! this . _platform . isBrowser ) {
148164 return ;
149165 }
150166
151- // Note: we listen to events in the capture phase so we can detect them even if the user stops
152- // propagation.
153-
154167 // On keydown record the origin and clear any touch event that may be in progress.
155- document . addEventListener ( 'keydown' , ( ) => {
168+ let documentKeydownListener = ( ) => {
156169 this . _lastTouchTarget = null ;
157170 this . _setOriginForCurrentEventQueue ( 'keyboard' ) ;
158- } , true ) ;
171+ } ;
159172
160173 // On mousedown record the origin only if there is not touch target, since a mousedown can
161174 // happen as a result of a touch event.
162- document . addEventListener ( 'mousedown' , ( ) => {
175+ let documentMousedownListener = ( ) => {
163176 if ( ! this . _lastTouchTarget ) {
164177 this . _setOriginForCurrentEventQueue ( 'mouse' ) ;
165178 }
166- } , true ) ;
179+ } ;
167180
168181 // When the touchstart event fires the focus event is not yet in the event queue. This means
169182 // we can't rely on the trick used above (setting timeout of 0ms). Instead we wait 650ms to
170183 // see if a focus happens.
171- document . addEventListener ( 'touchstart' , ( event : TouchEvent ) => {
184+ let documentTouchstartListener = ( event : TouchEvent ) => {
172185 if ( this . _touchTimeout != null ) {
173186 clearTimeout ( this . _touchTimeout ) ;
174187 }
175188 this . _lastTouchTarget = event . target ;
176189 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 ) ;
190+ } ;
181191
182192 // Make a note of when the window regains focus, so we can restore the origin info for the
183193 // focused element.
184- window . addEventListener ( 'focus' , ( ) => {
194+ let windowFocusListener = ( ) => {
185195 this . _windowFocused = true ;
186196 setTimeout ( ( ) => this . _windowFocused = false , 0 ) ;
197+ } ;
198+
199+ // Note: we listen to events in the capture phase so we can detect them even if the user stops
200+ // propagation.
201+ this . _ngZone . runOutsideAngular ( ( ) => {
202+ document . addEventListener ( 'keydown' , documentKeydownListener , true ) ;
203+ document . addEventListener ( 'mousedown' , documentMousedownListener , true ) ;
204+ document . addEventListener ( 'touchstart' , documentTouchstartListener ,
205+ supportsPassiveEventListeners ( ) ? ( { passive : true , capture : true } as any ) : true ) ;
206+ window . addEventListener ( 'focus' , windowFocusListener ) ;
187207 } ) ;
208+
209+ this . _unregisterGlobalListeners = ( ) => {
210+ document . removeEventListener ( 'keydown' , documentKeydownListener , true ) ;
211+ document . removeEventListener ( 'mousedown' , documentMousedownListener , true ) ;
212+ document . removeEventListener ( 'touchstart' , documentTouchstartListener ,
213+ supportsPassiveEventListeners ( ) ? ( { passive : true , capture : true } as any ) : true ) ;
214+ window . removeEventListener ( 'focus' , windowFocusListener ) ;
215+ } ;
216+ }
217+
218+ private _toggleClass ( element : Element , className : string , shouldSet : boolean ) {
219+ if ( shouldSet ) {
220+ element . classList . add ( className ) ;
221+ } else {
222+ element . classList . remove ( className ) ;
223+ }
188224 }
189225
190226 /**
@@ -196,16 +232,11 @@ export class FocusMonitor {
196232 const elementInfo = this . _elementInfo . get ( element ) ;
197233
198234 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' ) ;
235+ this . _toggleClass ( element , 'cdk-focused' , ! ! origin ) ;
236+ this . _toggleClass ( element , 'cdk-touch-focused' , origin === 'touch' ) ;
237+ this . _toggleClass ( element , 'cdk-keyboard-focused' , origin === 'keyboard' ) ;
238+ this . _toggleClass ( element , 'cdk-mouse-focused' , origin === 'mouse' ) ;
239+ this . _toggleClass ( element , 'cdk-program-focused' , origin === 'program' ) ;
209240 }
210241 }
211242
@@ -235,7 +266,7 @@ export class FocusMonitor {
235266 // result, this code will still consider it to have been caused by the touch event and will
236267 // apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
237268 // relatively small edge-case that can be worked around by using
238- // focusVia(parentEl, renderer, 'program') to focus the parent element.
269+ // focusVia(parentEl, 'program') to focus the parent element.
239270 //
240271 // If we decide that we absolutely must handle this case correctly, we can do so by listening
241272 // for the first focus event after the touchstart, and then the first blur event after that
@@ -304,6 +335,22 @@ export class FocusMonitor {
304335 this . _setClasses ( element ) ;
305336 elementInfo . subject . next ( null ) ;
306337 }
338+
339+ private _incrementMonitoredElementCount ( ) {
340+ // Register global listeners when first element is monitored.
341+ if ( ++ this . _monitoredElementCount == 1 ) {
342+ this . _registerGlobalListeners ( ) ;
343+ }
344+ }
345+
346+ private _decrementMonitoredElementCount ( ) {
347+ // Unregister global listeners when last element is unmonitored.
348+ if ( ! -- this . _monitoredElementCount ) {
349+ this . _unregisterGlobalListeners ( ) ;
350+ this . _unregisterGlobalListeners = ( ) => { } ;
351+ }
352+ }
353+
307354}
308355
309356
@@ -323,10 +370,9 @@ export class CdkMonitorFocus implements OnDestroy {
323370 private _monitorSubscription : Subscription ;
324371 @Output ( ) cdkFocusChange = new EventEmitter < FocusOrigin > ( ) ;
325372
326- constructor ( private _elementRef : ElementRef , private _focusMonitor : FocusMonitor ,
327- renderer : Renderer2 ) {
373+ constructor ( private _elementRef : ElementRef , private _focusMonitor : FocusMonitor ) {
328374 this . _monitorSubscription = this . _focusMonitor . monitor (
329- this . _elementRef . nativeElement , renderer ,
375+ this . _elementRef . nativeElement ,
330376 this . _elementRef . nativeElement . hasAttribute ( 'cdkMonitorSubtreeFocus' ) )
331377 . subscribe ( origin => this . cdkFocusChange . emit ( origin ) ) ;
332378 }
0 commit comments